mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cc53dc3b7 | |||
| bf5f23a0ab | |||
| 37851cd7f8 | |||
| e0ca9900d3 | |||
| f108197e5f | |||
| 20c2a3dc62 | |||
| 4a7fa1df08 | |||
| 685dbc9505 | |||
| 9aacceda28 | |||
| a7051e7d26 | |||
| 7926e81562 | |||
| 2002e093af | |||
| fc44f49f2d | |||
| 89fbc6aeca | |||
| 20e3f9c104 | |||
| 17fa92d4cf | |||
| a48a3a4141 | |||
| 7d5b638eac | |||
| 5f5fe0da90 | |||
| dd98814b2c | |||
| 4dd999178c | |||
| 01dce2a5e0 | |||
| 9622092c17 | |||
| 29da1487d4 | |||
| 357fb530e2 | |||
| b43683a259 | |||
| 59379649e2 | |||
| a62bc350c0 | |||
| 82ff4bb0df | |||
| c454f2ef3a | |||
| b93f640233 | |||
| 018e16e9fa | |||
| 41397072af | |||
| 43be448100 | |||
| 8c7f181002 | |||
| 5195868719 | |||
| a473e32c59 | |||
| be51dc9c55 | |||
| bea6c8cd8e | |||
| 351c35ef42 | |||
| 7f722b6f12 | |||
| 52f1a1e788 | |||
| f44a78730a | |||
| a9a5e046ea | |||
| 37386f9e28 | |||
| b66bfb1ee9 | |||
| caf9cd1596 | |||
| a4ebd2b23c | |||
| 5676ade6b7 | |||
| 319f8eac06 | |||
| d85132133a | |||
| b6d8af409c | |||
| 896a0980d5 | |||
| 7d395e5e27 | |||
| c3cc01d7e7 | |||
| ecbadc6087 | |||
| ff30623bdf | |||
| a43433ccb4 | |||
| 4d9db2a52c | |||
| e30b59851f | |||
| 36dd91be63 | |||
| c9639d851b | |||
| 4516c84128 | |||
| fa98f56318 | |||
| f85e783e8c | |||
| a882bc22dd | |||
| e12e3a2a41 | |||
| da31794d8d | |||
| 9912f6b181 | |||
| cb4cc281c6 | |||
| 571559114d | |||
| df26df07f1 | |||
| ffc7340bc9 | |||
| 1d58aaba83 | |||
| b2bb9345fe | |||
| 9686622b56 | |||
| f7644a9573 | |||
| e48e9464d7 | |||
| b72bc5d52b | |||
| 1220f0bcbd | |||
| 539410d5bb | |||
| 383b576d18 | |||
| 64a55a3ef3 | |||
| 9408201e57 | |||
| f75d6bf749 | |||
| 924d223866 | |||
| e9dcca1f19 | |||
| 00cc2abd23 | |||
| b76477167d | |||
| b41b249a6d | |||
| 71fcda2dd6 | |||
| c4453fbb31 | |||
| 79fa3f66a8 | |||
| 0ce64ac975 | |||
| 350aa9e4a3 | |||
| e5bbf972c7 | |||
| 4326e12e88 | |||
| 00aa3216ff | |||
| 3d6c01f020 | |||
| d3bf0ede67 | |||
| 2b02166d82 | |||
| 2fd36b4b11 | |||
| 8aa1c59873 | |||
| cd036b8004 | |||
| 989da239fb | |||
| 31626494d3 | |||
| 960a7ef075 | |||
| 60c4d22d2d | |||
| 13a094be00 | |||
| 7744cedd8c | |||
| ad42c1aeaf | |||
| 41f7bf42a3 | |||
| 0543aeb650 | |||
| 679071cc14 | |||
| 198afcc7d8 | |||
| 191a01a03c | |||
| fd653f8234 | |||
| 2149fed8c5 | |||
| 5609d18284 | |||
| 705b0b79fc | |||
| 32ad8e3a9c | |||
| e77428661c | |||
| e68cdf8cc1 |
@@ -2,6 +2,7 @@ name: Build container
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@@ -23,7 +24,8 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=match,pattern=v\d.\d.\d,value=latest
|
# publish :latest from the default branch
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -49,4 +51,4 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
# optional cache (speeds up rebuilds)
|
# optional cache (speeds up rebuilds)
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,6 @@ env/*
|
|||||||
__pycache__/*
|
__pycache__/*
|
||||||
meshview/__pycache__/*
|
meshview/__pycache__/*
|
||||||
alembic/__pycache__/*
|
alembic/__pycache__/*
|
||||||
meshtastic/protobuf/*
|
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
packets.db
|
packets.db
|
||||||
@@ -45,3 +44,4 @@ __pycache__/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
packets.db-journal
|
||||||
|
|||||||
+1
-2
@@ -35,7 +35,7 @@ RUN uv pip install --no-cache-dir --upgrade pip \
|
|||||||
COPY --chown=${APP_USER}:${APP_USER} . .
|
COPY --chown=${APP_USER}:${APP_USER} . .
|
||||||
|
|
||||||
# Patch config
|
# Patch config
|
||||||
RUN patch sample.config.ini < container/config.patch
|
COPY --chown=${APP_USER}:${APP_USER} container/config.ini /app/sample.config.ini
|
||||||
|
|
||||||
# Clean
|
# Clean
|
||||||
RUN rm -rf /app/.git* && \
|
RUN rm -rf /app/.git* && \
|
||||||
@@ -77,4 +77,3 @@ CMD ["--pid_dir", "/tmp", "--py_exec", "/opt/venv/bin/python", "--config", "/etc
|
|||||||
|
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
VOLUME [ "/etc/meshview", "/var/lib/meshview", "/var/log/meshview" ]
|
VOLUME [ "/etc/meshview", "/var/lib/meshview", "/var/log/meshview" ]
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -128,7 +128,11 @@ username =
|
|||||||
password =
|
password =
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
connection_string = sqlite+aiosqlite:///var/lib/meshview/packets.db
|
# SQLAlchemy async connection string.
|
||||||
|
# Examples:
|
||||||
|
# sqlite+aiosqlite:///var/lib/meshview/packets.db
|
||||||
|
# postgresql+asyncpg://user:pass@host:5432/meshview
|
||||||
|
connection_string = sqlite+aiosqlite:////var/lib/meshview/packets.db
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Backups
|
### Database Backups
|
||||||
|
|||||||
@@ -4,6 +4,39 @@
|
|||||||
|
|
||||||
The project serves as a real-time monitoring and diagnostic tool for the Meshtastic mesh network. It provides detailed insights into network activity, including message traffic, node positions, and telemetry data.
|
The project serves as a real-time monitoring and diagnostic tool for the Meshtastic mesh network. It provides detailed insights into network activity, including message traffic, node positions, and telemetry data.
|
||||||
|
|
||||||
|
### Version 3.0.5 — February 2026
|
||||||
|
- **IMPORTANT:** the predicted coverage feature requires the extra `pyitm` dependency. If it is not installed, the coverage API will return 503.
|
||||||
|
- Ubuntu install (inside the venv): `./env/bin/pip install pyitm`
|
||||||
|
- Coverage: predicted coverage overlay (Longley‑Rice area mode) with perimeter rendering and documentation.
|
||||||
|
- UI: added QR code display for quick node/app access.
|
||||||
|
- Gateways: persistent gateway tracking (`is_mqtt_gateway`) and UI indicators in nodes, map popups, and stats.
|
||||||
|
- Map UX: deterministic jitter for overlapping nodes; edges follow jittered positions.
|
||||||
|
- Tooling: Meshtastic protobuf updater script with `--check` and `UPSTREAM_REV.txt` tracking.
|
||||||
|
|
||||||
|
|
||||||
|
### Version 3.0.4 — Late January 2026
|
||||||
|
- Database: multi‑DB support, PostgreSQL scripts, WAL config for SQLite, cleanup query timing fixes, removal of import time columns, and various time‑handling fixes.
|
||||||
|
- UI/UX: extensive updates to node.html, nodelist.html, top.html, and packet.html (paging, stats, distance, status/favorites), plus net view changes to 12‑hour window.
|
||||||
|
- API/logic: weekly mesh query fix, node list performance improvement, backwards‑compatibility and other bug fixes.
|
||||||
|
- MQTT reader: configurable skip‑node list and secondary decryption keys.
|
||||||
|
- Docs/ops: multiple documentation updates, updated site list, container workflow fixes/tests, README updates.
|
||||||
|
|
||||||
|
### Version 3.0.2 — January 2026
|
||||||
|
- Changes to the Database to will make it so that there is a need for space when updating to the latest. SQlite requires to rebuild the database when droping a column. ( we are droping some of the old columns) so make sure you have 1.2x the size of the db of space in your environment. Depending on how big your db is it would take a long time.
|
||||||
|
|
||||||
|
### Version 3.0.1 — December 2025
|
||||||
|
|
||||||
|
#### 🌐 Multi-Language Support (i18n)
|
||||||
|
- New `/api/lang` endpoint for serving translations
|
||||||
|
- Section-based translation loading (e.g., `?section=firehose`)
|
||||||
|
- Default language controlled via config file language section
|
||||||
|
- JSON-based translation files for easy expansion
|
||||||
|
- Core pages updated to support `data-translate-lang` attributes
|
||||||
|
|
||||||
|
### 🛠 Improvements
|
||||||
|
- Updated UI elements across multiple templates for localization readiness
|
||||||
|
- General cleanup to support future language additions
|
||||||
|
|
||||||
### Version 3.0.0 update - November 2025
|
### Version 3.0.0 update - November 2025
|
||||||
|
|
||||||
**Major Infrastructure Improvements:**
|
**Major Infrastructure Improvements:**
|
||||||
@@ -67,23 +100,45 @@ See [README-Docker.md](README-Docker.md) for container deployment and [docs/](do
|
|||||||
|
|
||||||
Samples of currently running instances:
|
Samples of currently running instances:
|
||||||
|
|
||||||
- https://meshview.bayme.sh (SF Bay Area)
|
- https://meshview.bayme.sh (SF Bay Area - USA)
|
||||||
- https://www.svme.sh (Sacramento Valley)
|
- https://www.svme.sh (Sacramento Valley - USA)
|
||||||
- https://meshview.nyme.sh (New York)
|
- https://meshview.nyme.sh (New York - USA)
|
||||||
- https://meshview.socalmesh.org (LA Area)
|
- https://meshview.socalmesh.org (Los Angenles - USA)
|
||||||
- https://map.wpamesh.net (Western Pennsylvania)
|
- https://map.wpamesh.net (Western Pennsylvania - USA)
|
||||||
- https://meshview.chicagolandmesh.org (Chicago)
|
- https://meshview.chicagolandmesh.org (Chicago - USA)
|
||||||
- https://meshview.mt.gt (Canadaverse)
|
- https://meshview.freq51.net/ (Salt Lake City - USA)
|
||||||
|
- https://meshview.mt.gt (Canada)
|
||||||
|
- https://canadaverse.org (Canada)
|
||||||
- https://meshview.meshtastic.es (Spain)
|
- https://meshview.meshtastic.es (Spain)
|
||||||
- https://view.mtnme.sh (North Georgia / East Tennessee)
|
- https://view.mtnme.sh (North Georgia / East Tennessee - USA)
|
||||||
- https://meshview.lsinfra.de (Hessen - Germany)
|
- https://meshview.lsinfra.de (Hessen - Germany)
|
||||||
- https://map.nswmesh.au (Sydney - Australia)
|
- https://meshview.pvmesh.org (Pioneer Valley, Massachusetts - USA)
|
||||||
- https://meshview.pvmesh.org (Pioneer Valley, Massachusetts)
|
- https://meshview.louisianamesh.org (Louisiana - USA)
|
||||||
- https://meshview.louisianamesh.org (Louisiana)
|
- https://www.swlamesh.com (Southwest Louisiana- USA)
|
||||||
- https://meshview.meshcolombia.co/ (Colombia)
|
- https://meshview.meshcolombia.co (Colombia)
|
||||||
- https://meshview-salzburg.jmt.gr/ (Salzburg / Austria)
|
- https://meshview-salzburg.jmt.gr (Salzburg / Austria)
|
||||||
|
- https://map.cromesh.eu (Coatia)
|
||||||
|
- https://view.meshdresden.eu (Dresden / Germany)
|
||||||
|
- https://meshview.meshoregon.com (Oregon - USA)
|
||||||
|
- https://meshview.gamesh.net (Georgia - USA)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Updating from 2.x to 3.x
|
||||||
|
We are adding the use of Alembic. If using GitHub
|
||||||
|
Update your codebase by running the pull command
|
||||||
|
```bash
|
||||||
|
cd meshview
|
||||||
|
git pull origin master
|
||||||
|
```
|
||||||
|
Install Alembic in your environment
|
||||||
|
```bash
|
||||||
|
./env/bin/pip install alembic
|
||||||
|
```
|
||||||
|
Start your scripts or services. This process will update your database with the latest schema.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
### Using Docker (Recommended)
|
### Using Docker (Recommended)
|
||||||
@@ -184,6 +239,9 @@ acme_challenge =
|
|||||||
# The domain name of your site.
|
# The domain name of your site.
|
||||||
domain =
|
domain =
|
||||||
|
|
||||||
|
# Select language (this represents the name of the json file in the /lang directory)
|
||||||
|
language = es
|
||||||
|
|
||||||
# Site title to show in the browser title bar and headers.
|
# Site title to show in the browser title bar and headers.
|
||||||
title = Bay Area Mesh
|
title = Bay Area Mesh
|
||||||
|
|
||||||
@@ -241,7 +299,10 @@ password = large4cats
|
|||||||
# Database Configuration
|
# Database Configuration
|
||||||
# -------------------------
|
# -------------------------
|
||||||
[database]
|
[database]
|
||||||
# SQLAlchemy connection string. This one uses SQLite with asyncio support.
|
# SQLAlchemy async connection string.
|
||||||
|
# Examples:
|
||||||
|
# sqlite+aiosqlite:///packets.db
|
||||||
|
# postgresql+asyncpg://user:pass@host:5432/meshview
|
||||||
connection_string = sqlite+aiosqlite:///packets.db
|
connection_string = sqlite+aiosqlite:///packets.db
|
||||||
|
|
||||||
|
|
||||||
@@ -275,6 +336,20 @@ db_cleanup_logfile = dbcleanup.log
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## NOTE (PostgreSQL setup)**
|
||||||
|
If you want to use PostgreSQL instead of SQLite:
|
||||||
|
|
||||||
|
Install PostgreSQL for your OS.
|
||||||
|
Create a user and database:
|
||||||
|
```
|
||||||
|
`CREATE USER meshview WITH PASSWORD 'change_me';`
|
||||||
|
`CREATE DATABASE meshview OWNER meshview;`
|
||||||
|
```
|
||||||
|
Update `config.ini` example:
|
||||||
|
```
|
||||||
|
`connection_string = postgresql+asyncpg://meshview:change_me@localhost:5432/meshview`
|
||||||
|
```
|
||||||
|
|
||||||
## Running Meshview
|
## Running Meshview
|
||||||
|
|
||||||
Start the database manager:
|
Start the database manager:
|
||||||
@@ -444,16 +519,15 @@ db_cleanup_logfile = dbcleanup.log
|
|||||||
```
|
```
|
||||||
Once changes are done you need to restart the script for changes to load.
|
Once changes are done you need to restart the script for changes to load.
|
||||||
|
|
||||||
### Alternatively we can do it via your OS
|
### Alternatively we can do it via your OS (This example is Ubuntu like OS)
|
||||||
- Create and save bash script below. (Modify /path/to/file/ to the correct path)
|
- Create and save bash script below. (Modify /path/to/file/ to the correct path)
|
||||||
- Name it cleanup.sh
|
- Name it cleanup.sh
|
||||||
- Make it executable.
|
- Make it executable.
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DB_FILE="/path/to/file/packets.db"
|
DB_FILE="/path/to/file/packets.db"
|
||||||
|
|
||||||
|
|
||||||
# Stop DB service
|
# Stop DB service
|
||||||
sudo systemctl stop meshview-db.service
|
sudo systemctl stop meshview-db.service
|
||||||
sudo systemctl stop meshview-web.service
|
sudo systemctl stop meshview-web.service
|
||||||
@@ -462,10 +536,22 @@ sleep 5
|
|||||||
echo "Run cleanup..."
|
echo "Run cleanup..."
|
||||||
# Run cleanup queries
|
# Run cleanup queries
|
||||||
sqlite3 "$DB_FILE" <<EOF
|
sqlite3 "$DB_FILE" <<EOF
|
||||||
DELETE FROM packet WHERE import_time < datetime('now', '-14 day');
|
DELETE FROM packet
|
||||||
DELETE FROM packet_seen WHERE import_time < datetime('now', '-14 day');
|
WHERE import_time_us IS NOT NULL
|
||||||
DELETE FROM traceroute WHERE import_time < datetime('now', '-14 day');
|
AND import_time_us < (strftime('%s','now','-14 days') * 1000000);
|
||||||
DELETE FROM node WHERE last_update < datetime('now', '-14 day') OR last_update IS NULL OR last_update = '';
|
SELECT 'packet deleted: ' || changes();
|
||||||
|
DELETE FROM packet_seen
|
||||||
|
WHERE import_time_us IS NOT NULL
|
||||||
|
AND import_time_us < (strftime('%s','now','-14 days') * 1000000);
|
||||||
|
SELECT 'packet_seen deleted: ' || changes();
|
||||||
|
DELETE FROM traceroute
|
||||||
|
WHERE import_time_us IS NOT NULL
|
||||||
|
AND import_time_us < (strftime('%s','now','-14 days') * 1000000);
|
||||||
|
SELECT 'traceroute deleted: ' || changes();
|
||||||
|
DELETE FROM node
|
||||||
|
WHERE last_seen_us IS NULL
|
||||||
|
OR last_seen_us < (strftime('%s','now','-14 days') * 1000000);
|
||||||
|
SELECT 'node deleted: ' || changes();
|
||||||
VACUUM;
|
VACUUM;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -475,6 +561,80 @@ sudo systemctl start meshview-web.service
|
|||||||
|
|
||||||
echo "Database cleanup completed on $(date)"
|
echo "Database cleanup completed on $(date)"
|
||||||
|
|
||||||
|
```
|
||||||
|
- If you are using PostgreSQL, use this version instead (adjust credentials/DB name):
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB="postgresql://meshview@localhost:5432/meshview"
|
||||||
|
RETENTION_DAYS=14
|
||||||
|
BATCH_SIZE=100
|
||||||
|
|
||||||
|
PSQL="/usr/bin/psql"
|
||||||
|
|
||||||
|
echo "[$(date)] Starting batched cleanup..."
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
DELETED=$(
|
||||||
|
$PSQL "$DB" -At -v ON_ERROR_STOP=1 <<EOF
|
||||||
|
WITH cutoff AS (
|
||||||
|
SELECT (EXTRACT(EPOCH FROM (NOW() - INTERVAL '${RETENTION_DAYS} days')) * 1000000)::bigint AS ts
|
||||||
|
),
|
||||||
|
old_packets AS (
|
||||||
|
SELECT id
|
||||||
|
FROM packet, cutoff
|
||||||
|
WHERE import_time_us IS NOT NULL
|
||||||
|
AND import_time_us < cutoff.ts
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT ${BATCH_SIZE}
|
||||||
|
),
|
||||||
|
ps_del AS (
|
||||||
|
DELETE FROM packet_seen
|
||||||
|
WHERE packet_id IN (SELECT id FROM old_packets)
|
||||||
|
RETURNING 1
|
||||||
|
),
|
||||||
|
tr_del AS (
|
||||||
|
DELETE FROM traceroute
|
||||||
|
WHERE packet_id IN (SELECT id FROM old_packets)
|
||||||
|
RETURNING 1
|
||||||
|
),
|
||||||
|
p_del AS (
|
||||||
|
DELETE FROM packet
|
||||||
|
WHERE id IN (SELECT id FROM old_packets)
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) FROM p_del;
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "$DELETED" -eq 0 ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[$(date)] Packet cleanup complete"
|
||||||
|
|
||||||
|
echo "[$(date)] Cleaning old nodes..."
|
||||||
|
|
||||||
|
$PSQL "$DB" -v ON_ERROR_STOP=1 <<EOF
|
||||||
|
DELETE FROM node
|
||||||
|
WHERE last_seen_us IS NOT NULL
|
||||||
|
AND last_seen_us < (
|
||||||
|
EXTRACT(EPOCH FROM (NOW() - INTERVAL '${RETENTION_DAYS} days')) * 1000000
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[$(date)] Node cleanup complete"
|
||||||
|
|
||||||
|
$PSQL "$DB" -c "VACUUM (ANALYZE) packet_seen;"
|
||||||
|
$PSQL "$DB" -c "VACUUM (ANALYZE) traceroute;"
|
||||||
|
$PSQL "$DB" -c "VACUUM (ANALYZE) packet;"
|
||||||
|
$PSQL "$DB" -c "VACUUM (ANALYZE) node;"
|
||||||
|
|
||||||
|
echo "[$(date)] Cleanup finished"
|
||||||
```
|
```
|
||||||
- Schedule running the script on a regular basis.
|
- Schedule running the script on a regular basis.
|
||||||
- In this example it runs every night at 2:00am.
|
- In this example it runs every night at 2:00am.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""Add is_mqtt_gateway to node
|
||||||
|
|
||||||
|
Revision ID: 23dad03d2e42
|
||||||
|
Revises: a0c9c13e118f
|
||||||
|
Create Date: 2026-02-13 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "23dad03d2e42"
|
||||||
|
down_revision: str | None = "a0c9c13e118f"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("node", sa.Column("is_mqtt_gateway", sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("node", "is_mqtt_gateway")
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""Drop import_time columns.
|
||||||
|
|
||||||
|
Revision ID: 9f3b1a8d2c4f
|
||||||
|
Revises: 2b5a61bb2b75
|
||||||
|
Create Date: 2026-01-09 09:55:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "9f3b1a8d2c4f"
|
||||||
|
down_revision: str | None = "2b5a61bb2b75"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = sa.inspect(conn)
|
||||||
|
|
||||||
|
packet_indexes = {idx["name"] for idx in inspector.get_indexes("packet")}
|
||||||
|
packet_columns = {col["name"] for col in inspector.get_columns("packet")}
|
||||||
|
|
||||||
|
with op.batch_alter_table("packet", schema=None) as batch_op:
|
||||||
|
if "idx_packet_import_time" in packet_indexes:
|
||||||
|
batch_op.drop_index("idx_packet_import_time")
|
||||||
|
if "idx_packet_from_node_time" in packet_indexes:
|
||||||
|
batch_op.drop_index("idx_packet_from_node_time")
|
||||||
|
if "import_time" in packet_columns:
|
||||||
|
batch_op.drop_column("import_time")
|
||||||
|
|
||||||
|
packet_seen_columns = {col["name"] for col in inspector.get_columns("packet_seen")}
|
||||||
|
with op.batch_alter_table("packet_seen", schema=None) as batch_op:
|
||||||
|
if "import_time" in packet_seen_columns:
|
||||||
|
batch_op.drop_column("import_time")
|
||||||
|
|
||||||
|
traceroute_indexes = {idx["name"] for idx in inspector.get_indexes("traceroute")}
|
||||||
|
traceroute_columns = {col["name"] for col in inspector.get_columns("traceroute")}
|
||||||
|
with op.batch_alter_table("traceroute", schema=None) as batch_op:
|
||||||
|
if "idx_traceroute_import_time" in traceroute_indexes:
|
||||||
|
batch_op.drop_index("idx_traceroute_import_time")
|
||||||
|
if "import_time" in traceroute_columns:
|
||||||
|
batch_op.drop_column("import_time")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("traceroute", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("import_time", sa.DateTime(), nullable=True))
|
||||||
|
batch_op.create_index("idx_traceroute_import_time", ["import_time"], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table("packet_seen", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("import_time", sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table("packet", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("import_time", sa.DateTime(), nullable=True))
|
||||||
|
batch_op.create_index("idx_packet_import_time", [sa.text("import_time DESC")], unique=False)
|
||||||
|
batch_op.create_index(
|
||||||
|
"idx_packet_from_node_time",
|
||||||
|
["from_node_id", sa.text("import_time DESC")],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Add node_public_key table
|
||||||
|
|
||||||
|
Revision ID: a0c9c13e118f
|
||||||
|
Revises: d4d7b0c2e1a4
|
||||||
|
Create Date: 2026-02-06 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "a0c9c13e118f"
|
||||||
|
down_revision: str | None = "d4d7b0c2e1a4"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"node_public_key",
|
||||||
|
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||||
|
sa.Column("node_id", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column("public_key", sa.String(), nullable=False),
|
||||||
|
sa.Column("first_seen_us", sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column("last_seen_us", sa.BigInteger(), nullable=True),
|
||||||
|
)
|
||||||
|
op.create_index("idx_node_public_key_node_id", "node_public_key", ["node_id"], unique=False)
|
||||||
|
op.create_index(
|
||||||
|
"idx_node_public_key_public_key",
|
||||||
|
"node_public_key",
|
||||||
|
["public_key"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("idx_node_public_key_public_key", table_name="node_public_key")
|
||||||
|
op.drop_index("idx_node_public_key_node_id", table_name="node_public_key")
|
||||||
|
op.drop_table("node_public_key")
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""Add last_update_us to node and migrate data.
|
||||||
|
|
||||||
|
Revision ID: b7c3c2e3a1f0
|
||||||
|
Revises: 9f3b1a8d2c4f
|
||||||
|
Create Date: 2026-01-12 10:12:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "b7c3c2e3a1f0"
|
||||||
|
down_revision: str | None = "9f3b1a8d2c4f"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_datetime(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
dt = value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
text = value.replace("Z", "+00:00")
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(text)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
return dt.replace(tzinfo=UTC)
|
||||||
|
return dt.astimezone(UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
op.add_column("node", sa.Column("last_update_us", sa.BigInteger(), nullable=True))
|
||||||
|
op.create_index("idx_node_last_update_us", "node", ["last_update_us"], unique=False)
|
||||||
|
|
||||||
|
node = sa.table(
|
||||||
|
"node",
|
||||||
|
sa.column("id", sa.String()),
|
||||||
|
sa.column("last_update", sa.DateTime()),
|
||||||
|
sa.column("last_update_us", sa.BigInteger()),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = conn.execute(sa.select(node.c.id, node.c.last_update)).all()
|
||||||
|
for node_id, last_update in rows:
|
||||||
|
dt = _parse_datetime(last_update)
|
||||||
|
if dt is None:
|
||||||
|
continue
|
||||||
|
last_update_us = int(dt.timestamp() * 1_000_000)
|
||||||
|
conn.execute(
|
||||||
|
sa.update(node).where(node.c.id == node_id).values(last_update_us=last_update_us)
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn.dialect.name == "sqlite":
|
||||||
|
with op.batch_alter_table("node", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("last_update")
|
||||||
|
else:
|
||||||
|
op.drop_column("node", "last_update")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
op.add_column("node", sa.Column("last_update", sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
node = sa.table(
|
||||||
|
"node",
|
||||||
|
sa.column("id", sa.String()),
|
||||||
|
sa.column("last_update", sa.DateTime()),
|
||||||
|
sa.column("last_update_us", sa.BigInteger()),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = conn.execute(sa.select(node.c.id, node.c.last_update_us)).all()
|
||||||
|
for node_id, last_update_us in rows:
|
||||||
|
if last_update_us is None:
|
||||||
|
continue
|
||||||
|
dt = datetime.fromtimestamp(last_update_us / 1_000_000, tz=UTC).replace(tzinfo=None)
|
||||||
|
conn.execute(sa.update(node).where(node.c.id == node_id).values(last_update=dt))
|
||||||
|
|
||||||
|
if conn.dialect.name == "sqlite":
|
||||||
|
with op.batch_alter_table("node", schema=None) as batch_op:
|
||||||
|
batch_op.drop_index("idx_node_last_update_us")
|
||||||
|
batch_op.drop_column("last_update_us")
|
||||||
|
else:
|
||||||
|
op.drop_index("idx_node_last_update_us", table_name="node")
|
||||||
|
op.drop_column("node", "last_update_us")
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Drop last_update_us from node.
|
||||||
|
|
||||||
|
Revision ID: d4d7b0c2e1a4
|
||||||
|
Revises: b7c3c2e3a1f0
|
||||||
|
Create Date: 2026-01-12 10:20:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "d4d7b0c2e1a4"
|
||||||
|
down_revision: str | None = "b7c3c2e3a1f0"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
if conn.dialect.name == "sqlite":
|
||||||
|
with op.batch_alter_table("node", schema=None) as batch_op:
|
||||||
|
batch_op.drop_index("idx_node_last_update_us")
|
||||||
|
batch_op.drop_column("last_update_us")
|
||||||
|
else:
|
||||||
|
op.drop_index("idx_node_last_update_us", table_name="node")
|
||||||
|
op.drop_column("node", "last_update_us")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column("node", sa.Column("last_update_us", sa.BigInteger(), nullable=True))
|
||||||
|
op.create_index("idx_node_last_update_us", "node", ["last_update_us"], unique=False)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# -------------------------
|
||||||
|
# Server Configuration
|
||||||
|
# -------------------------
|
||||||
|
[server]
|
||||||
|
# The address to bind the server to. Use * to listen on all interfaces.
|
||||||
|
bind = 0.0.0.0
|
||||||
|
|
||||||
|
# Port to run the web server on.
|
||||||
|
port = 8081
|
||||||
|
|
||||||
|
# Path to TLS certificate (leave blank to disable HTTPS).
|
||||||
|
tls_cert =
|
||||||
|
|
||||||
|
# Path for the ACME challenge if using Let's Encrypt.
|
||||||
|
acme_challenge =
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Site Appearance & Behavior
|
||||||
|
# -------------------------
|
||||||
|
[site]
|
||||||
|
domain =
|
||||||
|
language = en
|
||||||
|
title = Bay Area Mesh
|
||||||
|
message = Real time data from around the bay area and beyond.
|
||||||
|
starting = /chat
|
||||||
|
|
||||||
|
nodes = True
|
||||||
|
conversations = True
|
||||||
|
everything = True
|
||||||
|
graphs = True
|
||||||
|
stats = True
|
||||||
|
net = True
|
||||||
|
map = True
|
||||||
|
top = True
|
||||||
|
|
||||||
|
map_top_left_lat = 39
|
||||||
|
map_top_left_lon = -123
|
||||||
|
map_bottom_right_lat = 36
|
||||||
|
map_bottom_right_lon = -121
|
||||||
|
|
||||||
|
map_interval = 3
|
||||||
|
firehose_interal = 3
|
||||||
|
|
||||||
|
weekly_net_message = Weekly Mesh check-in. We will keep it open on every Wednesday from 5:00pm for checkins. The message format should be (LONG NAME) - (CITY YOU ARE IN) #BayMeshNet.
|
||||||
|
net_tag = #BayMeshNet
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# MQTT Broker Configuration
|
||||||
|
# -------------------------
|
||||||
|
[mqtt]
|
||||||
|
server = mqtt.meshtastic.org
|
||||||
|
topics = ["msh/US/bayarea/#", "msh/US/CA/mrymesh/#", "msh/US/CA/sacvalley"]
|
||||||
|
port = 1883
|
||||||
|
username = meshdev
|
||||||
|
password = large4cats
|
||||||
|
skip_node_ids =
|
||||||
|
secondary_keys =
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Database Configuration
|
||||||
|
# -------------------------
|
||||||
|
[database]
|
||||||
|
connection_string = sqlite+aiosqlite:////var/lib/meshview/packets.db
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Database Cleanup Configuration
|
||||||
|
# -------------------------
|
||||||
|
[cleanup]
|
||||||
|
enabled = False
|
||||||
|
days_to_keep = 14
|
||||||
|
hour = 2
|
||||||
|
minute = 00
|
||||||
|
vacuum = False
|
||||||
|
|
||||||
|
backup_enabled = False
|
||||||
|
backup_dir = ./backups
|
||||||
|
backup_hour = 2
|
||||||
|
backup_minute = 00
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Logging Configuration
|
||||||
|
# -------------------------
|
||||||
|
[logging]
|
||||||
|
access_log = False
|
||||||
|
db_cleanup_logfile = /var/log/meshview/dbcleanup.log
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# MeshView Docker Container
|
|
||||||
|
|
||||||
> **Note:** This directory contains legacy Docker build files.
|
|
||||||
>
|
|
||||||
> **For current Docker usage instructions, please see [README-Docker.md](../README-Docker.md) in the project root.**
|
|
||||||
|
|
||||||
## Current Approach
|
|
||||||
|
|
||||||
Pre-built container images are automatically built and published to GitHub Container Registry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/pablorevilla-meshtastic/meshview:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
See **[README-Docker.md](../README-Docker.md)** for:
|
|
||||||
- Quick start instructions
|
|
||||||
- Volume mount configuration
|
|
||||||
- Docker Compose examples
|
|
||||||
- Backup configuration
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
## Legacy Build (Not Recommended)
|
|
||||||
|
|
||||||
If you need to build your own image for development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From project root
|
|
||||||
docker build -f Containerfile -t meshview:local .
|
|
||||||
```
|
|
||||||
|
|
||||||
The current Containerfile uses:
|
|
||||||
- **Base Image**: `python:3.13-slim` (Debian-based)
|
|
||||||
- **Build tool**: `uv` for fast dependency installation
|
|
||||||
- **User**: Non-root user `app` (UID 10001)
|
|
||||||
- **Exposed Port**: `8081`
|
|
||||||
- **Volumes**: `/etc/meshview`, `/var/lib/meshview`, `/var/log/meshview`
|
|
||||||
+233
-185
@@ -1,82 +1,38 @@
|
|||||||
|
|
||||||
# API Documentation
|
# API Documentation
|
||||||
|
|
||||||
## 1. Chat API
|
Base URL: `http(s)://<host>`
|
||||||
|
|
||||||
### GET `/api/chat`
|
All endpoints return JSON. Timestamps are either ISO 8601 strings or `*_us` values in
|
||||||
Returns the most recent chat messages.
|
microseconds since epoch.
|
||||||
|
|
||||||
**Query Parameters**
|
## 1. Nodes API
|
||||||
- `limit` (optional, int): Maximum number of messages to return. Default: `100`.
|
|
||||||
|
|
||||||
**Response Example**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"packets": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"import_time": "2025-07-22T12:45:00",
|
|
||||||
"from_node_id": 987654,
|
|
||||||
"from_node": "Alice",
|
|
||||||
"channel": "main",
|
|
||||||
"payload": "Hello, world!"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### GET `/api/chat/updates`
|
|
||||||
Returns chat messages imported after a given timestamp.
|
|
||||||
|
|
||||||
**Query Parameters**
|
|
||||||
- `last_time` (optional, ISO timestamp): Only messages imported after this time are returned.
|
|
||||||
|
|
||||||
**Response Example**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"packets": [
|
|
||||||
{
|
|
||||||
"id": 124,
|
|
||||||
"import_time": "2025-07-22T12:50:00",
|
|
||||||
"from_node_id": 987654,
|
|
||||||
"from_node": "Alice",
|
|
||||||
"channel": "main",
|
|
||||||
"payload": "New message!"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"latest_import_time": "2025-07-22T12:50:00"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Nodes API
|
|
||||||
|
|
||||||
### GET `/api/nodes`
|
### GET `/api/nodes`
|
||||||
Returns a list of all nodes, with optional filtering by last seen.
|
Returns a list of nodes, with optional filtering.
|
||||||
|
|
||||||
**Query Parameters**
|
Query Parameters
|
||||||
- `hours` (optional, int): Return nodes seen in the last N hours.
|
- `node_id` (optional, int): Exact node ID.
|
||||||
- `days` (optional, int): Return nodes seen in the last N days.
|
- `role` (optional, string): Node role.
|
||||||
- `last_seen_after` (optional, ISO timestamp): Return nodes seen after this time.
|
- `channel` (optional, string): Channel name.
|
||||||
|
- `hw_model` (optional, string): Hardware model.
|
||||||
|
- `days_active` (optional, int): Nodes seen within the last N days.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
"id": 42,
|
||||||
"node_id": 1234,
|
"node_id": 1234,
|
||||||
"long_name": "Alice",
|
"long_name": "Alice",
|
||||||
"short_name": "A",
|
"short_name": "A",
|
||||||
"channel": "main",
|
"hw_model": "T-Beam",
|
||||||
"last_seen": "2025-07-22T12:40:00",
|
|
||||||
"hardware": "T-Beam",
|
|
||||||
"firmware": "1.2.3",
|
"firmware": "1.2.3",
|
||||||
"role": "client",
|
"role": "client",
|
||||||
"last_lat": 37.7749,
|
"last_lat": 377749000,
|
||||||
"last_long": -122.4194
|
"last_long": -1224194000,
|
||||||
|
"channel": "main",
|
||||||
|
"last_seen_us": 1736370123456789
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -84,45 +40,58 @@ Returns a list of all nodes, with optional filtering by last seen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Packets API
|
## 2. Packets API
|
||||||
|
|
||||||
### GET `/api/packets`
|
### GET `/api/packets`
|
||||||
Returns a list of packets with optional filters.
|
Returns packets with optional filters.
|
||||||
|
|
||||||
**Query Parameters**
|
Query Parameters
|
||||||
- `limit` (optional, int): Maximum number of packets to return. Default: `200`.
|
- `packet_id` (optional, int): Return exactly one packet (overrides other filters).
|
||||||
- `since` (optional, ISO timestamp): Only packets imported after this timestamp are returned.
|
- `limit` (optional, int): Max packets to return, clamped 1-1000. Default: `50`.
|
||||||
|
- `since` (optional, int): Only packets imported after this microsecond timestamp.
|
||||||
|
- `portnum` (optional, int): Filter by port number.
|
||||||
|
- `contains` (optional, string): Payload substring filter.
|
||||||
|
- `from_node_id` (optional, int): Filter by sender node ID.
|
||||||
|
- `to_node_id` (optional, int): Filter by recipient node ID.
|
||||||
|
- `node_id` (optional, int): Legacy filter matching either from or to node ID.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"packets": [
|
"packets": [
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
|
"import_time_us": 1736370123456789,
|
||||||
|
"channel": "main",
|
||||||
"from_node_id": 5678,
|
"from_node_id": 5678,
|
||||||
"to_node_id": 91011,
|
"to_node_id": 91011,
|
||||||
"portnum": 1,
|
"portnum": 1,
|
||||||
"import_time": "2025-07-22T12:45:00",
|
"long_name": "Alice",
|
||||||
"payload": "Hello, Bob!"
|
"payload": "Hello, Bob!",
|
||||||
|
"to_long_name": "Bob",
|
||||||
|
"reply_id": 122
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"latest_import_time": 1736370123456789
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Notes
|
||||||
|
- For `portnum=1` (text messages), packets are filtered to remove sequence-only payloads.
|
||||||
|
- `latest_import_time` is returned when available for incremental polling (microseconds).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Channels API
|
## 3. Channels API
|
||||||
|
|
||||||
### GET `/api/channels`
|
### GET `/api/channels`
|
||||||
Returns a list of channels seen in a given time period.
|
Returns channels seen in a time period.
|
||||||
|
|
||||||
**Query Parameters**
|
Query Parameters
|
||||||
- `period_type` (optional, string): Time granularity (`hour` or `day`). Default: `hour`.
|
- `period_type` (optional, string): `hour` or `day`. Default: `hour`.
|
||||||
- `length` (optional, int): Number of periods to look back. Default: `24`.
|
- `length` (optional, int): Number of periods to look back. Default: `24`.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"channels": ["LongFast", "MediumFast", "ShortFast"]
|
"channels": ["LongFast", "MediumFast", "ShortFast"]
|
||||||
@@ -131,29 +100,21 @@ Returns a list of channels seen in a given time period.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Statistics API
|
## 4. Stats API
|
||||||
|
|
||||||
### GET `/api/stats`
|
### GET `/api/stats`
|
||||||
|
Returns packet statistics aggregated by time periods, with optional filtering.
|
||||||
|
|
||||||
Retrieve packet statistics aggregated by time periods, with optional filtering.
|
Query Parameters
|
||||||
|
- `period_type` (optional, string): `hour` or `day`. Default: `hour`.
|
||||||
---
|
- `length` (optional, int): Number of periods to include. Default: `24`.
|
||||||
|
- `channel` (optional, string): Filter by channel (case-insensitive).
|
||||||
## Query Parameters
|
- `portnum` (optional, int): Filter by port number.
|
||||||
|
- `to_node` (optional, int): Filter by destination node ID.
|
||||||
| Parameter | Type | Required | Default | Description |
|
- `from_node` (optional, int): Filter by source node ID.
|
||||||
|--------------|---------|----------|----------|-------------------------------------------------------------------------------------------------|
|
- `node` (optional, int): If provided, return combined `sent` and `seen` totals for that node.
|
||||||
| `period_type` | string | No | `hour` | Time granularity of the stats. Allowed values: `hour`, `day`. |
|
|
||||||
| `length` | integer | No | 24 | Number of periods to include (hours or days). |
|
|
||||||
| `channel` | string | No | — | Filter results by channel name (case-insensitive). |
|
|
||||||
| `portnum` | integer | No | — | Filter results by port number. |
|
|
||||||
| `to_node` | integer | No | — | Filter results to packets sent **to** this node ID. |
|
|
||||||
| `from_node` | integer | No | — | Filter results to packets sent **from** this node ID. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
|
Response Example (series)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"period_type": "hour",
|
"period_type": "hour",
|
||||||
@@ -163,65 +124,117 @@ Retrieve packet statistics aggregated by time periods, with optional filtering.
|
|||||||
"to_node": 12345678,
|
"to_node": 12345678,
|
||||||
"from_node": 87654321,
|
"from_node": 87654321,
|
||||||
"data": [
|
"data": [
|
||||||
|
{ "period": "2025-08-08 14:00", "count": 10 },
|
||||||
|
{ "period": "2025-08-08 15:00", "count": 7 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Example (`node` totals)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"node_id": 12345678,
|
||||||
|
"period_type": "hour",
|
||||||
|
"length": 24,
|
||||||
|
"sent": 42,
|
||||||
|
"seen": 58
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/api/stats/count`
|
||||||
|
Returns total packet counts, optionally filtered.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
- `packet_id` (optional, int): Filter packet_seen by packet ID.
|
||||||
|
- `period_type` (optional, string): `hour` or `day`.
|
||||||
|
- `length` (optional, int): Number of periods to include.
|
||||||
|
- `channel` (optional, string): Filter by channel.
|
||||||
|
- `from_node` (optional, int): Filter by source node ID.
|
||||||
|
- `to_node` (optional, int): Filter by destination node ID.
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_packets": 12345,
|
||||||
|
"total_seen": 67890
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/api/stats/top`
|
||||||
|
Returns nodes sorted by packets seen, with pagination.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
- `period_type` (optional, string): `hour` or `day`. Default: `day`.
|
||||||
|
- `length` (optional, int): Number of periods to include. Default: `1`.
|
||||||
|
- `channel` (optional, string): Filter by channel.
|
||||||
|
- `limit` (optional, int): Max nodes to return. Default: `20`, max `100`.
|
||||||
|
- `offset` (optional, int): Pagination offset. Default: `0`.
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 250,
|
||||||
|
"limit": 20,
|
||||||
|
"offset": 0,
|
||||||
|
"nodes": [
|
||||||
{
|
{
|
||||||
"period": "2025-08-08 14:00",
|
"node_id": 1234,
|
||||||
"count": 10
|
"long_name": "Alice",
|
||||||
},
|
"short_name": "A",
|
||||||
{
|
"channel": "main",
|
||||||
"period": "2025-08-08 15:00",
|
"sent": 100,
|
||||||
"count": 7
|
"seen": 240,
|
||||||
|
"avg": 2.4
|
||||||
}
|
}
|
||||||
// more entries...
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Edges API
|
## 5. Edges API
|
||||||
|
|
||||||
### GET `/api/edges`
|
### GET `/api/edges`
|
||||||
Returns network edges (connections between nodes) based on traceroutes and neighbor info.
|
Returns network edges (connections between nodes) based on traceroutes and neighbor info.
|
||||||
|
Traceroute edges are collected over the last 12 hours. Neighbor edges are based on
|
||||||
|
port 71 packets.
|
||||||
|
|
||||||
**Query Parameters**
|
Query Parameters
|
||||||
- `type` (optional, string): Filter by edge type (`traceroute` or `neighbor`). If omitted, returns both types.
|
- `type` (optional, string): `traceroute` or `neighbor`. If omitted, returns both.
|
||||||
|
- `node_id` (optional, int): Filter edges to only those touching a node.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"edges": [
|
"edges": [
|
||||||
{
|
{ "from": 12345678, "to": 87654321, "type": "traceroute" },
|
||||||
"from": 12345678,
|
{ "from": 11111111, "to": 22222222, "type": "neighbor" }
|
||||||
"to": 87654321,
|
|
||||||
"type": "traceroute"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": 11111111,
|
|
||||||
"to": 22222222,
|
|
||||||
"type": "neighbor"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Configuration API
|
## 6. Config API
|
||||||
|
|
||||||
### GET `/api/config`
|
### GET `/api/config`
|
||||||
Returns the current site configuration (safe subset exposed to clients).
|
Returns a safe subset of server configuration.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"site": {
|
"site": {
|
||||||
"domain": "meshview.example.com",
|
"domain": "example.com",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"title": "Bay Area Mesh",
|
"title": "Meshview",
|
||||||
"message": "Real time data from around the bay area",
|
"message": "",
|
||||||
"starting": "/chat",
|
"starting": "/chat",
|
||||||
"nodes": "true",
|
"nodes": "true",
|
||||||
"conversations": "true",
|
"chat": "true",
|
||||||
"everything": "true",
|
"everything": "true",
|
||||||
"graphs": "true",
|
"graphs": "true",
|
||||||
"stats": "true",
|
"stats": "true",
|
||||||
@@ -236,11 +249,11 @@ Returns the current site configuration (safe subset exposed to clients).
|
|||||||
"firehose_interval": 3,
|
"firehose_interval": 3,
|
||||||
"weekly_net_message": "Weekly Mesh check-in message.",
|
"weekly_net_message": "Weekly Mesh check-in message.",
|
||||||
"net_tag": "#BayMeshNet",
|
"net_tag": "#BayMeshNet",
|
||||||
"version": "2.0.8 ~ 10-22-25"
|
"version": "3.0.0"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
"server": "mqtt.bayme.sh",
|
"server": "mqtt.example.com",
|
||||||
"topics": ["msh/US/bayarea/#"]
|
"topics": ["msh/region/#"]
|
||||||
},
|
},
|
||||||
"cleanup": {
|
"cleanup": {
|
||||||
"enabled": "false",
|
"enabled": "false",
|
||||||
@@ -254,91 +267,126 @@ Returns the current site configuration (safe subset exposed to clients).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Language/Translations API
|
## 7. Language API
|
||||||
|
|
||||||
### GET `/api/lang`
|
### GET `/api/lang`
|
||||||
Returns translation strings for the UI.
|
Returns translation strings.
|
||||||
|
|
||||||
**Query Parameters**
|
Query Parameters
|
||||||
- `lang` (optional, string): Language code (e.g., `en`, `es`). Defaults to site language setting.
|
- `lang` (optional, string): Language code (e.g., `en`, `es`). Default from config or `en`.
|
||||||
- `section` (optional, string): Specific section to retrieve translations for.
|
- `section` (optional, string): Return only one section (e.g., `nodelist`, `firehose`).
|
||||||
|
|
||||||
**Response Example (full)**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"chat": {
|
"title": "Meshview",
|
||||||
"title": "Chat",
|
"search_placeholder": "Search..."
|
||||||
"send": "Send"
|
|
||||||
},
|
|
||||||
"map": {
|
|
||||||
"title": "Map",
|
|
||||||
"zoom_in": "Zoom In"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Example (section-specific)**
|
|
||||||
Request: `/api/lang?section=chat`
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "Chat",
|
|
||||||
"send": "Send"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Health Check API
|
## 8. Packets Seen API
|
||||||
|
|
||||||
|
### GET `/api/packets_seen/{packet_id}`
|
||||||
|
Returns packet_seen entries for a packet.
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
- `packet_id` (required, int): Packet ID.
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"seen": [
|
||||||
|
{
|
||||||
|
"packet_id": 123,
|
||||||
|
"node_id": 456,
|
||||||
|
"rx_time": "2025-07-22T12:45:00",
|
||||||
|
"hop_limit": 7,
|
||||||
|
"hop_start": 0,
|
||||||
|
"channel": "main",
|
||||||
|
"rx_snr": 5.0,
|
||||||
|
"rx_rssi": -90,
|
||||||
|
"topic": "msh/region/#",
|
||||||
|
"import_time_us": 1736370123456789
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Traceroute API
|
||||||
|
|
||||||
|
### GET `/api/traceroute/{packet_id}`
|
||||||
|
Returns traceroute details and derived paths for a packet.
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
- `packet_id` (required, int): Packet ID.
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packet": {
|
||||||
|
"id": 123,
|
||||||
|
"from": 111,
|
||||||
|
"to": 222,
|
||||||
|
"channel": "main"
|
||||||
|
},
|
||||||
|
"traceroute_packets": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"gateway_node_id": 333,
|
||||||
|
"done": true,
|
||||||
|
"forward_hops": [111, 444, 222],
|
||||||
|
"reverse_hops": [222, 444, 111]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unique_forward_paths": [
|
||||||
|
{ "path": [111, 444, 222], "count": 2 }
|
||||||
|
],
|
||||||
|
"unique_reverse_paths": [
|
||||||
|
[222, 444, 111]
|
||||||
|
],
|
||||||
|
"winning_paths": [
|
||||||
|
[111, 444, 222]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Health API
|
||||||
|
|
||||||
### GET `/health`
|
### GET `/health`
|
||||||
Health check endpoint for monitoring, load balancers, and orchestration systems.
|
Returns service health and database status.
|
||||||
|
|
||||||
**Response Example (Healthy)**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"timestamp": "2025-11-03T14:30:00.123456Z",
|
"timestamp": "2025-07-22T12:45:00+00:00",
|
||||||
"version": "3.0.0",
|
"version": "3.0.3",
|
||||||
"git_revision": "6416978",
|
"git_revision": "abc1234",
|
||||||
"database": "connected",
|
"database": "connected",
|
||||||
"database_size": "853.03 MB",
|
"database_size": "12.34 MB",
|
||||||
"database_size_bytes": 894468096
|
"database_size_bytes": 12939444
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Example (Unhealthy)**
|
|
||||||
Status Code: `503 Service Unavailable`
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "unhealthy",
|
|
||||||
"timestamp": "2025-11-03T14:30:00.123456Z",
|
|
||||||
"version": "2.0.8",
|
|
||||||
"git_revision": "6416978",
|
|
||||||
"database": "disconnected"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Version API
|
## 11. Version API
|
||||||
|
|
||||||
### GET `/version`
|
### GET `/version`
|
||||||
Returns detailed version information including semver, release date, and git revision.
|
Returns version metadata.
|
||||||
|
|
||||||
**Response Example**
|
Response Example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.0.8",
|
"version": "3.0.3",
|
||||||
"release_date": "2025-10-22",
|
"release_date": "2026-1-15",
|
||||||
"git_revision": "6416978a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q",
|
"git_revision": "abc1234",
|
||||||
"git_revision_short": "6416978"
|
"git_revision_short": "abc1234"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- All timestamps (`import_time`, `last_seen`) are returned in ISO 8601 format.
|
|
||||||
- `portnum` is an integer representing the packet type.
|
|
||||||
- `payload` is always a UTF-8 decoded string.
|
|
||||||
- Node IDs are integers (e.g., `12345678`).
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Coverage
|
||||||
|
|
||||||
|
## Predicted coverage
|
||||||
|
|
||||||
|
Meshview can display a predicted coverage boundary for a node. This is a **model**
|
||||||
|
estimate, not a guarantee of real-world performance.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
The coverage boundary is computed using the Longley-Rice / ITM **area mode**
|
||||||
|
propagation model. Area mode estimates average path loss over generic terrain
|
||||||
|
and does not use a terrain profile. This means it captures general distance
|
||||||
|
effects, but **does not** account for terrain shadows, buildings, or foliage.
|
||||||
|
|
||||||
|
### What you are seeing
|
||||||
|
|
||||||
|
The UI draws a **perimeter** (not a heatmap) that represents the furthest
|
||||||
|
distance where predicted signal strength is above a threshold (default
|
||||||
|
`-120 dBm`). The model is run radially from the node in multiple directions,
|
||||||
|
and the last point above the threshold forms the outline.
|
||||||
|
|
||||||
|
### Key parameters
|
||||||
|
|
||||||
|
- **Frequency**: default `907 MHz`
|
||||||
|
- **Transmit power**: default `20 dBm`
|
||||||
|
- **Antenna heights**: default `5 m` (TX) and `1.5 m` (RX)
|
||||||
|
- **Reliability**: default `0.5` (median)
|
||||||
|
- **Terrain irregularity**: default `90 m` (average terrain)
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
- No terrain or building data is used (area mode only).
|
||||||
|
- Results are sensitive to power, height, and threshold.
|
||||||
|
- Environmental factors can cause large real-world deviations.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
e1a6b3a868d735da72cd6c94c574d655129d390a
|
||||||
File diff suppressed because one or more lines are too long
@@ -770,6 +770,7 @@ class SharedContact(google.protobuf.message.Message):
|
|||||||
NODE_NUM_FIELD_NUMBER: builtins.int
|
NODE_NUM_FIELD_NUMBER: builtins.int
|
||||||
USER_FIELD_NUMBER: builtins.int
|
USER_FIELD_NUMBER: builtins.int
|
||||||
SHOULD_IGNORE_FIELD_NUMBER: builtins.int
|
SHOULD_IGNORE_FIELD_NUMBER: builtins.int
|
||||||
|
MANUALLY_VERIFIED_FIELD_NUMBER: builtins.int
|
||||||
node_num: builtins.int
|
node_num: builtins.int
|
||||||
"""
|
"""
|
||||||
The node number of the contact
|
The node number of the contact
|
||||||
@@ -778,6 +779,10 @@ class SharedContact(google.protobuf.message.Message):
|
|||||||
"""
|
"""
|
||||||
Add this contact to the blocked / ignored list
|
Add this contact to the blocked / ignored list
|
||||||
"""
|
"""
|
||||||
|
manually_verified: builtins.bool
|
||||||
|
"""
|
||||||
|
Set the IS_KEY_MANUALLY_VERIFIED bit
|
||||||
|
"""
|
||||||
@property
|
@property
|
||||||
def user(self) -> meshtastic.protobuf.mesh_pb2.User:
|
def user(self) -> meshtastic.protobuf.mesh_pb2.User:
|
||||||
"""
|
"""
|
||||||
@@ -790,9 +795,10 @@ class SharedContact(google.protobuf.message.Message):
|
|||||||
node_num: builtins.int = ...,
|
node_num: builtins.int = ...,
|
||||||
user: meshtastic.protobuf.mesh_pb2.User | None = ...,
|
user: meshtastic.protobuf.mesh_pb2.User | None = ...,
|
||||||
should_ignore: builtins.bool = ...,
|
should_ignore: builtins.bool = ...,
|
||||||
|
manually_verified: builtins.bool = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def HasField(self, field_name: typing.Literal["user", b"user"]) -> builtins.bool: ...
|
def HasField(self, field_name: typing.Literal["user", b"user"]) -> builtins.bool: ...
|
||||||
def ClearField(self, field_name: typing.Literal["node_num", b"node_num", "should_ignore", b"should_ignore", "user", b"user"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["manually_verified", b"manually_verified", "node_num", b"node_num", "should_ignore", b"should_ignore", "user", b"user"]) -> None: ...
|
||||||
|
|
||||||
global___SharedContact = SharedContact
|
global___SharedContact = SharedContact
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_chann
|
|||||||
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
|
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\"\x81\x01\n\nChannelSet\x12\x36\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBb\n\x13\x63om.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\"\x81\x01\n\nChannelSet\x12\x36\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBc\n\x14org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_CHANNELSET']._serialized_start=128
|
_globals['_CHANNELSET']._serialized_start=128
|
||||||
_globals['_CHANNELSET']._serialized_end=257
|
_globals['_CHANNELSET']._serialized_end=257
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/atak.proto\x12\x13meshtastic.protobuf\"\xa5\x02\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12-\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x1c.meshtastic.protobuf.Contact\x12)\n\x05group\x18\x03 \x01(\x0b\x32\x1a.meshtastic.protobuf.Group\x12+\n\x06status\x18\x04 \x01(\x0b\x32\x1b.meshtastic.protobuf.Status\x12\'\n\x03pli\x18\x05 \x01(\x0b\x32\x18.meshtastic.protobuf.PLIH\x00\x12,\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x1c.meshtastic.protobuf.GeoChatH\x00\x12\x10\n\x06\x64\x65tail\x18\x07 \x01(\x0cH\x00\x42\x11\n\x0fpayload_variant\"\\\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bto_callsign\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x05\n\x03_toB\x0e\n\x0c_to_callsign\"_\n\x05Group\x12-\n\x04role\x18\x01 \x01(\x0e\x32\x1f.meshtastic.protobuf.MemberRole\x12\'\n\x04team\x18\x02 \x01(\x0e\x32\x19.meshtastic.protobuf.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/atak.proto\x12\x13meshtastic.protobuf\"\xa5\x02\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12-\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x1c.meshtastic.protobuf.Contact\x12)\n\x05group\x18\x03 \x01(\x0b\x32\x1a.meshtastic.protobuf.Group\x12+\n\x06status\x18\x04 \x01(\x0b\x32\x1b.meshtastic.protobuf.Status\x12\'\n\x03pli\x18\x05 \x01(\x0b\x32\x18.meshtastic.protobuf.PLIH\x00\x12,\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x1c.meshtastic.protobuf.GeoChatH\x00\x12\x10\n\x06\x64\x65tail\x18\x07 \x01(\x0cH\x00\x42\x11\n\x0fpayload_variant\"\\\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bto_callsign\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x05\n\x03_toB\x0e\n\x0c_to_callsign\"_\n\x05Group\x12-\n\x04role\x18\x01 \x01(\x0e\x32\x1f.meshtastic.protobuf.MemberRole\x12\'\n\x04team\x18\x02 \x01(\x0e\x32\x19.meshtastic.protobuf.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42`\n\x14org.meshtastic.protoB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.atak_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.atak_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_TEAM']._serialized_start=721
|
_globals['_TEAM']._serialized_start=721
|
||||||
_globals['_TEAM']._serialized_end=913
|
_globals['_TEAM']._serialized_end=913
|
||||||
_globals['_MEMBERROLE']._serialized_start=915
|
_globals['_MEMBERROLE']._serialized_start=915
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\"-\n\x19\x43\x61nnedMessageModuleConfig\x12\x10\n\x08messages\x18\x01 \x01(\tBn\n\x13\x63om.geeksville.meshB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\"-\n\x19\x43\x61nnedMessageModuleConfig\x12\x10\n\x08messages\x18\x01 \x01(\tBo\n\x14org.meshtastic.protoB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedmessages_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedmessages_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
|
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
|
||||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
|
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\"\xc1\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\"E\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x17\n\x0fis_client_muted\x18\x02 \x01(\x08\"\xb3\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\"\xc1\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\">\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x10\n\x08is_muted\x18\x02 \x01(\x08\"\xb3\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x63\n\x14org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.channel_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.channel_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_CHANNELSETTINGS.fields_by_name['channel_num']._options = None
|
_CHANNELSETTINGS.fields_by_name['channel_num']._options = None
|
||||||
_CHANNELSETTINGS.fields_by_name['channel_num']._serialized_options = b'\030\001'
|
_CHANNELSETTINGS.fields_by_name['channel_num']._serialized_options = b'\030\001'
|
||||||
_globals['_CHANNELSETTINGS']._serialized_start=59
|
_globals['_CHANNELSETTINGS']._serialized_start=59
|
||||||
_globals['_CHANNELSETTINGS']._serialized_end=252
|
_globals['_CHANNELSETTINGS']._serialized_end=252
|
||||||
_globals['_MODULESETTINGS']._serialized_start=254
|
_globals['_MODULESETTINGS']._serialized_start=254
|
||||||
_globals['_MODULESETTINGS']._serialized_end=323
|
_globals['_MODULESETTINGS']._serialized_end=316
|
||||||
_globals['_CHANNEL']._serialized_start=326
|
_globals['_CHANNEL']._serialized_start=319
|
||||||
_globals['_CHANNEL']._serialized_end=505
|
_globals['_CHANNEL']._serialized_end=498
|
||||||
_globals['_CHANNEL_ROLE']._serialized_start=457
|
_globals['_CHANNEL_ROLE']._serialized_start=450
|
||||||
_globals['_CHANNEL_ROLE']._serialized_end=505
|
_globals['_CHANNEL_ROLE']._serialized_end=498
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -127,23 +127,23 @@ class ModuleSettings(google.protobuf.message.Message):
|
|||||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||||
|
|
||||||
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
||||||
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
|
IS_MUTED_FIELD_NUMBER: builtins.int
|
||||||
position_precision: builtins.int
|
position_precision: builtins.int
|
||||||
"""
|
"""
|
||||||
Bits of precision for the location sent in position packets.
|
Bits of precision for the location sent in position packets.
|
||||||
"""
|
"""
|
||||||
is_client_muted: builtins.bool
|
is_muted: builtins.bool
|
||||||
"""
|
"""
|
||||||
Controls whether or not the phone / clients should mute the current channel
|
Controls whether or not the client / device should mute the current channel
|
||||||
Useful for noisy public channels you don't necessarily want to disable
|
Useful for noisy public channels you don't necessarily want to disable
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
position_precision: builtins.int = ...,
|
position_precision: builtins.int = ...,
|
||||||
is_client_muted: builtins.bool = ...,
|
is_muted: builtins.bool = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def ClearField(self, field_name: typing.Literal["is_client_muted", b"is_client_muted", "position_precision", b"position_precision"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["is_muted", b"is_muted", "position_precision", b"position_precision"]) -> None: ...
|
||||||
|
|
||||||
global___ModuleSettings = ModuleSettings
|
global___ModuleSettings = ModuleSettings
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ from meshtastic.protobuf import localonly_pb2 as meshtastic_dot_protobuf_dot_loc
|
|||||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"\xc4\x03\n\rDeviceProfile\x12\x16\n\tlong_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nshort_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x15\n\x08ringtone\x18\x07 \x01(\tH\x06\x88\x01\x01\x12\x1c\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tH\x07\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesBe\n\x13\x63om.geeksville.meshB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"\xc4\x03\n\rDeviceProfile\x12\x16\n\tlong_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nshort_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x15\n\x08ringtone\x18\x07 \x01(\tH\x06\x88\x01\x01\x12\x1c\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tH\x07\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesBf\n\x14org.meshtastic.protoB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.clientonly_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.clientonly_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_DEVICEPROFILE']._serialized_start=131
|
_globals['_DEVICEPROFILE']._serialized_start=131
|
||||||
_globals['_DEVICEPROFILE']._serialized_end=583
|
_globals['_DEVICEPROFILE']._serialized_end=583
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -64,6 +64,7 @@ class Config(google.protobuf.message.Message):
|
|||||||
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
||||||
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||||
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
||||||
|
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
|
||||||
"""
|
"""
|
||||||
TRACKER: Config.DeviceConfig._Role.ValueType # 5
|
TRACKER: Config.DeviceConfig._Role.ValueType # 5
|
||||||
"""
|
"""
|
||||||
@@ -155,6 +156,7 @@ class Config(google.protobuf.message.Message):
|
|||||||
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
||||||
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||||
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
||||||
|
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
|
||||||
"""
|
"""
|
||||||
TRACKER: Config.DeviceConfig.Role.ValueType # 5
|
TRACKER: Config.DeviceConfig.Role.ValueType # 5
|
||||||
"""
|
"""
|
||||||
@@ -938,80 +940,20 @@ class Config(google.protobuf.message.Message):
|
|||||||
|
|
||||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||||
|
|
||||||
class _GpsCoordinateFormat:
|
class _DeprecatedGpsCoordinateFormat:
|
||||||
ValueType = typing.NewType("ValueType", builtins.int)
|
ValueType = typing.NewType("ValueType", builtins.int)
|
||||||
V: typing_extensions.TypeAlias = ValueType
|
V: typing_extensions.TypeAlias = ValueType
|
||||||
|
|
||||||
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._GpsCoordinateFormat.ValueType], builtins.type):
|
class _DeprecatedGpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType], builtins.type):
|
||||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||||
DEC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 0
|
UNUSED: Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType # 0
|
||||||
|
|
||||||
|
class DeprecatedGpsCoordinateFormat(_DeprecatedGpsCoordinateFormat, metaclass=_DeprecatedGpsCoordinateFormatEnumTypeWrapper):
|
||||||
"""
|
"""
|
||||||
GPS coordinates are displayed in the normal decimal degrees format:
|
Deprecated in 2.7.4: Unused
|
||||||
DD.DDDDDD DDD.DDDDDD
|
|
||||||
"""
|
|
||||||
DMS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 1
|
|
||||||
"""
|
|
||||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
|
||||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
|
||||||
"""
|
|
||||||
UTM: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 2
|
|
||||||
"""
|
|
||||||
Universal Transverse Mercator format:
|
|
||||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
|
||||||
"""
|
|
||||||
MGRS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 3
|
|
||||||
"""
|
|
||||||
Military Grid Reference System format:
|
|
||||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
|
||||||
E is easting, N is northing
|
|
||||||
"""
|
|
||||||
OLC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 4
|
|
||||||
"""
|
|
||||||
Open Location Code (aka Plus Codes).
|
|
||||||
"""
|
|
||||||
OSGR: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 5
|
|
||||||
"""
|
|
||||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
|
||||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
|
||||||
E is the easting, N is the northing
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
|
UNUSED: Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType # 0
|
||||||
"""
|
|
||||||
How the GPS coordinates are displayed on the OLED screen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 0
|
|
||||||
"""
|
|
||||||
GPS coordinates are displayed in the normal decimal degrees format:
|
|
||||||
DD.DDDDDD DDD.DDDDDD
|
|
||||||
"""
|
|
||||||
DMS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 1
|
|
||||||
"""
|
|
||||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
|
||||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
|
||||||
"""
|
|
||||||
UTM: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 2
|
|
||||||
"""
|
|
||||||
Universal Transverse Mercator format:
|
|
||||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
|
||||||
"""
|
|
||||||
MGRS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 3
|
|
||||||
"""
|
|
||||||
Military Grid Reference System format:
|
|
||||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
|
||||||
E is easting, N is northing
|
|
||||||
"""
|
|
||||||
OLC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 4
|
|
||||||
"""
|
|
||||||
Open Location Code (aka Plus Codes).
|
|
||||||
"""
|
|
||||||
OSGR: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 5
|
|
||||||
"""
|
|
||||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
|
||||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
|
||||||
E is the easting, N is the northing
|
|
||||||
"""
|
|
||||||
|
|
||||||
class _DisplayUnits:
|
class _DisplayUnits:
|
||||||
ValueType = typing.NewType("ValueType", builtins.int)
|
ValueType = typing.NewType("ValueType", builtins.int)
|
||||||
@@ -1221,12 +1163,13 @@ class Config(google.protobuf.message.Message):
|
|||||||
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
||||||
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
||||||
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
|
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
|
||||||
|
USE_LONG_NODE_NAME_FIELD_NUMBER: builtins.int
|
||||||
screen_on_secs: builtins.int
|
screen_on_secs: builtins.int
|
||||||
"""
|
"""
|
||||||
Number of seconds the screen stays on after pressing the user button or receiving a message
|
Number of seconds the screen stays on after pressing the user button or receiving a message
|
||||||
0 for default of one minute MAXUINT for always on
|
0 for default of one minute MAXUINT for always on
|
||||||
"""
|
"""
|
||||||
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType
|
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType
|
||||||
"""
|
"""
|
||||||
Deprecated in 2.7.4: Unused
|
Deprecated in 2.7.4: Unused
|
||||||
How the GPS coordinates are formatted on the OLED screen.
|
How the GPS coordinates are formatted on the OLED screen.
|
||||||
@@ -1274,11 +1217,16 @@ class Config(google.protobuf.message.Message):
|
|||||||
If false (default), the device will display the time in 24-hour format on screen.
|
If false (default), the device will display the time in 24-hour format on screen.
|
||||||
If true, the device will display the time in 12-hour format on screen.
|
If true, the device will display the time in 12-hour format on screen.
|
||||||
"""
|
"""
|
||||||
|
use_long_node_name: builtins.bool
|
||||||
|
"""
|
||||||
|
If false (default), the device will use short names for various display screens.
|
||||||
|
If true, node names will show in long format
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
screen_on_secs: builtins.int = ...,
|
screen_on_secs: builtins.int = ...,
|
||||||
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType = ...,
|
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType = ...,
|
||||||
auto_screen_carousel_secs: builtins.int = ...,
|
auto_screen_carousel_secs: builtins.int = ...,
|
||||||
compass_north_top: builtins.bool = ...,
|
compass_north_top: builtins.bool = ...,
|
||||||
flip_screen: builtins.bool = ...,
|
flip_screen: builtins.bool = ...,
|
||||||
@@ -1289,8 +1237,9 @@ class Config(google.protobuf.message.Message):
|
|||||||
wake_on_tap_or_motion: builtins.bool = ...,
|
wake_on_tap_or_motion: builtins.bool = ...,
|
||||||
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
||||||
use_12h_clock: builtins.bool = ...,
|
use_12h_clock: builtins.bool = ...,
|
||||||
|
use_long_node_name: builtins.bool = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "use_long_node_name", b"use_long_node_name", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
class LoRaConfig(google.protobuf.message.Message):
|
class LoRaConfig(google.protobuf.message.Message):
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"p\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x0c\n\x04ssid\x18\x02 \x01(\t\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x65\n\x13\x63om.geeksville.meshB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"p\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x0c\n\x04ssid\x18\x02 \x01(\t\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x66\n\x14org.meshtastic.protoB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connection_status_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connection_status_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=69
|
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=69
|
||||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=410
|
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=410
|
||||||
_globals['_WIFICONNECTIONSTATUS']._serialized_start=412
|
_globals['_WIFICONNECTIONSTATUS']._serialized_start=412
|
||||||
|
|||||||
@@ -13,28 +13,30 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xda\x04\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xb4\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\t\n\x05\x43ZECH\x10\x12\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x63\n\x13\x63om.geeksville.meshB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xff\x05\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\x12K\n\ngps_format\x18\x13 \x01(\x0e\x32\x37.meshtastic.protobuf.DeviceUIConfig.GpsCoordinateFormat\"V\n\x13GpsCoordinateFormat\x12\x07\n\x03\x44\x45\x43\x10\x00\x12\x07\n\x03\x44MS\x10\x01\x12\x07\n\x03UTM\x10\x02\x12\x08\n\x04MGRS\x10\x03\x12\x07\n\x03OLC\x10\x04\x12\x08\n\x04OSGR\x10\x05\x12\x07\n\x03MLS\x10\x06\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xc0\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\t\n\x05\x43ZECH\x10\x12\x12\n\n\x06\x44\x41NISH\x10\x13\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x64\n\x14org.meshtastic.protoB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_ui_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_ui_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_COMPASSMODE']._serialized_start=1113
|
_globals['_COMPASSMODE']._serialized_start=1278
|
||||||
_globals['_COMPASSMODE']._serialized_end=1175
|
_globals['_COMPASSMODE']._serialized_end=1340
|
||||||
_globals['_THEME']._serialized_start=1177
|
_globals['_THEME']._serialized_start=1342
|
||||||
_globals['_THEME']._serialized_end=1214
|
_globals['_THEME']._serialized_end=1379
|
||||||
_globals['_LANGUAGE']._serialized_start=1217
|
_globals['_LANGUAGE']._serialized_start=1382
|
||||||
_globals['_LANGUAGE']._serialized_end=1525
|
_globals['_LANGUAGE']._serialized_end=1702
|
||||||
_globals['_DEVICEUICONFIG']._serialized_start=61
|
_globals['_DEVICEUICONFIG']._serialized_start=61
|
||||||
_globals['_DEVICEUICONFIG']._serialized_end=663
|
_globals['_DEVICEUICONFIG']._serialized_end=828
|
||||||
_globals['_NODEFILTER']._serialized_start=666
|
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_start=742
|
||||||
_globals['_NODEFILTER']._serialized_end=833
|
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_end=828
|
||||||
_globals['_NODEHIGHLIGHT']._serialized_start=835
|
_globals['_NODEFILTER']._serialized_start=831
|
||||||
_globals['_NODEHIGHLIGHT']._serialized_end=961
|
_globals['_NODEFILTER']._serialized_end=998
|
||||||
_globals['_GEOPOINT']._serialized_start=963
|
_globals['_NODEHIGHLIGHT']._serialized_start=1000
|
||||||
_globals['_GEOPOINT']._serialized_end=1024
|
_globals['_NODEHIGHLIGHT']._serialized_end=1126
|
||||||
_globals['_MAP']._serialized_start=1026
|
_globals['_GEOPOINT']._serialized_start=1128
|
||||||
_globals['_MAP']._serialized_end=1111
|
_globals['_GEOPOINT']._serialized_end=1189
|
||||||
|
_globals['_MAP']._serialized_start=1191
|
||||||
|
_globals['_MAP']._serialized_end=1276
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ class _LanguageEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumT
|
|||||||
"""
|
"""
|
||||||
Czech
|
Czech
|
||||||
"""
|
"""
|
||||||
|
DANISH: _Language.ValueType # 19
|
||||||
|
"""
|
||||||
|
Danish
|
||||||
|
"""
|
||||||
SIMPLIFIED_CHINESE: _Language.ValueType # 30
|
SIMPLIFIED_CHINESE: _Language.ValueType # 30
|
||||||
"""
|
"""
|
||||||
Simplified Chinese (experimental)
|
Simplified Chinese (experimental)
|
||||||
@@ -259,6 +263,10 @@ CZECH: Language.ValueType # 18
|
|||||||
"""
|
"""
|
||||||
Czech
|
Czech
|
||||||
"""
|
"""
|
||||||
|
DANISH: Language.ValueType # 19
|
||||||
|
"""
|
||||||
|
Danish
|
||||||
|
"""
|
||||||
SIMPLIFIED_CHINESE: Language.ValueType # 30
|
SIMPLIFIED_CHINESE: Language.ValueType # 30
|
||||||
"""
|
"""
|
||||||
Simplified Chinese (experimental)
|
Simplified Chinese (experimental)
|
||||||
@@ -277,6 +285,91 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
|||||||
|
|
||||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||||
|
|
||||||
|
class _GpsCoordinateFormat:
|
||||||
|
ValueType = typing.NewType("ValueType", builtins.int)
|
||||||
|
V: typing_extensions.TypeAlias = ValueType
|
||||||
|
|
||||||
|
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DeviceUIConfig._GpsCoordinateFormat.ValueType], builtins.type):
|
||||||
|
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||||
|
DEC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 0
|
||||||
|
"""
|
||||||
|
GPS coordinates are displayed in the normal decimal degrees format:
|
||||||
|
DD.DDDDDD DDD.DDDDDD
|
||||||
|
"""
|
||||||
|
DMS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 1
|
||||||
|
"""
|
||||||
|
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||||
|
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||||
|
"""
|
||||||
|
UTM: DeviceUIConfig._GpsCoordinateFormat.ValueType # 2
|
||||||
|
"""
|
||||||
|
Universal Transverse Mercator format:
|
||||||
|
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||||
|
"""
|
||||||
|
MGRS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 3
|
||||||
|
"""
|
||||||
|
Military Grid Reference System format:
|
||||||
|
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||||
|
E is easting, N is northing
|
||||||
|
"""
|
||||||
|
OLC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 4
|
||||||
|
"""
|
||||||
|
Open Location Code (aka Plus Codes).
|
||||||
|
"""
|
||||||
|
OSGR: DeviceUIConfig._GpsCoordinateFormat.ValueType # 5
|
||||||
|
"""
|
||||||
|
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||||
|
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||||
|
E is the easting, N is the northing
|
||||||
|
"""
|
||||||
|
MLS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 6
|
||||||
|
"""
|
||||||
|
Maidenhead Locator System
|
||||||
|
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
|
||||||
|
"""
|
||||||
|
|
||||||
|
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
|
||||||
|
"""
|
||||||
|
How the GPS coordinates are displayed on the OLED screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 0
|
||||||
|
"""
|
||||||
|
GPS coordinates are displayed in the normal decimal degrees format:
|
||||||
|
DD.DDDDDD DDD.DDDDDD
|
||||||
|
"""
|
||||||
|
DMS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 1
|
||||||
|
"""
|
||||||
|
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||||
|
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||||
|
"""
|
||||||
|
UTM: DeviceUIConfig.GpsCoordinateFormat.ValueType # 2
|
||||||
|
"""
|
||||||
|
Universal Transverse Mercator format:
|
||||||
|
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||||
|
"""
|
||||||
|
MGRS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 3
|
||||||
|
"""
|
||||||
|
Military Grid Reference System format:
|
||||||
|
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||||
|
E is easting, N is northing
|
||||||
|
"""
|
||||||
|
OLC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 4
|
||||||
|
"""
|
||||||
|
Open Location Code (aka Plus Codes).
|
||||||
|
"""
|
||||||
|
OSGR: DeviceUIConfig.GpsCoordinateFormat.ValueType # 5
|
||||||
|
"""
|
||||||
|
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||||
|
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||||
|
E is the easting, N is the northing
|
||||||
|
"""
|
||||||
|
MLS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 6
|
||||||
|
"""
|
||||||
|
Maidenhead Locator System
|
||||||
|
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
|
||||||
|
"""
|
||||||
|
|
||||||
VERSION_FIELD_NUMBER: builtins.int
|
VERSION_FIELD_NUMBER: builtins.int
|
||||||
SCREEN_BRIGHTNESS_FIELD_NUMBER: builtins.int
|
SCREEN_BRIGHTNESS_FIELD_NUMBER: builtins.int
|
||||||
SCREEN_TIMEOUT_FIELD_NUMBER: builtins.int
|
SCREEN_TIMEOUT_FIELD_NUMBER: builtins.int
|
||||||
@@ -295,6 +388,7 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
|||||||
COMPASS_MODE_FIELD_NUMBER: builtins.int
|
COMPASS_MODE_FIELD_NUMBER: builtins.int
|
||||||
SCREEN_RGB_COLOR_FIELD_NUMBER: builtins.int
|
SCREEN_RGB_COLOR_FIELD_NUMBER: builtins.int
|
||||||
IS_CLOCKFACE_ANALOG_FIELD_NUMBER: builtins.int
|
IS_CLOCKFACE_ANALOG_FIELD_NUMBER: builtins.int
|
||||||
|
GPS_FORMAT_FIELD_NUMBER: builtins.int
|
||||||
version: builtins.int
|
version: builtins.int
|
||||||
"""
|
"""
|
||||||
A version integer used to invalidate saved files when we make incompatible changes.
|
A version integer used to invalidate saved files when we make incompatible changes.
|
||||||
@@ -345,6 +439,10 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
|||||||
Clockface analog style
|
Clockface analog style
|
||||||
true for analog clockface, false for digital clockface
|
true for analog clockface, false for digital clockface
|
||||||
"""
|
"""
|
||||||
|
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType
|
||||||
|
"""
|
||||||
|
How the GPS coordinates are formatted on the OLED screen.
|
||||||
|
"""
|
||||||
@property
|
@property
|
||||||
def node_filter(self) -> global___NodeFilter:
|
def node_filter(self) -> global___NodeFilter:
|
||||||
"""
|
"""
|
||||||
@@ -384,9 +482,10 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
|||||||
compass_mode: global___CompassMode.ValueType = ...,
|
compass_mode: global___CompassMode.ValueType = ...,
|
||||||
screen_rgb_color: builtins.int = ...,
|
screen_rgb_color: builtins.int = ...,
|
||||||
is_clockface_analog: builtins.bool = ...,
|
is_clockface_analog: builtins.bool = ...,
|
||||||
|
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def HasField(self, field_name: typing.Literal["map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight"]) -> builtins.bool: ...
|
def HasField(self, field_name: typing.Literal["map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight"]) -> builtins.bool: ...
|
||||||
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "compass_mode", b"compass_mode", "is_clockface_analog", b"is_clockface_analog", "language", b"language", "map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_rgb_color", b"screen_rgb_color", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "compass_mode", b"compass_mode", "gps_format", b"gps_format", "is_clockface_analog", b"is_clockface_analog", "language", b"language", "map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_rgb_color", b"screen_rgb_color", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
|
||||||
|
|
||||||
global___DeviceUIConfig = DeviceUIConfig
|
global___DeviceUIConfig = DeviceUIConfig
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_tel
|
|||||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\x94\x02\n\x08UserLite\x12\x13\n\x07macaddr\x18\x01 \x01(\x0c\x42\x02\x18\x01\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x12\n\npublic_key\x18\x07 \x01(\x0c\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"\xf0\x02\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x16\n\thops_away\x18\t \x01(\rH\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x10\n\x08next_hop\x18\x0c \x01(\r\x12\x10\n\x08\x62itfield\x18\r \x01(\rB\x0c\n\n_hops_away\"\xa1\x03\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x19\n\rdid_gps_reset\x18\x0b \x01(\x08\x42\x02\x18\x01\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\"}\n\x0cNodeDatabase\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\\\n\x05nodes\x18\x02 \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\x86\x02\n\x11\x42\x61\x63kupPreferences\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x07\x12\x30\n\x06\x63onfig\x18\x03 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12=\n\rmodule_config\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig\x12\x32\n\x08\x63hannels\x18\x05 \x01(\x0b\x32 .meshtastic.protobuf.ChannelFile\x12(\n\x05owner\x18\x06 \x01(\x0b\x32\x19.meshtastic.protobuf.UserBm\n\x13\x63om.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\x94\x02\n\x08UserLite\x12\x13\n\x07macaddr\x18\x01 \x01(\x0c\x42\x02\x18\x01\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x12\n\npublic_key\x18\x07 \x01(\x0c\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"\xf0\x02\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x16\n\thops_away\x18\t \x01(\rH\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x10\n\x08next_hop\x18\x0c \x01(\r\x12\x10\n\x08\x62itfield\x18\r \x01(\rB\x0c\n\n_hops_away\"\xa1\x03\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x19\n\rdid_gps_reset\x18\x0b \x01(\x08\x42\x02\x18\x01\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\"}\n\x0cNodeDatabase\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\\\n\x05nodes\x18\x02 \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\x86\x02\n\x11\x42\x61\x63kupPreferences\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x07\x12\x30\n\x06\x63onfig\x18\x03 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12=\n\rmodule_config\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig\x12\x32\n\x08\x63hannels\x18\x05 \x01(\x0b\x32 .meshtastic.protobuf.ChannelFile\x12(\n\x05owner\x18\x06 \x01(\x0b\x32\x19.meshtastic.protobuf.UserBn\n\x14org.meshtastic.protoB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
|
||||||
_USERLITE.fields_by_name['macaddr']._options = None
|
_USERLITE.fields_by_name['macaddr']._options = None
|
||||||
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001'
|
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001'
|
||||||
_DEVICESTATE.fields_by_name['no_save']._options = None
|
_DEVICESTATE.fields_by_name['no_save']._options = None
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\"s\n\nSensorData\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .meshtastic.protobuf.MessageType\x12\x15\n\x0b\x66loat_value\x18\x02 \x01(\x02H\x00\x12\x16\n\x0cuint32_value\x18\x03 \x01(\rH\x00\x42\x06\n\x04\x64\x61ta\"_\n\x12InterdeviceMessage\x12\x0e\n\x04nmea\x18\x01 \x01(\tH\x00\x12\x31\n\x06sensor\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.SensorDataH\x00\x42\x06\n\x04\x64\x61ta*\xd5\x01\n\x0bMessageType\x12\x07\n\x03\x41\x43K\x10\x00\x12\x15\n\x10\x43OLLECT_INTERVAL\x10\xa0\x01\x12\x0c\n\x07\x42\x45\x45P_ON\x10\xa1\x01\x12\r\n\x08\x42\x45\x45P_OFF\x10\xa2\x01\x12\r\n\x08SHUTDOWN\x10\xa3\x01\x12\r\n\x08POWER_ON\x10\xa4\x01\x12\x0f\n\nSCD41_TEMP\x10\xb0\x01\x12\x13\n\x0eSCD41_HUMIDITY\x10\xb1\x01\x12\x0e\n\tSCD41_CO2\x10\xb2\x01\x12\x0f\n\nAHT20_TEMP\x10\xb3\x01\x12\x13\n\x0e\x41HT20_HUMIDITY\x10\xb4\x01\x12\x0f\n\nTVOC_INDEX\x10\xb5\x01\x42\x66\n\x13\x63om.geeksville.meshB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\"s\n\nSensorData\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .meshtastic.protobuf.MessageType\x12\x15\n\x0b\x66loat_value\x18\x02 \x01(\x02H\x00\x12\x16\n\x0cuint32_value\x18\x03 \x01(\rH\x00\x42\x06\n\x04\x64\x61ta\"_\n\x12InterdeviceMessage\x12\x0e\n\x04nmea\x18\x01 \x01(\tH\x00\x12\x31\n\x06sensor\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.SensorDataH\x00\x42\x06\n\x04\x64\x61ta*\xd5\x01\n\x0bMessageType\x12\x07\n\x03\x41\x43K\x10\x00\x12\x15\n\x10\x43OLLECT_INTERVAL\x10\xa0\x01\x12\x0c\n\x07\x42\x45\x45P_ON\x10\xa1\x01\x12\r\n\x08\x42\x45\x45P_OFF\x10\xa2\x01\x12\r\n\x08SHUTDOWN\x10\xa3\x01\x12\r\n\x08POWER_ON\x10\xa4\x01\x12\x0f\n\nSCD41_TEMP\x10\xb0\x01\x12\x13\n\x0eSCD41_HUMIDITY\x10\xb1\x01\x12\x0e\n\tSCD41_CO2\x10\xb2\x01\x12\x0f\n\nAHT20_TEMP\x10\xb3\x01\x12\x13\n\x0e\x41HT20_HUMIDITY\x10\xb4\x01\x12\x0f\n\nTVOC_INDEX\x10\xb5\x01\x42g\n\x14org.meshtastic.protoB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interdevice_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interdevice_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\021InterdeviceProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021InterdeviceProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_MESSAGETYPE']._serialized_start=277
|
_globals['_MESSAGETYPE']._serialized_start=277
|
||||||
_globals['_MESSAGETYPE']._serialized_end=490
|
_globals['_MESSAGETYPE']._serialized_end=490
|
||||||
_globals['_SENSORDATA']._serialized_start=62
|
_globals['_SENSORDATA']._serialized_start=62
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config
|
|||||||
from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2
|
from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\xf0\x07\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBd\n\x13\x63om.geeksville.meshB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\xf0\x07\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBe\n\x14org.meshtastic.protoB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.localonly_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.localonly_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_LOCALCONFIG']._serialized_start=136
|
_globals['_LOCALCONFIG']._serialized_start=136
|
||||||
_globals['_LOCALCONFIG']._serialized_end=642
|
_globals['_LOCALCONFIG']._serialized_end=642
|
||||||
_globals['_LOCALMODULECONFIG']._serialized_start=645
|
_globals['_LOCALMODULECONFIG']._serialized_start=645
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -453,9 +453,9 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
|||||||
"""
|
"""
|
||||||
Seeed Tracker L1 EINK driver
|
Seeed Tracker L1 EINK driver
|
||||||
"""
|
"""
|
||||||
QWANTZ_TINY_ARMS: _HardwareModel.ValueType # 101
|
MUZI_R1_NEO: _HardwareModel.ValueType # 101
|
||||||
"""
|
"""
|
||||||
Reserved ID for future and past use
|
Muzi Works R1 Neo
|
||||||
"""
|
"""
|
||||||
T_DECK_PRO: _HardwareModel.ValueType # 102
|
T_DECK_PRO: _HardwareModel.ValueType # 102
|
||||||
"""
|
"""
|
||||||
@@ -465,9 +465,10 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
|||||||
"""
|
"""
|
||||||
Lilygo TLora Pager
|
Lilygo TLora Pager
|
||||||
"""
|
"""
|
||||||
GAT562_MESH_TRIAL_TRACKER: _HardwareModel.ValueType # 104
|
M5STACK_RESERVED: _HardwareModel.ValueType # 104
|
||||||
"""
|
"""
|
||||||
GAT562 Mesh Trial Tracker
|
M5Stack Reserved
|
||||||
|
0x68
|
||||||
"""
|
"""
|
||||||
WISMESH_TAG: _HardwareModel.ValueType # 105
|
WISMESH_TAG: _HardwareModel.ValueType # 105
|
||||||
"""
|
"""
|
||||||
@@ -494,6 +495,34 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
|||||||
"""
|
"""
|
||||||
New Heltec LoRA32 with ESP32-S3 CPU
|
New Heltec LoRA32 with ESP32-S3 CPU
|
||||||
"""
|
"""
|
||||||
|
M5STACK_C6L: _HardwareModel.ValueType # 111
|
||||||
|
"""
|
||||||
|
M5Stack C6L
|
||||||
|
"""
|
||||||
|
M5STACK_CARDPUTER_ADV: _HardwareModel.ValueType # 112
|
||||||
|
"""
|
||||||
|
M5Stack Cardputer Adv
|
||||||
|
"""
|
||||||
|
HELTEC_WIRELESS_TRACKER_V2: _HardwareModel.ValueType # 113
|
||||||
|
"""
|
||||||
|
ESP32S3 main controller with GPS and TFT screen.
|
||||||
|
"""
|
||||||
|
T_WATCH_ULTRA: _HardwareModel.ValueType # 114
|
||||||
|
"""
|
||||||
|
LilyGo T-Watch Ultra
|
||||||
|
"""
|
||||||
|
THINKNODE_M3: _HardwareModel.ValueType # 115
|
||||||
|
"""
|
||||||
|
Elecrow ThinkNode M3
|
||||||
|
"""
|
||||||
|
WISMESH_TAP_V2: _HardwareModel.ValueType # 116
|
||||||
|
"""
|
||||||
|
RAK WISMESH_TAP_V2 with ESP32-S3 CPU
|
||||||
|
"""
|
||||||
|
RAK3401: _HardwareModel.ValueType # 117
|
||||||
|
"""
|
||||||
|
RAK3401
|
||||||
|
"""
|
||||||
PRIVATE_HW: _HardwareModel.ValueType # 255
|
PRIVATE_HW: _HardwareModel.ValueType # 255
|
||||||
"""
|
"""
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -930,9 +959,9 @@ SEEED_WIO_TRACKER_L1_EINK: HardwareModel.ValueType # 100
|
|||||||
"""
|
"""
|
||||||
Seeed Tracker L1 EINK driver
|
Seeed Tracker L1 EINK driver
|
||||||
"""
|
"""
|
||||||
QWANTZ_TINY_ARMS: HardwareModel.ValueType # 101
|
MUZI_R1_NEO: HardwareModel.ValueType # 101
|
||||||
"""
|
"""
|
||||||
Reserved ID for future and past use
|
Muzi Works R1 Neo
|
||||||
"""
|
"""
|
||||||
T_DECK_PRO: HardwareModel.ValueType # 102
|
T_DECK_PRO: HardwareModel.ValueType # 102
|
||||||
"""
|
"""
|
||||||
@@ -942,9 +971,10 @@ T_LORA_PAGER: HardwareModel.ValueType # 103
|
|||||||
"""
|
"""
|
||||||
Lilygo TLora Pager
|
Lilygo TLora Pager
|
||||||
"""
|
"""
|
||||||
GAT562_MESH_TRIAL_TRACKER: HardwareModel.ValueType # 104
|
M5STACK_RESERVED: HardwareModel.ValueType # 104
|
||||||
"""
|
"""
|
||||||
GAT562 Mesh Trial Tracker
|
M5Stack Reserved
|
||||||
|
0x68
|
||||||
"""
|
"""
|
||||||
WISMESH_TAG: HardwareModel.ValueType # 105
|
WISMESH_TAG: HardwareModel.ValueType # 105
|
||||||
"""
|
"""
|
||||||
@@ -971,6 +1001,34 @@ HELTEC_V4: HardwareModel.ValueType # 110
|
|||||||
"""
|
"""
|
||||||
New Heltec LoRA32 with ESP32-S3 CPU
|
New Heltec LoRA32 with ESP32-S3 CPU
|
||||||
"""
|
"""
|
||||||
|
M5STACK_C6L: HardwareModel.ValueType # 111
|
||||||
|
"""
|
||||||
|
M5Stack C6L
|
||||||
|
"""
|
||||||
|
M5STACK_CARDPUTER_ADV: HardwareModel.ValueType # 112
|
||||||
|
"""
|
||||||
|
M5Stack Cardputer Adv
|
||||||
|
"""
|
||||||
|
HELTEC_WIRELESS_TRACKER_V2: HardwareModel.ValueType # 113
|
||||||
|
"""
|
||||||
|
ESP32S3 main controller with GPS and TFT screen.
|
||||||
|
"""
|
||||||
|
T_WATCH_ULTRA: HardwareModel.ValueType # 114
|
||||||
|
"""
|
||||||
|
LilyGo T-Watch Ultra
|
||||||
|
"""
|
||||||
|
THINKNODE_M3: HardwareModel.ValueType # 115
|
||||||
|
"""
|
||||||
|
Elecrow ThinkNode M3
|
||||||
|
"""
|
||||||
|
WISMESH_TAP_V2: HardwareModel.ValueType # 116
|
||||||
|
"""
|
||||||
|
RAK WISMESH_TAP_V2 with ESP32-S3 CPU
|
||||||
|
"""
|
||||||
|
RAK3401: HardwareModel.ValueType # 117
|
||||||
|
"""
|
||||||
|
RAK3401
|
||||||
|
"""
|
||||||
PRIVATE_HW: HardwareModel.ValueType # 255
|
PRIVATE_HW: HardwareModel.ValueType # 255
|
||||||
"""
|
"""
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -874,6 +874,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
|||||||
HEALTH_MEASUREMENT_ENABLED_FIELD_NUMBER: builtins.int
|
HEALTH_MEASUREMENT_ENABLED_FIELD_NUMBER: builtins.int
|
||||||
HEALTH_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
|
HEALTH_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
|
||||||
HEALTH_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
|
HEALTH_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
|
||||||
|
DEVICE_TELEMETRY_ENABLED_FIELD_NUMBER: builtins.int
|
||||||
device_update_interval: builtins.int
|
device_update_interval: builtins.int
|
||||||
"""
|
"""
|
||||||
Interval in seconds of how often we should try to send our
|
Interval in seconds of how often we should try to send our
|
||||||
@@ -934,6 +935,11 @@ class ModuleConfig(google.protobuf.message.Message):
|
|||||||
"""
|
"""
|
||||||
Enable/Disable the health telemetry module on-device display
|
Enable/Disable the health telemetry module on-device display
|
||||||
"""
|
"""
|
||||||
|
device_telemetry_enabled: builtins.bool
|
||||||
|
"""
|
||||||
|
Enable/Disable the device telemetry module to send metrics to the mesh
|
||||||
|
Note: We will still send telemtry to the connected phone / client every minute over the API
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -950,8 +956,9 @@ class ModuleConfig(google.protobuf.message.Message):
|
|||||||
health_measurement_enabled: builtins.bool = ...,
|
health_measurement_enabled: builtins.bool = ...,
|
||||||
health_update_interval: builtins.int = ...,
|
health_update_interval: builtins.int = ...,
|
||||||
health_screen_enabled: builtins.bool = ...,
|
health_screen_enabled: builtins.bool = ...,
|
||||||
|
device_telemetry_enabled: builtins.bool = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "device_telemetry_enabled", b"device_telemetry_enabled", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
class CannedMessageConfig(google.protobuf.message.Message):
|
class CannedMessageConfig(google.protobuf.message.Message):
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config
|
|||||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"j\n\x0fServiceEnvelope\x12/\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"\x83\x04\n\tMapReport\x12\x11\n\tlong_name\x18\x01 \x01(\t\x12\x12\n\nshort_name\x18\x02 \x01(\t\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x18\n\x10\x66irmware_version\x18\x05 \x01(\t\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12\x1e\n\x16num_online_local_nodes\x18\r \x01(\r\x12!\n\x19has_opted_report_location\x18\x0e \x01(\x08\x42_\n\x13\x63om.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"j\n\x0fServiceEnvelope\x12/\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"\x83\x04\n\tMapReport\x12\x11\n\tlong_name\x18\x01 \x01(\t\x12\x12\n\nshort_name\x18\x02 \x01(\t\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x18\n\x10\x66irmware_version\x18\x05 \x01(\t\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12\x1e\n\x16num_online_local_nodes\x18\r \x01(\r\x12!\n\x19has_opted_report_location\x18\x0e \x01(\x08\x42`\n\x14org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_SERVICEENVELOPE']._serialized_start=121
|
_globals['_SERVICEENVELOPE']._serialized_start=121
|
||||||
_globals['_SERVICEENVELOPE']._serialized_end=227
|
_globals['_SERVICEENVELOPE']._serialized_end=227
|
||||||
_globals['_MAPREPORT']._serialized_start=230
|
_globals['_MAPREPORT']._serialized_start=230
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBc\n\x13\x63om.geeksville.meshB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBd\n\x14org.meshtastic.protoB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.paxcount_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.paxcount_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_PAXCOUNT']._serialized_start=59
|
_globals['_PAXCOUNT']._serialized_start=59
|
||||||
_globals['_PAXCOUNT']._serialized_end=112
|
_globals['_PAXCOUNT']._serialized_end=112
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xf6\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42]\n\x13\x63om.geeksville.meshB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xf6\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42^\n\x14org.meshtastic.protoB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.portnums_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.portnums_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_PORTNUM']._serialized_start=60
|
_globals['_PORTNUM']._serialized_start=60
|
||||||
_globals['_PORTNUM']._serialized_end=690
|
_globals['_PORTNUM']._serialized_end=690
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBc\n\x13\x63om.geeksville.meshB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBd\n\x14org.meshtastic.protoB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.powermon_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.powermon_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_POWERMON']._serialized_start=60
|
_globals['_POWERMON']._serialized_start=60
|
||||||
_globals['_POWERMON']._serialized_end=284
|
_globals['_POWERMON']._serialized_end=284
|
||||||
_globals['_POWERMON_STATE']._serialized_start=73
|
_globals['_POWERMON_STATE']._serialized_start=73
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x63\n\x13\x63om.geeksville.meshB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x64\n\x14org.meshtastic.protoB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.remote_hardware_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.remote_hardware_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_HARDWAREMESSAGE']._serialized_start=67
|
_globals['_HARDWAREMESSAGE']._serialized_start=67
|
||||||
_globals['_HARDWAREMESSAGE']._serialized_end=290
|
_globals['_HARDWAREMESSAGE']._serialized_end=290
|
||||||
_globals['_HARDWAREMESSAGE_TYPE']._serialized_start=182
|
_globals['_HARDWAREMESSAGE_TYPE']._serialized_start=182
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\"\x1f\n\x0bRTTTLConfig\x12\x10\n\x08ringtone\x18\x01 \x01(\tBf\n\x13\x63om.geeksville.meshB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\"\x1f\n\x0bRTTTLConfig\x12\x10\n\x08ringtone\x18\x01 \x01(\tBg\n\x14org.meshtastic.protoB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_RTTTLCONFIG']._serialized_start=56
|
_globals['_RTTTLCONFIG']._serialized_start=56
|
||||||
_globals['_RTTTLCONFIG']._serialized_end=87
|
_globals['_RTTTLCONFIG']._serialized_end=87
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\"\xc0\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x0e\n\x04text\x18\x05 \x01(\x0cH\x00\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xbc\x02\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBj\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\"\xc0\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x0e\n\x04text\x18\x05 \x01(\x0cH\x00\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xbc\x02\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBk\n\x14org.meshtastic.protoB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storeforward_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storeforward_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_STOREANDFORWARD']._serialized_start=64
|
_globals['_STOREANDFORWARD']._serialized_start=64
|
||||||
_globals['_STOREANDFORWARD']._serialized_end=1024
|
_globals['_STOREANDFORWARD']._serialized_end=1024
|
||||||
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=366
|
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=366
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -203,6 +203,10 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
|||||||
"""
|
"""
|
||||||
TSL2561 light sensor
|
TSL2561 light sensor
|
||||||
"""
|
"""
|
||||||
|
BH1750: _TelemetrySensorType.ValueType # 45
|
||||||
|
"""
|
||||||
|
BH1750 light sensor
|
||||||
|
"""
|
||||||
|
|
||||||
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
||||||
"""
|
"""
|
||||||
@@ -389,6 +393,10 @@ TSL2561: TelemetrySensorType.ValueType # 44
|
|||||||
"""
|
"""
|
||||||
TSL2561 light sensor
|
TSL2561 light sensor
|
||||||
"""
|
"""
|
||||||
|
BH1750: TelemetrySensorType.ValueType # 45
|
||||||
|
"""
|
||||||
|
BH1750 light sensor
|
||||||
|
"""
|
||||||
global___TelemetrySensorType = TelemetrySensorType
|
global___TelemetrySensorType = TelemetrySensorType
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
@@ -1026,6 +1034,7 @@ class LocalStats(google.protobuf.message.Message):
|
|||||||
NUM_TX_RELAY_CANCELED_FIELD_NUMBER: builtins.int
|
NUM_TX_RELAY_CANCELED_FIELD_NUMBER: builtins.int
|
||||||
HEAP_TOTAL_BYTES_FIELD_NUMBER: builtins.int
|
HEAP_TOTAL_BYTES_FIELD_NUMBER: builtins.int
|
||||||
HEAP_FREE_BYTES_FIELD_NUMBER: builtins.int
|
HEAP_FREE_BYTES_FIELD_NUMBER: builtins.int
|
||||||
|
NUM_TX_DROPPED_FIELD_NUMBER: builtins.int
|
||||||
uptime_seconds: builtins.int
|
uptime_seconds: builtins.int
|
||||||
"""
|
"""
|
||||||
How long the device has been running since the last reboot (in seconds)
|
How long the device has been running since the last reboot (in seconds)
|
||||||
@@ -1080,6 +1089,10 @@ class LocalStats(google.protobuf.message.Message):
|
|||||||
"""
|
"""
|
||||||
Number of bytes free in the heap
|
Number of bytes free in the heap
|
||||||
"""
|
"""
|
||||||
|
num_tx_dropped: builtins.int
|
||||||
|
"""
|
||||||
|
Number of packets that were dropped because the transmit queue was full.
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1096,8 +1109,9 @@ class LocalStats(google.protobuf.message.Message):
|
|||||||
num_tx_relay_canceled: builtins.int = ...,
|
num_tx_relay_canceled: builtins.int = ...,
|
||||||
heap_total_bytes: builtins.int = ...,
|
heap_total_bytes: builtins.int = ...,
|
||||||
heap_free_bytes: builtins.int = ...,
|
heap_free_bytes: builtins.int = ...,
|
||||||
|
num_tx_dropped: builtins.int = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_dropped", b"num_tx_dropped", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
|
||||||
|
|
||||||
global___LocalStats = LocalStats
|
global___LocalStats = LocalStats
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\"\xbf\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x0b\n\x03seq\x18\x02 \x01(\r\x12\r\n\x05\x63rc16\x18\x03 \x01(\r\x12\x0e\n\x06\x62uffer\x18\x04 \x01(\x0c\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x61\n\x13\x63om.geeksville.meshB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\"\xbf\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x0b\n\x03seq\x18\x02 \x01(\r\x12\r\n\x05\x63rc16\x18\x03 \x01(\r\x12\x0e\n\x06\x62uffer\x18\x04 \x01(\x0c\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x62\n\x14org.meshtastic.protoB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_pb2', _globals)
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||||
_globals['_XMODEM']._serialized_start=58
|
_globals['_XMODEM']._serialized_start=58
|
||||||
_globals['_XMODEM']._serialized_end=249
|
_globals['_XMODEM']._serialized_end=249
|
||||||
_globals['_XMODEM_CONTROL']._serialized_start=166
|
_globals['_XMODEM_CONTROL']._serialized_start=166
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
__version__ = "3.0.0"
|
__version__ = "3.0.5"
|
||||||
__release_date__ = "2025-11-05"
|
__release_date__ = "2026-2-6"
|
||||||
|
|
||||||
|
|
||||||
def get_git_revision():
|
def get_git_revision():
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ parser = argparse.ArgumentParser(description="MeshView Configuration Loader")
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config", type=str, default="config.ini", help="Path to config.ini file (default: config.ini)"
|
"--config", type=str, default="config.ini", help="Path to config.ini file (default: config.ini)"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
# Initialize config parser
|
# Initialize config parser
|
||||||
config_parser = configparser.ConfigParser()
|
config_parser = configparser.ConfigParser()
|
||||||
|
|||||||
+14
-4
@@ -1,3 +1,4 @@
|
|||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from meshview import models
|
from meshview import models
|
||||||
@@ -9,10 +10,19 @@ async_session = None
|
|||||||
def init_database(database_connection_string):
|
def init_database(database_connection_string):
|
||||||
global engine, async_session
|
global engine, async_session
|
||||||
kwargs = {"echo": False}
|
kwargs = {"echo": False}
|
||||||
# Ensure SQLite is opened in read-only mode
|
url = make_url(database_connection_string)
|
||||||
database_connection_string += "?mode=ro"
|
connect_args = {}
|
||||||
kwargs["connect_args"] = {"uri": True}
|
|
||||||
engine = create_async_engine(database_connection_string, **kwargs)
|
if url.drivername.startswith("sqlite"):
|
||||||
|
query = dict(url.query)
|
||||||
|
query.setdefault("mode", "ro")
|
||||||
|
url = url.set(query=query)
|
||||||
|
connect_args["uri"] = True
|
||||||
|
|
||||||
|
if connect_args:
|
||||||
|
kwargs["connect_args"] = connect_args
|
||||||
|
|
||||||
|
engine = create_async_engine(url, **kwargs)
|
||||||
async_session = async_sessionmaker(
|
async_session = async_sessionmaker(
|
||||||
bind=engine,
|
bind=engine,
|
||||||
class_=AsyncSession,
|
class_=AsyncSession,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import logging
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_optional_deps() -> None:
|
||||||
|
if find_spec("pyitm") is None:
|
||||||
|
logger.warning(
|
||||||
|
"Optional dependency missing: pyitm. "
|
||||||
|
"Coverage prediction is disabled. "
|
||||||
|
"Run: ./env/bin/pip install -r requirements.txt"
|
||||||
|
)
|
||||||
+175
-49
@@ -13,60 +13,118 @@
|
|||||||
"go to node": "Go to Node",
|
"go to node": "Go to Node",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"portnum_options": {
|
"portnum_options": {
|
||||||
|
"0": "Unknown",
|
||||||
"1": "Text Message",
|
"1": "Text Message",
|
||||||
|
"2": "Remote Hardware",
|
||||||
"3": "Position",
|
"3": "Position",
|
||||||
"4": "Node Info",
|
"4": "Node Info",
|
||||||
|
"5": "Routing",
|
||||||
|
"6": "Admin",
|
||||||
|
"7": "Text (Compressed)",
|
||||||
|
"8": "Waypoint",
|
||||||
|
"9": "Audio",
|
||||||
|
"10": "Detection Sensor",
|
||||||
|
"11": "Alert",
|
||||||
|
"12": "Key Verification",
|
||||||
|
"32": "Reply",
|
||||||
|
"33": "IP Tunnel",
|
||||||
|
"34": "Paxcounter",
|
||||||
|
"35": "Store Forward++",
|
||||||
|
"36": "Node Status",
|
||||||
|
"64": "Serial",
|
||||||
|
"65": "Store & Forward",
|
||||||
|
"66": "Range Test",
|
||||||
"67": "Telemetry",
|
"67": "Telemetry",
|
||||||
|
"68": "ZPS",
|
||||||
|
"69": "Simulator",
|
||||||
"70": "Traceroute",
|
"70": "Traceroute",
|
||||||
"71": "Neighbor Info"
|
"71": "Neighbor Info",
|
||||||
}
|
"72": "ATAK",
|
||||||
|
"73": "Map Report",
|
||||||
|
"74": "Power Stress",
|
||||||
|
"76": "Reticulum Tunnel",
|
||||||
|
"77": "Cayenne",
|
||||||
|
"256": "Private App",
|
||||||
|
"257": "ATAK Forwarder"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
|
"chat_title": "Chats:",
|
||||||
"replying_to": "Replying to:",
|
"replying_to": "Replying to:",
|
||||||
"view_packet_details": "View packet details"
|
"view_packet_details": "View packet details"
|
||||||
},
|
},
|
||||||
"nodelist": {
|
"nodelist": {
|
||||||
"search_placeholder": "Search by name or ID...",
|
"search_placeholder": "Search by name or ID...",
|
||||||
"all_roles": "All Roles",
|
"all_roles": "All Roles",
|
||||||
"all_channels": "All Channels",
|
"all_channels": "All Channels",
|
||||||
"all_hw_models": "All HW Models",
|
"all_hw": "All HW Models",
|
||||||
"all_firmware": "All Firmware",
|
"all_firmware": "All Firmware",
|
||||||
"export_csv": "Export CSV",
|
|
||||||
"clear_filters": "Clear Filters",
|
"show_favorites": "⭐ Show Favorites",
|
||||||
"showing": "Showing",
|
"show_all": "⭐ Show All",
|
||||||
"nodes": "nodes",
|
"export_csv": "Export CSV",
|
||||||
"short": "Short",
|
"clear_filters": "Clear Filters",
|
||||||
"long_name": "Long Name",
|
|
||||||
"hw_model": "HW Model",
|
"showing_nodes": "Showing",
|
||||||
"firmware": "Firmware",
|
"nodes_suffix": "nodes",
|
||||||
"role": "Role",
|
|
||||||
"last_lat": "Last Latitude",
|
"loading_nodes": "Loading nodes...",
|
||||||
"last_long": "Last Longitude",
|
"error_loading_nodes": "Error loading nodes",
|
||||||
"channel": "Channel",
|
"no_nodes_found": "No nodes found",
|
||||||
"last_update": "Last Update",
|
|
||||||
"loading_nodes": "Loading nodes...",
|
"short_name": "Short",
|
||||||
"no_nodes": "No nodes found",
|
"long_name": "Long Name",
|
||||||
"error_nodes": "Error loading nodes"
|
"hw_model": "HW Model",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"role": "Role",
|
||||||
|
"last_lat": "Last Latitude",
|
||||||
|
"last_long": "Last Longitude",
|
||||||
|
"channel": "Channel",
|
||||||
|
"mqtt_gateway": "MQTT",
|
||||||
|
"last_seen": "Last Seen",
|
||||||
|
"favorite": "Favorite",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
|
||||||
|
"time_just_now": "just now",
|
||||||
|
"time_min_ago": "min ago",
|
||||||
|
"time_hr_ago": "hr ago",
|
||||||
|
"time_day_ago": "day ago",
|
||||||
|
"time_days_ago": "days ago"
|
||||||
},
|
},
|
||||||
|
|
||||||
"net": {
|
"net": {
|
||||||
"number_of_checkins": "Number of Check-ins:",
|
"net_title": "Weekly Net:",
|
||||||
"view_packet_details": "View packet details",
|
"total_messages": "Number of messages:",
|
||||||
"view_all_packets_from_node": "View all packets from this node",
|
"view_packet_details": "More details"
|
||||||
"no_packets_found": "No packets found."
|
|
||||||
},
|
},
|
||||||
"map": {
|
|
||||||
"channel": "Channel:",
|
"map": {
|
||||||
"model": "Model:",
|
"show_routers_only": "Show Routers Only",
|
||||||
"role": "Role:",
|
"show_mqtt_only": "Show MQTT Gateways Only",
|
||||||
|
"share_view": "Share This View",
|
||||||
|
"reset_filters": "Reset Filters To Defaults",
|
||||||
|
"unmapped_packets_title": "Unmapped Packets",
|
||||||
|
"unmapped_packets_empty": "No recent unmapped packets.",
|
||||||
|
"channel_label": "Channel:",
|
||||||
|
"model_label": "Model:",
|
||||||
|
"role_label": "Role:",
|
||||||
|
"mqtt_gateway": "MQTT Gateway:",
|
||||||
"last_seen": "Last seen:",
|
"last_seen": "Last seen:",
|
||||||
"firmware": "Firmware:",
|
"firmware": "Firmware:",
|
||||||
"show_routers_only": "Show Routers Only",
|
"yes": "Yes",
|
||||||
"share_view": "Share This View"
|
"no": "No",
|
||||||
|
"link_copied": "Link Copied!",
|
||||||
|
"legend_traceroute": "Traceroute (with arrows)",
|
||||||
|
"legend_neighbor": "Neighbor"
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"stats":
|
"stats":
|
||||||
{
|
{
|
||||||
"mesh_stats_summary": "Mesh Statistics - Summary (all available in Database)",
|
"mesh_stats_summary": "Mesh Statistics - Summary (all available in Database)",
|
||||||
"total_nodes": "Total Nodes",
|
"total_nodes": "Total Nodes",
|
||||||
|
"total_gateways": "Total Gateways",
|
||||||
"total_packets": "Total Packets",
|
"total_packets": "Total Packets",
|
||||||
"total_packets_seen": "Total Packets Seen",
|
"total_packets_seen": "Total Packets Seen",
|
||||||
"packets_per_day_all": "Packets per Day - All Ports (Last 14 Days)",
|
"packets_per_day_all": "Packets per Day - All Ports (Last 14 Days)",
|
||||||
@@ -77,26 +135,29 @@
|
|||||||
"hardware_breakdown": "Hardware Breakdown",
|
"hardware_breakdown": "Hardware Breakdown",
|
||||||
"role_breakdown": "Role Breakdown",
|
"role_breakdown": "Role Breakdown",
|
||||||
"channel_breakdown": "Channel Breakdown",
|
"channel_breakdown": "Channel Breakdown",
|
||||||
|
"gateway_channel_breakdown": "Gateway Channel Breakdown",
|
||||||
|
"gateway_role_breakdown": "Gateway Role Breakdown",
|
||||||
|
"gateway_firmware_breakdown": "Gateway Firmware Breakdown",
|
||||||
|
"no_gateways": "No gateways found",
|
||||||
"expand_chart": "Expand Chart",
|
"expand_chart": "Expand Chart",
|
||||||
"export_csv": "Export CSV",
|
"export_csv": "Export CSV",
|
||||||
"all_channels": "All Channels",
|
"all_channels": "All Channels",
|
||||||
"node_id": "Node ID"
|
"node_id": "Node ID"
|
||||||
},
|
},
|
||||||
"top":
|
"top": {
|
||||||
{
|
"top_traffic_nodes": "Top Nodes Traffic",
|
||||||
"top_traffic_nodes": "Top Traffic Nodes (last 24 hours)",
|
"channel": "Channel",
|
||||||
"chart_description_1": "This chart shows a bell curve (normal distribution) based on the total \"Times Seen\" values for all nodes. It helps visualize how frequently nodes are heard, relative to the average.",
|
"search": "Search",
|
||||||
"chart_description_2": "This \"Times Seen\" value is the closest that we can get to Mesh utilization by node.",
|
"search_placeholder": "Search nodes...",
|
||||||
"mean_label": "Mean:",
|
|
||||||
"stddev_label": "Standard Deviation:",
|
|
||||||
"long_name": "Long Name",
|
"long_name": "Long Name",
|
||||||
"short_name": "Short Name",
|
"short_name": "Short Name",
|
||||||
"channel": "Channel",
|
"packets_sent": "Sent (24h)",
|
||||||
"packets_sent": "Packets Sent",
|
"times_seen": "Seen (24h)",
|
||||||
"times_seen": "Times Seen",
|
"avg_gateways": "Avg Gateways",
|
||||||
"seen_percent": "Seen % of Mean",
|
"showing_nodes": "Showing",
|
||||||
"no_nodes": "No top traffic nodes available."
|
"nodes_suffix": "nodes"
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodegraph":
|
"nodegraph":
|
||||||
{
|
{
|
||||||
"channel_label": "Channel:",
|
"channel_label": "Channel:",
|
||||||
@@ -119,7 +180,6 @@
|
|||||||
"to": "To",
|
"to": "To",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"links": "Links",
|
"links": "Links",
|
||||||
|
|
||||||
"unknown_app": "UNKNOWN APP",
|
"unknown_app": "UNKNOWN APP",
|
||||||
"text_message": "Text Message",
|
"text_message": "Text Message",
|
||||||
"position": "Position",
|
"position": "Position",
|
||||||
@@ -131,11 +191,77 @@
|
|||||||
"telemetry": "Telemetry",
|
"telemetry": "Telemetry",
|
||||||
"trace_route": "Trace Route",
|
"trace_route": "Trace Route",
|
||||||
"neighbor_info": "Neighbor Info",
|
"neighbor_info": "Neighbor Info",
|
||||||
|
|
||||||
"direct_to_mqtt": "direct to MQTT",
|
"direct_to_mqtt": "direct to MQTT",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
"graph": "Graph"
|
"graph": "Graph"
|
||||||
}
|
},
|
||||||
|
"node": {
|
||||||
|
"specifications": "Specifications",
|
||||||
|
"node_id": "Node ID",
|
||||||
|
"long_name": "Long Name",
|
||||||
|
"short_name": "Short Name",
|
||||||
|
"hw_model": "Hardware Model",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"role": "Role",
|
||||||
|
"mqtt_gateway": "MQTT Gateway",
|
||||||
|
"channel": "Channel",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
|
"first_update": "First Update",
|
||||||
|
"last_update": "Last Update",
|
||||||
|
"battery_voltage": "Battery & Voltage",
|
||||||
|
"air_channel": "Air & Channel Utilization",
|
||||||
|
"environment": "Environment Metrics",
|
||||||
|
"neighbors_chart": "Neighbors (Signal-to-Noise)",
|
||||||
|
"expand": "Expand",
|
||||||
|
"export_csv": "Export CSV",
|
||||||
|
"time": "Time",
|
||||||
|
"packet_id": "Packet ID",
|
||||||
|
"from": "From",
|
||||||
|
"to": "To",
|
||||||
|
"port": "Port",
|
||||||
|
"direct_to_mqtt": "Direct to MQTT",
|
||||||
|
"all_broadcast": "All",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"last_24h": "24h",
|
||||||
|
"packets_sent": "Packets sent",
|
||||||
|
"times_seen": "Times seen",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"copy_import_url": "Copy Import URL",
|
||||||
|
"show_qr_code": "Show QR Code",
|
||||||
|
"toggle_coverage": "Predicted Coverage",
|
||||||
|
"location_required": "Location required for coverage",
|
||||||
|
"coverage_help": "Coverage Help",
|
||||||
|
"share_contact_qr": "Share Contact QR",
|
||||||
|
"copy_url": "Copy URL",
|
||||||
|
"copied": "Copied!",
|
||||||
|
"potential_impersonation": "Potential Impersonation Detected",
|
||||||
|
"scan_qr_to_add": "Scan this QR code to add this node as a contact on another device."
|
||||||
|
},
|
||||||
|
"packet": {
|
||||||
|
"loading": "Loading packet information...",
|
||||||
|
"packet_id_label": "Packet ID",
|
||||||
|
"from_node": "From Node",
|
||||||
|
"to_node": "To Node",
|
||||||
|
"channel": "Channel",
|
||||||
|
"port": "Port",
|
||||||
|
"raw_payload": "Raw Payload",
|
||||||
|
"decoded_telemetry": "Decoded Telemetry",
|
||||||
|
"location": "Location",
|
||||||
|
"seen_by": "Seen By",
|
||||||
|
"gateway": "Gateway",
|
||||||
|
"rssi": "RSSI",
|
||||||
|
"snr": "SNR",
|
||||||
|
"hops": "Hop",
|
||||||
|
"time": "Time",
|
||||||
|
"packet_source": "Packet Source",
|
||||||
|
"distance": "Distance",
|
||||||
|
"node_id_short": "Node ID",
|
||||||
|
"all_broadcast": "All",
|
||||||
|
"direct_to_mqtt": "Direct to MQTT"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
+183
-72
@@ -1,71 +1,126 @@
|
|||||||
{
|
{
|
||||||
"base": {
|
"base": {
|
||||||
"conversations": "Conversaciones",
|
"chat": "Conversaciones",
|
||||||
"nodes": "Nodos",
|
"nodes": "Nodos",
|
||||||
"everything": "Mostrar Todo",
|
"everything": "Mostrar todo",
|
||||||
"graph": "Gráficos de la Malla",
|
"graphs": "Gráficos de la Malla",
|
||||||
"net": "Red Semanal",
|
"net": "Red Semanal",
|
||||||
"map": "Mapa en Vivo",
|
"map": "Mapa en Vivo",
|
||||||
"stats": "Estadísticas",
|
"stats": "Estadísticas",
|
||||||
"top": "Nodos con Mayor Tráfico",
|
"top": "Nodos con Mayor Tráfico",
|
||||||
"footer": "Visita <strong><a href=\"https://github.com/pablorevilla-meshtastic/meshview\">Meshview</a></strong> en Github.",
|
"footer": "Visita <strong><a href=\"https://github.com/pablorevilla-meshtastic/meshview\">Meshview</a></strong> en Github.",
|
||||||
"node id": "ID de Nodo",
|
"node_id": "ID de Nodo",
|
||||||
"go to node": "Ir al nodo",
|
"go_to_node": "Ir al nodo",
|
||||||
"all": "Todos",
|
"all": "Todos",
|
||||||
"portnum_options": {
|
"portnum_options": {
|
||||||
|
"0": "Desconocido",
|
||||||
"1": "Mensaje de Texto",
|
"1": "Mensaje de Texto",
|
||||||
|
"2": "Hardware Remoto",
|
||||||
"3": "Ubicación",
|
"3": "Ubicación",
|
||||||
"4": "Información del Nodo",
|
"4": "Información del Nodo",
|
||||||
|
"5": "Enrutamiento",
|
||||||
|
"6": "Administración",
|
||||||
|
"7": "Texto (Comprimido)",
|
||||||
|
"8": "Punto de Referencia",
|
||||||
|
"9": "Audio",
|
||||||
|
"10": "Sensor de Detección",
|
||||||
|
"11": "Alerta",
|
||||||
|
"12": "Verificación de Clave",
|
||||||
|
"32": "Respuesta",
|
||||||
|
"33": "Túnel IP",
|
||||||
|
"34": "Paxcounter",
|
||||||
|
"35": "Store Forward++",
|
||||||
|
"36": "Estado del Nodo",
|
||||||
|
"64": "Serial",
|
||||||
|
"65": "Store & Forward",
|
||||||
|
"66": "Prueba de Alcance",
|
||||||
"67": "Telemetría",
|
"67": "Telemetría",
|
||||||
|
"68": "ZPS",
|
||||||
|
"69": "Simulador",
|
||||||
"70": "Traceroute",
|
"70": "Traceroute",
|
||||||
"71": "Información de Vecinos"
|
"71": "Información de Vecinos",
|
||||||
|
"72": "ATAK",
|
||||||
|
"73": "Reporte de Mapa",
|
||||||
|
"74": "Prueba de Energía",
|
||||||
|
"76": "Túnel Reticulum",
|
||||||
|
"77": "Cayenne",
|
||||||
|
"256": "App Privada",
|
||||||
|
"257": "ATAK Forwarder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"chat": {
|
"chat": {
|
||||||
"replying_to": "Respondiendo a:",
|
"chat_title": "Conversaciones:",
|
||||||
"view_packet_details": "Ver detalles del paquete"
|
"replying_to": "Respondiendo a:",
|
||||||
|
"view_packet_details": "Ver detalles del paquete"
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodelist": {
|
"nodelist": {
|
||||||
"search_placeholder": "Buscar por nombre o ID...",
|
"search_placeholder": "Buscar por nombre o ID...",
|
||||||
"all_roles": "Todos los Roles",
|
"all_roles": "Todos los roles",
|
||||||
"all_channels": "Todos los Canales",
|
"all_channels": "Todos los canales",
|
||||||
"all_hw_models": "Todos los Modelos",
|
"all_hw": "Todos los modelos",
|
||||||
"all_firmware": "Todo el Firmware",
|
"all_firmware": "Todo el firmware",
|
||||||
|
"show_favorites": "⭐ Mostrar favoritos",
|
||||||
|
"show_all": "⭐ Mostrar todos",
|
||||||
"export_csv": "Exportar CSV",
|
"export_csv": "Exportar CSV",
|
||||||
"clear_filters": "Limpiar Filtros",
|
"clear_filters": "Limpiar filtros",
|
||||||
"showing": "Mostrando",
|
"showing_nodes": "Mostrando",
|
||||||
"nodes": "nodos",
|
"nodes_suffix": "nodos",
|
||||||
"short": "Corto",
|
"loading_nodes": "Cargando nodos...",
|
||||||
"long_name": "Largo",
|
"error_loading_nodes": "Error al cargar nodos",
|
||||||
"hw_model": "Modelo",
|
"no_nodes_found": "No se encontraron nodos",
|
||||||
|
"short_name": "Corto",
|
||||||
|
"long_name": "Nombre largo",
|
||||||
|
"hw_model": "Modelo HW",
|
||||||
"firmware": "Firmware",
|
"firmware": "Firmware",
|
||||||
"role": "Rol",
|
"role": "Rol",
|
||||||
"last_lat": "Última Latitud",
|
"last_lat": "Última latitud",
|
||||||
"last_long": "Última Longitud",
|
"last_long": "Última longitud",
|
||||||
"channel": "Canal",
|
"channel": "Canal",
|
||||||
"last_update": "Última Actualización",
|
"mqtt_gateway": "MQTT",
|
||||||
"loading_nodes": "Cargando nodos...",
|
"last_seen": "Última vez visto",
|
||||||
"no_nodes": "No se encontraron nodos",
|
"favorite": "Favorito",
|
||||||
"error_nodes": "Error al cargar nodos"
|
"yes": "Sí",
|
||||||
|
"no": "No",
|
||||||
|
"time_just_now": "justo ahora",
|
||||||
|
"time_min_ago": "min atrás",
|
||||||
|
"time_hr_ago": "h atrás",
|
||||||
|
"time_day_ago": "día atrás",
|
||||||
|
"time_days_ago": "días atrás"
|
||||||
},
|
},
|
||||||
|
|
||||||
"net": {
|
"net": {
|
||||||
"number_of_checkins": "Número de registros:",
|
"net_title": "Red Semanal:",
|
||||||
"view_packet_details": "Ver detalles del paquete",
|
"total_messages": "Número de mensajes:",
|
||||||
"view_all_packets_from_node": "Ver todos los paquetes de este nodo",
|
"view_packet_details": "Más Detalles"
|
||||||
"no_packets_found": "No se encontraron paquetes."
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"map": {
|
"map": {
|
||||||
"channel": "Canal:",
|
"filter_routers_only": "Mostrar solo enrutadores",
|
||||||
"model": "Modelo:",
|
"show_routers_only": "Mostrar solo enrutadores",
|
||||||
"role": "Rol:",
|
"show_mqtt_only": "Mostrar solo gateways MQTT",
|
||||||
|
"share_view": "Compartir esta vista",
|
||||||
|
"reset_filters": "Restablecer filtros",
|
||||||
|
"unmapped_packets_title": "Paquetes sin mapa",
|
||||||
|
"unmapped_packets_empty": "No hay paquetes sin mapa recientes.",
|
||||||
|
"channel_label": "Canal:",
|
||||||
|
"model_label": "Modelo:",
|
||||||
|
"role_label": "Rol:",
|
||||||
|
"mqtt_gateway": "Gateway MQTT:",
|
||||||
"last_seen": "Visto por última vez:",
|
"last_seen": "Visto por última vez:",
|
||||||
"firmware": "Firmware:",
|
"firmware": "Firmware:",
|
||||||
"show_routers_only": "Mostrar solo enrutadores",
|
"yes": "Sí",
|
||||||
"share_view": "Compartir esta vista"
|
"no": "No",
|
||||||
|
"link_copied": "¡Enlace copiado!",
|
||||||
|
"legend_traceroute": "Ruta de traceroute (flechas de dirección)",
|
||||||
|
"legend_neighbor": "Vínculo de vecinos"
|
||||||
},
|
},
|
||||||
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"mesh_stats_summary": "Estadísticas de la Malla - Resumen (completas en la base de datos)",
|
"mesh_stats_summary": "Estadísticas de la Malla - Resumen (completas en la base de datos)",
|
||||||
"total_nodes": "Nodos Totales",
|
"total_nodes": "Nodos Totales",
|
||||||
|
"total_gateways": "Gateways Totales",
|
||||||
"total_packets": "Paquetes Totales",
|
"total_packets": "Paquetes Totales",
|
||||||
"total_packets_seen": "Paquetes Totales Vistos",
|
"total_packets_seen": "Paquetes Totales Vistos",
|
||||||
"packets_per_day_all": "Paquetes por Día - Todos los Puertos (Últimos 14 Días)",
|
"packets_per_day_all": "Paquetes por Día - Todos los Puertos (Últimos 14 Días)",
|
||||||
@@ -76,26 +131,30 @@
|
|||||||
"hardware_breakdown": "Distribución de Hardware",
|
"hardware_breakdown": "Distribución de Hardware",
|
||||||
"role_breakdown": "Distribución de Roles",
|
"role_breakdown": "Distribución de Roles",
|
||||||
"channel_breakdown": "Distribución de Canales",
|
"channel_breakdown": "Distribución de Canales",
|
||||||
|
"gateway_channel_breakdown": "Desglose de canales de gateways",
|
||||||
|
"gateway_role_breakdown": "Desglose de roles de gateways",
|
||||||
|
"gateway_firmware_breakdown": "Desglose de firmware de gateways",
|
||||||
|
"no_gateways": "No se encontraron gateways",
|
||||||
"expand_chart": "Ampliar Gráfico",
|
"expand_chart": "Ampliar Gráfico",
|
||||||
"export_csv": "Exportar CSV",
|
"export_csv": "Exportar CSV",
|
||||||
"all_channels": "Todos los Canales"
|
"all_channels": "Todos los Canales"
|
||||||
},
|
},
|
||||||
|
|
||||||
"top": {
|
"top": {
|
||||||
"top_traffic_nodes": "Tráfico (últimas 24 horas)",
|
"top_traffic_nodes": "Tráfico de Nodos (24h)",
|
||||||
"chart_description_1": "Este gráfico muestra una curva normal (distribución normal) basada en el valor total de \"Veces Visto\" para todos los nodos. Ayuda a visualizar con qué frecuencia se detectan los nodos en relación con el promedio.",
|
"channel": "Canal",
|
||||||
"chart_description_2": "Este valor de \"Veces Visto\" es lo más aproximado que tenemos al nivel de uso de la malla por nodo.",
|
"search": "Buscar",
|
||||||
"mean_label": "Media:",
|
"search_placeholder": "Buscar nodos...",
|
||||||
"stddev_label": "Desviación Estándar:",
|
|
||||||
"long_name": "Nombre Largo",
|
"long_name": "Nombre Largo",
|
||||||
"short_name": "Nombre Corto",
|
"short_name": "Nombre Corto",
|
||||||
"channel": "Canal",
|
"packets_sent": "Enviados (24h)",
|
||||||
"packets_sent": "Paquetes Enviados",
|
"times_seen": "Visto (24h)",
|
||||||
"times_seen": "Veces Visto",
|
"avg_gateways": "Promedio de Gateways",
|
||||||
"seen_percent": "% Visto respecto a la Media",
|
"showing_nodes": "Mostrando",
|
||||||
"no_nodes": "No hay nodos con mayor tráfico disponibles."
|
"nodes_suffix": "nodos"
|
||||||
},
|
},
|
||||||
"nodegraph":
|
|
||||||
{
|
"nodegraph": {
|
||||||
"channel_label": "Canal:",
|
"channel_label": "Canal:",
|
||||||
"search_placeholder": "Buscar nodo...",
|
"search_placeholder": "Buscar nodo...",
|
||||||
"search_button": "Buscar",
|
"search_button": "Buscar",
|
||||||
@@ -109,34 +168,86 @@
|
|||||||
"unknown": "Desconocido",
|
"unknown": "Desconocido",
|
||||||
"node_not_found": "¡Nodo no encontrado en el canal actual!"
|
"node_not_found": "¡Nodo no encontrado en el canal actual!"
|
||||||
},
|
},
|
||||||
"firehose":
|
|
||||||
{
|
|
||||||
"live_feed": "📡 Flujo en Vivo",
|
|
||||||
"pause": "Pausar",
|
|
||||||
"resume": "Continuar",
|
|
||||||
"time": "Hora",
|
|
||||||
"packet_id": "ID del Paquete",
|
|
||||||
"from": "De",
|
|
||||||
"to": "Para",
|
|
||||||
"port": "Puerto",
|
|
||||||
"links": "Enlaces",
|
|
||||||
|
|
||||||
"unknown_app": "APLICACIÓN DESCONOCIDA",
|
"firehose": {
|
||||||
"text_message": "Mensaje de Texto",
|
"live_feed": "📡 Flujo en vivo",
|
||||||
"position": "Posición",
|
"pause": "Pausar",
|
||||||
"node_info": "Información del Nodo",
|
"resume": "Reanudar",
|
||||||
"routing": "Enrutamiento",
|
"time": "Hora",
|
||||||
"administration": "Administración",
|
"packet_id": "ID de paquete",
|
||||||
"waypoint": "Punto de Ruta",
|
"from": "De",
|
||||||
"store_forward": "Almacenar y Reenviar",
|
"to": "A",
|
||||||
"telemetry": "Telemetría",
|
"port": "Puerto",
|
||||||
"trace_route": "Rastreo de Ruta",
|
"direct_to_mqtt": "Directo a MQTT",
|
||||||
"neighbor_info": "Información de Vecinos",
|
"all_broadcast": "Todos"
|
||||||
|
},
|
||||||
|
|
||||||
"direct_to_mqtt": "Directo a MQTT",
|
"node": {
|
||||||
"all": "Todos",
|
"specifications": "Especificaciones",
|
||||||
"map": "Mapa",
|
"node_id": "ID de Nodo",
|
||||||
"graph": "Gráfico"
|
"long_name": "Nombre Largo",
|
||||||
}
|
"short_name": "Nombre Corto",
|
||||||
|
"hw_model": "Modelo de Hardware",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"role": "Rol",
|
||||||
|
"mqtt_gateway": "Gateway MQTT",
|
||||||
|
"channel": "Canal",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"first_update": "Primera Actualización",
|
||||||
|
"last_update": "Última Actualización",
|
||||||
|
"battery_voltage": "Batería y voltaje",
|
||||||
|
"air_channel": "Utilización del aire y del canal",
|
||||||
|
"environment": "Métricas Ambientales",
|
||||||
|
"neighbors_chart": "Vecinos (Relación Señal/Ruido)",
|
||||||
|
"expand": "Ampliar",
|
||||||
|
"export_csv": "Exportar CSV",
|
||||||
|
"time": "Hora",
|
||||||
|
"packet_id": "ID del Paquete",
|
||||||
|
"from": "De",
|
||||||
|
"to": "A",
|
||||||
|
"port": "Puerto",
|
||||||
|
"direct_to_mqtt": "Directo a MQTT",
|
||||||
|
"all_broadcast": "Todos",
|
||||||
|
"statistics": "Estadísticas",
|
||||||
|
"last_24h": "24h",
|
||||||
|
"packets_sent": "Paquetes enviados",
|
||||||
|
"times_seen": "Veces visto",
|
||||||
|
"yes": "Sí",
|
||||||
|
"no": "No",
|
||||||
|
"copy_import_url": "Copiar URL de importación",
|
||||||
|
"show_qr_code": "Mostrar código QR",
|
||||||
|
"toggle_coverage": "Cobertura predicha",
|
||||||
|
"location_required": "Se requiere ubicación para la cobertura",
|
||||||
|
"coverage_help": "Ayuda de cobertura",
|
||||||
|
"share_contact_qr": "Compartir contacto QR",
|
||||||
|
"copy_url": "Copiar URL",
|
||||||
|
"copied": "¡Copiado!",
|
||||||
|
"potential_impersonation": "Posible suplantación detectada",
|
||||||
|
"scan_qr_to_add": "Escanea este código QR para agregar este nodo como contacto en otro dispositivo."
|
||||||
|
},
|
||||||
|
|
||||||
|
"packet": {
|
||||||
|
"loading": "Cargando información del paquete...",
|
||||||
|
"packet_id_label": "ID del Paquete",
|
||||||
|
"from_node": "De",
|
||||||
|
"to_node": "A",
|
||||||
|
"channel": "Canal",
|
||||||
|
"port": "Puerto",
|
||||||
|
"raw_payload": "Payload sin procesar",
|
||||||
|
"decoded_telemetry": "Telemetría Decodificada",
|
||||||
|
"location": "Ubicación",
|
||||||
|
"seen_by": "Visto por",
|
||||||
|
"gateway": "Gateway",
|
||||||
|
"rssi": "RSSI",
|
||||||
|
"snr": "SNR",
|
||||||
|
"hops": "Saltos",
|
||||||
|
"time": "Hora",
|
||||||
|
"packet_source": "Origen del Paquete",
|
||||||
|
"distance": "Distancia",
|
||||||
|
"node_id_short": "ID de Nodo",
|
||||||
|
"all_broadcast": "Todos",
|
||||||
|
"direct_to_mqtt": "Directo a MQTT",
|
||||||
|
"signal": "Señal"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -186,19 +186,24 @@ async def create_migration_status_table(engine: AsyncEngine) -> None:
|
|||||||
text("""
|
text("""
|
||||||
CREATE TABLE IF NOT EXISTS migration_status (
|
CREATE TABLE IF NOT EXISTS migration_status (
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
in_progress BOOLEAN NOT NULL DEFAULT 0,
|
in_progress BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Insert initial row if not exists
|
result = await conn.execute(
|
||||||
await conn.execute(
|
|
||||||
text("""
|
text("""
|
||||||
INSERT OR IGNORE INTO migration_status (id, in_progress)
|
SELECT 1 FROM migration_status WHERE id = 1
|
||||||
VALUES (1, 0)
|
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
if result.first() is None:
|
||||||
|
await conn.execute(
|
||||||
|
text("""
|
||||||
|
INSERT INTO migration_status (id, in_progress)
|
||||||
|
VALUES (1, FALSE)
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def set_migration_in_progress(engine: AsyncEngine, in_progress: bool) -> None:
|
async def set_migration_in_progress(engine: AsyncEngine, in_progress: bool) -> None:
|
||||||
|
|||||||
+18
-15
@@ -1,5 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, ForeignKey, Index, desc
|
from sqlalchemy import BigInteger, ForeignKey, Index, desc
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
@@ -22,7 +20,7 @@ class Node(Base):
|
|||||||
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
channel: Mapped[str] = mapped_column(nullable=True)
|
channel: Mapped[str] = mapped_column(nullable=True)
|
||||||
last_update: Mapped[datetime] = mapped_column(nullable=True)
|
is_mqtt_gateway: Mapped[bool] = mapped_column(nullable=True)
|
||||||
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
last_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
last_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
|
|
||||||
@@ -33,11 +31,7 @@ class Node(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {column.name: getattr(self, column.name) for column in self.__table__.columns}
|
||||||
column.name: getattr(self, column.name)
|
|
||||||
for column in self.__table__.columns
|
|
||||||
if column.name != "last_update"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Packet(Base):
|
class Packet(Base):
|
||||||
@@ -55,17 +49,13 @@ class Packet(Base):
|
|||||||
overlaps="from_node",
|
overlaps="from_node",
|
||||||
)
|
)
|
||||||
payload: Mapped[bytes] = mapped_column(nullable=True)
|
payload: Mapped[bytes] = mapped_column(nullable=True)
|
||||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
|
||||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
channel: Mapped[str] = mapped_column(nullable=True)
|
channel: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_packet_from_node_id", "from_node_id"),
|
Index("idx_packet_from_node_id", "from_node_id"),
|
||||||
Index("idx_packet_to_node_id", "to_node_id"),
|
Index("idx_packet_to_node_id", "to_node_id"),
|
||||||
Index("idx_packet_import_time", desc("import_time")),
|
|
||||||
Index("idx_packet_import_time_us", desc("import_time_us")),
|
Index("idx_packet_import_time_us", desc("import_time_us")),
|
||||||
# Composite index for /top endpoint performance - filters by from_node_id AND import_time
|
|
||||||
Index("idx_packet_from_node_time", "from_node_id", desc("import_time")),
|
|
||||||
Index("idx_packet_from_node_time_us", "from_node_id", desc("import_time_us")),
|
Index("idx_packet_from_node_time_us", "from_node_id", desc("import_time_us")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +76,6 @@ class PacketSeen(Base):
|
|||||||
rx_snr: Mapped[float] = mapped_column(nullable=True)
|
rx_snr: Mapped[float] = mapped_column(nullable=True)
|
||||||
rx_rssi: Mapped[int] = mapped_column(nullable=True)
|
rx_rssi: Mapped[int] = mapped_column(nullable=True)
|
||||||
topic: Mapped[str] = mapped_column(nullable=True)
|
topic: Mapped[str] = mapped_column(nullable=True)
|
||||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
|
||||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
@@ -108,11 +97,25 @@ class Traceroute(Base):
|
|||||||
gateway_node_id: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
gateway_node_id: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
done: Mapped[bool] = mapped_column(nullable=True)
|
done: Mapped[bool] = mapped_column(nullable=True)
|
||||||
route: Mapped[bytes] = mapped_column(nullable=True)
|
route: Mapped[bytes] = mapped_column(nullable=True)
|
||||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
|
||||||
route_return: Mapped[bytes] = mapped_column(nullable=True)
|
route_return: Mapped[bytes] = mapped_column(nullable=True)
|
||||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_traceroute_import_time", "import_time"),
|
Index("idx_traceroute_packet_id", "packet_id"),
|
||||||
Index("idx_traceroute_import_time_us", "import_time_us"),
|
Index("idx_traceroute_import_time_us", "import_time_us"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NodePublicKey(Base):
|
||||||
|
__tablename__ = "node_public_key"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
node_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||||
|
public_key: Mapped[str] = mapped_column(nullable=False)
|
||||||
|
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
|
last_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_node_public_key_node_id", "node_id"),
|
||||||
|
Index("idx_node_public_key_public_key", "public_key"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from meshview import models
|
from meshview import models
|
||||||
@@ -5,9 +7,26 @@ from meshview import models
|
|||||||
|
|
||||||
def init_database(database_connection_string):
|
def init_database(database_connection_string):
|
||||||
global engine, async_session
|
global engine, async_session
|
||||||
engine = create_async_engine(
|
|
||||||
database_connection_string, echo=False, connect_args={"timeout": 900}
|
url = make_url(database_connection_string)
|
||||||
)
|
kwargs = {"echo": False}
|
||||||
|
|
||||||
|
if url.drivername.startswith("sqlite"):
|
||||||
|
kwargs["connect_args"] = {"timeout": 900} # seconds
|
||||||
|
|
||||||
|
engine = create_async_engine(url, **kwargs)
|
||||||
|
|
||||||
|
# Enforce SQLite pragmas on every new DB connection
|
||||||
|
if url.drivername.startswith("sqlite"):
|
||||||
|
|
||||||
|
@event.listens_for(engine.sync_engine, "connect")
|
||||||
|
def _set_sqlite_pragmas(dbapi_conn, _):
|
||||||
|
cursor = dbapi_conn.cursor()
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
cursor.execute("PRAGMA busy_timeout=900000;") # ms
|
||||||
|
cursor.execute("PRAGMA synchronous=NORMAL;")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+83
-11
@@ -8,9 +8,11 @@ import aiomqtt
|
|||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
|
from meshtastic.protobuf.mesh_pb2 import Data
|
||||||
from meshtastic.protobuf.mqtt_pb2 import ServiceEnvelope
|
from meshtastic.protobuf.mqtt_pb2 import ServiceEnvelope
|
||||||
|
from meshview.config import CONFIG
|
||||||
|
|
||||||
KEY = base64.b64decode("1PG7OiApB1nwvP+rz05pAQ==")
|
PRIMARY_KEY = base64.b64decode("1PG7OiApB1nwvP+rz05pAQ==")
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -21,24 +23,94 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(packet):
|
def _parse_skip_node_ids():
|
||||||
|
mqtt_config = CONFIG.get("mqtt", {})
|
||||||
|
raw_value = mqtt_config.get("skip_node_ids", "")
|
||||||
|
if not raw_value:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
if isinstance(raw_value, str):
|
||||||
|
raw_value = raw_value.strip()
|
||||||
|
if not raw_value:
|
||||||
|
return set()
|
||||||
|
values = [v.strip() for v in raw_value.split(",") if v.strip()]
|
||||||
|
else:
|
||||||
|
values = [raw_value]
|
||||||
|
|
||||||
|
skip_ids = set()
|
||||||
|
for value in values:
|
||||||
|
try:
|
||||||
|
skip_ids.add(int(value, 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.warning("Invalid node id in mqtt.skip_node_ids: %s", value)
|
||||||
|
return skip_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_quotes(value):
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||||
|
return value[1:-1]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_secondary_keys():
|
||||||
|
mqtt_config = CONFIG.get("mqtt", {})
|
||||||
|
raw_value = mqtt_config.get("secondary_keys", "")
|
||||||
|
if not raw_value:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(raw_value, str):
|
||||||
|
raw_value = raw_value.strip()
|
||||||
|
if not raw_value:
|
||||||
|
return []
|
||||||
|
values = [v.strip() for v in raw_value.split(",") if v.strip()]
|
||||||
|
else:
|
||||||
|
values = [raw_value]
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
for value in values:
|
||||||
|
try:
|
||||||
|
cleaned = _strip_quotes(str(value).strip())
|
||||||
|
if cleaned:
|
||||||
|
keys.append(base64.b64decode(cleaned))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.warning("Invalid base64 key in mqtt.secondary_keys: %s", value)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
SKIP_NODE_IDS = _parse_skip_node_ids()
|
||||||
|
SECONDARY_KEYS = _parse_secondary_keys()
|
||||||
|
|
||||||
|
logger.info("Primary key: %s", PRIMARY_KEY)
|
||||||
|
if SECONDARY_KEYS:
|
||||||
|
logger.info("Secondary keys: %s", SECONDARY_KEYS)
|
||||||
|
else:
|
||||||
|
logger.info("Secondary keys: []")
|
||||||
|
|
||||||
|
|
||||||
|
# Thank you to "Robert Grizzell" for the decryption code!
|
||||||
|
# https://github.com/rgrizzell
|
||||||
|
def decrypt(packet, key):
|
||||||
if packet.HasField("decoded"):
|
if packet.HasField("decoded"):
|
||||||
return
|
return True
|
||||||
packet_id = packet.id.to_bytes(8, "little")
|
packet_id = packet.id.to_bytes(8, "little")
|
||||||
from_node_id = getattr(packet, "from").to_bytes(8, "little")
|
from_node_id = getattr(packet, "from").to_bytes(8, "little")
|
||||||
nonce = packet_id + from_node_id
|
nonce = packet_id + from_node_id
|
||||||
|
|
||||||
cipher = Cipher(algorithms.AES(KEY), modes.CTR(nonce))
|
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce))
|
||||||
decryptor = cipher.decryptor()
|
decryptor = cipher.decryptor()
|
||||||
raw_proto = decryptor.update(packet.encrypted) + decryptor.finalize()
|
raw_proto = decryptor.update(packet.encrypted) + decryptor.finalize()
|
||||||
try:
|
try:
|
||||||
packet.decoded.ParseFromString(raw_proto)
|
data = Data()
|
||||||
|
data.ParseFromString(raw_proto)
|
||||||
|
packet.decoded.CopyFrom(data)
|
||||||
except DecodeError:
|
except DecodeError:
|
||||||
pass
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def get_topic_envelopes(mqtt_server, mqtt_port, topics, mqtt_user, mqtt_passwd):
|
async def get_topic_envelopes(mqtt_server, mqtt_port, topics, mqtt_user, mqtt_passwd):
|
||||||
identifier = str(random.getrandbits(16))
|
identifier = str(random.getrandbits(16))
|
||||||
|
keyring = [PRIMARY_KEY, *SECONDARY_KEYS]
|
||||||
msg_count = 0
|
msg_count = 0
|
||||||
start_time = None
|
start_time = None
|
||||||
while True:
|
while True:
|
||||||
@@ -65,14 +137,14 @@ async def get_topic_envelopes(mqtt_server, mqtt_port, topics, mqtt_user, mqtt_pa
|
|||||||
except DecodeError:
|
except DecodeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
decrypt(envelope.packet)
|
for key in keyring:
|
||||||
# print(envelope.packet.decoded)
|
if decrypt(envelope.packet, key):
|
||||||
|
break
|
||||||
if not envelope.packet.decoded:
|
if not envelope.packet.decoded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip packets from specific node
|
# Skip packets from configured node IDs
|
||||||
# FIXME: make this configurable as a list of node IDs to skip
|
if getattr(envelope.packet, "from", None) in SKIP_NODE_IDS:
|
||||||
if getattr(envelope.packet, "from", None) == 2144342101:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg_count += 1
|
msg_count += 1
|
||||||
|
|||||||
+84
-43
@@ -1,14 +1,21 @@
|
|||||||
import datetime
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from meshtastic.protobuf.config_pb2 import Config
|
from meshtastic.protobuf.config_pb2 import Config
|
||||||
from meshtastic.protobuf.mesh_pb2 import HardwareModel
|
from meshtastic.protobuf.mesh_pb2 import HardwareModel
|
||||||
from meshtastic.protobuf.portnums_pb2 import PortNum
|
from meshtastic.protobuf.portnums_pb2 import PortNum
|
||||||
from meshview import decode_payload, mqtt_database
|
from meshview import decode_payload, mqtt_database
|
||||||
from meshview.models import Node, Packet, PacketSeen, Traceroute
|
from meshview.models import Node, NodePublicKey, Packet, PacketSeen, Traceroute
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MQTT_GATEWAY_CACHE: set[int] = set()
|
||||||
|
|
||||||
|
|
||||||
async def process_envelope(topic, env):
|
async def process_envelope(topic, env):
|
||||||
@@ -37,8 +44,7 @@ async def process_envelope(topic, env):
|
|||||||
await session.execute(select(Node).where(Node.node_id == node_id))
|
await session.execute(select(Node).where(Node.node_id == node_id))
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
|
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
now_us = int(time.time() * 1_000_000)
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
|
||||||
|
|
||||||
if node:
|
if node:
|
||||||
node.node_id = node_id
|
node.node_id = node_id
|
||||||
@@ -50,7 +56,6 @@ async def process_envelope(topic, env):
|
|||||||
node.last_lat = map_report.latitude_i
|
node.last_lat = map_report.latitude_i
|
||||||
node.last_long = map_report.longitude_i
|
node.last_long = map_report.longitude_i
|
||||||
node.firmware = map_report.firmware_version
|
node.firmware = map_report.firmware_version
|
||||||
node.last_update = now
|
|
||||||
node.last_seen_us = now_us
|
node.last_seen_us = now_us
|
||||||
if node.first_seen_us is None:
|
if node.first_seen_us is None:
|
||||||
node.first_seen_us = now_us
|
node.first_seen_us = now_us
|
||||||
@@ -66,7 +71,6 @@ async def process_envelope(topic, env):
|
|||||||
firmware=map_report.firmware_version,
|
firmware=map_report.firmware_version,
|
||||||
last_lat=map_report.latitude_i,
|
last_lat=map_report.latitude_i,
|
||||||
last_long=map_report.longitude_i,
|
last_long=map_report.longitude_i,
|
||||||
last_update=now,
|
|
||||||
first_seen_us=now_us,
|
first_seen_us=now_us,
|
||||||
last_seen_us=now_us,
|
last_seen_us=now_us,
|
||||||
)
|
)
|
||||||
@@ -82,29 +86,43 @@ async def process_envelope(topic, env):
|
|||||||
async with mqtt_database.async_session() as session:
|
async with mqtt_database.async_session() as session:
|
||||||
# --- Packet insert with ON CONFLICT DO NOTHING
|
# --- Packet insert with ON CONFLICT DO NOTHING
|
||||||
result = await session.execute(select(Packet).where(Packet.id == env.packet.id))
|
result = await session.execute(select(Packet).where(Packet.id == env.packet.id))
|
||||||
# FIXME: Not Used
|
|
||||||
# new_packet = False
|
|
||||||
packet = result.scalar_one_or_none()
|
packet = result.scalar_one_or_none()
|
||||||
if not packet:
|
if not packet:
|
||||||
# FIXME: Not Used
|
now_us = int(time.time() * 1_000_000)
|
||||||
# new_packet = True
|
packet_values = {
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
"id": env.packet.id,
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
"portnum": env.packet.decoded.portnum,
|
||||||
stmt = (
|
"from_node_id": getattr(env.packet, "from"),
|
||||||
sqlite_insert(Packet)
|
"to_node_id": env.packet.to,
|
||||||
.values(
|
"payload": env.packet.SerializeToString(),
|
||||||
id=env.packet.id,
|
"import_time_us": now_us,
|
||||||
portnum=env.packet.decoded.portnum,
|
"channel": env.channel_id,
|
||||||
from_node_id=getattr(env.packet, "from"),
|
}
|
||||||
to_node_id=env.packet.to,
|
dialect = session.get_bind().dialect.name
|
||||||
payload=env.packet.SerializeToString(),
|
stmt = None
|
||||||
import_time=now,
|
|
||||||
import_time_us=now_us,
|
if dialect == "sqlite":
|
||||||
channel=env.channel_id,
|
stmt = (
|
||||||
|
sqlite_insert(Packet)
|
||||||
|
.values(**packet_values)
|
||||||
|
.on_conflict_do_nothing(index_elements=["id"])
|
||||||
)
|
)
|
||||||
.on_conflict_do_nothing(index_elements=["id"])
|
elif dialect == "postgresql":
|
||||||
)
|
stmt = (
|
||||||
await session.execute(stmt)
|
pg_insert(Packet)
|
||||||
|
.values(**packet_values)
|
||||||
|
.on_conflict_do_nothing(index_elements=["id"])
|
||||||
|
)
|
||||||
|
|
||||||
|
if stmt is not None:
|
||||||
|
await session.execute(stmt)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
async with session.begin_nested():
|
||||||
|
session.add(Packet(**packet_values))
|
||||||
|
await session.flush()
|
||||||
|
except IntegrityError:
|
||||||
|
pass
|
||||||
|
|
||||||
# --- PacketSeen (no conflict handling here, normal insert)
|
# --- PacketSeen (no conflict handling here, normal insert)
|
||||||
|
|
||||||
@@ -115,6 +133,12 @@ async def process_envelope(topic, env):
|
|||||||
else:
|
else:
|
||||||
node_id = int(env.gateway_id[1:], 16)
|
node_id = int(env.gateway_id[1:], 16)
|
||||||
|
|
||||||
|
if node_id not in MQTT_GATEWAY_CACHE:
|
||||||
|
MQTT_GATEWAY_CACHE.add(node_id)
|
||||||
|
await session.execute(
|
||||||
|
update(Node).where(Node.node_id == node_id).values(is_mqtt_gateway=True)
|
||||||
|
)
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(PacketSeen).where(
|
select(PacketSeen).where(
|
||||||
PacketSeen.packet_id == env.packet.id,
|
PacketSeen.packet_id == env.packet.id,
|
||||||
@@ -123,8 +147,7 @@ async def process_envelope(topic, env):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not result.scalar_one_or_none():
|
if not result.scalar_one_or_none():
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
now_us = int(time.time() * 1_000_000)
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
|
||||||
seen = PacketSeen(
|
seen = PacketSeen(
|
||||||
packet_id=env.packet.id,
|
packet_id=env.packet.id,
|
||||||
node_id=int(env.gateway_id[1:], 16),
|
node_id=int(env.gateway_id[1:], 16),
|
||||||
@@ -135,7 +158,6 @@ async def process_envelope(topic, env):
|
|||||||
hop_limit=env.packet.hop_limit,
|
hop_limit=env.packet.hop_limit,
|
||||||
hop_start=env.packet.hop_start,
|
hop_start=env.packet.hop_start,
|
||||||
topic=topic,
|
topic=topic,
|
||||||
import_time=now,
|
|
||||||
import_time_us=now_us,
|
import_time_us=now_us,
|
||||||
)
|
)
|
||||||
session.add(seen)
|
session.add(seen)
|
||||||
@@ -167,8 +189,7 @@ async def process_envelope(topic, env):
|
|||||||
await session.execute(select(Node).where(Node.id == user.id))
|
await session.execute(select(Node).where(Node.id == user.id))
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
|
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
now_us = int(time.time() * 1_000_000)
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
|
||||||
|
|
||||||
if node:
|
if node:
|
||||||
node.node_id = node_id
|
node.node_id = node_id
|
||||||
@@ -177,7 +198,6 @@ async def process_envelope(topic, env):
|
|||||||
node.hw_model = hw_model
|
node.hw_model = hw_model
|
||||||
node.role = role
|
node.role = role
|
||||||
node.channel = env.channel_id
|
node.channel = env.channel_id
|
||||||
node.last_update = now
|
|
||||||
node.last_seen_us = now_us
|
node.last_seen_us = now_us
|
||||||
if node.first_seen_us is None:
|
if node.first_seen_us is None:
|
||||||
node.first_seen_us = now_us
|
node.first_seen_us = now_us
|
||||||
@@ -190,11 +210,32 @@ async def process_envelope(topic, env):
|
|||||||
hw_model=hw_model,
|
hw_model=hw_model,
|
||||||
role=role,
|
role=role,
|
||||||
channel=env.channel_id,
|
channel=env.channel_id,
|
||||||
last_update=now,
|
|
||||||
first_seen_us=now_us,
|
first_seen_us=now_us,
|
||||||
last_seen_us=now_us,
|
last_seen_us=now_us,
|
||||||
)
|
)
|
||||||
session.add(node)
|
session.add(node)
|
||||||
|
|
||||||
|
if user.public_key:
|
||||||
|
public_key_hex = user.public_key.hex()
|
||||||
|
existing_key = (
|
||||||
|
await session.execute(
|
||||||
|
select(NodePublicKey).where(
|
||||||
|
NodePublicKey.node_id == node_id,
|
||||||
|
NodePublicKey.public_key == public_key_hex,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_key:
|
||||||
|
existing_key.last_seen_us = now_us
|
||||||
|
else:
|
||||||
|
new_key = NodePublicKey(
|
||||||
|
node_id=node_id,
|
||||||
|
public_key=public_key_hex,
|
||||||
|
first_seen_us=now_us,
|
||||||
|
last_seen_us=now_us,
|
||||||
|
)
|
||||||
|
session.add(new_key)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing NODEINFO_APP: {e}")
|
print(f"Error processing NODEINFO_APP: {e}")
|
||||||
|
|
||||||
@@ -209,11 +250,9 @@ async def process_envelope(topic, env):
|
|||||||
await session.execute(select(Node).where(Node.node_id == from_node_id))
|
await session.execute(select(Node).where(Node.node_id == from_node_id))
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
if node:
|
if node:
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
now_us = int(time.time() * 1_000_000)
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
|
||||||
node.last_lat = position.latitude_i
|
node.last_lat = position.latitude_i
|
||||||
node.last_long = position.longitude_i
|
node.last_long = position.longitude_i
|
||||||
node.last_update = now
|
|
||||||
node.last_seen_us = now_us
|
node.last_seen_us = now_us
|
||||||
if node.first_seen_us is None:
|
if node.first_seen_us is None:
|
||||||
node.first_seen_us = now_us
|
node.first_seen_us = now_us
|
||||||
@@ -223,21 +262,23 @@ async def process_envelope(topic, env):
|
|||||||
if env.packet.decoded.portnum == PortNum.TRACEROUTE_APP:
|
if env.packet.decoded.portnum == PortNum.TRACEROUTE_APP:
|
||||||
packet_id = env.packet.id
|
packet_id = env.packet.id
|
||||||
if packet_id is not None:
|
if packet_id is not None:
|
||||||
now = datetime.datetime.now(datetime.UTC)
|
now_us = int(time.time() * 1_000_000)
|
||||||
now_us = int(now.timestamp() * 1_000_000)
|
|
||||||
session.add(
|
session.add(
|
||||||
Traceroute(
|
Traceroute(
|
||||||
packet_id=packet_id,
|
packet_id=packet_id,
|
||||||
route=env.packet.decoded.payload,
|
route=env.packet.decoded.payload,
|
||||||
done=not env.packet.decoded.want_response,
|
done=not env.packet.decoded.want_response,
|
||||||
gateway_node_id=int(env.gateway_id[1:], 16),
|
gateway_node_id=int(env.gateway_id[1:], 16),
|
||||||
import_time=now,
|
|
||||||
import_time_us=now_us,
|
import_time_us=now_us,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
# if new_packet:
|
|
||||||
# await packet.awaitable_attrs.to_node
|
async def load_gateway_cache():
|
||||||
# await packet.awaitable_attrs.from_node
|
async with mqtt_database.async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Node.node_id).where(Node.is_mqtt_gateway == True) # noqa: E712
|
||||||
|
)
|
||||||
|
MQTT_GATEWAY_CACHE.update(result.scalars().all())
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import math
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyitm import itm
|
||||||
|
|
||||||
|
ITM_AVAILABLE = True
|
||||||
|
except Exception:
|
||||||
|
itm = None
|
||||||
|
ITM_AVAILABLE = False
|
||||||
|
|
||||||
|
DEFAULT_CLIMATE = 5 # Continental temperate
|
||||||
|
DEFAULT_GROUND = 0.005 # Average ground conductivity
|
||||||
|
DEFAULT_EPS_DIELECT = 15.0
|
||||||
|
DEFAULT_DELTA_H = 90.0
|
||||||
|
DEFAULT_RELIABILITY = 0.5
|
||||||
|
DEFAULT_MIN_DBM = -130.0
|
||||||
|
DEFAULT_MAX_DBM = -80.0
|
||||||
|
DEFAULT_THRESHOLD_DBM = -120.0
|
||||||
|
EARTH_RADIUS_KM = 6371.0
|
||||||
|
BEARING_STEP_DEG = 5
|
||||||
|
|
||||||
|
|
||||||
|
def destination_point(
|
||||||
|
lat: float, lon: float, bearing_deg: float, distance_km: float
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
lat1 = math.radians(lat)
|
||||||
|
lon1 = math.radians(lon)
|
||||||
|
bearing = math.radians(bearing_deg)
|
||||||
|
|
||||||
|
d = distance_km / EARTH_RADIUS_KM
|
||||||
|
|
||||||
|
lat2 = math.asin(
|
||||||
|
math.sin(lat1) * math.cos(d) + math.cos(lat1) * math.sin(d) * math.cos(bearing)
|
||||||
|
)
|
||||||
|
|
||||||
|
lon2 = lon1 + math.atan2(
|
||||||
|
math.sin(bearing) * math.sin(d) * math.cos(lat1),
|
||||||
|
math.cos(d) - math.sin(lat1) * math.sin(lat2),
|
||||||
|
)
|
||||||
|
|
||||||
|
return math.degrees(lat2), math.degrees(lon2)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=512)
|
||||||
|
def compute_coverage(
|
||||||
|
lat: float,
|
||||||
|
lon: float,
|
||||||
|
freq_mhz: float,
|
||||||
|
tx_dbm: float,
|
||||||
|
tx_height_m: float,
|
||||||
|
rx_height_m: float,
|
||||||
|
radius_km: float,
|
||||||
|
step_km: float,
|
||||||
|
reliability: float,
|
||||||
|
) -> list[tuple[float, float, float]]:
|
||||||
|
if not ITM_AVAILABLE:
|
||||||
|
return []
|
||||||
|
|
||||||
|
points = []
|
||||||
|
distance = max(step_km, 1.0)
|
||||||
|
while distance <= radius_km:
|
||||||
|
for bearing in range(0, 360, BEARING_STEP_DEG):
|
||||||
|
rx_lat, rx_lon = destination_point(lat, lon, bearing, distance)
|
||||||
|
try:
|
||||||
|
loss_db, _ = itm.area(
|
||||||
|
ModVar=2,
|
||||||
|
deltaH=DEFAULT_DELTA_H,
|
||||||
|
tht_m=tx_height_m,
|
||||||
|
rht_m=rx_height_m,
|
||||||
|
dist_km=distance,
|
||||||
|
TSiteCriteria=0,
|
||||||
|
RSiteCriteria=0,
|
||||||
|
eps_dielect=DEFAULT_EPS_DIELECT,
|
||||||
|
sgm_conductivity=DEFAULT_GROUND,
|
||||||
|
eno_ns_surfref=301,
|
||||||
|
frq_mhz=freq_mhz,
|
||||||
|
radio_climate=DEFAULT_CLIMATE,
|
||||||
|
pol=1,
|
||||||
|
pctTime=reliability,
|
||||||
|
pctLoc=0.5,
|
||||||
|
pctConf=0.5,
|
||||||
|
)
|
||||||
|
except itm.InputError:
|
||||||
|
continue
|
||||||
|
rx_dbm = tx_dbm - loss_db
|
||||||
|
points.append((rx_lat, rx_lon, rx_dbm))
|
||||||
|
distance += step_km
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=512)
|
||||||
|
def compute_perimeter(
|
||||||
|
lat: float,
|
||||||
|
lon: float,
|
||||||
|
freq_mhz: float,
|
||||||
|
tx_dbm: float,
|
||||||
|
tx_height_m: float,
|
||||||
|
rx_height_m: float,
|
||||||
|
radius_km: float,
|
||||||
|
step_km: float,
|
||||||
|
reliability: float,
|
||||||
|
threshold_dbm: float,
|
||||||
|
) -> list[tuple[float, float]]:
|
||||||
|
if not ITM_AVAILABLE:
|
||||||
|
return []
|
||||||
|
|
||||||
|
perimeter = []
|
||||||
|
distance = max(step_km, 1.0)
|
||||||
|
for bearing in range(0, 360, BEARING_STEP_DEG):
|
||||||
|
last_point = None
|
||||||
|
dist = distance
|
||||||
|
while dist <= radius_km:
|
||||||
|
try:
|
||||||
|
loss_db, _ = itm.area(
|
||||||
|
ModVar=2,
|
||||||
|
deltaH=DEFAULT_DELTA_H,
|
||||||
|
tht_m=tx_height_m,
|
||||||
|
rht_m=rx_height_m,
|
||||||
|
dist_km=dist,
|
||||||
|
TSiteCriteria=0,
|
||||||
|
RSiteCriteria=0,
|
||||||
|
eps_dielect=DEFAULT_EPS_DIELECT,
|
||||||
|
sgm_conductivity=DEFAULT_GROUND,
|
||||||
|
eno_ns_surfref=301,
|
||||||
|
frq_mhz=freq_mhz,
|
||||||
|
radio_climate=DEFAULT_CLIMATE,
|
||||||
|
pol=1,
|
||||||
|
pctTime=reliability,
|
||||||
|
pctLoc=0.5,
|
||||||
|
pctConf=0.5,
|
||||||
|
)
|
||||||
|
except itm.InputError:
|
||||||
|
dist += step_km
|
||||||
|
continue
|
||||||
|
|
||||||
|
rx_dbm = tx_dbm - loss_db
|
||||||
|
if rx_dbm >= threshold_dbm:
|
||||||
|
last_point = destination_point(lat, lon, bearing, dist)
|
||||||
|
dist += step_km
|
||||||
|
|
||||||
|
if last_point:
|
||||||
|
perimeter.append(last_point)
|
||||||
|
|
||||||
|
return perimeter
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>API Documentation - Config</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body { margin: 0; background: #ffffff; color: #000; }
|
|
||||||
#swagger-ui { background: #ffffff; color: #000; }
|
|
||||||
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
|
|
||||||
.swagger-ui .topbar,
|
|
||||||
.swagger-ui .info,
|
|
||||||
.swagger-ui .opblock-summary-description,
|
|
||||||
.swagger-ui .parameters-col_description,
|
|
||||||
.swagger-ui .response-col_description { color: #000 !important; }
|
|
||||||
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
|
|
||||||
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
|
|
||||||
.swagger-ui .opblock-section-header { color: #000 !important; }
|
|
||||||
.swagger-ui .parameters,
|
|
||||||
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
|
|
||||||
.swagger-ui table { color: #000 !important; }
|
|
||||||
.swagger-ui a { color: #1a0dab !important; }
|
|
||||||
.swagger-ui input,
|
|
||||||
.swagger-ui select,
|
|
||||||
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const spec = {
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
title: "Site Config API",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "API for retrieving the site configuration. This endpoint does not take any parameters."
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
"/api/config": {
|
|
||||||
get: {
|
|
||||||
summary: "Get site configuration",
|
|
||||||
description: "Returns the current site configuration object.",
|
|
||||||
responses: {
|
|
||||||
"200": {
|
|
||||||
description: "Successful response",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
site_config: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: true,
|
|
||||||
example: {
|
|
||||||
site_name: "MeshView",
|
|
||||||
firehose_interval: 1000,
|
|
||||||
starting: "/nodes",
|
|
||||||
theme: "dark"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
description: "Server error",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
error: { type: "string", example: "Internal server error" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
SwaggerUIBundle({
|
|
||||||
spec,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
layout: "BaseLayout"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>API Documentation - Edges</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
#swagger-ui { background: #ffffff; color: #000; }
|
|
||||||
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
|
|
||||||
.swagger-ui .topbar,
|
|
||||||
.swagger-ui .info,
|
|
||||||
.swagger-ui .opblock-summary-description,
|
|
||||||
.swagger-ui .parameters-col_description,
|
|
||||||
.swagger-ui .response-col_description { color: #000 !important; }
|
|
||||||
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
|
|
||||||
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
|
|
||||||
.swagger-ui .opblock-section-header { color: #000 !important; }
|
|
||||||
.swagger-ui .parameters,
|
|
||||||
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
|
|
||||||
.swagger-ui table { color: #000 !important; }
|
|
||||||
.swagger-ui a { color: #1a0dab !important; }
|
|
||||||
.swagger-ui input,
|
|
||||||
.swagger-ui select,
|
|
||||||
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const spec = {
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
title: "Network Edges API",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "API for retrieving network edges derived from traceroutes and neighbor info packets, with optional type filtering."
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
"/api/edges": {
|
|
||||||
get: {
|
|
||||||
summary: "Get network edges",
|
|
||||||
description: "Returns edges between nodes in the network. Optionally filter by type (`traceroute` or `neighbor`).",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "type",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Optional filter to only return edges of this type (`traceroute` or `neighbor`).",
|
|
||||||
schema: { type: "string", enum: ["traceroute", "neighbor"] }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
responses: {
|
|
||||||
"200": {
|
|
||||||
description: "Successful response",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
edges: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
from: { type: "integer", example: 101 },
|
|
||||||
to: { type: "integer", example: 102 },
|
|
||||||
type: { type: "string", example: "traceroute" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
description: "Invalid request parameters",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: { type: "object", properties: { error: { type: "string" } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
SwaggerUIBundle({
|
|
||||||
spec,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
layout: "BaseLayout"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>API Documentation - Nodes</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body { margin: 0; background: #ffffff; color: #000; }
|
|
||||||
#swagger-ui { background: #ffffff; color: #000; }
|
|
||||||
.swagger-ui { background-color: #ffffff !important; color: #000 !important; }
|
|
||||||
.swagger-ui .topbar,
|
|
||||||
.swagger-ui .info,
|
|
||||||
.swagger-ui .opblock-summary-description,
|
|
||||||
.swagger-ui .parameters-col_description,
|
|
||||||
.swagger-ui .response-col_description { color: #000 !important; }
|
|
||||||
.swagger-ui .opblock { background-color: #f9f9f9 !important; border-color: #ddd !important; }
|
|
||||||
.swagger-ui .opblock-summary { background-color: #eaeaea !important; color: #000 !important; }
|
|
||||||
.swagger-ui .opblock-section-header { color: #000 !important; }
|
|
||||||
.swagger-ui .parameters,
|
|
||||||
.swagger-ui .response { background-color: #fafafa !important; color: #000 !important; }
|
|
||||||
.swagger-ui table { color: #000 !important; }
|
|
||||||
.swagger-ui a { color: #1a0dab !important; }
|
|
||||||
.swagger-ui input,
|
|
||||||
.swagger-ui select,
|
|
||||||
.swagger-ui textarea { background-color: #fff !important; color: #000 !important; border: 1px solid #ccc !important; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const spec = {
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
title: "Network Nodes API",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "API for retrieving nodes in the network with optional filters by last seen time."
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
"/api/nodes": {
|
|
||||||
get: {
|
|
||||||
summary: "Get network nodes",
|
|
||||||
description: "Returns a list of nodes with optional filtering by recent activity.",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "hours",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Return nodes seen in the last X hours.",
|
|
||||||
schema: { type: "integer", example: 24 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "days",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Return nodes seen in the last X days.",
|
|
||||||
schema: { type: "integer", example: 7 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_seen_after",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Return nodes last seen after this ISO8601 timestamp.",
|
|
||||||
schema: { type: "string", format: "date-time", example: "2025-08-25T14:00:00" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
responses: {
|
|
||||||
"200": {
|
|
||||||
description: "Successful response",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
nodes: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
node_id: { type: "integer", example: 101 },
|
|
||||||
long_name: { type: "string", example: "Node Alpha" },
|
|
||||||
short_name: { type: "string", example: "A" },
|
|
||||||
channel: { type: "string", example: "2" },
|
|
||||||
last_seen: { type: "string", format: "date-time", example: "2025-08-25T12:00:00" },
|
|
||||||
last_lat: { type: "number", format: "float", example: 37.7749 },
|
|
||||||
last_long: { type: "number", format: "float", example: -122.4194 },
|
|
||||||
hardware: { type: "string", example: "Heltec V3" },
|
|
||||||
firmware: { type: "string", example: "1.0.5" },
|
|
||||||
role: { type: "string", example: "router" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
description: "Invalid request parameters",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: { type: "object", properties: { error: { type: "string" } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
description: "Server error",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: { type: "object", properties: { error: { type: "string" } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
SwaggerUIBundle({
|
|
||||||
spec,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
layout: "BaseLayout"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>API Documentation - Packets</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
#swagger-ui {
|
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override Swagger UI colors for white background */
|
|
||||||
.swagger-ui {
|
|
||||||
background-color: #ffffff !important;
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .topbar,
|
|
||||||
.swagger-ui .info,
|
|
||||||
.swagger-ui .opblock-summary-description,
|
|
||||||
.swagger-ui .parameters-col_description,
|
|
||||||
.swagger-ui .response-col_description {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock {
|
|
||||||
background-color: #f9f9f9 !important;
|
|
||||||
border-color: #ddd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock-summary {
|
|
||||||
background-color: #eaeaea !important;
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock-section-header {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .parameters,
|
|
||||||
.swagger-ui .response {
|
|
||||||
background-color: #fafafa !important;
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui table {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui a {
|
|
||||||
color: #1a0dab !important; /* classic link blue */
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui input,
|
|
||||||
.swagger-ui select,
|
|
||||||
.swagger-ui textarea {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: #000 !important;
|
|
||||||
border: 1px solid #ccc !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const spec = {
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
title: "Packets API",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "API for retrieving packet records with optional filters."
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
"/api/packets": {
|
|
||||||
get: {
|
|
||||||
summary: "Get packets",
|
|
||||||
description: "Returns a list of recent packets, optionally filtered by a timestamp and limited by count.",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "limit",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Maximum number of packets to return. Default is 200.",
|
|
||||||
schema: {
|
|
||||||
type: "integer",
|
|
||||||
default: 200
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "since",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Only return packets imported after this ISO8601 timestamp (e.g., `2025-08-12T14:15:20`).",
|
|
||||||
schema: {
|
|
||||||
type: "string",
|
|
||||||
format: "date-time"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
responses: {
|
|
||||||
"200": {
|
|
||||||
description: "Successful response",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
packets: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
id: { type: "integer", example: 196988973 },
|
|
||||||
from_node_id: { type: "integer", example: 2381019191 },
|
|
||||||
to_node_id: { type: "integer", example: 1234567890 },
|
|
||||||
portnum: { type: "integer", example: 1 },
|
|
||||||
import_time: { type: "string", format: "date-time", example: "2025-08-12T14:15:20.503827" },
|
|
||||||
payload: { type: "string", example: "Hello Mesh" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
description: "Internal server error",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
error: { type: "string", example: "Failed to fetch packets" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
SwaggerUIBundle({
|
|
||||||
spec,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
layout: "BaseLayout"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>API Documentation - Packet Stats</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
#swagger-ui {
|
|
||||||
background: #ffffff;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override Swagger UI colors for white background */
|
|
||||||
.swagger-ui {
|
|
||||||
background-color: #ffffff !important;
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .topbar,
|
|
||||||
.swagger-ui .info,
|
|
||||||
.swagger-ui .opblock-summary-description,
|
|
||||||
.swagger-ui .parameters-col_description,
|
|
||||||
.swagger-ui .response-col_description {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock {
|
|
||||||
background-color: #f9f9f9 !important;
|
|
||||||
border-color: #ddd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock-summary {
|
|
||||||
background-color: #eaeaea !important;
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .opblock-section-header {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui .parameters,
|
|
||||||
.swagger-ui .response {
|
|
||||||
background-color: #fafafa !important;
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui table {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui a {
|
|
||||||
color: #1a0dab !important; /* classic link blue */
|
|
||||||
}
|
|
||||||
|
|
||||||
.swagger-ui input,
|
|
||||||
.swagger-ui select,
|
|
||||||
.swagger-ui textarea {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: #000 !important;
|
|
||||||
border: 1px solid #ccc !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const spec = {
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
title: "Packet Statistics API",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "API for retrieving packet statistics over a given period with optional filters."
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
"/api/stats": {
|
|
||||||
get: {
|
|
||||||
summary: "Get packet statistics",
|
|
||||||
description: "Returns packet statistics for a given period type and length, with optional filters.",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "period_type",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Type of period to group by (`hour` or `day`). Default is `hour`.",
|
|
||||||
schema: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["hour", "day"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "length",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Number of periods to include. Default is 24.",
|
|
||||||
schema: {
|
|
||||||
type: "integer",
|
|
||||||
default: 24
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "channel",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Filter by channel name.",
|
|
||||||
schema: {
|
|
||||||
type: "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "portnum",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Filter by port number.",
|
|
||||||
schema: {
|
|
||||||
type: "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "to_node",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Filter by destination node ID.",
|
|
||||||
schema: {
|
|
||||||
type: "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "from_node",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
description: "Filter by source node ID.",
|
|
||||||
schema: {
|
|
||||||
type: "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
responses: {
|
|
||||||
"200": {
|
|
||||||
description: "Successful response",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
hourly: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
period_type: { type: "string" },
|
|
||||||
length: { type: "integer" },
|
|
||||||
filters: { type: "object" },
|
|
||||||
data: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
period: { type: "string", example: "2025-08-06 19:00" },
|
|
||||||
node_id: { type: "integer" },
|
|
||||||
long_name: { type: "string" },
|
|
||||||
short_name: { type: "string" },
|
|
||||||
packets: { type: "integer" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
description: "Invalid request parameters",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
error: { type: "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
SwaggerUIBundle({
|
|
||||||
spec,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
layout: "BaseLayout"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+254
-72
@@ -1,91 +1,273 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<title>Meshview API Documentation</title>
|
||||||
<title>API Index</title>
|
<style>
|
||||||
<style>
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
background: #121212;
|
||||||
background-color: #1e1e1e;
|
color: #eee;
|
||||||
color: #eaeaea;
|
font-family: monospace;
|
||||||
margin: 0;
|
margin: 20px;
|
||||||
padding: 0;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
header {
|
h1, h2, h3 { color: #79c0ff; }
|
||||||
background: #2a2a2a;
|
code {
|
||||||
padding: 20px;
|
background: #1e1e1e;
|
||||||
text-align: center;
|
padding: 3px 6px;
|
||||||
font-size: 1.6em;
|
border-radius: 4px;
|
||||||
font-weight: bold;
|
color: #ffd479;
|
||||||
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
.container {
|
.endpoint {
|
||||||
max-width: 800px;
|
border: 1px solid #333;
|
||||||
margin: 30px auto;
|
padding: 12px;
|
||||||
padding: 20px;
|
margin-bottom: 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1a1a1a;
|
||||||
}
|
}
|
||||||
ul {
|
.method {
|
||||||
list-style: none;
|
display: inline-block;
|
||||||
padding: 0;
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
li {
|
.get { background: #0066cc; }
|
||||||
background: #272b2f;
|
.path { font-weight: bold; color: #fff; }
|
||||||
border: 1px solid #474b4e;
|
table {
|
||||||
padding: 15px 20px;
|
width: 100%;
|
||||||
margin-bottom: 15px;
|
border-collapse: collapse;
|
||||||
border-radius: 10px;
|
margin-top: 8px;
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
}
|
||||||
li:hover {
|
th, td {
|
||||||
background: #33383d;
|
border: 1px solid #444;
|
||||||
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
a {
|
th {
|
||||||
color: #4cafef;
|
background: #222;
|
||||||
text-decoration: none;
|
color: #9ddcff;
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
}
|
||||||
p {
|
.example {
|
||||||
margin: 8px 0 0 0;
|
margin-top: 10px;
|
||||||
font-size: 0.9em;
|
padding: 10px;
|
||||||
color: #bbbbbb;
|
background: #161616;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<h1>Meshview API Documentation</h1>
|
||||||
API Index
|
<p>This page describes all REST endpoints provided by Meshview.</p>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<ul>
|
<!------------------------------ NODES ------------------------------>
|
||||||
<li>
|
<h2>/api/nodes</h2>
|
||||||
<a href="/api-chat">Chat API</a>
|
|
||||||
<p> View chat messages.</p>
|
<div class="endpoint">
|
||||||
</li>
|
<span class="method get">GET</span>
|
||||||
<li>
|
<span class="path">/api/nodes</span>
|
||||||
<a href="/api-nodes">Node API</a>
|
<p>Returns a list of mesh nodes.</p>
|
||||||
<p>Retrieve node information.</p>
|
|
||||||
</li>
|
<h3>Query Parameters</h3>
|
||||||
<li>
|
<table>
|
||||||
<a href="/api-packets">Packet API</a>
|
<tr><th>Parameter</th><th>Description</th></tr>
|
||||||
<p>Access raw packet data.</p>
|
<tr><td>role</td><td>Filter by node role</td></tr>
|
||||||
</li>
|
<tr><td>channel</td><td>Filter by channel</td></tr>
|
||||||
<li>
|
<tr><td>hw_model</td><td>Hardware model filter</td></tr>
|
||||||
<a href="/api-stats">Statistics API </a>
|
<tr><td>days_active</td><td>Only nodes seen within X days</td></tr>
|
||||||
<p>View system and traffic statistics.</p>
|
</table>
|
||||||
</li>
|
|
||||||
<li>
|
<div class="example">
|
||||||
<a href="/api-edges">Edges API</a>
|
<b>Example:</b><br>
|
||||||
<p>Get edges details.</p>
|
<code>/api/nodes?days_active=3</code>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
|
||||||
<a href="/api-config">Configuration API</a>
|
|
||||||
<p>Get and update configuration details.</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ PACKETS ------------------------------>
|
||||||
|
<h2>/api/packets</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/packets</span>
|
||||||
|
<p>Fetch packets with many filters. Returns decoded packet data.</p>
|
||||||
|
|
||||||
|
<h3>Query Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Parameter</th><th>Description</th></tr>
|
||||||
|
<tr><td>packet_id</td><td>Return exactly one packet</td></tr>
|
||||||
|
<tr><td>limit</td><td>Max number of results (1–100)</td></tr>
|
||||||
|
<tr><td>since</td><td>Only packets newer than import_time_us</td></tr>
|
||||||
|
<tr><td>from_node_id</td><td>Filter by sender node</td></tr>
|
||||||
|
<tr><td>to_node_id</td><td>Filter by destination node</td></tr>
|
||||||
|
<tr><td>node_id</td><td>Legacy: match either from or to</td></tr>
|
||||||
|
<tr><td>portnum</td><td>Filter by port number</td></tr>
|
||||||
|
<tr><td>contains</td><td>Substring filter for payload</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<b>Example:</b><br>
|
||||||
|
<code>/api/packets?from_node_id=123&limit=100</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ PACKETS SEEN ------------------------------>
|
||||||
|
<h2>/api/packets_seen/{packet_id}</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/packets_seen/<packet_id></span>
|
||||||
|
<p>Returns list of gateways that heard the packet (RSSI/SNR/hops).</p>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<b>Example:</b><br>
|
||||||
|
<code>/api/packets_seen/3314808102</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ STATS ------------------------------>
|
||||||
|
<h2>/api/stats</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/stats</span>
|
||||||
|
<p>Returns aggregated packet statistics for a node or globally.</p>
|
||||||
|
|
||||||
|
<h3>Query Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Parameter</th><th>Description</th></tr>
|
||||||
|
<tr><td>period_type</td><td>"hour" or "day"</td></tr>
|
||||||
|
<tr><td>length</td><td>How many hours/days</td></tr>
|
||||||
|
<tr><td>node</td><td>Node ID for combined sent+seen stats</td></tr>
|
||||||
|
<tr><td>from_node</td><td>Filter by sender</td></tr>
|
||||||
|
<tr><td>to_node</td><td>Filter by receiver</td></tr>
|
||||||
|
<tr><td>portnum</td><td>Filter by port</td></tr>
|
||||||
|
<tr><td>channel</td><td>Filter by channel</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<b>Example:</b><br>
|
||||||
|
<code>/api/stats?node=1128180332&period_type=day&length=1</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ STATS COUNT ------------------------------>
|
||||||
|
<h2>/api/stats/count</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/stats/count</span>
|
||||||
|
<p>
|
||||||
|
Returns <b>total packets</b> and <b>total packet_seen entries</b>.
|
||||||
|
When no filters are provided, it returns global totals.
|
||||||
|
When filters are specified, they narrow the time, channel,
|
||||||
|
direction, or specific packet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Query Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Parameter</th><th>Description</th></tr>
|
||||||
|
<tr><td>period_type</td><td>"hour" or "day"</td></tr>
|
||||||
|
<tr><td>length</td><td>Number of hours or days (depends on period_type)</td></tr>
|
||||||
|
<tr><td>channel</td><td>Filter by channel</td></tr>
|
||||||
|
<tr><td>from_node</td><td>Only packets sent by this node</td></tr>
|
||||||
|
<tr><td>to_node</td><td>Only packets received by this node</td></tr>
|
||||||
|
<tr><td>packet_id</td><td>Filter seen counts for specific packet_id</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<b>Example:</b><br>
|
||||||
|
<code>/api/stats/count?from_node=1128180332&period_type=day&length=1</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ EDGES ------------------------------>
|
||||||
|
<h2>/api/edges</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/edges</span>
|
||||||
|
<p>Returns traceroute and/or neighbor edges for graph rendering.</p>
|
||||||
|
|
||||||
|
<h3>Query Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Parameter</th><th>Description</th></tr>
|
||||||
|
<tr><td>type</td><td>"traceroute", "neighbor", or omitted for both</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<b>Example:</b><br>
|
||||||
|
<code>/api/edges?type=neighbor</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ CONFIG ------------------------------>
|
||||||
|
<h2>/api/config</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/config</span>
|
||||||
|
<p>Returns Meshview configuration (site, MQTT, cleanup, etc.).</p>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<code>/api/config</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ LANG ------------------------------>
|
||||||
|
<h2>/api/lang</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/api/lang</span>
|
||||||
|
<p>Returns translated text for the UI.</p>
|
||||||
|
|
||||||
|
<h3>Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>lang</th><td>Language code (e.g. "en")</td></tr>
|
||||||
|
<tr><th>section</th><td>Optional UI section (firehose, map, top...)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="example">
|
||||||
|
<code>/api/lang?lang=en§ion=firehose</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ HEALTH ------------------------------>
|
||||||
|
<h2>/health</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/health</span>
|
||||||
|
<p>Returns API + database status.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!------------------------------ VERSION ------------------------------>
|
||||||
|
<h2>/version</h2>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method get">GET</span>
|
||||||
|
<span class="path">/version</span>
|
||||||
|
<p>Returns Meshview version and Git revision.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
<hr>
|
||||||
|
<p style="text-align:center; color:#666;">Meshview API — generated documentation</p>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+40
-20
@@ -44,6 +44,7 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
|
|||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin></script>
|
||||||
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js" crossorigin></script>
|
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js" crossorigin></script>
|
||||||
|
<script src="/static/portmaps.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(async function(){
|
(async function(){
|
||||||
@@ -75,8 +76,8 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr){
|
function timeAgoFromUs(us){
|
||||||
const diff = Date.now() - new Date(dateStr);
|
const diff = Date.now() - (us / 1000);
|
||||||
const s=Math.floor(diff/1000), m=Math.floor(s/60), h=Math.floor(m/60), d=Math.floor(h/24);
|
const s=Math.floor(diff/1000), m=Math.floor(s/60), h=Math.floor(m/60), d=Math.floor(h/24);
|
||||||
if(d>0) return d+'d'; if(h>0) return h+'h'; if(m>0) return m+'m'; return s+'s';
|
if(d>0) return d+'d'; if(h>0) return h+'h'; if(m>0) return m+'m'; return s+'s';
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
|
|||||||
const channels = new Set();
|
const channels = new Set();
|
||||||
const activeBlinks = new Map();
|
const activeBlinks = new Map();
|
||||||
|
|
||||||
const portMap = {1:"Text",67:"Telemetry",3:"Position",70:"Traceroute",4:"Node Info",71:"Neighbour Info",73:"Map Report"};
|
const portMap = window.PORT_LABEL_MAP;
|
||||||
|
|
||||||
nodes.forEach(node=>{
|
nodes.forEach(node=>{
|
||||||
if(isInvalidCoord(node)) return;
|
if(isInvalidCoord(node)) return;
|
||||||
@@ -118,7 +119,7 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
|
|||||||
<b>Channel:</b> ${node.channel}<br>
|
<b>Channel:</b> ${node.channel}<br>
|
||||||
<b>Model:</b> ${node.hw_model}<br>
|
<b>Model:</b> ${node.hw_model}<br>
|
||||||
<b>Role:</b> ${node.role}<br>`;
|
<b>Role:</b> ${node.role}<br>`;
|
||||||
if(node.last_update) popupContent+=`<b>Last seen:</b> ${timeAgo(node.last_update)}<br>`;
|
if(node.last_seen_us) popupContent+=`<b>Last seen:</b> ${timeAgoFromUs(node.last_seen_us)}<br>`;
|
||||||
if(node.firmware) popupContent+=`<b>Firmware:</b> ${node.firmware}<br>`;
|
if(node.firmware) popupContent+=`<b>Firmware:</b> ${node.firmware}<br>`;
|
||||||
|
|
||||||
marker.on('click', e=>{
|
marker.on('click', e=>{
|
||||||
@@ -190,32 +191,51 @@ body { margin: 0; font-family: monospace; background: #121212; color: #eee; }
|
|||||||
activeBlinks.set(marker,interval);
|
activeBlinks.set(marker,interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastImportTime=null;
|
let lastImportTimeUs = null;
|
||||||
|
|
||||||
async function fetchLatestPacket(){
|
async function fetchLatestPacket(){
|
||||||
try{
|
try{
|
||||||
const res=await fetch(`/api/packets?limit=1`);
|
const res = await fetch(`/api/packets?limit=1`);
|
||||||
const data=await res.json();
|
const data = await res.json();
|
||||||
lastImportTime=data.packets?.[0]?.import_time || new Date().toISOString();
|
lastImportTimeUs = data.packets?.[0]?.import_time_us || 0;
|
||||||
}catch(err){ console.error(err); }
|
}catch(err){
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNewPackets(){
|
async function fetchNewPackets(){
|
||||||
if(!lastImportTime) return;
|
if (!lastImportTimeUs) return;
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const res=await fetch(`/api/packets?since=${lastImportTime}`);
|
const res = await fetch(`/api/packets?since=${lastImportTimeUs}`);
|
||||||
const data=await res.json();
|
const data = await res.json();
|
||||||
if(!data.packets || !data.packets.length) return;
|
if(!data.packets || !data.packets.length) return;
|
||||||
let latest=lastImportTime;
|
|
||||||
data.packets.forEach(packet=>{
|
let latest = lastImportTimeUs;
|
||||||
if(packet.import_time && packet.import_time>latest) latest=packet.import_time;
|
|
||||||
const marker=markerById[packet.from_node_id];
|
data.packets.forEach(packet => {
|
||||||
const nodeData=nodeMap.get(packet.from_node_id);
|
|
||||||
if(marker && nodeData) blinkNode(marker,nodeData.long_name,packet.portnum);
|
// Track newest microsecond timestamp
|
||||||
|
if (packet.import_time_us && packet.import_time_us > latest) {
|
||||||
|
latest = packet.import_time_us;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up marker and blink it
|
||||||
|
const marker = markerById[packet.from_node_id];
|
||||||
|
const nodeData = nodeMap.get(packet.from_node_id);
|
||||||
|
if (marker && nodeData) {
|
||||||
|
blinkNode(marker, nodeData.long_name, packet.portnum);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
lastImportTime=latest;
|
|
||||||
}catch(err){ console.error(err); }
|
lastImportTimeUs = latest;
|
||||||
|
|
||||||
|
}catch(err){
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if(mapInterval>0){ fetchLatestPacket(); setInterval(fetchNewPackets,mapInterval*1000); }
|
if(mapInterval>0){ fetchLatestPacket(); setInterval(fetchNewPackets,mapInterval*1000); }
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Mesh Nodes Live Map</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<style>
|
|
||||||
body { margin: 0; }
|
|
||||||
#map { height: 100vh; width: 100%; }
|
|
||||||
|
|
||||||
#legend {
|
|
||||||
position: absolute; bottom: 10px; right: 10px;
|
|
||||||
background: rgba(0,0,0,0.7);
|
|
||||||
color: white; padding: 8px 12px;
|
|
||||||
font-family: monospace; font-size: 13px;
|
|
||||||
border-radius: 5px; z-index: 1000;
|
|
||||||
}
|
|
||||||
.legend-item { display: flex; align-items: center; margin-bottom: 4px; }
|
|
||||||
.legend-color { width: 16px; height: 16px; margin-right: 6px; border-radius: 4px; }
|
|
||||||
|
|
||||||
.pulse-label span {
|
|
||||||
background: rgba(0,0,0,0.6);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
pointer-events: none;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="map"></div>
|
|
||||||
<div id="legend"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script>
|
|
||||||
const map = L.map("map");
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(map);
|
|
||||||
|
|
||||||
const nodeMarkers = new Map();
|
|
||||||
let lastPacketTime = null;
|
|
||||||
|
|
||||||
const portColors = {
|
|
||||||
1:"red",
|
|
||||||
67:"cyan",
|
|
||||||
3:"orange",
|
|
||||||
70:"purple",
|
|
||||||
4:"yellow",
|
|
||||||
71:"brown",
|
|
||||||
73:"pink"
|
|
||||||
};
|
|
||||||
const portLabels = {
|
|
||||||
1:"Text",
|
|
||||||
67:"Telemetry",
|
|
||||||
3:"Position",
|
|
||||||
70:"Traceroute",
|
|
||||||
4:"Node Info",
|
|
||||||
71:"Neighbour Info",
|
|
||||||
73:"Map Report"
|
|
||||||
};
|
|
||||||
function getPulseColor(portnum) { return portColors[portnum] || "green"; }
|
|
||||||
|
|
||||||
// Legend
|
|
||||||
const legend = document.getElementById("legend");
|
|
||||||
for (const [port, color] of Object.entries(portColors)) {
|
|
||||||
const item = document.createElement("div");
|
|
||||||
item.className = "legend-item";
|
|
||||||
const colorBox = document.createElement("div");
|
|
||||||
colorBox.className = "legend-color";
|
|
||||||
colorBox.style.background = color;
|
|
||||||
const label = document.createElement("span");
|
|
||||||
label.textContent = `${portLabels[port] || "Custom"} (${port})`;
|
|
||||||
item.appendChild(colorBox);
|
|
||||||
item.appendChild(label);
|
|
||||||
legend.appendChild(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pulse marker
|
|
||||||
function pulseMarker(marker, highlightColor = "red") {
|
|
||||||
if (!marker || marker.activePulse) return;
|
|
||||||
marker.activePulse = true;
|
|
||||||
|
|
||||||
const originalColor = marker.options.originalColor;
|
|
||||||
const originalRadius = marker.options.originalRadius;
|
|
||||||
marker.bringToFront();
|
|
||||||
|
|
||||||
const nodeInfo = marker.options.nodeInfo || {};
|
|
||||||
const portLabel = marker.currentPortLabel || "";
|
|
||||||
const displayName = `${nodeInfo.long_name || nodeInfo.short_name || "Unknown"}${portLabel ? ` (<i>${portLabel}</i>)` : ""}`;
|
|
||||||
|
|
||||||
marker.bindTooltip(displayName, {
|
|
||||||
permanent: true,
|
|
||||||
direction: 'top',
|
|
||||||
className: 'pulse-label',
|
|
||||||
offset: [0, -10],
|
|
||||||
html: true
|
|
||||||
}).openTooltip();
|
|
||||||
|
|
||||||
const flashDuration = 2000, fadeDuration = 1000, flashInterval = 100, maxRadius = originalRadius + 5;
|
|
||||||
let flashTime = 0;
|
|
||||||
|
|
||||||
const flashTimer = setInterval(() => {
|
|
||||||
flashTime += flashInterval;
|
|
||||||
const isOn = (flashTime / flashInterval) % 2 === 0;
|
|
||||||
marker.setStyle({ fillColor: isOn ? highlightColor : originalColor, radius: isOn ? maxRadius : originalRadius });
|
|
||||||
|
|
||||||
if (flashTime >= flashDuration) {
|
|
||||||
clearInterval(flashTimer);
|
|
||||||
const fadeStart = performance.now();
|
|
||||||
function fade(now) {
|
|
||||||
const t = Math.min((now - fadeStart) / fadeDuration, 1);
|
|
||||||
const radius = originalRadius + (maxRadius - originalRadius) * (1 - t);
|
|
||||||
marker.setStyle({ fillColor: highlightColor, radius: radius, fillOpacity: 1 });
|
|
||||||
if (t < 1) requestAnimationFrame(fade);
|
|
||||||
else {
|
|
||||||
marker.setStyle({ fillColor: originalColor, radius: originalRadius, fillOpacity: 1 });
|
|
||||||
marker.unbindTooltip();
|
|
||||||
marker.activePulse = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestAnimationFrame(fade);
|
|
||||||
}
|
|
||||||
}, flashInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Load nodes ---
|
|
||||||
async function loadNodes() {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/nodes");
|
|
||||||
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
const nodes = data.nodes || [];
|
|
||||||
|
|
||||||
nodes.forEach(node => {
|
|
||||||
const lat = node.last_lat / 1e7;
|
|
||||||
const lng = node.last_long / 1e7;
|
|
||||||
if (lat && lng && !isNaN(lat) && !isNaN(lng)) {
|
|
||||||
const color = "blue";
|
|
||||||
const marker = L.circleMarker([lat,lng], {
|
|
||||||
radius:7, color:"white", fillColor:color, fillOpacity:1, weight:0.7
|
|
||||||
}).addTo(map);
|
|
||||||
marker.options.originalColor = color;
|
|
||||||
marker.options.originalRadius = 7;
|
|
||||||
marker.options.nodeInfo = node;
|
|
||||||
marker.bindPopup(`<b>${node.long_name||node.short_name||"Unknown"}</b><br>ID: ${node.node_id}<br>Role: ${node.role}`);
|
|
||||||
nodeMarkers.set(node.node_id, marker);
|
|
||||||
} else {
|
|
||||||
nodeMarkers.set(node.node_id, {options:{nodeInfo:node}});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const markersWithCoords = Array.from(nodeMarkers.values()).filter(m=>m instanceof L.CircleMarker);
|
|
||||||
if(markersWithCoords.length>0) await setMapBoundsFromConfig();
|
|
||||||
else map.setView([37.77,-122.42],9);
|
|
||||||
|
|
||||||
} catch(err){
|
|
||||||
console.error("Failed to load nodes:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Map bounds ---
|
|
||||||
async function setMapBoundsFromConfig() {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/config");
|
|
||||||
const config = await res.json();
|
|
||||||
const topLat = parseFloat(config.site.map_top_left_lat);
|
|
||||||
const topLon = parseFloat(config.site.map_top_left_lon);
|
|
||||||
const bottomLat = parseFloat(config.site.map_bottom_right_lat);
|
|
||||||
const bottomLon = parseFloat(config.site.map_bottom_right_lon);
|
|
||||||
|
|
||||||
if ([topLat, topLon, bottomLat, bottomLon].some(v => isNaN(v))) {
|
|
||||||
throw new Error("Map bounds contain NaN");
|
|
||||||
}
|
|
||||||
|
|
||||||
map.fitBounds([[topLat, topLon], [bottomLat, bottomLon]]);
|
|
||||||
} catch(err) {
|
|
||||||
console.error("Failed to load map bounds from config:", err);
|
|
||||||
map.setView([37.77,-122.42],9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Poll packets ---
|
|
||||||
async function pollPackets() {
|
|
||||||
try {
|
|
||||||
let url = "/api/packets?limit=10";
|
|
||||||
if (lastPacketTime) url += `&since=${lastPacketTime}`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
const packets = data.packets || [];
|
|
||||||
if (packets.length) lastPacketTime = packets[0].import_time;
|
|
||||||
|
|
||||||
packets.forEach(pkt => {
|
|
||||||
const marker = nodeMarkers.get(pkt.from_node_id);
|
|
||||||
if (marker instanceof L.CircleMarker) { // only real markers
|
|
||||||
marker.currentPortLabel = portLabels[pkt.portnum] || `${pkt.portnum}`;
|
|
||||||
pulseMarker(marker, getPulseColor(pkt.portnum));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(err){
|
|
||||||
console.error("Failed to fetch packets:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Initialize ---
|
|
||||||
loadNodes().then(() => setInterval(pollPackets, 1000));
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Shared port label/color definitions for UI pages.
|
||||||
|
// Port numbers defined in: https://github.com/meshtastic/protobufs/blob/master/meshtastic/portnums.proto
|
||||||
|
window.PORT_LABEL_MAP = {
|
||||||
|
0: "Unknown",
|
||||||
|
1: "Text",
|
||||||
|
2: "Remote Hardware",
|
||||||
|
3: "Position",
|
||||||
|
4: "Node Info",
|
||||||
|
5: "Routing",
|
||||||
|
6: "Admin",
|
||||||
|
7: "Text (Compressed)",
|
||||||
|
8: "Waypoint",
|
||||||
|
9: "Audio",
|
||||||
|
10: "Detection Sensor",
|
||||||
|
11: "Alert",
|
||||||
|
12: "Key Verification",
|
||||||
|
32: "Reply",
|
||||||
|
33: "IP Tunnel",
|
||||||
|
34: "Paxcounter",
|
||||||
|
35: "Store Forward++",
|
||||||
|
36: "Node Status",
|
||||||
|
64: "Serial",
|
||||||
|
65: "Store & Forward",
|
||||||
|
66: "Range Test",
|
||||||
|
67: "Telemetry",
|
||||||
|
68: "ZPS",
|
||||||
|
69: "Simulator",
|
||||||
|
70: "Traceroute",
|
||||||
|
71: "Neighbor",
|
||||||
|
72: "ATAK",
|
||||||
|
73: "Map Report",
|
||||||
|
74: "Power Stress",
|
||||||
|
76: "Reticulum Tunnel",
|
||||||
|
77: "Cayenne",
|
||||||
|
256: "Private App",
|
||||||
|
257: "ATAK Forwarder",
|
||||||
|
};
|
||||||
|
|
||||||
|
window.PORT_COLOR_MAP = {
|
||||||
|
0: "#6c757d", // gray - Unknown
|
||||||
|
1: "#1f77b4", // blue - Text
|
||||||
|
2: "#795548", // brown - Remote Hardware
|
||||||
|
3: "#2ca02c", // green - Position
|
||||||
|
4: "#ffbf00", // yellow - Node Info
|
||||||
|
5: "#ff7f0e", // orange - Routing
|
||||||
|
6: "#20c997", // teal - Admin
|
||||||
|
7: "#6a51a3", // purple - Text (Compressed)
|
||||||
|
8: "#fd7e14", // orange - Waypoint
|
||||||
|
9: "#e91e63", // pink - Audio
|
||||||
|
10: "#ff9800", // amber - Detection Sensor
|
||||||
|
11: "#f44336", // bright red - Alert
|
||||||
|
12: "#9c27b0", // purple - Key Verification
|
||||||
|
32: "#00bcd4", // cyan - Reply
|
||||||
|
33: "#607d8b", // blue-gray - IP Tunnel
|
||||||
|
34: "#8d6e63", // brown-gray - Paxcounter
|
||||||
|
35: "#8bc34a", // light green - Store Forward++
|
||||||
|
36: "#4caf50", // green - Node Status
|
||||||
|
64: "#9e9e9e", // gray - Serial
|
||||||
|
65: "#6610f2", // indigo - Store & Forward
|
||||||
|
66: "#cddc39", // lime - Range Test
|
||||||
|
67: "#17a2b8", // info blue - Telemetry
|
||||||
|
68: "#3f51b5", // indigo - ZPS
|
||||||
|
69: "#673ab7", // deep purple - Simulator
|
||||||
|
70: "#f44336", // bright red - Traceroute
|
||||||
|
71: "#e377c2", // pink - Neighbor
|
||||||
|
72: "#2196f3", // blue - ATAK
|
||||||
|
73: "#9999ff", // light purple - Map Report
|
||||||
|
74: "#ff5722", // deep orange - Power Stress
|
||||||
|
76: "#009688", // teal - Reticulum Tunnel
|
||||||
|
77: "#4db6ac", // teal accent - Cayenne
|
||||||
|
256: "#757575", // dark gray - Private App
|
||||||
|
257: "#1976d2", // blue - ATAK Forwarder
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aliases for pages that expect different names.
|
||||||
|
window.PORT_MAP = window.PORT_LABEL_MAP;
|
||||||
|
window.PORT_COLORS = window.PORT_COLOR_MAP;
|
||||||
+114
-80
@@ -1,11 +1,14 @@
|
|||||||
from datetime import datetime, timedelta
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from sqlalchemy import and_, func, or_, select, text
|
from sqlalchemy import Text, and_, cast, func, or_, select
|
||||||
from sqlalchemy.orm import lazyload
|
from sqlalchemy.orm import lazyload
|
||||||
|
|
||||||
from meshview import database, models
|
from meshview import database, models
|
||||||
from meshview.models import Node, Packet, PacketSeen, Traceroute
|
from meshview.models import Node, Packet, PacketSeen, Traceroute
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_node(node_id):
|
async def get_node(node_id):
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
@@ -27,18 +30,12 @@ async def get_fuzzy_nodes(query):
|
|||||||
async def get_packets(
|
async def get_packets(
|
||||||
from_node_id=None,
|
from_node_id=None,
|
||||||
to_node_id=None,
|
to_node_id=None,
|
||||||
node_id=None, # legacy: match either from OR to
|
node_id=None, # legacy
|
||||||
portnum=None,
|
portnum=None,
|
||||||
after=None,
|
after=None,
|
||||||
contains=None, # NEW: SQL-level substring match
|
contains=None, # substring search
|
||||||
limit=50,
|
limit=50,
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
SQLAlchemy 2.0 async ORM version.
|
|
||||||
Supports strict from/to/node filtering, substring payload search,
|
|
||||||
portnum, since, and limit.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
stmt = select(models.Packet)
|
stmt = select(models.Packet)
|
||||||
conditions = []
|
conditions = []
|
||||||
@@ -51,36 +48,40 @@ async def get_packets(
|
|||||||
if to_node_id is not None:
|
if to_node_id is not None:
|
||||||
conditions.append(models.Packet.to_node_id == to_node_id)
|
conditions.append(models.Packet.to_node_id == to_node_id)
|
||||||
|
|
||||||
# Legacy node ID filter: match either direction
|
# Legacy node_id (either direction)
|
||||||
if node_id is not None:
|
if node_id is not None:
|
||||||
conditions.append(
|
conditions.append(
|
||||||
or_(models.Packet.from_node_id == node_id, models.Packet.to_node_id == node_id)
|
or_(
|
||||||
|
models.Packet.from_node_id == node_id,
|
||||||
|
models.Packet.to_node_id == node_id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Port filter
|
# Port filter
|
||||||
if portnum is not None:
|
if portnum is not None:
|
||||||
conditions.append(models.Packet.portnum == portnum)
|
conditions.append(models.Packet.portnum == portnum)
|
||||||
|
|
||||||
# Timestamp filter
|
# Timestamp filter using microseconds
|
||||||
if after is not None:
|
if after is not None:
|
||||||
conditions.append(models.Packet.import_time_us > after)
|
conditions.append(models.Packet.import_time_us > after)
|
||||||
|
|
||||||
# Case-insensitive substring search on UTF-8 payload (stored as BLOB)
|
# Case-insensitive substring search on payload (BLOB → TEXT)
|
||||||
if contains:
|
if contains:
|
||||||
contains_lower = contains.lower()
|
contains_lower = f"%{contains.lower()}%"
|
||||||
conditions.append(func.lower(models.Packet.payload).like(f"%{contains_lower}%"))
|
payload_text = cast(models.Packet.payload, Text)
|
||||||
|
conditions.append(func.lower(payload_text).like(contains_lower))
|
||||||
|
|
||||||
# Apply all conditions
|
# Apply WHERE conditions
|
||||||
if conditions:
|
if conditions:
|
||||||
stmt = stmt.where(and_(*conditions))
|
stmt = stmt.where(and_(*conditions))
|
||||||
|
|
||||||
# Order newest → oldest
|
# Order by newest first
|
||||||
stmt = stmt.order_by(models.Packet.import_time_us.desc())
|
stmt = stmt.order_by(models.Packet.import_time_us.desc())
|
||||||
|
|
||||||
# Apply limit
|
# Limit
|
||||||
stmt = stmt.limit(limit)
|
stmt = stmt.limit(limit)
|
||||||
|
|
||||||
# Execute query
|
# Run query
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
@@ -94,8 +95,10 @@ async def get_packets_from(node_id=None, portnum=None, since=None, limit=500):
|
|||||||
if portnum:
|
if portnum:
|
||||||
q = q.where(Packet.portnum == portnum)
|
q = q.where(Packet.portnum == portnum)
|
||||||
if since:
|
if since:
|
||||||
q = q.where(Packet.import_time > (datetime.now() - since))
|
now_us = int(datetime.now().timestamp() * 1_000_000)
|
||||||
result = await session.execute(q.limit(limit).order_by(Packet.import_time.desc()))
|
start_us = now_us - int(since.total_seconds() * 1_000_000)
|
||||||
|
q = q.where(Packet.import_time_us > start_us)
|
||||||
|
result = await session.execute(q.limit(limit).order_by(Packet.import_time_us.desc()))
|
||||||
return result.scalars()
|
return result.scalars()
|
||||||
|
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ async def get_packets_seen(packet_id):
|
|||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(PacketSeen)
|
select(PacketSeen)
|
||||||
.where(PacketSeen.packet_id == packet_id)
|
.where(PacketSeen.packet_id == packet_id)
|
||||||
.order_by(PacketSeen.import_time.desc())
|
.order_by(PacketSeen.import_time_us.desc())
|
||||||
)
|
)
|
||||||
return result.scalars()
|
return result.scalars()
|
||||||
|
|
||||||
@@ -132,18 +135,21 @@ async def get_traceroute(packet_id):
|
|||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Traceroute)
|
select(Traceroute)
|
||||||
.where(Traceroute.packet_id == packet_id)
|
.where(Traceroute.packet_id == packet_id)
|
||||||
.order_by(Traceroute.import_time)
|
.order_by(Traceroute.import_time_us)
|
||||||
)
|
)
|
||||||
return result.scalars()
|
return result.scalars()
|
||||||
|
|
||||||
|
|
||||||
async def get_traceroutes(since):
|
async def get_traceroutes(since):
|
||||||
|
if isinstance(since, datetime):
|
||||||
|
since_us = int(since.timestamp() * 1_000_000)
|
||||||
|
else:
|
||||||
|
since_us = int(since)
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Traceroute)
|
select(Traceroute)
|
||||||
.join(Packet)
|
.where(Traceroute.import_time_us > since_us)
|
||||||
.where(Traceroute.import_time > since)
|
.order_by(Traceroute.import_time_us)
|
||||||
.order_by(Traceroute.import_time)
|
|
||||||
)
|
)
|
||||||
stream = await session.stream_scalars(stmt)
|
stream = await session.stream_scalars(stmt)
|
||||||
async for tr in stream:
|
async for tr in stream:
|
||||||
@@ -151,6 +157,8 @@ async def get_traceroutes(since):
|
|||||||
|
|
||||||
|
|
||||||
async def get_mqtt_neighbors(since):
|
async def get_mqtt_neighbors(since):
|
||||||
|
now_us = int(datetime.now().timestamp() * 1_000_000)
|
||||||
|
start_us = now_us - int(since.total_seconds() * 1_000_000)
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(PacketSeen, Packet)
|
select(PacketSeen, Packet)
|
||||||
@@ -158,7 +166,7 @@ async def get_mqtt_neighbors(since):
|
|||||||
.where(
|
.where(
|
||||||
(PacketSeen.hop_limit == PacketSeen.hop_start)
|
(PacketSeen.hop_limit == PacketSeen.hop_start)
|
||||||
& (PacketSeen.hop_start != 0)
|
& (PacketSeen.hop_start != 0)
|
||||||
& (PacketSeen.import_time > (datetime.now() - since))
|
& (PacketSeen.import_time_us > start_us)
|
||||||
)
|
)
|
||||||
.options(
|
.options(
|
||||||
lazyload(Packet.from_node),
|
lazyload(Packet.from_node),
|
||||||
@@ -171,9 +179,9 @@ async def get_mqtt_neighbors(since):
|
|||||||
async def get_total_node_count(channel: str = None) -> int:
|
async def get_total_node_count(channel: str = None) -> int:
|
||||||
try:
|
try:
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
q = select(func.count(Node.id)).where(
|
now_us = int(datetime.now(timezone.utc).timestamp() * 1_000_000) # noqa: UP017
|
||||||
Node.last_update > datetime.now() - timedelta(days=1)
|
cutoff_us = now_us - 86400 * 1_000_000
|
||||||
)
|
q = select(func.count(Node.id)).where(Node.last_seen_us > cutoff_us)
|
||||||
|
|
||||||
if channel:
|
if channel:
|
||||||
q = q.where(Node.channel == channel)
|
q = q.where(Node.channel == channel)
|
||||||
@@ -188,26 +196,32 @@ async def get_total_node_count(channel: str = None) -> int:
|
|||||||
async def get_top_traffic_nodes():
|
async def get_top_traffic_nodes():
|
||||||
try:
|
try:
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
result = await session.execute(
|
now_us = int(datetime.now(timezone.utc).timestamp() * 1_000_000) # noqa: UP017
|
||||||
text("""
|
cutoff_us = now_us - 86400 * 1_000_000
|
||||||
SELECT
|
total_packets_sent = func.count(func.distinct(Packet.id)).label("total_packets_sent")
|
||||||
n.node_id,
|
total_times_seen = func.count(PacketSeen.packet_id).label("total_times_seen")
|
||||||
n.long_name,
|
|
||||||
n.short_name,
|
stmt = (
|
||||||
n.channel,
|
select(
|
||||||
COUNT(DISTINCT p.id) AS total_packets_sent,
|
Node.node_id,
|
||||||
COUNT(ps.packet_id) AS total_times_seen
|
Node.long_name,
|
||||||
FROM node n
|
Node.short_name,
|
||||||
LEFT JOIN packet p ON n.node_id = p.from_node_id
|
Node.channel,
|
||||||
AND p.import_time >= DATETIME('now', 'localtime', '-24 hours')
|
total_packets_sent,
|
||||||
LEFT JOIN packet_seen ps ON p.id = ps.packet_id
|
total_times_seen,
|
||||||
GROUP BY n.node_id, n.long_name, n.short_name
|
)
|
||||||
HAVING total_packets_sent > 0
|
.select_from(Node)
|
||||||
ORDER BY total_times_seen DESC;
|
.outerjoin(
|
||||||
""")
|
Packet,
|
||||||
|
(Packet.from_node_id == Node.node_id) & (Packet.import_time_us >= cutoff_us),
|
||||||
|
)
|
||||||
|
.outerjoin(PacketSeen, PacketSeen.packet_id == Packet.id)
|
||||||
|
.group_by(Node.node_id, Node.long_name, Node.short_name, Node.channel)
|
||||||
|
.having(total_packets_sent > 0)
|
||||||
|
.order_by(total_times_seen.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = result.fetchall()
|
rows = (await session.execute(stmt)).all()
|
||||||
|
|
||||||
nodes = [
|
nodes = [
|
||||||
{
|
{
|
||||||
@@ -230,44 +244,42 @@ async def get_top_traffic_nodes():
|
|||||||
async def get_node_traffic(node_id: int):
|
async def get_node_traffic(node_id: int):
|
||||||
try:
|
try:
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
result = await session.execute(
|
now_us = int(datetime.now(timezone.utc).timestamp() * 1_000_000) # noqa: UP017
|
||||||
text("""
|
cutoff_us = now_us - 86400 * 1_000_000
|
||||||
SELECT
|
packet_count = func.count().label("packet_count")
|
||||||
node.long_name, packet.portnum,
|
|
||||||
COUNT(*) AS packet_count
|
stmt = (
|
||||||
FROM packet
|
select(Node.long_name, Packet.portnum, packet_count)
|
||||||
JOIN node ON packet.from_node_id = node.node_id
|
.select_from(Packet)
|
||||||
WHERE node.node_id = :node_id
|
.join(Node, Packet.from_node_id == Node.node_id)
|
||||||
AND packet.import_time >= DATETIME('now', 'localtime', '-24 hours')
|
.where(Node.node_id == node_id)
|
||||||
GROUP BY packet.portnum
|
.where(Packet.import_time_us >= cutoff_us)
|
||||||
ORDER BY packet_count DESC;
|
.group_by(Node.long_name, Packet.portnum)
|
||||||
"""),
|
.order_by(packet_count.desc())
|
||||||
{"node_id": node_id},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Map the result to include node.long_name and packet data
|
result = await session.execute(stmt)
|
||||||
traffic_data = [
|
return [
|
||||||
{
|
{
|
||||||
"long_name": row[0], # node.long_name
|
"long_name": row.long_name,
|
||||||
"portnum": row[1], # packet.portnum
|
"portnum": row.portnum,
|
||||||
"packet_count": row[2], # COUNT(*) as packet_count
|
"packet_count": row.packet_count,
|
||||||
}
|
}
|
||||||
for row in result.all()
|
for row in result.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
return traffic_data
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log the error or handle it as needed
|
# Log the error or handle it as needed
|
||||||
print(f"Error fetching node traffic: {str(e)}")
|
print(f"Error fetching node traffic: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
async def get_nodes(node_id=None, role=None, channel=None, hw_model=None, days_active=None):
|
||||||
"""
|
"""
|
||||||
Fetches nodes from the database based on optional filtering criteria.
|
Fetches nodes from the database based on optional filtering criteria.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
node_id
|
||||||
role (str, optional): The role of the node (converted to uppercase for consistency).
|
role (str, optional): The role of the node (converted to uppercase for consistency).
|
||||||
channel (str, optional): The communication channel associated with the node.
|
channel (str, optional): The communication channel associated with the node.
|
||||||
hw_model (str, optional): The hardware model of the node.
|
hw_model (str, optional): The hardware model of the node.
|
||||||
@@ -283,6 +295,12 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
|||||||
query = select(Node)
|
query = select(Node)
|
||||||
|
|
||||||
# Apply filters based on provided parameters
|
# Apply filters based on provided parameters
|
||||||
|
if node_id is not None:
|
||||||
|
try:
|
||||||
|
node_id_int = int(node_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
node_id_int = node_id
|
||||||
|
query = query.where(Node.node_id == node_id_int)
|
||||||
if role is not None:
|
if role is not None:
|
||||||
query = query.where(Node.role == role.upper()) # Ensure role is uppercase
|
query = query.where(Node.role == role.upper()) # Ensure role is uppercase
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
@@ -291,10 +309,12 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
|||||||
query = query.where(Node.hw_model == hw_model)
|
query = query.where(Node.hw_model == hw_model)
|
||||||
|
|
||||||
if days_active is not None:
|
if days_active is not None:
|
||||||
query = query.where(Node.last_update > datetime.now() - timedelta(days_active))
|
now_us = int(datetime.now(timezone.utc).timestamp() * 1_000_000) # noqa: UP017
|
||||||
|
cutoff_us = now_us - int(timedelta(days_active).total_seconds() * 1_000_000)
|
||||||
|
query = query.where(Node.last_seen_us > cutoff_us)
|
||||||
|
|
||||||
# Exclude nodes where last_update is an empty string
|
# Exclude nodes with missing last_seen_us
|
||||||
query = query.where(Node.last_update != "")
|
query = query.where(Node.last_seen_us.is_not(None))
|
||||||
|
|
||||||
# Order results by long_name in ascending order
|
# Order results by long_name in ascending order
|
||||||
query = query.order_by(Node.short_name.asc())
|
query = query.order_by(Node.short_name.asc())
|
||||||
@@ -305,7 +325,7 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
|||||||
return nodes # Return the list of nodes
|
return nodes # Return the list of nodes
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
print("error reading DB") # Consider using logging instead of print
|
logger.exception("error reading DB")
|
||||||
return [] # Return an empty list in case of failure
|
return [] # Return an empty list in case of failure
|
||||||
|
|
||||||
|
|
||||||
@@ -317,22 +337,36 @@ async def get_packet_stats(
|
|||||||
to_node: int | None = None,
|
to_node: int | None = None,
|
||||||
from_node: int | None = None,
|
from_node: int | None = None,
|
||||||
):
|
):
|
||||||
now = datetime.now()
|
now = datetime.now(timezone.utc) # noqa: UP017
|
||||||
|
|
||||||
if period_type == "hour":
|
if period_type == "hour":
|
||||||
start_time = now - timedelta(hours=length)
|
start_time = now - timedelta(hours=length)
|
||||||
time_format = '%Y-%m-%d %H:00'
|
time_format_sqlite = "%Y-%m-%d %H:00"
|
||||||
|
time_format_pg = "YYYY-MM-DD HH24:00"
|
||||||
elif period_type == "day":
|
elif period_type == "day":
|
||||||
start_time = now - timedelta(days=length)
|
start_time = now - timedelta(days=length)
|
||||||
time_format = '%Y-%m-%d'
|
time_format_sqlite = "%Y-%m-%d"
|
||||||
|
time_format_pg = "YYYY-MM-DD"
|
||||||
else:
|
else:
|
||||||
raise ValueError("period_type must be 'hour' or 'day'")
|
raise ValueError("period_type must be 'hour' or 'day'")
|
||||||
|
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
|
dialect = session.get_bind().dialect.name
|
||||||
|
if dialect == "postgresql":
|
||||||
|
period_expr = func.to_char(
|
||||||
|
func.to_timestamp(Packet.import_time_us / 1_000_000.0),
|
||||||
|
time_format_pg,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
period_expr = func.strftime(
|
||||||
|
time_format_sqlite,
|
||||||
|
func.datetime(Packet.import_time_us / 1_000_000, "unixepoch"),
|
||||||
|
)
|
||||||
|
|
||||||
q = select(
|
q = select(
|
||||||
func.strftime(time_format, Packet.import_time).label('period'),
|
period_expr.label("period"),
|
||||||
func.count().label('count'),
|
func.count().label("count"),
|
||||||
).where(Packet.import_time >= start_time)
|
).where(Packet.import_time_us >= int(start_time.timestamp() * 1_000_000))
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
if channel:
|
if channel:
|
||||||
|
|||||||
@@ -53,8 +53,9 @@
|
|||||||
|
|
||||||
<!-- ⭐ CHAT TITLE WITH ICON, aligned to container ⭐ -->
|
<!-- ⭐ CHAT TITLE WITH ICON, aligned to container ⭐ -->
|
||||||
<div class="container px-2">
|
<div class="container px-2">
|
||||||
<h2 data-translate="chat_title" style="color:white; margin:0 0 10px 0;">
|
<h2 style="color:white; margin:0 0 10px 0;">
|
||||||
💬 Chat
|
<span class="icon">💬</span>
|
||||||
|
<span data-translate="chat_title"></span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -71,24 +72,45 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const packetMap = new Map();
|
const packetMap = new Map();
|
||||||
let chatLang = {};
|
let chatLang = {};
|
||||||
|
|
||||||
function applyTranslations(dict, root = document) {
|
/* ==========================================================
|
||||||
|
TRANSLATIONS FOR CHAT PAGE
|
||||||
|
========================================================== */
|
||||||
|
function applyTranslations(dict, root=document) {
|
||||||
root.querySelectorAll("[data-translate]").forEach(el => {
|
root.querySelectorAll("[data-translate]").forEach(el => {
|
||||||
const key = el.dataset.translate;
|
const key = el.dataset.translate;
|
||||||
const val = dict[key];
|
const val = dict[key];
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
if (el.placeholder) el.placeholder = val;
|
if (el.placeholder) el.placeholder = val;
|
||||||
else if (el.tagName === "INPUT" && el.value) el.value = val;
|
|
||||||
else if (key === "footer") el.innerHTML = val;
|
|
||||||
else el.textContent = val;
|
else el.textContent = val;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadChatLang() {
|
||||||
|
try {
|
||||||
|
const cfg = await window._siteConfigPromise;
|
||||||
|
const langCode = cfg?.site?.language || "en";
|
||||||
|
const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`);
|
||||||
|
chatLang = await res.json();
|
||||||
|
|
||||||
|
// Apply to existing DOM
|
||||||
|
applyTranslations(chatLang);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Chat translation load failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
SAFE HTML
|
||||||
|
========================================================== */
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.textContent = text ?? "";
|
div.textContent = text ?? "";
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
RENDERING PACKETS
|
||||||
|
========================================================== */
|
||||||
function renderPacket(packet, highlight = false) {
|
function renderPacket(packet, highlight = false) {
|
||||||
if (renderedPacketIds.has(packet.id)) return;
|
if (renderedPacketIds.has(packet.id)) return;
|
||||||
renderedPacketIds.add(packet.id);
|
renderedPacketIds.add(packet.id);
|
||||||
@@ -139,20 +161,31 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "row chat-packet" + (highlight ? " flash" : "");
|
div.className = "row chat-packet" + (highlight ? " flash" : "");
|
||||||
div.dataset.packetId = packet.id;
|
div.dataset.packetId = packet.id;
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<span class="col-2 timestamp" title="${packet.import_time_us}">${formattedTimestamp}</span>
|
<span class="col-2 timestamp" title="${packet.import_time_us}">
|
||||||
|
${formattedTimestamp}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="col-2 channel">
|
<span class="col-2 channel">
|
||||||
<a href="/packet/${packet.id}" title="${chatLang.view_packet_details || 'View details'}">🔎</a>
|
<a href="/packet/${packet.id}" title="${chatLang.view_packet_details || 'View details'}">🔎</a>
|
||||||
${escapeHtml(packet.channel || "")}
|
${escapeHtml(packet.channel || "")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="col-3 nodename">
|
<span class="col-3 nodename">
|
||||||
<a href="/node/${packet.from_node_id}">
|
<a href="/node/${packet.from_node_id}">
|
||||||
${escapeHtml((packet.long_name || "").trim() || `Node ${packet.from_node_id}`)}
|
${escapeHtml((packet.long_name || "").trim() || `Node ${packet.from_node_id}`)}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="col-5 message">${escapeHtml(packet.payload)}${replyHtml}</span>
|
|
||||||
|
<span class="col-5 message">
|
||||||
|
${escapeHtml(packet.payload)}${replyHtml}
|
||||||
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
chatContainer.prepend(div);
|
chatContainer.prepend(div);
|
||||||
|
|
||||||
|
// Translate newly added DOM
|
||||||
applyTranslations(chatLang, div);
|
applyTranslations(chatLang, div);
|
||||||
|
|
||||||
if (highlight) setTimeout(() => div.classList.remove("flash"), 2500);
|
if (highlight) setTimeout(() => div.classList.remove("flash"), 2500);
|
||||||
@@ -161,26 +194,27 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
function renderPacketsEnsureDescending(packets, highlight=false) {
|
function renderPacketsEnsureDescending(packets, highlight=false) {
|
||||||
if (!Array.isArray(packets) || packets.length===0) return;
|
if (!Array.isArray(packets) || packets.length===0) return;
|
||||||
const sortedDesc = packets.slice().sort((a,b)=>{
|
const sortedDesc = packets.slice().sort((a,b)=>{
|
||||||
const aTime =
|
const aTime = a.import_time_us || (new Date(a.import_time).getTime() * 1000);
|
||||||
(a.import_time_us && a.import_time_us > 0)
|
const bTime = b.import_time_us || (new Date(b.import_time).getTime() * 1000);
|
||||||
? a.import_time_us
|
|
||||||
: (a.import_time ? new Date(a.import_time).getTime() * 1000 : 0);
|
|
||||||
const bTime =
|
|
||||||
(b.import_time_us && b.import_time_us > 0)
|
|
||||||
? b.import_time_us
|
|
||||||
: (b.import_time ? new Date(b.import_time).getTime() * 1000 : 0);
|
|
||||||
return bTime - aTime;
|
return bTime - aTime;
|
||||||
});
|
});
|
||||||
for (let i=sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i], highlight);
|
for (let i=sortedDesc.length-1; i>=0; i--) {
|
||||||
|
renderPacket(sortedDesc[i], highlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
FETCHING PACKETS
|
||||||
|
========================================================== */
|
||||||
async function fetchInitial() {
|
async function fetchInitial() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/packets?portnum=1&limit=100");
|
const resp = await fetch("/api/packets?portnum=1&limit=100");
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets);
|
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets);
|
||||||
lastTime = data?.latest_import_time || lastTime;
|
lastTime = data?.latest_import_time || lastTime;
|
||||||
} catch(err){ console.error("Initial fetch error:", err); }
|
} catch(err){
|
||||||
|
console.error("Initial fetch error:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUpdates() {
|
async function fetchUpdates() {
|
||||||
@@ -192,21 +226,19 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets, true);
|
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets, true);
|
||||||
lastTime = data?.latest_import_time || lastTime;
|
lastTime = data?.latest_import_time || lastTime;
|
||||||
} catch(err){ console.error("Fetch updates error:", err); }
|
} catch(err){
|
||||||
|
console.error("Fetch updates error:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChatLang() {
|
/* ==========================================================
|
||||||
try {
|
INIT
|
||||||
const cfg = await window._siteConfigPromise;
|
========================================================== */
|
||||||
const langCode = cfg?.site?.language || "en";
|
await loadChatLang(); // load translations FIRST
|
||||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`);
|
await fetchInitial(); // then fetch initial packets
|
||||||
chatLang = await res.json();
|
|
||||||
applyTranslations(chatLang);
|
|
||||||
} catch(err){ console.error("Chat translation load failed:", err); }
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([loadChatLang(), fetchInitial()]);
|
|
||||||
setInterval(fetchUpdates, 5000);
|
setInterval(fetchUpdates, 5000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+198
-106
@@ -12,6 +12,16 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.port-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Packet table */
|
||||||
.packet-table {
|
.packet-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -31,6 +41,7 @@
|
|||||||
.packet-table tr:nth-of-type(odd) { background-color: #272b2f; }
|
.packet-table tr:nth-of-type(odd) { background-color: #272b2f; }
|
||||||
.packet-table tr:nth-of-type(even) { background-color: #212529; }
|
.packet-table tr:nth-of-type(even) { background-color: #212529; }
|
||||||
|
|
||||||
|
/* Port tag */
|
||||||
.port-tag {
|
.port-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
@@ -39,29 +50,22 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.port-0 { background-color: #6c757d; }
|
|
||||||
.port-1 { background-color: #007bff; }
|
|
||||||
.port-3 { background-color: #28a745; }
|
|
||||||
.port-4 { background-color: #ffc107; }
|
|
||||||
.port-5 { background-color: #dc3545; }
|
|
||||||
.port-6 { background-color: #20c997; }
|
|
||||||
.port-65 { background-color: #ff66b3; }
|
|
||||||
.port-67 { background-color: #17a2b8; }
|
|
||||||
.port-70 { background-color: #6f42c1; }
|
|
||||||
.port-71 { background-color: #fd7e14; }
|
|
||||||
|
|
||||||
.to-mqtt { font-style: italic; color: #aaa; }
|
.to-mqtt { font-style: italic; color: #aaa; }
|
||||||
|
|
||||||
|
/* Payload rows */
|
||||||
.payload-row { display: none; background-color: #1b1e22; }
|
.payload-row { display: none; background-color: #1b1e22; }
|
||||||
.payload-cell {
|
.payload-cell {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
color: #b0bec5;
|
color: #b0bec5;
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
.packet-table tr.expanded + .payload-row { display: table-row; }
|
.packet-table tr.expanded + .payload-row {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle arrow */
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
@@ -70,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
.toggle-btn:hover { color: #fff; }
|
.toggle-btn:hover { color: #fff; }
|
||||||
|
|
||||||
/* Link next to port tag */
|
/* Inline link next to port tag */
|
||||||
.inline-link {
|
.inline-link {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -84,9 +88,16 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<form class="d-flex align-items-center justify-content-between mb-3">
|
<form class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h5 class="mb-0" data-translate-lang="live_feed">📡 Live Feed</h5>
|
<h2 class="mb-0" data-translate-lang="live_feed">📡 Live Feed</h2>
|
||||||
<button type="button" id="pause-button" class="btn btn-sm btn-outline-secondary" data-translate-lang="pause">Pause</button>
|
|
||||||
|
<button type="button"
|
||||||
|
id="pause-button"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
data-translate-lang="pause">
|
||||||
|
Pause
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="packet-table">
|
<table class="packet-table">
|
||||||
@@ -101,90 +112,82 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="packet_list"></tbody>
|
<tbody id="packet_list"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/portmaps.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let lastImportTimeUs = null;
|
/* ======================================================
|
||||||
let updatesPaused = false;
|
FIREHOSE TRANSLATION SYSTEM (isolated from base)
|
||||||
let nodeMap = {};
|
====================================================== */
|
||||||
let updateInterval = 3000;
|
|
||||||
let firehoseTranslations = {};
|
let firehoseTranslations = {};
|
||||||
|
|
||||||
function applyTranslations(translations, root=document) {
|
function applyTranslationsFirehose(translations, root=document) {
|
||||||
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
root
|
||||||
const key = el.dataset.translateLang;
|
.querySelectorAll("[data-translate-lang]")
|
||||||
if (translations[key]) el.textContent = translations[key];
|
.forEach(el => {
|
||||||
});
|
const key = el.dataset.translateLang;
|
||||||
|
if (!translations[key]) return;
|
||||||
|
|
||||||
|
if (el.tagName === "INPUT" && el.placeholder !== undefined) {
|
||||||
|
el.placeholder = translations[key];
|
||||||
|
} else {
|
||||||
|
el.textContent = translations[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTranslations() {
|
async function loadTranslationsFirehose() {
|
||||||
try {
|
try {
|
||||||
const cfg = await window._siteConfigPromise;
|
const cfg = await window._siteConfigPromise;
|
||||||
const langCode = cfg?.site?.language || "en";
|
const lang = cfg?.site?.language || "en";
|
||||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=firehose`);
|
|
||||||
|
const res = await fetch(`/api/lang?lang=${lang}§ion=firehose`);
|
||||||
firehoseTranslations = await res.json();
|
firehoseTranslations = await res.json();
|
||||||
applyTranslations(firehoseTranslations, document);
|
|
||||||
|
applyTranslationsFirehose(firehoseTranslations);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Firehose translation load failed:", err);
|
console.error("Firehose translation load failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PORT_MAP = {
|
/* ======================================================
|
||||||
0: "UNKNOWN APP",
|
NODE LOOKUP
|
||||||
1: "Text Message",
|
====================================================== */
|
||||||
3: "Position",
|
let nodeMap = {};
|
||||||
4: "Node Info",
|
|
||||||
5: "Routing",
|
|
||||||
6: "Administration",
|
|
||||||
8: "Waypoint",
|
|
||||||
65: "Store Forward",
|
|
||||||
67: "Telemetry",
|
|
||||||
70: "Trace Route",
|
|
||||||
71: "Neighbor Info",
|
|
||||||
};
|
|
||||||
|
|
||||||
const PORT_COLORS = {
|
|
||||||
0: "#6c757d",
|
|
||||||
1: "#007bff",
|
|
||||||
3: "#28a745",
|
|
||||||
4: "#ffc107",
|
|
||||||
5: "#dc3545",
|
|
||||||
6: "#20c997",
|
|
||||||
65: "#6610f2",
|
|
||||||
67: "#17a2b8",
|
|
||||||
68: "#fd7e14",
|
|
||||||
69: "#6f42c1",
|
|
||||||
70: "#ff4444",
|
|
||||||
71: "#ff66cc",
|
|
||||||
72: "#00cc99",
|
|
||||||
73: "#9999ff",
|
|
||||||
74: "#cc00cc",
|
|
||||||
75: "#ffbb33",
|
|
||||||
76: "#00bcd4",
|
|
||||||
77: "#8bc34a",
|
|
||||||
78: "#795548"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load node names
|
|
||||||
async function loadNodes() {
|
async function loadNodes() {
|
||||||
const res = await fetch("/api/nodes");
|
try {
|
||||||
if (!res.ok) return;
|
const res = await fetch("/api/nodes");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
for (const n of data.nodes || []) {
|
|
||||||
nodeMap[n.node_id] = n.long_name || n.short_name || n.id || n.node_id;
|
for (const n of data.nodes || []) {
|
||||||
|
nodeMap[n.node_id] = n.long_name || n.short_name || n.id || n.node_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeMap[4294967295] = firehoseTranslations.all_broadcast || "All";
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed loading nodes:", err);
|
||||||
}
|
}
|
||||||
nodeMap[4294967295] = "All";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeName(id) {
|
function nodeName(id) {
|
||||||
if (id === 4294967295) return `<span class="to-mqtt">All</span>`;
|
|
||||||
return nodeMap[id] || id;
|
return nodeMap[id] || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
PORT COLORS & NAMES
|
||||||
|
====================================================== */
|
||||||
|
const PORT_MAP = window.PORT_MAP || {};
|
||||||
|
const PORT_COLORS = window.PORT_COLORS || {};
|
||||||
|
|
||||||
function portLabel(portnum, payload, linksHtml) {
|
function portLabel(portnum, payload, linksHtml) {
|
||||||
const name = PORT_MAP[portnum] || "Unknown";
|
const name = PORT_MAP[portnum] || "Unknown";
|
||||||
const color = PORT_COLORS[portnum] || "#6c757d";
|
const color = PORT_COLORS[portnum] || "#6c757d";
|
||||||
const safePayload = payload ? payload.replace(/"/g, """) : "";
|
|
||||||
|
const safePayload = payload
|
||||||
|
? payload.replace(/"/g, """)
|
||||||
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<span class="port-tag" style="background-color:${color}" title="${safePayload}">
|
<span class="port-tag" style="background-color:${color}" title="${safePayload}">
|
||||||
@@ -195,31 +198,70 @@ function portLabel(portnum, payload, linksHtml) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLocalTime(importTimeUs) {
|
/* ======================================================
|
||||||
const ms = importTimeUs / 1000;
|
TIME FORMAT
|
||||||
|
====================================================== */
|
||||||
|
function formatTimes(importTimeUs) {
|
||||||
|
const ms = Number(importTimeUs) / 1000;
|
||||||
|
if (!Number.isFinite(ms)) {
|
||||||
|
return { local: "—", utc: "—", epoch: "—" };
|
||||||
|
}
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
const local = date.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZoneName: "short"
|
||||||
|
});
|
||||||
|
const utc = date.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC",
|
||||||
|
timeZoneName: "short"
|
||||||
|
});
|
||||||
|
return { local, utc, epoch: String(importTimeUs) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logPacketTimes(packet) {
|
||||||
|
const times = formatTimes(packet.import_time_us);
|
||||||
|
console.log(
|
||||||
|
"[firehose] packet time",
|
||||||
|
"id=" + packet.id,
|
||||||
|
"epoch_us=" + times.epoch,
|
||||||
|
"local=" + times.local,
|
||||||
|
"utc=" + times.utc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
FIREHOSE FETCHING
|
||||||
|
====================================================== */
|
||||||
|
let lastImportTimeUs = null;
|
||||||
|
let updatesPaused = false;
|
||||||
|
let updateInterval = 3000;
|
||||||
|
|
||||||
async function configureFirehose() {
|
async function configureFirehose() {
|
||||||
try {
|
try {
|
||||||
const cfg = await window._siteConfigPromise;
|
const cfg = await window._siteConfigPromise;
|
||||||
const intervalSec = cfg?.site?.firehose_interval;
|
const sec = cfg?.site?.firehose_interval;
|
||||||
if (intervalSec && !isNaN(intervalSec)) updateInterval = parseInt(intervalSec) * 1000;
|
if (sec && !isNaN(sec)) updateInterval = sec * 1000;
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("Failed to read firehose interval:", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUpdates() {
|
async function fetchUpdates() {
|
||||||
if (updatesPaused) return;
|
if (updatesPaused) return;
|
||||||
|
|
||||||
const url = new URL("/api/packets", window.location.origin);
|
const url = new URL("/api/packets", window.location.origin);
|
||||||
if (lastImportTimeUs) url.searchParams.set("since", lastImportTimeUs);
|
url.searchParams.set("limit", 100);
|
||||||
url.searchParams.set("limit", 50);
|
|
||||||
|
if (lastImportTimeUs)
|
||||||
|
url.searchParams.set("since", lastImportTimeUs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const packets = data.packets || [];
|
const packets = data.packets || [];
|
||||||
if (!packets.length) return;
|
if (!packets.length) return;
|
||||||
@@ -227,19 +269,35 @@ async function fetchUpdates() {
|
|||||||
const list = document.getElementById("packet_list");
|
const list = document.getElementById("packet_list");
|
||||||
|
|
||||||
for (const pkt of packets.reverse()) {
|
for (const pkt of packets.reverse()) {
|
||||||
const from = pkt.from_node_id === 4294967295
|
logPacketTimes(pkt);
|
||||||
? `<span class="to-mqtt">All</span>`
|
|
||||||
: `<a href="/node/${pkt.from_node_id}" style="text-decoration:underline; color:inherit;">${nodeMap[pkt.from_node_id] || pkt.from_node_id}</a>`;
|
|
||||||
|
|
||||||
const to = pkt.to_node_id === 1
|
/* FROM — includes translation */
|
||||||
? `<span class="to-mqtt">direct to MQTT</span>`
|
const from =
|
||||||
: pkt.to_node_id === 4294967295
|
pkt.from_node_id === 4294967295
|
||||||
? `<span class="to-mqtt">All</span>`
|
? `<span class="to-mqtt" data-translate-lang="all_broadcast">
|
||||||
: `<a href="/node/${pkt.to_node_id}" style="text-decoration:underline; color:inherit;">${nodeMap[pkt.to_node_id] || pkt.to_node_id}</a>`;
|
${firehoseTranslations.all_broadcast || "All"}
|
||||||
|
</span>`
|
||||||
|
: `<a href="/node/${pkt.from_node_id}" style="text-decoration:underline; color:inherit;">
|
||||||
|
${nodeMap[pkt.from_node_id] || pkt.from_node_id}
|
||||||
|
</a>`;
|
||||||
|
|
||||||
|
/* TO — includes translation */
|
||||||
|
const to =
|
||||||
|
pkt.to_node_id === 1
|
||||||
|
? `<span class="to-mqtt" data-translate-lang="direct_to_mqtt">
|
||||||
|
${firehoseTranslations.direct_to_mqtt || "direct to MQTT"}
|
||||||
|
</span>`
|
||||||
|
: pkt.to_node_id === 4294967295
|
||||||
|
? `<span class="to-mqtt" data-translate-lang="all_broadcast">
|
||||||
|
${firehoseTranslations.all_broadcast || "All"}
|
||||||
|
</span>`
|
||||||
|
: `<a href="/node/${pkt.to_node_id}" style="text-decoration:underline; color:inherit;">
|
||||||
|
${nodeMap[pkt.to_node_id] || pkt.to_node_id}
|
||||||
|
</a>`;
|
||||||
|
|
||||||
// Inline link next to port tag
|
|
||||||
let inlineLinks = "";
|
let inlineLinks = "";
|
||||||
|
|
||||||
|
// Position link
|
||||||
if (pkt.portnum === 3 && pkt.payload) {
|
if (pkt.portnum === 3 && pkt.payload) {
|
||||||
const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
|
const latMatch = pkt.payload.match(/latitude_i:\s*(-?\d+)/);
|
||||||
const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
|
const lonMatch = pkt.payload.match(/longitude_i:\s*(-?\d+)/);
|
||||||
@@ -247,36 +305,59 @@ async function fetchUpdates() {
|
|||||||
if (latMatch && lonMatch) {
|
if (latMatch && lonMatch) {
|
||||||
const lat = parseInt(latMatch[1]) / 1e7;
|
const lat = parseInt(latMatch[1]) / 1e7;
|
||||||
const lon = parseInt(lonMatch[1]) / 1e7;
|
const lon = parseInt(lonMatch[1]) / 1e7;
|
||||||
inlineLinks += ` <a class="inline-link" href="https://www.google.com/maps?q=${lat},${lon}" target="_blank">📍</a>`;
|
inlineLinks += ` <a class="inline-link"
|
||||||
|
href="https://www.google.com/maps?q=${lat},${lon}"
|
||||||
|
target="_blank">📍</a>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traceroute link
|
||||||
if (pkt.portnum === 70) {
|
if (pkt.portnum === 70) {
|
||||||
let traceId = pkt.id;
|
let traceId = pkt.id;
|
||||||
const match = pkt.payload.match(/ID:\s*(\d+)/i);
|
const match = pkt.payload.match(/ID:\s*(\d+)/i);
|
||||||
if (match) traceId = match[1];
|
if (match) traceId = match[1];
|
||||||
|
|
||||||
inlineLinks += ` <a class="inline-link" href="/graph/traceroute/${traceId}" target="_blank">⮕</a>`;
|
inlineLinks += ` <a class="inline-link"
|
||||||
|
href="/graph/traceroute/${traceId}"
|
||||||
|
target="_blank">⮕</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const safePayload = (pkt.payload || "").replace(/</g, "<").replace(/>/g, ">");
|
const safePayload = (pkt.payload || "")
|
||||||
const localTime = formatLocalTime(pkt.import_time_us);
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<tr class="packet-row" data-id="${pkt.id}">
|
<tr class="packet-row">
|
||||||
<td>${localTime}</td>
|
|
||||||
<td><span class="toggle-btn">▶</span> <a href="/packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
<td>
|
||||||
|
${formatTimes(pkt.import_time_us).local}<br>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span class="toggle-btn">▶</span>
|
||||||
|
<a href="/packet/${pkt.id}"
|
||||||
|
style="text-decoration:underline; color:inherit;">
|
||||||
|
${pkt.id}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>${from}</td>
|
<td>${from}</td>
|
||||||
<td>${to}</td>
|
<td>${to}</td>
|
||||||
<td>${portLabel(pkt.portnum, pkt.payload, inlineLinks)}</td>
|
<td>${portLabel(pkt.portnum, pkt.payload, inlineLinks)}</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr class="payload-row">
|
<tr class="payload-row">
|
||||||
<td colspan="5" class="payload-cell">${safePayload}</td>
|
<td colspan="5" class="payload-cell">${safePayload}</td>
|
||||||
</tr>`;
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
list.insertAdjacentHTML("afterbegin", html);
|
list.insertAdjacentHTML("afterbegin", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit table size
|
||||||
while (list.rows.length > 400) list.deleteRow(-1);
|
while (list.rows.length > 400) list.deleteRow(-1);
|
||||||
|
|
||||||
lastImportTimeUs = packets[packets.length - 1].import_time_us;
|
lastImportTimeUs = packets[packets.length - 1].import_time_us;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -284,29 +365,40 @@ async function fetchUpdates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialize ---
|
/* ======================================================
|
||||||
|
INITIALIZE PAGE
|
||||||
|
====================================================== */
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
const pauseBtn = document.getElementById("pause-button");
|
const pauseBtn = document.getElementById("pause-button");
|
||||||
|
|
||||||
pauseBtn.addEventListener("click", () => {
|
pauseBtn.addEventListener("click", () => {
|
||||||
updatesPaused = !updatesPaused;
|
updatesPaused = !updatesPaused;
|
||||||
pauseBtn.textContent = updatesPaused
|
|
||||||
? (firehoseTranslations.resume || "Resume")
|
pauseBtn.textContent =
|
||||||
: (firehoseTranslations.pause || "Pause");
|
updatesPaused
|
||||||
|
? (firehoseTranslations.resume || "Resume")
|
||||||
|
: (firehoseTranslations.pause || "Pause");
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", e => {
|
||||||
const btn = e.target.closest(".toggle-btn");
|
const btn = e.target.closest(".toggle-btn");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
|
||||||
const row = btn.closest(".packet-row");
|
const row = btn.closest(".packet-row");
|
||||||
row.classList.toggle("expanded");
|
row.classList.toggle("expanded");
|
||||||
btn.textContent = row.classList.contains("expanded") ? "▼" : "▶";
|
|
||||||
|
btn.textContent =
|
||||||
|
row.classList.contains("expanded") ? "▼" : "▶";
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadTranslations();
|
await loadTranslationsFirehose();
|
||||||
await configureFirehose();
|
await configureFirehose();
|
||||||
await loadNodes();
|
await loadNodes();
|
||||||
|
|
||||||
fetchUpdates();
|
fetchUpdates();
|
||||||
setInterval(fetchUpdates, updateInterval);
|
setInterval(fetchUpdates, updateInterval);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+550
-144
@@ -7,59 +7,282 @@
|
|||||||
<style>
|
<style>
|
||||||
.legend { background:white;padding:8px;line-height:1.5;border-radius:5px;box-shadow:0 0 10px rgba(0,0,0,0.3);font-size:14px;color:black; }
|
.legend { background:white;padding:8px;line-height:1.5;border-radius:5px;box-shadow:0 0 10px rgba(0,0,0,0.3);font-size:14px;color:black; }
|
||||||
.legend i { width:12px;height:12px;display:inline-block;margin-right:6px;border-radius:50%; }
|
.legend i { width:12px;height:12px;display:inline-block;margin-right:6px;border-radius:50%; }
|
||||||
|
|
||||||
#filter-container { text-align:center;margin-top:10px; }
|
#filter-container { text-align:center;margin-top:10px; }
|
||||||
.filter-checkbox { margin:0 10px; }
|
.filter-checkbox { margin:0 10px; }
|
||||||
#share-button, #reset-filters-button {
|
|
||||||
|
#share-button,
|
||||||
|
#reset-filters-button {
|
||||||
padding:5px 15px;border:none;border-radius:4px;font-size:14px;cursor:pointer;color:white;
|
padding:5px 15px;border:none;border-radius:4px;font-size:14px;cursor:pointer;color:white;
|
||||||
}
|
}
|
||||||
#share-button { margin-left:20px; background-color:#4CAF50; }
|
#share-button { margin-left:20px; background-color:#4CAF50; }
|
||||||
#share-button:hover { background-color:#45a049; }
|
#share-button:hover { background-color:#45a049; }
|
||||||
#share-button:active { background-color:#3d8b40; }
|
#share-button:active { background-color:#3d8b40; }
|
||||||
|
|
||||||
#reset-filters-button { margin-left:10px; background-color:#f44336; }
|
#reset-filters-button { margin-left:10px; background-color:#f44336; }
|
||||||
#reset-filters-button:hover { background-color:#da190b; }
|
#reset-filters-button:hover { background-color:#da190b; }
|
||||||
#reset-filters-button:active { background-color:#c41e0d; }
|
#reset-filters-button:active { background-color:#c41e0d; }
|
||||||
|
|
||||||
.blinking-tooltip { background:white;color:black;border:1px solid black;border-radius:4px;padding:2px 5px; }
|
.blinking-tooltip { background:white;color:black;border:1px solid black;border-radius:4px;padding:2px 5px; }
|
||||||
|
|
||||||
|
#map-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 270px);
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#unmapped-packets {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 15px;
|
||||||
|
z-index: 600;
|
||||||
|
width: 220px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
#unmapped-packets h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
#unmapped-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#unmapped-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px dotted #e0e0e0;
|
||||||
|
}
|
||||||
|
#unmapped-list li:last-child { border-bottom: none; }
|
||||||
|
.unmapped-node { font-weight: 400; color: #000; }
|
||||||
|
.unmapped-empty { color: #666; font-style: italic; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="map" style="width:100%;height:calc(100vh - 270px)"></div>
|
|
||||||
<div id="filter-container">
|
<div id="map-wrapper">
|
||||||
<input type="checkbox" class="filter-checkbox" id="filter-routers-only"> Show Routers Only
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<div id="unmapped-packets">
|
||||||
|
<h3 data-translate-lang="unmapped_packets_title">Unmapped Packets</h3>
|
||||||
|
<ul id="unmapped-list">
|
||||||
|
<li class="unmapped-empty" data-translate-lang="unmapped_packets_empty">
|
||||||
|
No recent unmapped packets.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="map-legend"
|
||||||
|
class="legend"
|
||||||
|
style="position:absolute;
|
||||||
|
bottom:30px;
|
||||||
|
right:15px;
|
||||||
|
z-index:500;
|
||||||
|
pointer-events:none;">
|
||||||
|
<div>
|
||||||
|
<i style="background:orange; width:15px; height:3px; border-radius:0;"></i>
|
||||||
|
<span data-translate-lang="legend_traceroute">Traceroute Path (arrowed)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:6px;">
|
||||||
|
<i style="background:gray; width:15px; height:3px; border-radius:0;"></i>
|
||||||
|
<span data-translate-lang="legend_neighbor">Neighbor Link</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="filter-container">
|
||||||
|
<input type="checkbox" class="filter-checkbox" id="filter-routers-only">
|
||||||
|
<span data-translate-lang="show_routers_only">Show Routers Only</span>
|
||||||
|
<input type="checkbox" class="filter-checkbox" id="filter-mqtt-only">
|
||||||
|
<span data-translate-lang="show_mqtt_only">Show MQTT Gateways Only</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:5px;">
|
<div style="text-align:center;margin-top:5px;">
|
||||||
<button id="share-button" onclick="shareCurrentView()">🔗 Share This View</button>
|
<button id="share-button" onclick="shareCurrentView()" data-translate-lang="share_view">
|
||||||
<button id="reset-filters-button" onclick="resetFiltersToDefaults()">↺ Reset Filters To Defaults</button>
|
🔗 Share This View
|
||||||
|
</button>
|
||||||
|
<button id="reset-filters-button" onclick="resetFiltersToDefaults()" data-translate-lang="reset_filters">
|
||||||
|
↺ Reset Filters To Defaults
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
crossorigin=""></script>
|
crossorigin=""></script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js"
|
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js"
|
||||||
integrity="sha384-FhPn/2P/fJGhQLeNWDn9B/2Gml2bPOrKJwFqJXgR3xOPYxWg5mYQ5XZdhUSugZT0"
|
integrity="sha384-FhPn/2P/fJGhQLeNWDn9B/2Gml2bPOrKJwFqJXgR3xOPYxWg5mYQ5XZdhUSugZT0"
|
||||||
crossorigin></script>
|
crossorigin></script>
|
||||||
|
<script src="/static/portmaps.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ---------------------- Map Initialization ----------------------
|
/* ======================================================
|
||||||
var map = L.map('map');
|
MAP PAGE TRANSLATION SYSTEM
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom:19, attribution:'© OpenStreetMap' }).addTo(map);
|
====================================================== */
|
||||||
|
|
||||||
// ---------------------- Globals ----------------------
|
let mapTranslations = {};
|
||||||
var nodes=[], markers={}, markerById={}, nodeMap = new Map();
|
|
||||||
var edgesData=[], edgeLayer = L.layerGroup().addTo(map), selectedNodeId = null;
|
async function loadTranslationsMap() {
|
||||||
|
try {
|
||||||
|
const cfg = await window._siteConfigPromise;
|
||||||
|
const lang = cfg?.site?.language || "en";
|
||||||
|
const res = await fetch(`/api/lang?lang=${lang}§ion=map`);
|
||||||
|
mapTranslations = await res.json();
|
||||||
|
applyTranslationsMap();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Map translation load failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslationsMap(root = document) {
|
||||||
|
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||||
|
const key = el.dataset.translateLang;
|
||||||
|
const val = mapTranslations[key];
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
if (el.tagName === "INPUT" && el.placeholder !== undefined) {
|
||||||
|
el.placeholder = val;
|
||||||
|
} else {
|
||||||
|
el.textContent = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
EXISTING MAP LOGIC
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
|
var map = L.map('map');
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
{ maxZoom:19, attribution:'© OpenStreetMap' }).addTo(map);
|
||||||
|
|
||||||
|
// Data structures
|
||||||
|
var nodes = [], markers = {}, markerById = {}, nodeMap = new Map();
|
||||||
|
var edgeLayer = L.layerGroup().addTo(map), selectedNodeId = null;
|
||||||
var activeBlinks = new Map(), lastImportTime = null;
|
var activeBlinks = new Map(), lastImportTime = null;
|
||||||
var mapInterval = 0;
|
var mapInterval = 0;
|
||||||
const portMap = {1:"Text",67:"Telemetry",3:"Position",70:"Traceroute",4:"Node Info",71:"Neighbour Info",73:"Map Report"};
|
var unmappedPackets = [];
|
||||||
const palette = ["#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6","#bcf60c","#fabebe","#008080","#e6beff","#9a6324","#fffac8","#800000","#aaffc3","#808000","#ffd8b1","#000075","#808080"];
|
const UNMAPPED_LIMIT = 50;
|
||||||
|
const UNMAPPED_TTL_MS = 5000;
|
||||||
|
|
||||||
|
const portMap = window.PORT_LABEL_MAP;
|
||||||
|
|
||||||
|
const palette = ["#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6","#bcf60c","#fabebe",
|
||||||
|
"#008080","#e6beff","#9a6324","#fffac8","#800000","#aaffc3","#808000","#ffd8b1",
|
||||||
|
"#000075","#808080"];
|
||||||
|
|
||||||
const colorMap = new Map(); let nextColorIndex = 0;
|
const colorMap = new Map(); let nextColorIndex = 0;
|
||||||
const channelSet = new Set();
|
const channelSet = new Set();
|
||||||
|
|
||||||
// ---------------------- Helpers ----------------------
|
map.on("popupopen", function (e) {
|
||||||
function timeAgo(date){ const diff=Date.now()-new Date(date), s=Math.floor(diff/1000), m=Math.floor(s/60), h=Math.floor(m/60), d=Math.floor(h/24); return d>0?d+"d":h>0?h+"h":m>0?m+"m":s+"s"; }
|
const popupEl = e.popup.getElement();
|
||||||
function hashToColor(str){ if(colorMap.has(str)) return colorMap.get(str); const c=palette[nextColorIndex++%palette.length]; colorMap.set(str,c); return c; }
|
if (popupEl) applyTranslationsMap(popupEl);
|
||||||
function isInvalidCoord(n){ return !n||!n.lat||!n.long||n.lat===0||n.long===0||Number.isNaN(n.lat)||Number.isNaN(n.long); }
|
});
|
||||||
|
|
||||||
|
function timeAgoFromUs(us){
|
||||||
|
const diff = Date.now() - (us / 1000);
|
||||||
|
const s = Math.floor(diff/1000), m = Math.floor(s/60),
|
||||||
|
h = Math.floor(m/60), d = Math.floor(h/24);
|
||||||
|
return d>0?d+"d":h>0?h+"h":m>0?m+"m":s+"s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashToColor(str){
|
||||||
|
if(colorMap.has(str)) return colorMap.get(str);
|
||||||
|
const c = palette[nextColorIndex++ % palette.length];
|
||||||
|
colorMap.set(str,c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelFor(key, fallback){
|
||||||
|
return mapTranslations[key] || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNodePopup(node){
|
||||||
|
const labels = {
|
||||||
|
channel: labelFor("channel_label", "Channel"),
|
||||||
|
model: labelFor("model_label", "Model"),
|
||||||
|
role: labelFor("role_label", "Role"),
|
||||||
|
mqtt: labelFor("mqtt_gateway", "MQTT Gateway"),
|
||||||
|
lastSeen: labelFor("last_seen", "Last Seen"),
|
||||||
|
firmware: labelFor("firmware", "Firmware"),
|
||||||
|
yes: mapTranslations.yes || "Yes",
|
||||||
|
no: mapTranslations.no || "No"
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<b><a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||||
|
|
||||||
|
<b>${labels.channel}</b> ${node.channel}<br>
|
||||||
|
<b>${labels.model}</b> ${node.hw_model}<br>
|
||||||
|
<b>${labels.role}</b> ${node.role}<br>
|
||||||
|
<b>${labels.mqtt}</b> ${node.is_mqtt_gateway ? labels.yes : labels.no}<br>
|
||||||
|
|
||||||
|
${
|
||||||
|
node.last_seen_us
|
||||||
|
? `<b>${labels.lastSeen}</b> ${timeAgoFromUs(node.last_seen_us)}<br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
${
|
||||||
|
node.firmware
|
||||||
|
? `<b>${labels.firmware}</b> ${node.firmware}<br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashToUnit(str){
|
||||||
|
let h = 2166136261;
|
||||||
|
for(let i=0;i<str.length;i++){
|
||||||
|
h ^= str.charCodeAt(i);
|
||||||
|
h = Math.imul(h, 16777619);
|
||||||
|
}
|
||||||
|
return (h >>> 0) / 0xffffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jitterLatLng(lat, lon, key){
|
||||||
|
const meters = 15; // small, visually separates overlaps
|
||||||
|
const angle = hashToUnit(String(key)) * Math.PI * 2;
|
||||||
|
const r = meters * (0.3 + 0.7 * hashToUnit(`r:${key}`));
|
||||||
|
const dLat = (r * Math.cos(angle)) / 111320;
|
||||||
|
const dLon = (r * Math.sin(angle)) / (111320 * Math.cos(lat * Math.PI / 180));
|
||||||
|
return [lat + dLat, lon + dLon];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInvalidCoord(n){
|
||||||
|
return !n || !n.lat || !n.long || n.lat === 0 || n.long === 0 ||
|
||||||
|
Number.isNaN(n.lat) || Number.isNaN(n.long);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeLatLng(n){
|
||||||
|
const marker = markerById[n.key];
|
||||||
|
if(marker){
|
||||||
|
return marker.getLatLng();
|
||||||
|
}
|
||||||
|
return { lat: n.lat, lng: n.long };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
PACKET FETCHING (unchanged)
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
// ---------------------- Packet Fetching ----------------------
|
|
||||||
function fetchLatestPacket(){
|
function fetchLatestPacket(){
|
||||||
fetch(`/api/packets?limit=1`)
|
fetch(`/api/packets?limit=1`)
|
||||||
.then(r=>r.json())
|
.then(r=>r.json())
|
||||||
@@ -72,6 +295,7 @@ function fetchLatestPacket(){
|
|||||||
function fetchNewPackets(){
|
function fetchNewPackets(){
|
||||||
if(mapInterval <= 0) return;
|
if(mapInterval <= 0) return;
|
||||||
if(lastImportTime===null) return;
|
if(lastImportTime===null) return;
|
||||||
|
|
||||||
const url = new URL(`/api/packets`, window.location.origin);
|
const url = new URL(`/api/packets`, window.location.origin);
|
||||||
url.searchParams.set("since", lastImportTime);
|
url.searchParams.set("since", lastImportTime);
|
||||||
url.searchParams.set("limit", 50);
|
url.searchParams.set("limit", 50);
|
||||||
@@ -81,19 +305,26 @@ function fetchNewPackets(){
|
|||||||
.then(data=>{
|
.then(data=>{
|
||||||
if(!data.packets || data.packets.length===0) return;
|
if(!data.packets || data.packets.length===0) return;
|
||||||
let latest = lastImportTime;
|
let latest = lastImportTime;
|
||||||
|
|
||||||
data.packets.forEach(pkt=>{
|
data.packets.forEach(pkt=>{
|
||||||
if(pkt.import_time_us > latest) latest = pkt.import_time_us;
|
if(pkt.import_time_us > latest) latest = pkt.import_time_us;
|
||||||
|
|
||||||
const marker = markerById[pkt.from_node_id];
|
const marker = markerById[pkt.from_node_id];
|
||||||
const nodeData = nodeMap.get(pkt.from_node_id);
|
const nodeData = nodeMap.get(pkt.from_node_id);
|
||||||
if(marker && nodeData) blinkNode(marker,nodeData.long_name,pkt.portnum);
|
if(marker && nodeData) {
|
||||||
|
blinkNode(marker,nodeData.long_name,pkt.portnum);
|
||||||
|
} else {
|
||||||
|
addUnmappedPacket(pkt, nodeData);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
lastImportTime = latest;
|
lastImportTime = latest;
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Polling ----------------------
|
|
||||||
let packetInterval=null;
|
let packetInterval=null;
|
||||||
|
|
||||||
function startPacketFetcher(){
|
function startPacketFetcher(){
|
||||||
if(mapInterval<=0) return;
|
if(mapInterval<=0) return;
|
||||||
if(!packetInterval){
|
if(!packetInterval){
|
||||||
@@ -101,65 +332,58 @@ function startPacketFetcher(){
|
|||||||
packetInterval=setInterval(fetchNewPackets,mapInterval*1000);
|
packetInterval=setInterval(fetchNewPackets,mapInterval*1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPacketFetcher(){
|
function stopPacketFetcher(){
|
||||||
if(packetInterval){
|
if(packetInterval){
|
||||||
clearInterval(packetInterval);
|
clearInterval(packetInterval);
|
||||||
packetInterval=null;
|
packetInterval=null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("visibilitychange",()=>{
|
document.addEventListener("visibilitychange",()=>{
|
||||||
document.hidden?stopPacketFetcher():startPacketFetcher();
|
document.hidden?stopPacketFetcher():startPacketFetcher();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------- WAIT FOR CONFIG ----------------------
|
|
||||||
async function waitForConfig() {
|
async function waitForConfig() {
|
||||||
while (typeof window._siteConfigPromise === "undefined") {
|
while (typeof window._siteConfigPromise === "undefined") {
|
||||||
console.log("Waiting for _siteConfigPromise...");
|
|
||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise(r => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cfg = await window._siteConfigPromise;
|
const cfg = await window._siteConfigPromise;
|
||||||
if (!cfg || !cfg.site) throw new Error("Config missing site object");
|
return cfg.site || {};
|
||||||
return cfg.site;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading site config:", err);
|
console.error("Error loading site config:", err);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Load Config & Start Polling ----------------------
|
|
||||||
async function initMapPolling() {
|
async function initMapPolling() {
|
||||||
try {
|
try {
|
||||||
const site = await waitForConfig();
|
const site = await waitForConfig();
|
||||||
mapInterval = parseInt(site.map_interval, 10) || 0;
|
mapInterval = parseInt(site.map_interval, 10) || 0;
|
||||||
|
|
||||||
// ---- Check URL params ----
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const lat = parseFloat(params.get('lat'));
|
const lat = parseFloat(params.get('lat'));
|
||||||
const lng = parseFloat(params.get('lng'));
|
const lng = parseFloat(params.get('lng'));
|
||||||
const zoom = parseInt(params.get('zoom'), 10);
|
const zoom = parseInt(params.get('zoom'), 10);
|
||||||
|
|
||||||
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
|
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
|
||||||
map.setView([lat, lng], zoom);
|
map.setView([lat, lng], zoom);
|
||||||
window.configBoundsApplied = true;
|
window.configBoundsApplied = true;
|
||||||
setTimeout(() => map.invalidateSize(), 100);
|
setTimeout(() => map.invalidateSize(), 100);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const topLeft = [parseFloat(site.map_top_left_lat), parseFloat(site.map_top_left_lon)];
|
const tl = [parseFloat(site.map_top_left_lat), parseFloat(site.map_top_left_lon)];
|
||||||
const bottomRight = [parseFloat(site.map_bottom_right_lat), parseFloat(site.map_bottom_right_lon)];
|
const br = [parseFloat(site.map_bottom_right_lat), parseFloat(site.map_bottom_right_lon)];
|
||||||
if (topLeft.every(isFinite) && bottomRight.every(isFinite)) {
|
|
||||||
map.fitBounds([topLeft, bottomRight]);
|
if (tl.every(isFinite) && br.every(isFinite)) {
|
||||||
|
map.fitBounds([tl, br]);
|
||||||
window.configBoundsApplied = true;
|
window.configBoundsApplied = true;
|
||||||
setTimeout(() => map.invalidateSize(), 100);
|
setTimeout(() => map.invalidateSize(), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapInterval > 0) {
|
if (mapInterval > 0) startPacketFetcher();
|
||||||
console.log(`Starting map polling every ${mapInterval}s`);
|
|
||||||
startPacketFetcher();
|
|
||||||
} else {
|
|
||||||
console.log("Map polling disabled (map_interval=0)");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load /api/config:", err);
|
console.error("Failed to load /api/config:", err);
|
||||||
@@ -168,169 +392,351 @@ async function initMapPolling() {
|
|||||||
|
|
||||||
initMapPolling();
|
initMapPolling();
|
||||||
|
|
||||||
// ---------------------- Load Nodes + Edges ----------------------
|
/* ======================================================
|
||||||
fetch('/api/nodes?days_active=3').then(r=>r.json()).then(data=>{
|
LOAD NODES
|
||||||
if(!data.nodes) return;
|
====================================================== */
|
||||||
nodes = data.nodes.map(n=>({
|
|
||||||
key: n.node_id!==null?n.node_id:n.id,
|
fetch('/api/nodes?days_active=3')
|
||||||
id: n.id,
|
.then(r=>r.json())
|
||||||
node_id: n.node_id,
|
.then(data=>{
|
||||||
lat: n.last_lat?n.last_lat/1e7:null,
|
if(!data.nodes) return;
|
||||||
long: n.last_long?n.last_long/1e7:null,
|
|
||||||
long_name: n.long_name||"",
|
nodes = data.nodes.map(n=>({
|
||||||
short_name: n.short_name||"",
|
key: n.node_id ?? n.id,
|
||||||
channel: n.channel||"",
|
id: n.id,
|
||||||
hw_model: n.hw_model||"",
|
node_id: n.node_id,
|
||||||
role: n.role||"",
|
lat: n.last_lat ? n.last_lat/1e7 : null,
|
||||||
firmware: n.firmware||"",
|
long: n.last_long ? n.last_long/1e7 : null,
|
||||||
last_update: n.last_update||"",
|
long_name: n.long_name || "",
|
||||||
isRouter: n.role? n.role.toLowerCase().includes("router"):false
|
short_name: n.short_name || "",
|
||||||
}));
|
channel: n.channel || "",
|
||||||
nodes.forEach(n=>{ nodeMap.set(n.key,n); if(n.channel) channelSet.add(n.channel); });
|
hw_model: n.hw_model || "",
|
||||||
renderNodesOnMap();
|
role: n.role || "",
|
||||||
createChannelFilters();
|
firmware: n.firmware || "",
|
||||||
return fetch('/api/edges');
|
last_seen_us: n.last_seen_us || null,
|
||||||
}).then(r=>r?r.json():null).then(data=>{
|
is_mqtt_gateway: n.is_mqtt_gateway === true,
|
||||||
if(data && data.edges) edgesData=data.edges;
|
isRouter: (n.role||"").toLowerCase().includes("router")
|
||||||
}).catch(console.error);
|
}));
|
||||||
|
|
||||||
|
nodes.forEach(n=>{
|
||||||
|
nodeMap.set(n.key, n);
|
||||||
|
if(n.channel) channelSet.add(n.channel);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderNodesOnMap();
|
||||||
|
createChannelFilters();
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
RENDER NODES
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
// ---------------------- Render Nodes ----------------------
|
|
||||||
function renderNodesOnMap(){
|
function renderNodesOnMap(){
|
||||||
const bounds = L.latLngBounds();
|
|
||||||
nodes.forEach(node=>{
|
nodes.forEach(node=>{
|
||||||
if(isInvalidCoord(node)) return;
|
if(isInvalidCoord(node)) return;
|
||||||
|
|
||||||
const color = hashToColor(node.channel);
|
const color = hashToColor(node.channel);
|
||||||
const opts = { radius: node.isRouter?9:7, color:"white", fillColor:color, fillOpacity:1, weight:0.7 };
|
|
||||||
const marker = L.circleMarker([node.lat,node.long],opts).addTo(map);
|
const [jLat, jLon] = jitterLatLng(node.lat, node.long, node.key);
|
||||||
|
const marker = L.circleMarker([jLat,jLon], {
|
||||||
|
radius: node.isRouter ? 9 : 7,
|
||||||
|
color: "white",
|
||||||
|
fillColor: color,
|
||||||
|
fillOpacity: 1,
|
||||||
|
weight: 0.7
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
marker.nodeId = node.key;
|
marker.nodeId = node.key;
|
||||||
marker.originalColor = color;
|
marker.originalColor = color;
|
||||||
markerById[node.key] = marker;
|
markerById[node.key] = marker;
|
||||||
const popup = `<b><a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})</b><br>
|
|
||||||
<b>Channel:</b> ${node.channel}<br>
|
marker.bindPopup(buildNodePopup(node));
|
||||||
<b>Model:</b> ${node.hw_model}<br>
|
marker.on('click', () => {
|
||||||
<b>Role:</b> ${node.role}<br>
|
onNodeClick(node);
|
||||||
${node.last_update? `<b>Last seen:</b> ${timeAgo(node.last_update)}<br>`:""}
|
marker.setPopupContent(buildNodePopup(node));
|
||||||
${node.firmware? `<b>Firmware:</b> ${node.firmware}<br>`:""}`;
|
marker.openPopup();
|
||||||
marker.on('click',()=>{ onNodeClick(node); marker.bindPopup(popup).openPopup(); setTimeout(()=>marker.closePopup(),3000); });
|
});
|
||||||
bounds.extend(marker.getLatLng());
|
|
||||||
});
|
});
|
||||||
if(!window.configBoundsApplied && bounds.isValid()){
|
|
||||||
map.fitBounds(bounds);
|
setTimeout(() => applyTranslationsMap(), 50);
|
||||||
setTimeout(()=>map.invalidateSize(),100);
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
UNMAPPED PACKETS LIST
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
|
function addUnmappedPacket(pkt, nodeData){
|
||||||
|
if(nodeData && !isInvalidCoord(nodeData)) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = {
|
||||||
|
id: pkt.id,
|
||||||
|
key: `${pkt.id ?? "x"}-${pkt.import_time_us ?? now}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
import_time_us: pkt.import_time_us || 0,
|
||||||
|
from_node_id: pkt.from_node_id,
|
||||||
|
long_name: pkt.long_name || (nodeData?.long_name || ""),
|
||||||
|
portnum: pkt.portnum,
|
||||||
|
payload: (pkt.payload || "").trim(),
|
||||||
|
expires_at: now + UNMAPPED_TTL_MS
|
||||||
|
};
|
||||||
|
|
||||||
|
unmappedPackets.unshift(entry);
|
||||||
|
pruneUnmappedPackets(now);
|
||||||
|
if(unmappedPackets.length > UNMAPPED_LIMIT){
|
||||||
|
unmappedPackets = unmappedPackets.slice(0, UNMAPPED_LIMIT);
|
||||||
|
}
|
||||||
|
renderUnmappedPackets();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
pruneUnmappedPackets(Date.now());
|
||||||
|
renderUnmappedPackets();
|
||||||
|
}, UNMAPPED_TTL_MS + 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneUnmappedPackets(now){
|
||||||
|
unmappedPackets = unmappedPackets.filter(p => p.expires_at > now);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUnmappedPackets(){
|
||||||
|
pruneUnmappedPackets(Date.now());
|
||||||
|
const list = document.getElementById("unmapped-list");
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
if(unmappedPackets.length === 0){
|
||||||
|
const empty = document.createElement("li");
|
||||||
|
empty.className = "unmapped-empty";
|
||||||
|
empty.dataset.translateLang = "unmapped_packets_empty";
|
||||||
|
empty.textContent = "No recent unmapped packets.";
|
||||||
|
list.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmappedPackets.forEach(p=>{
|
||||||
|
const li = document.createElement("li");
|
||||||
|
|
||||||
|
const node = document.createElement("span");
|
||||||
|
node.className = "unmapped-node";
|
||||||
|
const type = portMap[p.portnum] || `Port ${p.portnum ?? "?"}`;
|
||||||
|
const name = p.long_name || `Node ${p.from_node_id ?? "?"}`;
|
||||||
|
node.textContent = `${name} (${type})`;
|
||||||
|
|
||||||
|
li.appendChild(node);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
⭐ NEW: DYNAMIC EDGE LOADING
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
|
async function onNodeClick(node){
|
||||||
|
selectedNodeId = node.key;
|
||||||
|
edgeLayer.clearLayers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/edges?node_id=${node.key}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const edges = data.edges || [];
|
||||||
|
|
||||||
|
edges.forEach(edge=>{
|
||||||
|
const f = nodeMap.get(edge.from);
|
||||||
|
const t = nodeMap.get(edge.to);
|
||||||
|
|
||||||
|
if(!f || !t || isInvalidCoord(f) || isInvalidCoord(t)) return;
|
||||||
|
|
||||||
|
const color = edge.type === "neighbor" ? "gray" : "orange";
|
||||||
|
const fLatLng = getNodeLatLng(f);
|
||||||
|
const tLatLng = getNodeLatLng(t);
|
||||||
|
const line = L.polyline([[fLatLng.lat, fLatLng.lng], [tLatLng.lat, tLatLng.lng]], {
|
||||||
|
color, weight: 3
|
||||||
|
}).addTo(edgeLayer);
|
||||||
|
|
||||||
|
if(edge.type === "traceroute"){
|
||||||
|
L.polylineDecorator(line, {
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
offset: '100%',
|
||||||
|
repeat: 0,
|
||||||
|
symbol: L.Symbol.arrowHead({
|
||||||
|
pixelSize:5,
|
||||||
|
polygon:false,
|
||||||
|
pathOptions:{stroke:true,color}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).addTo(edgeLayer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err){
|
||||||
|
console.error("Failed to load edges for node", node.key, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Render Edges ----------------------
|
map.on('click', e=>{
|
||||||
function onNodeClick(node){
|
if(!e.originalEvent.target.classList.contains('leaflet-interactive')){
|
||||||
selectedNodeId = node.key;
|
edgeLayer.clearLayers();
|
||||||
edgeLayer.clearLayers();
|
selectedNodeId=null;
|
||||||
edgesData.forEach(edge=>{
|
}
|
||||||
if(edge.from!==node.key && edge.to!==node.key) return;
|
});
|
||||||
const f=nodeMap.get(edge.from), t=nodeMap.get(edge.to);
|
|
||||||
if(!f||!t||isInvalidCoord(f)||isInvalidCoord(t)) return;
|
/* ======================================================
|
||||||
const color=edge.type==="neighbor"?"gray":"orange";
|
BLINKING
|
||||||
const l=L.polyline([[f.lat,f.long],[t.lat,t.long]],{color,weight:3}).addTo(edgeLayer);
|
====================================================== */
|
||||||
if(edge.type==="traceroute"){
|
|
||||||
L.polylineDecorator(l,{patterns:[{offset:'100%',repeat:0,symbol:L.Symbol.arrowHead({pixelSize:5,polygon:false,pathOptions:{stroke:true,color}})}]}).addTo(edgeLayer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
map.on('click',e=>{ if(!e.originalEvent.target.classList.contains('leaflet-interactive')){ edgeLayer.clearLayers(); selectedNodeId=null; } });
|
|
||||||
|
|
||||||
// ---------------------- Packet Blinking ----------------------
|
|
||||||
function blinkNode(marker,longName,portnum){
|
function blinkNode(marker,longName,portnum){
|
||||||
if(!map.hasLayer(marker)) return;
|
if(!map.hasLayer(marker)) return;
|
||||||
if(activeBlinks.has(marker)){ clearInterval(activeBlinks.get(marker)); marker.setStyle({fillColor:marker.originalColor}); if(marker.tooltip) map.removeLayer(marker.tooltip); }
|
|
||||||
let blinkCount=0;
|
if(activeBlinks.has(marker)){
|
||||||
const portName = portMap[portnum]||`Port ${portnum}`;
|
clearInterval(activeBlinks.get(marker));
|
||||||
const tooltip = L.tooltip({permanent:true,direction:'top',offset:[0,-marker.options.radius-5],className:'blinking-tooltip'})
|
marker.setStyle({ fillColor: marker.originalColor });
|
||||||
.setContent(`${longName} (${portName})`).setLatLng(marker.getLatLng()).addTo(map);
|
if(marker.tooltip) map.removeLayer(marker.tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
let blinkCount = 0;
|
||||||
|
const tooltip = L.tooltip({
|
||||||
|
permanent:true,
|
||||||
|
direction:'top',
|
||||||
|
offset:[0,-marker.options.radius-5],
|
||||||
|
className:'blinking-tooltip'
|
||||||
|
})
|
||||||
|
.setContent(`${longName} (${portMap[portnum] || "Port "+portnum})`)
|
||||||
|
.setLatLng(marker.getLatLng())
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
marker.tooltip = tooltip;
|
marker.tooltip = tooltip;
|
||||||
|
|
||||||
const interval = setInterval(()=>{
|
const interval = setInterval(()=>{
|
||||||
if(map.hasLayer(marker)){ marker.setStyle({fillColor: blinkCount%2===0?'yellow':marker.originalColor}); marker.bringToFront(); }
|
if(map.hasLayer(marker)){
|
||||||
|
marker.setStyle({
|
||||||
|
fillColor: blinkCount%2===0 ? 'yellow' : marker.originalColor
|
||||||
|
});
|
||||||
|
marker.bringToFront();
|
||||||
|
}
|
||||||
blinkCount++;
|
blinkCount++;
|
||||||
if(blinkCount>7){ clearInterval(interval); marker.setStyle({fillColor:marker.originalColor}); map.removeLayer(tooltip); activeBlinks.delete(marker); }
|
|
||||||
|
if(blinkCount>7){
|
||||||
|
clearInterval(interval);
|
||||||
|
marker.setStyle({ fillColor: marker.originalColor });
|
||||||
|
map.removeLayer(tooltip);
|
||||||
|
activeBlinks.delete(marker);
|
||||||
|
}
|
||||||
|
|
||||||
},500);
|
},500);
|
||||||
activeBlinks.set(marker,interval);
|
|
||||||
|
activeBlinks.set(marker, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Channel Filters ----------------------
|
/* ======================================================
|
||||||
|
CHANNEL FILTERS
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
function createChannelFilters(){
|
function createChannelFilters(){
|
||||||
const filterContainer = document.getElementById("filter-container");
|
const filterContainer = document.getElementById("filter-container");
|
||||||
const savedState = JSON.parse(localStorage.getItem("mapFilters") || "{}");
|
const saved = JSON.parse(localStorage.getItem("mapFilters") || "{}");
|
||||||
|
|
||||||
channelSet.forEach(channel=>{
|
channelSet.forEach(channel=>{
|
||||||
const checkbox = document.createElement("input");
|
const cb=document.createElement("input");
|
||||||
checkbox.type = "checkbox";
|
cb.type="checkbox";
|
||||||
checkbox.className = "filter-checkbox";
|
cb.className="filter-checkbox";
|
||||||
checkbox.id = `filter-channel-${channel}`;
|
cb.id=`filter-channel-${channel}`;
|
||||||
checkbox.checked = savedState[channel] !== false;
|
cb.checked = saved[channel] !== false;
|
||||||
checkbox.addEventListener("change", saveFiltersToLocalStorage);
|
|
||||||
checkbox.addEventListener("change", updateNodeVisibility);
|
|
||||||
filterContainer.appendChild(checkbox);
|
|
||||||
|
|
||||||
const label = document.createElement("label");
|
cb.addEventListener("change", saveFiltersToLocalStorage);
|
||||||
label.htmlFor = checkbox.id;
|
cb.addEventListener("change", updateNodeVisibility);
|
||||||
label.innerText = channel;
|
|
||||||
|
filterContainer.appendChild(cb);
|
||||||
|
|
||||||
|
const label=document.createElement("label");
|
||||||
|
label.htmlFor=cb.id;
|
||||||
|
label.innerText=channel;
|
||||||
label.style.color = hashToColor(channel);
|
label.style.color = hashToColor(channel);
|
||||||
filterContainer.appendChild(label);
|
filterContainer.appendChild(label);
|
||||||
});
|
});
|
||||||
|
|
||||||
const routerOnly = document.getElementById("filter-routers-only");
|
const routerOnly=document.getElementById("filter-routers-only");
|
||||||
routerOnly.checked = savedState["routersOnly"] || false;
|
const mqttOnly=document.getElementById("filter-mqtt-only");
|
||||||
|
routerOnly.checked = saved["routersOnly"] || false;
|
||||||
|
mqttOnly.checked = saved["mqttOnly"] || false;
|
||||||
|
|
||||||
routerOnly.addEventListener("change", saveFiltersToLocalStorage);
|
routerOnly.addEventListener("change", saveFiltersToLocalStorage);
|
||||||
routerOnly.addEventListener("change", updateNodeVisibility);
|
routerOnly.addEventListener("change", updateNodeVisibility);
|
||||||
|
mqttOnly.addEventListener("change", saveFiltersToLocalStorage);
|
||||||
|
mqttOnly.addEventListener("change", updateNodeVisibility);
|
||||||
|
|
||||||
updateNodeVisibility();
|
updateNodeVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFiltersToLocalStorage(){
|
function saveFiltersToLocalStorage(){
|
||||||
const state = {};
|
const state = {};
|
||||||
channelSet.forEach(ch => {
|
channelSet.forEach(ch=>{
|
||||||
const el = document.getElementById(`filter-channel-${ch}`);
|
state[ch] = document.getElementById(`filter-channel-${ch}`).checked;
|
||||||
state[ch] = el.checked;
|
|
||||||
});
|
});
|
||||||
state["routersOnly"] = document.getElementById("filter-routers-only").checked;
|
state["routersOnly"] = document.getElementById("filter-routers-only").checked;
|
||||||
|
state["mqttOnly"] = document.getElementById("filter-mqtt-only").checked;
|
||||||
|
|
||||||
localStorage.setItem("mapFilters", JSON.stringify(state));
|
localStorage.setItem("mapFilters", JSON.stringify(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodeVisibility(){
|
function updateNodeVisibility(){
|
||||||
const showRoutersOnly = document.getElementById("filter-routers-only").checked;
|
const routerOnly = document.getElementById("filter-routers-only").checked;
|
||||||
const activeChannels = Array.from(channelSet).filter(ch=>document.getElementById(`filter-channel-${ch}`).checked);
|
const mqttOnly = document.getElementById("filter-mqtt-only").checked;
|
||||||
|
const activeChannels = [...channelSet].filter(ch =>
|
||||||
|
document.getElementById(`filter-channel-${ch}`).checked
|
||||||
|
);
|
||||||
|
|
||||||
nodes.forEach(n=>{
|
nodes.forEach(n=>{
|
||||||
const marker = markerById[n.key];
|
const marker = markerById[n.key];
|
||||||
if(marker){
|
if(marker){
|
||||||
const visible = (!showRoutersOnly || n.isRouter) && activeChannels.includes(n.channel);
|
const visible =
|
||||||
if(visible) map.addLayer(marker); else map.removeLayer(marker);
|
(!routerOnly || n.isRouter) &&
|
||||||
|
(!mqttOnly || n.is_mqtt_gateway) &&
|
||||||
|
activeChannels.includes(n.channel);
|
||||||
|
|
||||||
|
visible ? map.addLayer(marker) : map.removeLayer(marker);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Share / Reset ----------------------
|
/* ======================================================
|
||||||
function shareCurrentView() {
|
SHARE / RESET
|
||||||
const center = map.getCenter();
|
====================================================== */
|
||||||
const zoom = map.getZoom();
|
|
||||||
const lat = center.lat.toFixed(6);
|
|
||||||
const lng = center.lng.toFixed(6);
|
|
||||||
|
|
||||||
const shareUrl = `${window.location.origin}/map?lat=${lat}&lng=${lng}&zoom=${zoom}`;
|
function shareCurrentView() {
|
||||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
const c = map.getCenter();
|
||||||
const button = document.getElementById('share-button');
|
const url = `${window.location.origin}/map?lat=${c.lat.toFixed(6)}&lng=${c.lng.toFixed(6)}&zoom=${map.getZoom()}`;
|
||||||
const originalText = button.textContent;
|
|
||||||
button.textContent = '✓ Link Copied!';
|
navigator.clipboard.writeText(url).then(()=>{
|
||||||
button.style.backgroundColor = '#2196F3';
|
const btn = document.getElementById('share-button');
|
||||||
setTimeout(() => {
|
const old = btn.textContent;
|
||||||
button.textContent = originalText;
|
btn.textContent = '✓ ' + (mapTranslations.link_copied || 'Link Copied!');
|
||||||
button.style.backgroundColor = '#4CAF50';
|
btn.style.backgroundColor = '#2196F3';
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
btn.textContent = old;
|
||||||
|
btn.style.backgroundColor = '#4CAF50';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}).catch(() => alert('Share this link:\n' + shareUrl));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFiltersToDefaults(){
|
function resetFiltersToDefaults(){
|
||||||
document.getElementById("filter-routers-only").checked = false;
|
document.getElementById("filter-routers-only").checked = false;
|
||||||
channelSet.forEach(ch=>document.getElementById(`filter-channel-${ch}`).checked = true);
|
document.getElementById("filter-mqtt-only").checked = false;
|
||||||
|
channelSet.forEach(ch => {
|
||||||
|
document.getElementById(`filter-channel-${ch}`).checked = true;
|
||||||
|
});
|
||||||
saveFiltersToLocalStorage();
|
saveFiltersToLocalStorage();
|
||||||
updateNodeVisibility();
|
updateNodeVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
TRANSLATION LOAD
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
loadTranslationsMap();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+119
-70
@@ -23,20 +23,29 @@
|
|||||||
.channel { font-style: italic; color: #bbb; }
|
.channel { font-style: italic; color: #bbb; }
|
||||||
.channel a { font-style: normal; color: #999; }
|
.channel a { font-style: normal; color: #999; }
|
||||||
|
|
||||||
@keyframes flash { 0% { background-color: #ffe066; } 100% { background-color: inherit; } }
|
|
||||||
.chat-packet.flash { animation: flash 3.5s ease-out; }
|
|
||||||
|
|
||||||
.replying-to { font-size: 0.8em; color: #aaa; margin-top: 2px; padding-left: 10px; }
|
|
||||||
.replying-to .reply-preview { color: #aaa; }
|
|
||||||
|
|
||||||
#weekly-message { margin: 15px 0; font-weight: bold; color: #ffeb3b; }
|
#weekly-message { margin: 15px 0; font-weight: bold; color: #ffeb3b; }
|
||||||
#total-count { margin-bottom: 10px; font-style: italic; color: #ccc; }
|
#total-count { margin-bottom: 10px; font-style: italic; color: #ccc; }
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="weekly-message">Loading weekly message...</div>
|
<!-- ⭐ NET TITLE WITH ICON ⭐ -->
|
||||||
<div id="total-count">Total messages: 0</div>
|
<div class="container px-2">
|
||||||
|
<h2 style="color:white; margin:0 0 10px 0;">
|
||||||
|
<span class="icon">💬</span>
|
||||||
|
<span data-translate-lang="net_title"></span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Weekly network message -->
|
||||||
|
<div id="weekly-message"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Total message count -->
|
||||||
|
<div id="total-count">
|
||||||
|
<span data-translate-lang="total_messages">Total messages:</span>
|
||||||
|
<span id="total-count-value">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="chat-container">
|
<div id="chat-container">
|
||||||
<div class="container" id="chat-log"></div>
|
<div class="container" id="chat-log"></div>
|
||||||
@@ -45,140 +54,180 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
const chatContainer = document.querySelector("#chat-log");
|
const chatContainer = document.querySelector("#chat-log");
|
||||||
const totalCountEl = document.querySelector("#total-count");
|
|
||||||
const weeklyMessageEl = document.querySelector("#weekly-message");
|
const weeklyMessageEl = document.querySelector("#weekly-message");
|
||||||
if (!chatContainer || !totalCountEl || !weeklyMessageEl) {
|
const totalCountValueEl = document.querySelector("#total-count-value");
|
||||||
console.error("Required elements not found");
|
|
||||||
|
if (!chatContainer || !weeklyMessageEl || !totalCountValueEl) {
|
||||||
|
console.error("Required elements missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedPacketIds = new Set();
|
const renderedPacketIds = new Set();
|
||||||
const packetMap = new Map();
|
let netTranslations = {};
|
||||||
let chatTranslations = {};
|
|
||||||
let netTag = "";
|
let netTag = "";
|
||||||
|
|
||||||
function updateTotalCount() {
|
/* -----------------------------------
|
||||||
totalCountEl.textContent = `Total messages: ${renderedPacketIds.size}`;
|
Escape HTML safely
|
||||||
}
|
----------------------------------- */
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.textContent = text ?? "";
|
div.textContent = text ?? "";
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTranslations(translations, root = document) {
|
/* -----------------------------------
|
||||||
|
Apply translations
|
||||||
|
----------------------------------- */
|
||||||
|
function applyTranslations(trans, root=document) {
|
||||||
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||||
const key = el.dataset.translateLang;
|
const key = el.dataset.translateLang;
|
||||||
if (translations[key]) el.textContent = translations[key];
|
if (trans[key]) el.textContent = trans[key];
|
||||||
});
|
});
|
||||||
root.querySelectorAll("[data-translate-lang-title]").forEach(el => {
|
root.querySelectorAll("[data-translate-lang-title]").forEach(el => {
|
||||||
const key = el.dataset.translateLangTitle;
|
const key = el.dataset.translateLangTitle;
|
||||||
if (translations[key]) el.title = translations[key];
|
if (trans[key]) el.title = trans[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------
|
||||||
|
Update count
|
||||||
|
----------------------------------- */
|
||||||
|
function updateTotalCount() {
|
||||||
|
totalCountValueEl.textContent = renderedPacketIds.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------
|
||||||
|
Render single packet
|
||||||
|
----------------------------------- */
|
||||||
function renderPacket(packet) {
|
function renderPacket(packet) {
|
||||||
if (renderedPacketIds.has(packet.id)) return;
|
if (renderedPacketIds.has(packet.id)) return;
|
||||||
renderedPacketIds.add(packet.id);
|
renderedPacketIds.add(packet.id);
|
||||||
packetMap.set(packet.id, packet);
|
|
||||||
|
|
||||||
const date = new Date(packet.import_time_us / 1000);
|
const date = new Date(packet.import_time_us / 1000);
|
||||||
const formattedTime = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true });
|
|
||||||
const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getDate().toString().padStart(2, "0")}/${date.getFullYear()}`;
|
|
||||||
const formattedTimestamp = `${formattedTime} - ${formattedDate}`;
|
|
||||||
|
|
||||||
let replyHtml = "";
|
const timeStr = date.toLocaleTimeString([], {
|
||||||
if (packet.reply_id) {
|
hour: "numeric",
|
||||||
const parent = packetMap.get(packet.reply_id);
|
minute: "2-digit",
|
||||||
if (parent) {
|
second: "2-digit",
|
||||||
replyHtml = `<div class="replying-to">
|
hour12: true
|
||||||
<div class="reply-preview">
|
});
|
||||||
<i data-translate-lang="replying_to"></i>
|
|
||||||
<strong>${escapeHtml((parent.long_name || "").trim() || `Node ${parent.from_node_id}`)}</strong>:
|
const dateStr =
|
||||||
${escapeHtml(parent.payload || "")}
|
`${String(date.getMonth()+1).padStart(2,"0")}/`+
|
||||||
</div>
|
`${String(date.getDate()).padStart(2,"0")}/`+
|
||||||
</div>`;
|
date.getFullYear();
|
||||||
} else {
|
|
||||||
replyHtml = `<div class="replying-to">
|
const timestamp = `${timeStr} - ${dateStr}`;
|
||||||
<i data-translate-lang="replying_to"></i>
|
|
||||||
<a href="/packet/${packet.reply_id}">${packet.reply_id}</a>
|
const fromName =
|
||||||
</div>`;
|
(packet.long_name || "").trim() ||
|
||||||
}
|
`${netTranslations.node_fallback} ${packet.from_node_id}`;
|
||||||
}
|
|
||||||
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "row chat-packet";
|
div.className = "row chat-packet";
|
||||||
div.dataset.packetId = packet.id;
|
div.dataset.packetId = packet.id;
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<span class="col-2 timestamp" title="${packet.import_time_us}">${formattedTimestamp}</span>
|
<span class="col-2 timestamp" title="${packet.import_time_us}">
|
||||||
|
${timestamp}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="col-2 channel">
|
<span class="col-2 channel">
|
||||||
<a href="/packet/${packet.id}" data-translate-lang-title="view_packet_details">✉️</a>
|
<a href="/packet/${packet.id}"
|
||||||
|
data-translate-lang-title="view_packet_details">✉️</a>
|
||||||
${escapeHtml(packet.channel || "")}
|
${escapeHtml(packet.channel || "")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="col-3 nodename">
|
<span class="col-3 nodename">
|
||||||
<a href="/packet_list/${packet.from_node_id}">
|
<a href="/node/${packet.from_node_id}">
|
||||||
${escapeHtml((packet.long_name || "").trim() || `Node ${packet.from_node_id}`)}
|
${escapeHtml(fromName)}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="col-5 message">${escapeHtml(packet.payload)}${replyHtml}</span>
|
|
||||||
|
<span class="col-5 message">
|
||||||
|
${escapeHtml(packet.payload).replace(/\n/g,"<br>")}
|
||||||
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
chatContainer.prepend(div);
|
chatContainer.prepend(div);
|
||||||
applyTranslations(chatTranslations, div);
|
applyTranslations(netTranslations, div);
|
||||||
updateTotalCount();
|
updateTotalCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------
|
||||||
|
Sort descending by time
|
||||||
|
----------------------------------- */
|
||||||
function renderPacketsEnsureDescending(packets) {
|
function renderPacketsEnsureDescending(packets) {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!packets || !packets.length) return;
|
||||||
const sortedDesc = packets.slice().sort((a, b) => b.import_time_us - a.import_time_us);
|
const sorted = packets.slice().sort((a, b) => b.import_time_us - a.import_time_us);
|
||||||
for (let i = sortedDesc.length - 1; i >= 0; i--) renderPacket(sortedDesc[i]);
|
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||||
|
renderPacket(sorted[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------
|
||||||
|
Fetch initial net-tagged packets
|
||||||
|
----------------------------------- */
|
||||||
async function fetchInitialPackets(tag) {
|
async function fetchInitialPackets(tag) {
|
||||||
if (!tag) {
|
if (!tag) return;
|
||||||
console.warn("No net_tag defined, skipping packet fetch.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
console.log("Fetching packets for netTag:", tag);
|
const sixDaysAgoMs = Date.now() - 6*24*60*60*1000;
|
||||||
const sixDaysAgoMs = Date.now() - (6 * 24 * 60 * 60 * 1000);
|
|
||||||
const sinceUs = Math.floor(sixDaysAgoMs * 1000);
|
const sinceUs = Math.floor(sixDaysAgoMs * 1000);
|
||||||
const resp = await fetch(`/api/packets?portnum=1&contains=${encodeURIComponent(tag)}&since=${sinceUs}`);
|
|
||||||
|
const url =
|
||||||
|
`/api/packets?portnum=1&contains=${encodeURIComponent(tag)}&since=${sinceUs}&limit=1000`;
|
||||||
|
|
||||||
|
const resp = await fetch(url);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
console.log("Packets received:", data?.packets?.length);
|
|
||||||
if (data?.packets?.length) renderPacketsEnsureDescending(data.packets);
|
if (data?.packets?.length)
|
||||||
|
renderPacketsEnsureDescending(data.packets);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Initial fetch error:", err);
|
console.error("Initial fetch error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------
|
||||||
|
Load translations from section=net
|
||||||
|
----------------------------------- */
|
||||||
async function loadTranslations(cfg) {
|
async function loadTranslations(cfg) {
|
||||||
try {
|
try {
|
||||||
const langCode = cfg?.site?.language || "en";
|
const lang = cfg?.site?.language || "en";
|
||||||
const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`);
|
const res = await fetch(`/api/lang?lang=${lang}§ion=net`);
|
||||||
chatTranslations = await res.json();
|
netTranslations = await res.json();
|
||||||
applyTranslations(chatTranslations, document);
|
applyTranslations(netTranslations, document);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Chat translation load failed:", err);
|
console.error("Failed loading translations", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MAIN LOGIC ---
|
/* -----------------------------------
|
||||||
|
MAIN
|
||||||
|
----------------------------------- */
|
||||||
try {
|
try {
|
||||||
const cfg = await window._siteConfigPromise; // ✅ Already fetched by base.html
|
const cfg = await window._siteConfigPromise;
|
||||||
const site = cfg?.site || {};
|
const site = cfg?.site || {};
|
||||||
|
|
||||||
// Populate from config
|
|
||||||
netTag = site.net_tag || "";
|
netTag = site.net_tag || "";
|
||||||
weeklyMessageEl.textContent = site.weekly_net_message || "Weekly message not set.";
|
|
||||||
|
weeklyMessageEl.textContent = site.weekly_net_message || "";
|
||||||
|
|
||||||
|
|
||||||
await loadTranslations(cfg);
|
await loadTranslations(cfg);
|
||||||
await fetchInitialPackets(netTag);
|
await fetchInitialPackets(netTag);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Initialization failed:", err);
|
console.error("Initialization failed:", err);
|
||||||
weeklyMessageEl.textContent = "Failed to load site config.";
|
|
||||||
|
weeklyMessageEl.textContent =
|
||||||
|
netTranslations.failed_to_load_site_config ||
|
||||||
|
"Failed to load site config.";
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+1292
-314
File diff suppressed because it is too large
Load Diff
+439
-114
@@ -1,12 +1,31 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
overflow-x: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 80%;
|
/* FIX: allow table to keep natural width so scrolling works */
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Desktop scroll wrapper */
|
||||||
|
#node-list {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto; /* allows horizontal scroll */
|
||||||
|
overflow-y: hidden;
|
||||||
|
/* !!! removed display:flex because it prevents scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
#node-list table {
|
||||||
|
width: max-content; /* table keeps its natural width */
|
||||||
|
min-width: 100%; /* won't shrink smaller than viewport */
|
||||||
|
}
|
||||||
th, td {
|
th, td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
@@ -84,7 +103,23 @@ select, .export-btn, .search-box, .clear-btn {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.node-status {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #2a6a8a;
|
||||||
|
background: #0d2a3a;
|
||||||
|
color: #9fd4ff;
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
.node-status.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Favorite stars */
|
||||||
.favorite-star {
|
.favorite-star {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
@@ -100,6 +135,7 @@ select, .export-btn, .search-box, .clear-btn {
|
|||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Favorite filter button */
|
||||||
.favorites-btn {
|
.favorites-btn {
|
||||||
background-color: #ffd700;
|
background-color: #ffd700;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -114,49 +150,171 @@ select, .export-btn, .search-box, .clear-btn {
|
|||||||
background-color: #ff6b6b;
|
background-color: #ff6b6b;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------- */
|
||||||
|
/* MOBILE CARD VIEW */
|
||||||
|
/* --------------------------------------------- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
/* Hide desktop view */
|
||||||
|
#node-list table {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show mobile cards */
|
||||||
|
#mobile-node-list {
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
/* If you want horizontal swiping, uncomment:
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card-field {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card-field b {
|
||||||
|
color: #9fd4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-star {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<input type="text" id="search-box" class="search-box" placeholder="Search by name or ID..." />
|
|
||||||
|
|
||||||
<select id="role-filter"><option value="">All Roles</option></select>
|
<input
|
||||||
<select id="channel-filter"><option value="">All Channels</option></select>
|
type="text"
|
||||||
<select id="hw-filter"><option value="">All HW Models</option></select>
|
id="search-box"
|
||||||
<select id="firmware-filter"><option value="">All Firmware</option></select>
|
class="search-box"
|
||||||
|
data-translate-lang="search_placeholder"
|
||||||
|
placeholder="Search by name or ID or HEX ID..."
|
||||||
|
/>
|
||||||
|
|
||||||
<button class="favorites-btn" id="favorites-btn">⭐ Show Favorites</button>
|
<select id="role-filter">
|
||||||
<button class="export-btn" id="export-btn">Export CSV</button>
|
<option value="" data-translate-lang="all_roles">All Roles</option>
|
||||||
<button class="clear-btn" id="clear-btn">Clear Filters</button>
|
</select>
|
||||||
|
|
||||||
|
<select id="channel-filter">
|
||||||
|
<option value="" data-translate-lang="all_channels">All Channels</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="hw-filter">
|
||||||
|
<option value="" data-translate-lang="all_hw">All HW Models</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="firmware-filter">
|
||||||
|
<option value="" data-translate-lang="all_firmware">All Firmware</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="favorites-btn" id="favorites-btn" data-translate-lang="show_favorites">
|
||||||
|
⭐ Show Favorites
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="export-btn" id="export-btn" data-translate-lang="export_csv">
|
||||||
|
Export CSV
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="clear-btn" id="clear-btn" data-translate-lang="clear_filters">
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="count-container">
|
<div class="count-container">
|
||||||
Showing <span id="node-count">0</span> nodes
|
<span data-translate-lang="showing_nodes">Showing</span>
|
||||||
|
<span id="node-count">0</span>
|
||||||
|
<span data-translate-lang="nodes_suffix">nodes</span>
|
||||||
|
<span id="node-status" class="node-status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop table -->
|
||||||
<div id="node-list">
|
<div id="node-list">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Short <span class="sort-icon">▲</span></th>
|
<th data-translate-lang="short_name">Short <span class="sort-icon">▲</span></th>
|
||||||
<th>Long Name <span class="sort-icon"></span></th>
|
<th data-translate-lang="long_name">Long Name <span class="sort-icon"></span></th>
|
||||||
<th>HW Model <span class="sort-icon"></span></th>
|
<th data-translate-lang="hw_model">HW Model <span class="sort-icon"></span></th>
|
||||||
<th>Firmware <span class="sort-icon"></span></th>
|
<th data-translate-lang="firmware">Firmware <span class="sort-icon"></span></th>
|
||||||
<th>Role <span class="sort-icon"></span></th>
|
<th data-translate-lang="role">Role <span class="sort-icon"></span></th>
|
||||||
<th>Last Latitude <span class="sort-icon"></span></th>
|
<th data-translate-lang="last_lat">Last Latitude <span class="sort-icon"></span></th>
|
||||||
<th>Last Longitude <span class="sort-icon"></span></th>
|
<th data-translate-lang="last_long">Last Longitude <span class="sort-icon"></span></th>
|
||||||
<th>Channel <span class="sort-icon"></span></th>
|
<th data-translate-lang="channel">Channel <span class="sort-icon"></span></th>
|
||||||
<th>Last Seen <span class="sort-icon"></span></th>
|
<th data-translate-lang="mqtt_gateway">MQTT <span class="sort-icon"></span></th>
|
||||||
<th> </th>
|
<th data-translate-lang="last_seen">Last Seen <span class="sort-icon"></span></th>
|
||||||
|
<th data-translate-lang="favorite"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="node-table-body">
|
<tbody id="node-table-body">
|
||||||
<tr><td colspan="10" style="text-align:center; color:white;">Loading nodes...</td></tr>
|
<tr>
|
||||||
|
<td colspan="11" style="text-align:center; color:white;" data-translate-lang="loading_nodes">
|
||||||
|
Loading nodes...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Card View -->
|
||||||
|
<div id="mobile-node-list" style="display:none;"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// =====================================================
|
||||||
|
// TRANSLATIONS
|
||||||
|
// =====================================================
|
||||||
|
let nodelistTranslations = {};
|
||||||
|
|
||||||
|
function applyTranslationsNodelist() {
|
||||||
|
document.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||||
|
const key = el.dataset.translateLang;
|
||||||
|
if (nodelistTranslations[key]) {
|
||||||
|
if (el.tagName === "INPUT" && el.placeholder) {
|
||||||
|
el.placeholder = nodelistTranslations[key];
|
||||||
|
} else {
|
||||||
|
el.textContent = nodelistTranslations[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTranslationsNodelist() {
|
||||||
|
try {
|
||||||
|
const cfg = await window._siteConfigPromise;
|
||||||
|
const lang = cfg?.site?.language || "en";
|
||||||
|
const res = await fetch(`/api/lang?lang=${lang}§ion=nodelist`);
|
||||||
|
nodelistTranslations = await res.json();
|
||||||
|
applyTranslationsNodelist();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load nodelist translations:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// GLOBALS
|
// GLOBALS
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -164,44 +322,63 @@ let allNodes = [];
|
|||||||
let sortColumn = "short_name";
|
let sortColumn = "short_name";
|
||||||
let sortAsc = true;
|
let sortAsc = true;
|
||||||
let showOnlyFavorites = false;
|
let showOnlyFavorites = false;
|
||||||
|
let favoritesSet = new Set();
|
||||||
|
let isBusy = false;
|
||||||
|
let statusHideTimer = null;
|
||||||
|
let statusShownAt = 0;
|
||||||
|
const minStatusMs = 300;
|
||||||
|
|
||||||
const headers = document.querySelectorAll("thead th");
|
const headers = document.querySelectorAll("thead th");
|
||||||
const keyMap = [
|
const keyMap = [
|
||||||
"short_name","long_name","hw_model","firmware","role",
|
"short_name","long_name","hw_model","firmware","role",
|
||||||
"last_lat","last_long","channel","last_seen_us"
|
"last_lat","last_long","channel","is_mqtt_gateway","last_seen_us"
|
||||||
];
|
];
|
||||||
|
|
||||||
// =====================================================
|
function debounce(fn, delay = 250) {
|
||||||
// FAVORITES SYSTEM (localStorage)
|
let t;
|
||||||
// =====================================================
|
return (...args) => {
|
||||||
function getFavorites() {
|
clearTimeout(t);
|
||||||
|
t = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFrame() {
|
||||||
|
return new Promise(resolve => requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFavorites() {
|
||||||
const favorites = localStorage.getItem('nodelist_favorites');
|
const favorites = localStorage.getItem('nodelist_favorites');
|
||||||
return favorites ? JSON.parse(favorites) : [];
|
if (!favorites) {
|
||||||
}
|
favoritesSet = new Set();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function saveFavorites(favs) {
|
try {
|
||||||
localStorage.setItem('nodelist_favorites', JSON.stringify(favs));
|
const parsed = JSON.parse(favorites);
|
||||||
|
favoritesSet = new Set(Array.isArray(parsed) ? parsed : []);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to parse favorites, resetting.", err);
|
||||||
|
favoritesSet = new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveFavorites() {
|
||||||
|
localStorage.setItem('nodelist_favorites', JSON.stringify([...favoritesSet]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFavorite(nodeId) {
|
function toggleFavorite(nodeId) {
|
||||||
let favs = getFavorites();
|
if (favoritesSet.has(nodeId)) {
|
||||||
const idx = favs.indexOf(nodeId);
|
favoritesSet.delete(nodeId);
|
||||||
if (idx >= 0) favs.splice(idx, 1);
|
} else {
|
||||||
else favs.push(nodeId);
|
favoritesSet.add(nodeId);
|
||||||
saveFavorites(favs);
|
}
|
||||||
|
saveFavorites();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFavorite(nodeId) {
|
function isFavorite(nodeId) {
|
||||||
return getFavorites().includes(nodeId);
|
return favoritesSet.has(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
function timeAgoFromMs(msTimestamp) {
|
||||||
// "TIME AGO" FORMATTER
|
if (!msTimestamp) return "N/A";
|
||||||
// =====================================================
|
const diff = Date.now() - msTimestamp;
|
||||||
function timeAgo(usTimestamp) {
|
|
||||||
if (!usTimestamp) return "N/A";
|
|
||||||
const ms = usTimestamp / 1000;
|
|
||||||
const diff = Date.now() - ms;
|
|
||||||
|
|
||||||
if (diff < 60000) return "just now";
|
if (diff < 60000) return "just now";
|
||||||
const mins = Math.floor(diff / 60000);
|
const mins = Math.floor(diff / 60000);
|
||||||
@@ -209,77 +386,129 @@ function timeAgo(usTimestamp) {
|
|||||||
const hrs = Math.floor(mins / 60);
|
const hrs = Math.floor(mins / 60);
|
||||||
if (hrs < 24) return `${hrs} hr ago`;
|
if (hrs < 24) return `${hrs} hr ago`;
|
||||||
const days = Math.floor(hrs / 24);
|
const days = Math.floor(hrs / 24);
|
||||||
return `${days} day${days > 1 ? "s" : ""} ago`;
|
return `${days} days ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// DOM LOADED: FETCH NODES
|
// DOM LOADED
|
||||||
// =====================================================
|
// =====================================================
|
||||||
document.addEventListener("DOMContentLoaded", async function() {
|
document.addEventListener("DOMContentLoaded", async function() {
|
||||||
|
|
||||||
|
await loadTranslationsNodelist();
|
||||||
|
loadFavorites();
|
||||||
|
|
||||||
const tbody = document.getElementById("node-table-body");
|
const tbody = document.getElementById("node-table-body");
|
||||||
|
const mobileList = document.getElementById("mobile-node-list");
|
||||||
|
|
||||||
const roleFilter = document.getElementById("role-filter");
|
const roleFilter = document.getElementById("role-filter");
|
||||||
const channelFilter = document.getElementById("channel-filter");
|
const channelFilter = document.getElementById("channel-filter");
|
||||||
const hwFilter = document.getElementById("hw-filter");
|
const hwFilter = document.getElementById("hw-filter");
|
||||||
const firmwareFilter = document.getElementById("firmware-filter");
|
const firmwareFilter = document.getElementById("firmware-filter");
|
||||||
const searchBox = document.getElementById("search-box");
|
const searchBox = document.getElementById("search-box");
|
||||||
const countSpan = document.getElementById("node-count");
|
const countSpan = document.getElementById("node-count");
|
||||||
|
const statusSpan = document.getElementById("node-status");
|
||||||
const exportBtn = document.getElementById("export-btn");
|
const exportBtn = document.getElementById("export-btn");
|
||||||
const clearBtn = document.getElementById("clear-btn");
|
const clearBtn = document.getElementById("clear-btn");
|
||||||
const favoritesBtn = document.getElementById("favorites-btn");
|
const favoritesBtn = document.getElementById("favorites-btn");
|
||||||
|
|
||||||
|
let lastIsMobile = (window.innerWidth <= 768);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setStatus("Loading nodes…");
|
||||||
|
await nextFrame();
|
||||||
const res = await fetch("/api/nodes?days_active=3");
|
const res = await fetch("/api/nodes?days_active=3");
|
||||||
if (!res.ok) throw new Error("Failed to fetch nodes");
|
if (!res.ok) throw new Error("Failed to fetch nodes");
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
allNodes = data.nodes;
|
|
||||||
|
allNodes = data.nodes.map(n => {
|
||||||
|
const firmware = n.firmware || n.firmware_version || "";
|
||||||
|
const last_seen_us = n.last_seen_us || 0;
|
||||||
|
const last_seen_ms = last_seen_us ? (last_seen_us / 1000) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
firmware,
|
||||||
|
last_seen_us,
|
||||||
|
last_seen_ms,
|
||||||
|
_search: [
|
||||||
|
n.node_id,
|
||||||
|
n.id,
|
||||||
|
n.long_name,
|
||||||
|
n.short_name
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
populateFilters(allNodes);
|
populateFilters(allNodes);
|
||||||
renderTable(allNodes);
|
applyFilters(); // ensures initial sort + render uses same path
|
||||||
updateSortIcons();
|
updateSortIcons();
|
||||||
|
setStatus("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tbody.innerHTML = `<tr><td colspan="10" style="text-align:center; color:red;">Error loading nodes: ${err.message}</td></tr>`;
|
tbody.innerHTML = `<tr>
|
||||||
|
<td colspan="11" style="text-align:center; color:red;">
|
||||||
|
${nodelistTranslations.error_loading_nodes || "Error loading nodes"}
|
||||||
|
</td></tr>`;
|
||||||
|
setStatus("");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
roleFilter.addEventListener("change", applyFilters);
|
roleFilter.addEventListener("change", applyFilters);
|
||||||
channelFilter.addEventListener("change", applyFilters);
|
channelFilter.addEventListener("change", applyFilters);
|
||||||
hwFilter.addEventListener("change", applyFilters);
|
hwFilter.addEventListener("change", applyFilters);
|
||||||
firmwareFilter.addEventListener("change", applyFilters);
|
firmwareFilter.addEventListener("change", applyFilters);
|
||||||
searchBox.addEventListener("input", applyFilters);
|
|
||||||
|
// Debounced only for search typing
|
||||||
|
searchBox.addEventListener("input", debounce(applyFilters, 250));
|
||||||
|
|
||||||
exportBtn.addEventListener("click", exportToCSV);
|
exportBtn.addEventListener("click", exportToCSV);
|
||||||
clearBtn.addEventListener("click", clearFilters);
|
clearBtn.addEventListener("click", clearFilters);
|
||||||
favoritesBtn.addEventListener("click", toggleFavoritesFilter);
|
favoritesBtn.addEventListener("click", toggleFavoritesFilter);
|
||||||
|
|
||||||
// STAR CLICK HANDLER
|
// Favorite star click handler (delegated)
|
||||||
tbody.addEventListener("click", e => {
|
document.addEventListener("click", e => {
|
||||||
if (e.target.classList.contains('favorite-star')) {
|
if (e.target.classList.contains('favorite-star')) {
|
||||||
const nodeId = parseInt(e.target.dataset.nodeId);
|
const nodeId = parseInt(e.target.dataset.nodeId, 10);
|
||||||
const isFav = isFavorite(nodeId);
|
const fav = isFavorite(nodeId);
|
||||||
|
|
||||||
if (isFav) {
|
if (fav) {
|
||||||
e.target.classList.remove("active");
|
e.target.classList.remove("active");
|
||||||
e.target.textContent = "☆";
|
e.target.textContent = "☆";
|
||||||
} else {
|
} else {
|
||||||
e.target.classList.add("active");
|
e.target.classList.add("active");
|
||||||
e.target.textContent = "★";
|
e.target.textContent = "★";
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFavorite(nodeId);
|
toggleFavorite(nodeId);
|
||||||
|
applyFilters();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// SORTING
|
|
||||||
headers.forEach((th, index) => {
|
headers.forEach((th, index) => {
|
||||||
th.addEventListener("click", () => {
|
th.addEventListener("click", () => {
|
||||||
let key = keyMap[index];
|
const key = keyMap[index];
|
||||||
|
// ignore clicks on the "favorite" (last header) which has no sort key
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
sortAsc = (sortColumn === key) ? !sortAsc : true;
|
sortAsc = (sortColumn === key) ? !sortAsc : true;
|
||||||
sortColumn = key;
|
sortColumn = key;
|
||||||
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// =====================================================
|
// Re-render on breakpoint change so mobile/desktop view switches instantly
|
||||||
// FILTER POPULATION
|
window.addEventListener("resize", debounce(() => {
|
||||||
// =====================================================
|
const isMobile = (window.innerWidth <= 768);
|
||||||
|
if (isMobile !== lastIsMobile) {
|
||||||
|
lastIsMobile = isMobile;
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
}, 150));
|
||||||
|
|
||||||
function populateFilters(nodes) {
|
function populateFilters(nodes) {
|
||||||
const roles = new Set(), channels = new Set(), hws = new Set(), fws = new Set();
|
const roles = new Set(), channels = new Set(), hws = new Set(), fws = new Set();
|
||||||
|
|
||||||
@@ -305,20 +534,18 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// FAVORITES FILTER
|
|
||||||
// =====================================================
|
|
||||||
function toggleFavoritesFilter() {
|
function toggleFavoritesFilter() {
|
||||||
showOnlyFavorites = !showOnlyFavorites;
|
showOnlyFavorites = !showOnlyFavorites;
|
||||||
favoritesBtn.textContent = showOnlyFavorites ? "⭐ Show All" : "⭐ Show Favorites";
|
favoritesBtn.textContent = showOnlyFavorites
|
||||||
|
? "Show All"
|
||||||
|
: "⭐ Show Favorites";
|
||||||
favoritesBtn.classList.toggle("active", showOnlyFavorites);
|
favoritesBtn.classList.toggle("active", showOnlyFavorites);
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
async function applyFilters() {
|
||||||
// APPLY FILTERS + SORT
|
setStatus("Updating…");
|
||||||
// =====================================================
|
await nextFrame();
|
||||||
function applyFilters() {
|
|
||||||
const searchTerm = searchBox.value.trim().toLowerCase();
|
const searchTerm = searchBox.value.trim().toLowerCase();
|
||||||
|
|
||||||
let filtered = allNodes.filter(n => {
|
let filtered = allNodes.filter(n => {
|
||||||
@@ -326,90 +553,150 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
const channelMatch = !channelFilter.value || n.channel === channelFilter.value;
|
const channelMatch = !channelFilter.value || n.channel === channelFilter.value;
|
||||||
const hwMatch = !hwFilter.value || n.hw_model === hwFilter.value;
|
const hwMatch = !hwFilter.value || n.hw_model === hwFilter.value;
|
||||||
const fwMatch = !firmwareFilter.value || n.firmware === firmwareFilter.value;
|
const fwMatch = !firmwareFilter.value || n.firmware === firmwareFilter.value;
|
||||||
const searchMatch =
|
|
||||||
!searchTerm ||
|
|
||||||
(n.long_name && n.long_name.toLowerCase().includes(searchTerm)) ||
|
|
||||||
(n.short_name && n.short_name.toLowerCase().includes(searchTerm)) ||
|
|
||||||
n.node_id.toString().includes(searchTerm);
|
|
||||||
|
|
||||||
|
const searchMatch = !searchTerm || n._search.includes(searchTerm);
|
||||||
const favMatch = !showOnlyFavorites || isFavorite(n.node_id);
|
const favMatch = !showOnlyFavorites || isFavorite(n.node_id);
|
||||||
|
|
||||||
return roleMatch && channelMatch && hwMatch && fwMatch && searchMatch && favMatch;
|
return roleMatch && channelMatch && hwMatch && fwMatch && searchMatch && favMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// IMPORTANT: Always sort the filtered subset to preserve expected behavior
|
||||||
filtered = sortNodes(filtered, sortColumn, sortAsc);
|
filtered = sortNodes(filtered, sortColumn, sortAsc);
|
||||||
|
|
||||||
renderTable(filtered);
|
renderTable(filtered);
|
||||||
updateSortIcons();
|
updateSortIcons();
|
||||||
|
setStatus("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// RENDER TABLE
|
|
||||||
// =====================================================
|
|
||||||
function renderTable(nodes) {
|
function renderTable(nodes) {
|
||||||
tbody.innerHTML = "";
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
const shouldRenderTable = !isMobile;
|
||||||
|
|
||||||
|
if (shouldRenderTable) {
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
mobileList.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableFrag = shouldRenderTable ? document.createDocumentFragment() : null;
|
||||||
|
const mobileFrag = shouldRenderTable ? null : document.createDocumentFragment();
|
||||||
|
|
||||||
if (!nodes.length) {
|
if (!nodes.length) {
|
||||||
tbody.innerHTML = `<tr><td colspan="10" style="text-align:center; color:white;">No nodes found</td></tr>`;
|
if (shouldRenderTable) {
|
||||||
|
tbody.innerHTML = `<tr>
|
||||||
|
<td colspan="11" style="text-align:center; color:white;">
|
||||||
|
${nodelistTranslations.no_nodes_found || "No nodes found"}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
} else {
|
||||||
|
mobileList.innerHTML = `<div style="text-align:center; color:white;">
|
||||||
|
${nodelistTranslations.no_nodes_found || "No nodes found"}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
countSpan.textContent = 0;
|
countSpan.textContent = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const isFav = isFavorite(node.node_id);
|
const fav = isFavorite(node.node_id);
|
||||||
const star = isFav ? "★" : "☆";
|
const star = fav ? "★" : "☆";
|
||||||
|
|
||||||
const row = document.createElement("tr");
|
if (shouldRenderTable) {
|
||||||
row.innerHTML = `
|
// DESKTOP TABLE ROW
|
||||||
<td>${node.short_name || "N/A"}</td>
|
const row = document.createElement("tr");
|
||||||
<td><a href="/node/${node.node_id}">${node.long_name || "N/A"}</a></td>
|
row.innerHTML = `
|
||||||
<td>${node.hw_model || "N/A"}</td>
|
<td>${node.short_name || "N/A"}</td>
|
||||||
<td>${node.firmware || "N/A"}</td>
|
<td><a href="/node/${node.node_id}">${node.long_name || "N/A"}</a></td>
|
||||||
<td>${node.role || "N/A"}</td>
|
<td>${node.hw_model || "N/A"}</td>
|
||||||
<td>${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"}</td>
|
<td>${node.firmware || "N/A"}</td>
|
||||||
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
|
<td>${node.role || "N/A"}</td>
|
||||||
<td>${node.channel || "N/A"}</td>
|
<td>${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"}</td>
|
||||||
<td>${timeAgo(node.last_seen_us)}</td>
|
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
|
||||||
<td style="text-align:center;">
|
<td>${node.channel || "N/A"}</td>
|
||||||
<span class="favorite-star ${isFav ? "active" : ""}" data-node-id="${node.node_id}">${star}</span>
|
<td>${node.is_mqtt_gateway ? (nodelistTranslations.yes || "Yes") : (nodelistTranslations.no || "No")}</td>
|
||||||
</td>
|
<td>${timeAgoFromMs(node.last_seen_ms)}</td>
|
||||||
`;
|
<td style="text-align:center;">
|
||||||
tbody.appendChild(row);
|
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
|
||||||
|
${star}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableFrag.appendChild(row);
|
||||||
|
} else {
|
||||||
|
// MOBILE CARD VIEW
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "node-card";
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="node-card-header">
|
||||||
|
<span>${node.short_name || node.long_name || node.node_id}</span>
|
||||||
|
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
|
||||||
|
${star}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-card-field"><b>ID:</b> ${node.node_id}</div>
|
||||||
|
<div class="node-card-field"><b>Name:</b> ${node.long_name || "N/A"}</div>
|
||||||
|
<div class="node-card-field"><b>HW:</b> ${node.hw_model || "N/A"}</div>
|
||||||
|
<div class="node-card-field"><b>Firmware:</b> ${node.firmware || "N/A"}</div>
|
||||||
|
<div class="node-card-field"><b>Role:</b> ${node.role || "N/A"}</div>
|
||||||
|
<div class="node-card-field"><b>Location:</b>
|
||||||
|
${node.last_lat ? (node.last_lat / 1e7).toFixed(5) : "N/A"},
|
||||||
|
${node.last_long ? (node.last_long / 1e7).toFixed(5) : "N/A"}
|
||||||
|
</div>
|
||||||
|
<div class="node-card-field"><b>Channel:</b> ${node.channel || "N/A"}</div>
|
||||||
|
<div class="node-card-field"><b>Last Seen:</b> ${timeAgoFromMs(node.last_seen_ms)}</div>
|
||||||
|
|
||||||
|
<a href="/node/${node.node_id}" style="color:#9fd4ff; text-decoration:underline; margin-top:5px; display:block;">
|
||||||
|
View Node →
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
mobileFrag.appendChild(card);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle correct view
|
||||||
|
mobileList.style.display = isMobile ? "block" : "none";
|
||||||
|
|
||||||
countSpan.textContent = nodes.length;
|
countSpan.textContent = nodes.length;
|
||||||
|
|
||||||
|
if (shouldRenderTable) {
|
||||||
|
tbody.appendChild(tableFrag);
|
||||||
|
} else {
|
||||||
|
mobileList.appendChild(mobileFrag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// CLEAR FILTERS
|
|
||||||
// =====================================================
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
roleFilter.value = "";
|
roleFilter.value = "";
|
||||||
channelFilter.value = "";
|
channelFilter.value = "";
|
||||||
hwFilter.value = "";
|
hwFilter.value = "";
|
||||||
firmwareFilter.value = "";
|
firmwareFilter.value = "";
|
||||||
searchBox.value = "";
|
searchBox.value = "";
|
||||||
|
|
||||||
sortColumn = "short_name";
|
sortColumn = "short_name";
|
||||||
sortAsc = true;
|
sortAsc = true;
|
||||||
showOnlyFavorites = false;
|
showOnlyFavorites = false;
|
||||||
|
|
||||||
favoritesBtn.textContent = "⭐ Show Favorites";
|
favoritesBtn.textContent = "⭐ Show Favorites";
|
||||||
favoritesBtn.classList.remove("active");
|
favoritesBtn.classList.remove("active");
|
||||||
|
|
||||||
renderTable(allNodes);
|
applyFilters();
|
||||||
updateSortIcons();
|
updateSortIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// EXPORT CSV
|
|
||||||
// =====================================================
|
|
||||||
function exportToCSV() {
|
function exportToCSV() {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const headerList = Array.from(headers).map(h => `"${h.innerText.replace(/▲|▼/g,'')}"`);
|
const headerList = Array.from(headers).map(h =>
|
||||||
|
`"${h.innerText.replace(/▲|▼/g,'')}"`
|
||||||
|
);
|
||||||
rows.push(headerList.join(","));
|
rows.push(headerList.join(","));
|
||||||
|
|
||||||
const trs = tbody.querySelectorAll("tr");
|
const trs = tbody.querySelectorAll("tr");
|
||||||
trs.forEach(tr => {
|
trs.forEach(tr => {
|
||||||
const cells = Array.from(tr.children).map(td => `"${td.innerText.replace(/"/g,'""')}"`);
|
const cells = Array.from(tr.children).map(td =>
|
||||||
|
`"${td.innerText.replace(/"/g,'""')}"`
|
||||||
|
);
|
||||||
rows.push(cells.join(","));
|
rows.push(cells.join(","));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -420,36 +707,74 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// SORT NODES
|
|
||||||
// =====================================================
|
|
||||||
function sortNodes(nodes, key, asc) {
|
function sortNodes(nodes, key, asc) {
|
||||||
return [...nodes].sort((a, b) => {
|
return [...nodes].sort((a, b) => {
|
||||||
let A = a[key];
|
let A = a[key];
|
||||||
let B = b[key];
|
let B = b[key];
|
||||||
|
|
||||||
// special handling for timestamp
|
|
||||||
if (key === "last_seen_us") {
|
if (key === "last_seen_us") {
|
||||||
A = A || 0;
|
A = A || 0;
|
||||||
B = B || 0;
|
B = B || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "is_mqtt_gateway") {
|
||||||
|
A = A ? 1 : 0;
|
||||||
|
B = B ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize strings for stable sorting
|
||||||
|
if (typeof A === "string") A = A.toLowerCase();
|
||||||
|
if (typeof B === "string") B = B.toLowerCase();
|
||||||
|
|
||||||
if (A < B) return asc ? -1 : 1;
|
if (A < B) return asc ? -1 : 1;
|
||||||
if (A > B) return asc ? 1 : -1;
|
if (A > B) return asc ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// SORT ICONS
|
|
||||||
// =====================================================
|
|
||||||
function updateSortIcons() {
|
function updateSortIcons() {
|
||||||
headers.forEach((th, i) => {
|
headers.forEach((th, i) => {
|
||||||
const span = th.querySelector(".sort-icon");
|
const span = th.querySelector(".sort-icon");
|
||||||
if (!span) return;
|
if (!span) return;
|
||||||
span.textContent = keyMap[i] === sortColumn ? (sortAsc ? "▲" : "▼") : "";
|
span.textContent =
|
||||||
|
keyMap[i] === sortColumn ? (sortAsc ? "▲" : "▼") : "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setStatus(message) {
|
||||||
|
if (!statusSpan) return;
|
||||||
|
if (statusHideTimer) {
|
||||||
|
clearTimeout(statusHideTimer);
|
||||||
|
statusHideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
statusShownAt = Date.now();
|
||||||
|
console.log("[nodelist] status:", message);
|
||||||
|
statusSpan.textContent = message;
|
||||||
|
statusSpan.classList.add("active");
|
||||||
|
isBusy = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Date.now() - statusShownAt;
|
||||||
|
const remaining = Math.max(0, minStatusMs - elapsed);
|
||||||
|
if (remaining > 0) {
|
||||||
|
statusHideTimer = setTimeout(() => {
|
||||||
|
statusHideTimer = null;
|
||||||
|
console.log("[nodelist] status: cleared");
|
||||||
|
statusSpan.textContent = "";
|
||||||
|
statusSpan.classList.remove("active");
|
||||||
|
isBusy = false;
|
||||||
|
}, remaining);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[nodelist] status: cleared");
|
||||||
|
statusSpan.textContent = "";
|
||||||
|
statusSpan.classList.remove("active");
|
||||||
|
isBusy = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+231
-159
@@ -1,6 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Packet Details{%endblock%}
|
{% block title %}Packet Details{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="/static/portmaps.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
@@ -48,7 +52,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- SOURCE MARKER (slightly bigger) --- */
|
/* --- SOURCE MARKER --- */
|
||||||
.source-marker {
|
.source-marker {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -97,26 +101,27 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container mt-4 mb-5 packet-container">
|
<div class="container mt-4 mb-5 packet-container">
|
||||||
|
|
||||||
<div id="loading">Loading packet information...</div>
|
<div id="loading" data-translate-lang="loading">Loading packet information...</div>
|
||||||
<div id="packet-card" class="packet-card d-none"></div>
|
<div id="packet-card" class="packet-card d-none"></div>
|
||||||
|
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
||||||
<div id="seen-container" class="mt-4 d-none">
|
<div id="seen-container" class="mt-4 d-none">
|
||||||
<h5 style="color:#ccc; margin:15px 0 10px 0;">
|
<h5 style="color:#ccc; margin:15px 0 10px 0;">
|
||||||
📡 Seen By <span id="seen-count" style="color:#4da6ff;"></span>
|
📡 <span data-translate-lang="seen_by">Seen By</span>
|
||||||
|
<span id="seen-count" style="color:#4da6ff;"></span>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-dark table-sm seen-table">
|
<table class="table table-dark table-sm seen-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Gateway</th>
|
<th data-translate-lang="gateway">Gateway</th>
|
||||||
<th>RSSI</th>
|
<th data-translate-lang="rssi">RSSI</th>
|
||||||
<th>SNR</th>
|
<th data-translate-lang="snr">SNR</th>
|
||||||
<th>Hop</th>
|
<th data-translate-lang="hops">Hops</th>
|
||||||
<th>Channel</th>
|
<th data-translate-lang="channel">Channel</th>
|
||||||
<th>Time</th>
|
<th data-translate-lang="time">Time</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="seen-table-body"></tbody>
|
<tbody id="seen-table-body"></tbody>
|
||||||
@@ -126,8 +131,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
PACKET PAGE TRANSLATION
|
||||||
|
====================================================== */
|
||||||
|
let packetTranslations = {};
|
||||||
|
|
||||||
|
async function loadTranslationsPacket() {
|
||||||
|
try {
|
||||||
|
const cfg = await window._siteConfigPromise;
|
||||||
|
const lang = cfg?.site?.language || "en";
|
||||||
|
const res = await fetch(`/api/lang?lang=${lang}§ion=packet`);
|
||||||
|
packetTranslations = await res.json();
|
||||||
|
applyTranslationsPacket(packetTranslations);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Packet translations failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslationsPacket(dict, root = document) {
|
||||||
|
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||||
|
const key = el.dataset.translateLang;
|
||||||
|
if (dict[key]) el.textContent = dict[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
PACKET PAGE MAIN
|
||||||
|
====================================================== */
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
|
await loadTranslationsPacket(); // <-- IMPORTANT
|
||||||
|
|
||||||
const packetCard = document.getElementById("packet-card");
|
const packetCard = document.getElementById("packet-card");
|
||||||
const loading = document.getElementById("loading");
|
const loading = document.getElementById("loading");
|
||||||
const mapDiv = document.getElementById("map");
|
const mapDiv = document.getElementById("map");
|
||||||
@@ -140,23 +176,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
const match = window.location.pathname.match(/\/packet\/(\d+)/);
|
const match = window.location.pathname.match(/\/packet\/(\d+)/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
loading.textContent = "Invalid packet URL";
|
loading.textContent = packetTranslations.invalid_url || "Invalid packet URL";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const packetId = match[1];
|
const packetId = match[1];
|
||||||
|
|
||||||
/* PORT NAME MAP */
|
/* PORT LABELS (NOT TRANSLATED) */
|
||||||
const PORT_NAMES = {
|
const PORT_NAMES = window.PORT_LABEL_MAP;
|
||||||
0:"UNKNOWN APP",
|
|
||||||
1:"Text",
|
|
||||||
3:"Position",
|
|
||||||
4:"Node Info",
|
|
||||||
5:"Routing",
|
|
||||||
6:"Admin",
|
|
||||||
67:"Telemetry",
|
|
||||||
70:"Traceroute",
|
|
||||||
71:"Neighbor"
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Fetch packet
|
Fetch packet
|
||||||
@@ -164,28 +190,31 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const packetRes = await fetch(`/api/packets?packet_id=${packetId}`);
|
const packetRes = await fetch(`/api/packets?packet_id=${packetId}`);
|
||||||
const packetData = await packetRes.json();
|
const packetData = await packetRes.json();
|
||||||
if (!packetData.packets.length) {
|
if (!packetData.packets.length) {
|
||||||
loading.textContent = "Packet not found.";
|
loading.textContent = packetTranslations.not_found || "Packet not found.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const p = packetData.packets[0];
|
const p = packetData.packets[0];
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Fetch all nodes
|
Load nodes for names & positions
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
const nodesRes = await fetch("/api/nodes");
|
const nodesRes = await fetch("/api/nodes");
|
||||||
const nodesData = await nodesRes.json();
|
const nodesData = await nodesRes.json();
|
||||||
const nodeLookup = {};
|
const nodeLookup = {};
|
||||||
(nodesData.nodes || []).forEach(n => nodeLookup[n.node_id] = n);
|
(nodesData.nodes || []).forEach(n => nodeLookup[n.node_id] = n);
|
||||||
|
|
||||||
const fromNodeObj = nodeLookup[p.from_node_id];
|
const fromNodeObj = nodeLookup[p.from_node_id];
|
||||||
const toNodeObj = nodeLookup[p.to_node_id];
|
const toNodeObj = nodeLookup[p.to_node_id];
|
||||||
|
|
||||||
const fromNodeLabel = fromNodeObj?.long_name || p.from_node_id;
|
const fromNodeLabel = fromNodeObj?.long_name || p.from_node_id;
|
||||||
|
|
||||||
const toNodeLabel =
|
const toNodeLabel =
|
||||||
p.to_node_id == 4294967295 ? "All" : (toNodeObj?.long_name || p.to_node_id);
|
p.to_node_id == 4294967295
|
||||||
|
? (packetTranslations.all_broadcast || "All")
|
||||||
|
: (toNodeObj?.long_name || p.to_node_id);
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Parse payload for lat/lon if this *packet* is a position packet
|
Parse payload for lat/lon
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
let lat = null, lon = null;
|
let lat = null, lon = null;
|
||||||
const parsed = {};
|
const parsed = {};
|
||||||
@@ -195,14 +224,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const [k, v] = line.split(":").map(x=>x.trim());
|
const [k, v] = line.split(":").map(x=>x.trim());
|
||||||
if (k && v !== undefined) {
|
if (k && v !== undefined) {
|
||||||
parsed[k] = v;
|
parsed[k] = v;
|
||||||
if (k === "latitude_i") lat = Number(v) / 1e7;
|
if (k === "latitude_i") lat = Number(v) / 1e7;
|
||||||
if (k === "longitude_i") lon = Number(v) / 1e7;
|
if (k === "longitude_i") lon = Number(v) / 1e7;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Render packet header & details
|
Render card
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
const time = p.import_time_us
|
const time = p.import_time_us
|
||||||
? new Date(p.import_time_us / 1000).toLocaleString()
|
? new Date(p.import_time_us / 1000).toLocaleString()
|
||||||
@@ -216,42 +245,47 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
packetCard.innerHTML = `
|
packetCard.innerHTML = `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>Packet ID: <i>${p.id}</i></span>
|
<span>
|
||||||
|
<span data-translate-lang="packet_id_label">${packetTranslations.packet_id_label || "Packet ID:"}</span>
|
||||||
|
<i>${p.id}</i>
|
||||||
|
</span>
|
||||||
<small>${time}</small>
|
<small>${time}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>From Node:</dt>
|
<dt data-translate-lang="from_node">${packetTranslations.from_node || "From Node"}:</dt>
|
||||||
<dd><a href="/node/${p.from_node_id}">${fromNodeLabel}</a></dd>
|
<dd><a href="/node/${p.from_node_id}">${fromNodeLabel}</a></dd>
|
||||||
<dt>To Node:</dt>
|
|
||||||
|
<dt data-translate-lang="to_node">${packetTranslations.to_node || "To Node"}:</dt>
|
||||||
<dd>${
|
<dd>${
|
||||||
p.to_node_id === 4294967295
|
p.to_node_id === 4294967295
|
||||||
? `<i>All</i>`
|
? `<i data-translate-lang="all_broadcast">${packetTranslations.all_broadcast || "All"}</i>`
|
||||||
: p.to_node_id === 1
|
: p.to_node_id === 1
|
||||||
? `<i>Direct to MQTT</i>`
|
? `<i data-translate-lang="direct_to_mqtt">${packetTranslations.direct_to_mqtt || "Direct to MQTT"}</i>`
|
||||||
: `<a href="/node/${p.to_node_id}">${toNodeLabel}</a>`
|
: `<a href="/node/${p.to_node_id}">${toNodeLabel}</a>`
|
||||||
}</dd>
|
}</dd>
|
||||||
|
|
||||||
|
<dt data-translate-lang="channel">${packetTranslations.channel || "Channel"}:</dt>
|
||||||
|
<dd>${p.channel ?? "—"}</dd>
|
||||||
|
|
||||||
<dt>Channel:</dt><dd>${p.channel ?? "—"}</dd>
|
<dt data-translate-lang="port">${packetTranslations.port || "Port"}:</dt>
|
||||||
|
|
||||||
<dt>Port:</dt>
|
|
||||||
<dd><i>${PORT_NAMES[p.portnum] || "UNKNOWN APP"}</i> (${p.portnum})</dd>
|
<dd><i>${PORT_NAMES[p.portnum] || "UNKNOWN APP"}</i> (${p.portnum})</dd>
|
||||||
|
|
||||||
<dt>Raw Payload:</dt>
|
<dt data-translate-lang="raw_payload">${packetTranslations.raw_payload || "From Raw Payload"}:</dt>
|
||||||
<dd><pre>${escapeHtml(p.payload ?? "—")}</pre></dd>
|
<dd><pre>${escapeHtml(p.payload ?? "—")}</pre></dd>
|
||||||
|
|
||||||
${
|
${
|
||||||
telemetryExtras.length
|
telemetryExtras.length
|
||||||
? `<dt>Decoded Telemetry</dt>
|
? `<dt data-translate-lang="decoded_telemetry">${packetTranslations.decoded_telemetry || "Decoded Telemetry"}</dt>
|
||||||
<dd><pre>${telemetryExtras.join("\n")}</pre></dd>`
|
<dd><pre>${telemetryExtras.join("\n")}</pre></dd>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
${
|
${
|
||||||
lat && lon
|
lat && lon
|
||||||
? `<dt>Location:</dt><dd>${lat.toFixed(6)}, ${lon.toFixed(6)}</dd>`
|
? `<dt data-translate-lang="location">${packetTranslations.location || "Location:"}</dt>
|
||||||
|
<dd>${lat.toFixed(6)}, ${lon.toFixed(6)}</dd>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
@@ -262,22 +296,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
packetCard.classList.remove("d-none");
|
packetCard.classList.remove("d-none");
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Map initialization
|
Map setup
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
const map = L.map("map");
|
const map = L.map("map");
|
||||||
mapDiv.style.display = "block";
|
mapDiv.style.display = "block";
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 })
|
||||||
maxZoom: 19
|
.addTo(map);
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const allBounds = [];
|
const allBounds = [];
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
ALWAYS SHOW SOURCE POSITION
|
Determine packet source location
|
||||||
Priority:
|
|
||||||
1) position from packet payload
|
|
||||||
2) fallback: last_lat/last_long from /api/nodes
|
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
let srcLat = lat;
|
let srcLat = lat;
|
||||||
let srcLon = lon;
|
let srcLon = lon;
|
||||||
@@ -304,167 +334,208 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
sourceMarker.bindPopup(`
|
sourceMarker.bindPopup(`
|
||||||
<div style="font-size:0.9em">
|
<div style="font-size:0.9em">
|
||||||
<b>Packet Source</b><br>
|
<b data-translate-lang="packet_source">${packetTranslations.packet_source || "Packet Source"}</b><br>
|
||||||
Lat: ${srcLat.toFixed(6)}<br>
|
Lat: ${srcLat.toFixed(6)}<br>
|
||||||
Lon: ${srcLon.toFixed(6)}<br>
|
Lon: ${srcLon.toFixed(6)}<br>
|
||||||
From Node: ${fromNodeLabel}<br>
|
<span data-translate-lang="from_node">${packetTranslations.from_node || "From Node:"}</span> ${fromNodeLabel}<br>
|
||||||
Channel: ${p.channel ?? "—"}<br>
|
<span data-translate-lang="channel">${packetTranslations.channel || "Channel:"}</span> ${p.channel ?? "—"}<br>
|
||||||
Port: ${PORT_NAMES[p.portnum] || "UNKNOWN"} (${p.portnum})
|
<span data-translate-lang="port">${packetTranslations.port || "Port:"}</span> ${PORT_NAMES[p.portnum] || "UNKNOWN"} (${p.portnum})
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
map.setView([0,0], 2);
|
map.setView([0,0], 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Color for hop indicator markers (warm → cold)
|
Colors for hops (warm → cold)
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
function hopColor(hopValue){
|
function hopColor(hopValue){
|
||||||
const colors = [
|
const colors = [
|
||||||
"#ff3b30",
|
"#ff3b30","#ff6b22","#ff9f0c","#ffd60a",
|
||||||
"#ff6b22",
|
"#87d957","#57d9c4","#3db2ff","#1e63ff"
|
||||||
"#ff9f0c",
|
|
||||||
"#ffd60a",
|
|
||||||
"#87d957",
|
|
||||||
"#57d9c4",
|
|
||||||
"#3db2ff",
|
|
||||||
"#1e63ff"
|
|
||||||
];
|
];
|
||||||
let h = Number(hopValue);
|
let h = Number(hopValue);
|
||||||
if (isNaN(h)) return "#aaa";
|
if (isNaN(h)) return "#aaa";
|
||||||
if (h < 0) h = 0;
|
return colors[Math.min(Math.max(h, 0), 7)];
|
||||||
if (h > 7) h = 7;
|
|
||||||
return colors[h];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Distance helper */
|
/* ---------------------------------------------
|
||||||
function haversine(lat1,lon1,lat2,lon2){
|
Distance helper
|
||||||
const R=6371;
|
----------------------------------------------*/
|
||||||
const dLat=(lat2-lat1)*Math.PI/180;
|
function haversine(lat1, lon1, lat2, lon2){
|
||||||
const dLon=(lon2-lon1)*Math.PI/180;
|
const R = 6371;
|
||||||
const a=Math.sin(dLat/2)**2+
|
const dLat = (lat2-lat1)*Math.PI/180;
|
||||||
|
const dLon = (lon2-lon1)*Math.PI/180;
|
||||||
|
const a = Math.sin(dLat/2)**2 +
|
||||||
Math.cos(lat1*Math.PI/180)*
|
Math.cos(lat1*Math.PI/180)*
|
||||||
Math.cos(lat2*Math.PI/180)*
|
Math.cos(lat2*Math.PI/180)*
|
||||||
Math.sin(dLon/2)**2;
|
Math.sin(dLon/2)**2;
|
||||||
return R*(2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)));
|
return R * (2*Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Fetch packets_seen
|
Load packets_seen
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
const seenRes = await fetch(`/api/packets_seen/${packetId}`);
|
const seenRes = await fetch(`/api/packets_seen/${packetId}`);
|
||||||
const seenData = await seenRes.json();
|
const seenData = await seenRes.json();
|
||||||
const seenList = seenData.seen ?? [];
|
const seenList = seenData.seen ?? [];
|
||||||
|
|
||||||
/* sort by hop_start descending (warm → cold) */
|
/* ---------------------------------------------
|
||||||
const seenSorted = seenList.slice().sort((a,b)=>{
|
Sort by hop count (highest first)
|
||||||
const A=a.hop_start??-999;
|
----------------------------------------------*/
|
||||||
const B=b.hop_start??-999;
|
const seenSorted = seenList.slice().sort((a,b)=>{
|
||||||
return B-A;
|
const ha = (a.hop_start ?? 0) - (a.hop_limit ?? 0);
|
||||||
});
|
const hb = (b.hop_start ?? 0) - (b.hop_limit ?? 0);
|
||||||
|
return hb - ha;
|
||||||
|
});
|
||||||
|
|
||||||
if (seenSorted.length){
|
if (seenSorted.length){
|
||||||
seenContainer.classList.remove("d-none");
|
seenContainer.classList.remove("d-none");
|
||||||
seenCountSpan.textContent=`(${seenSorted.length} gateways)`;
|
seenCountSpan.textContent = `(${seenSorted.length})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Gateway markers and seen table
|
GROUP BY HOP COUNT
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
seenTableBody.innerHTML = seenSorted.map(s=>{
|
const hopGroups = {};
|
||||||
const node=nodeLookup[s.node_id];
|
|
||||||
const label=node?(node.long_name||node.node_id):s.node_id;
|
|
||||||
|
|
||||||
const timeStr = s.import_time_us
|
seenSorted.forEach(s => {
|
||||||
? new Date(s.import_time_us/1000).toLocaleTimeString()
|
const hopValue = Math.max(
|
||||||
: "—";
|
0,
|
||||||
|
(s.hop_start ?? 0) - (s.hop_limit ?? 0)
|
||||||
|
);
|
||||||
|
if (!hopGroups[hopValue]) hopGroups[hopValue] = [];
|
||||||
|
hopGroups[hopValue].push(s);
|
||||||
|
});
|
||||||
|
|
||||||
if(node?.last_lat && node.last_long){
|
function formatHopDisplay(hopKey, hopStart){
|
||||||
const rlat=node.last_lat/1e7;
|
const startVal = hopStart ?? "—";
|
||||||
const rlon=node.last_long/1e7;
|
return `${hopKey}/${startVal}`;
|
||||||
allBounds.push([rlat,rlon]);
|
}
|
||||||
|
|
||||||
const start = Number(s.hop_start ?? 0);
|
/* ---------------------------------------------
|
||||||
const limit = Number(s.hop_limit ?? 0);
|
Render grouped gateway table + map markers
|
||||||
const hopValue = start - limit;
|
----------------------------------------------*/
|
||||||
|
seenTableBody.innerHTML = Object.keys(hopGroups)
|
||||||
|
.sort((a,b) => Number(a) - Number(b)) // 0 hop first
|
||||||
|
.map(hopKey => {
|
||||||
|
|
||||||
const color = hopColor(hopValue);
|
const hopLabel =
|
||||||
|
hopKey === "0"
|
||||||
|
? (packetTranslations.direct || "Direct (0 hops)")
|
||||||
|
: `${hopKey} ${packetTranslations.hops || "hops"}`;
|
||||||
|
|
||||||
const iconHtml = `
|
const rows = hopGroups[hopKey].map(s => {
|
||||||
<div style="
|
const node = nodeLookup[s.node_id];
|
||||||
background:${color};
|
const label = node?.long_name || s.node_id;
|
||||||
width:24px;
|
|
||||||
height:24px;
|
|
||||||
border-radius:50%;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:center;
|
|
||||||
color:white;
|
|
||||||
font-size:11px;
|
|
||||||
font-weight:700;
|
|
||||||
border:2px solid rgba(0,0,0,0.35);
|
|
||||||
box-shadow:0 0 5px rgba(0,0,0,0.45);
|
|
||||||
">${hopValue}</div>`;
|
|
||||||
|
|
||||||
const marker=L.marker([rlat,rlon],{
|
const hopDisplay = formatHopDisplay(hopKey, s.hop_start);
|
||||||
icon:L.divIcon({
|
const timeStr = s.import_time_us
|
||||||
html:iconHtml,
|
? new Date(s.import_time_us/1000).toLocaleTimeString()
|
||||||
className:"",
|
: "—";
|
||||||
iconSize:[24,24],
|
|
||||||
iconAnchor:[12,12]
|
|
||||||
})
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
let distKm=null,distMi=null;
|
/* ---------------- MAP MARKERS (UNCHANGED) ---------------- */
|
||||||
if(srcLat&&srcLon){
|
if (node?.last_lat && node.last_long){
|
||||||
distKm=haversine(srcLat,srcLon,rlat,rlon);
|
const rlat = node.last_lat/1e7;
|
||||||
distMi=distKm*0.621371;
|
const rlon = node.last_long/1e7;
|
||||||
}
|
allBounds.push([rlat, rlon]);
|
||||||
|
let distanceKm = null;
|
||||||
|
if (srcLat && srcLon) {
|
||||||
|
distanceKm = haversine(srcLat, srcLon, rlat, rlon);
|
||||||
|
}
|
||||||
|
const distanceMi = distanceKm !== null ? distanceKm * 0.621371 : null;
|
||||||
|
const color = hopColor(hopKey);
|
||||||
|
|
||||||
marker.bindPopup(`
|
const marker = L.marker([rlat,rlon],{
|
||||||
|
icon: L.divIcon({
|
||||||
|
html: `
|
||||||
|
<div style="
|
||||||
|
background:${color};
|
||||||
|
width:24px; height:24px;
|
||||||
|
border-radius:50%;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
color:white;
|
||||||
|
font-size:11px;
|
||||||
|
font-weight:700;
|
||||||
|
border:2px solid rgba(0,0,0,0.35);
|
||||||
|
box-shadow:0 0 5px rgba(0,0,0,0.45);
|
||||||
|
">${hopKey}</div>`,
|
||||||
|
className: "",
|
||||||
|
iconSize:[24,24],
|
||||||
|
iconAnchor:[12,12]
|
||||||
|
})
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
marker.bindPopup(`
|
||||||
<div style="font-size:0.9em">
|
<div style="font-size:0.9em">
|
||||||
<b>${node?.long_name || s.node_id}</b><br>
|
<b>${label}</b><br>
|
||||||
Node ID: <a href="/node/${s.node_id}">${s.node_id}</a><br>
|
<span data-translate-lang="node_id_short">Node ID</span>:
|
||||||
|
<a href="/node/${s.node_id}">${s.node_id}</a><br>
|
||||||
HW: ${node?.hw_model ?? "—"}<br>
|
HW: ${node?.hw_model ?? "—"}<br>
|
||||||
Channel: ${s.channel ?? "—"}<br><br>
|
<span data-translate-lang="channel">Channel</span>: ${s.channel ?? "—"}<br>
|
||||||
<b>Signal</b><br>
|
${
|
||||||
|
distanceKm !== null
|
||||||
|
? `<span data-translate-lang="distance">Distance</span>:
|
||||||
|
${distanceKm.toFixed(1)} km / ${distanceMi.toFixed(1)} mi<br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<b data-translate-lang="signal">Signal</b><br>
|
||||||
RSSI: ${s.rx_rssi ?? "—"}<br>
|
RSSI: ${s.rx_rssi ?? "—"}<br>
|
||||||
SNR: ${s.rx_snr ?? "—"}<br><br>
|
SNR: ${s.rx_snr ?? "—"}<br><br>
|
||||||
<b>Hops</b>: ${hopValue}<br>
|
|
||||||
<b>Distance</b><br>
|
<b data-translate-lang="hops">Hops</b>: ${hopDisplay}
|
||||||
${
|
|
||||||
distKm
|
|
||||||
? `${distKm.toFixed(2)} km (${distMi.toFixed(2)} mi)`
|
|
||||||
: "—"
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td><a href="/node/${s.node_id}">${label}</a></td>
|
||||||
|
<td>${s.rx_rssi ?? "—"}</td>
|
||||||
|
<td>${s.rx_snr ?? "—"}</td>
|
||||||
|
<td>${hopDisplay}</td>
|
||||||
|
<td>${s.channel ?? "—"}</td>
|
||||||
|
<td>${timeStr}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/node/${s.node_id}">${label}</a></td>
|
<td colspan="6"
|
||||||
<td>${s.rx_rssi ?? "—"}</td>
|
style="
|
||||||
<td>${s.rx_snr ?? "—"}</td>
|
background:#1f2327;
|
||||||
<td>${s.hop_start ?? "—"} → ${s.hop_limit ?? "—"}</td>
|
font-weight:700;
|
||||||
<td>${s.channel ?? "—"}</td>
|
color:#9ecbff;
|
||||||
<td>${timeStr}</td>
|
border-top:1px solid #444;
|
||||||
</tr>`;
|
padding:8px 12px;
|
||||||
|
">
|
||||||
|
🔁 ${hopLabel} (${hopGroups[hopKey].length})
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
${rows}
|
||||||
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Fit map to all markers
|
Fit map around all markers
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
if(allBounds.length>0){
|
if (allBounds.length > 0){
|
||||||
map.fitBounds(allBounds,{padding:[40,40]});
|
map.fitBounds(allBounds, { padding:[40,40] });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------
|
/* ---------------------------------------------
|
||||||
Escape HTML
|
Escape HTML helper
|
||||||
----------------------------------------------*/
|
----------------------------------------------*/
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
return (unsafe??"").replace(/[&<"'>]/g,m=>({
|
return (unsafe ?? "").replace(/[&<"'>]/g, m => ({
|
||||||
"&":"&",
|
"&":"&",
|
||||||
"<":"<",
|
"<":"<",
|
||||||
">":">",
|
">":">",
|
||||||
@@ -475,4 +546,5 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||||
|
<script src="/static/portmaps.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -111,6 +112,10 @@
|
|||||||
<p data-translate-lang="total_packets_seen">Total Packets Seen</p>
|
<p data-translate-lang="total_packets_seen">Total Packets Seen</p>
|
||||||
<div class="summary-count" id="summary_seen">0</div>
|
<div class="summary-count" id="summary_seen">0</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="summary-card" style="flex:1;">
|
||||||
|
<p data-translate-lang="total_gateways">Total Gateways</p>
|
||||||
|
<div class="summary-count" id="summary_gateways">0</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Daily Charts -->
|
<!-- Daily Charts -->
|
||||||
@@ -189,6 +194,28 @@
|
|||||||
<button class="export-btn" data-chart="chart_channel" data-translate-lang="export_csv">Export CSV</button>
|
<button class="export-btn" data-chart="chart_channel" data-translate-lang="export_csv">Export CSV</button>
|
||||||
<div id="chart_channel" class="chart"></div>
|
<div id="chart_channel" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Gateway breakdown charts -->
|
||||||
|
<div class="card-section">
|
||||||
|
<p class="section-header" data-translate-lang="gateway_channel_breakdown">Gateway Channel Breakdown</p>
|
||||||
|
<button class="expand-btn" data-chart="chart_gateway_channel" data-translate-lang="expand_chart">Expand Chart</button>
|
||||||
|
<button class="export-btn" data-chart="chart_gateway_channel" data-translate-lang="export_csv">Export CSV</button>
|
||||||
|
<div id="chart_gateway_channel" class="chart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-section">
|
||||||
|
<p class="section-header" data-translate-lang="gateway_role_breakdown">Gateway Role Breakdown</p>
|
||||||
|
<button class="expand-btn" data-chart="chart_gateway_role" data-translate-lang="expand_chart">Expand Chart</button>
|
||||||
|
<button class="export-btn" data-chart="chart_gateway_role" data-translate-lang="export_csv">Export CSV</button>
|
||||||
|
<div id="chart_gateway_role" class="chart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-section">
|
||||||
|
<p class="section-header" data-translate-lang="gateway_firmware_breakdown">Gateway Firmware Breakdown</p>
|
||||||
|
<button class="expand-btn" data-chart="chart_gateway_firmware" data-translate-lang="expand_chart">Expand Chart</button>
|
||||||
|
<button class="export-btn" data-chart="chart_gateway_firmware" data-translate-lang="export_csv">Export CSV</button>
|
||||||
|
<div id="chart_gateway_firmware" class="chart"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal for expanded charts -->
|
<!-- Modal for expanded charts -->
|
||||||
@@ -205,14 +232,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PORTNUM_LABELS = {
|
const PORTNUM_LABELS = window.PORT_LABEL_MAP;
|
||||||
1: "Text Messages",
|
|
||||||
3: "Position",
|
|
||||||
4: "Node Info",
|
|
||||||
67: "Telemetry",
|
|
||||||
70: "Traceroute",
|
|
||||||
71: "Neighbor Info"
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Fetch & Processing ---
|
// --- Fetch & Processing ---
|
||||||
async function fetchStats(period_type,length,portnum=null,channel=null){
|
async function fetchStats(period_type,length,portnum=null,channel=null){
|
||||||
@@ -345,6 +365,7 @@ function renderPieChart(elId,data,name){
|
|||||||
return chart;
|
return chart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Packet Type Pie Chart ---
|
// --- Packet Type Pie Chart ---
|
||||||
async function fetchPacketTypeBreakdown(channel=null) {
|
async function fetchPacketTypeBreakdown(channel=null) {
|
||||||
const portnums = [1,3,4,67,70,71];
|
const portnums = [1,3,4,67,70,71];
|
||||||
@@ -368,6 +389,7 @@ async function fetchPacketTypeBreakdown(channel=null) {
|
|||||||
let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71;
|
let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71;
|
||||||
let chartDailyAll, chartDailyPortnum1;
|
let chartDailyAll, chartDailyPortnum1;
|
||||||
let chartHwModel, chartRole, chartChannel;
|
let chartHwModel, chartRole, chartChannel;
|
||||||
|
let chartGatewayChannel, chartGatewayRole, chartGatewayFirmware;
|
||||||
let chartPacketTypes;
|
let chartPacketTypes;
|
||||||
|
|
||||||
async function init(){
|
async function init(){
|
||||||
@@ -414,10 +436,31 @@ async function init(){
|
|||||||
chartRole=renderPieChart("chart_role",processCountField(nodes,"role"),"Role");
|
chartRole=renderPieChart("chart_role",processCountField(nodes,"role"),"Role");
|
||||||
chartChannel=renderPieChart("chart_channel",processCountField(nodes,"channel"),"Channel");
|
chartChannel=renderPieChart("chart_channel",processCountField(nodes,"channel"),"Channel");
|
||||||
|
|
||||||
|
const gateways = nodes.filter(n => n.is_mqtt_gateway);
|
||||||
|
chartGatewayChannel = renderPieChart(
|
||||||
|
"chart_gateway_channel",
|
||||||
|
processCountField(gateways, "channel"),
|
||||||
|
"Gateway Channel"
|
||||||
|
);
|
||||||
|
chartGatewayRole = renderPieChart(
|
||||||
|
"chart_gateway_role",
|
||||||
|
processCountField(gateways, "role"),
|
||||||
|
"Gateway Role"
|
||||||
|
);
|
||||||
|
chartGatewayFirmware = renderPieChart(
|
||||||
|
"chart_gateway_firmware",
|
||||||
|
processCountField(gateways, "firmware"),
|
||||||
|
"Gateway Firmware"
|
||||||
|
);
|
||||||
|
|
||||||
const summaryNodesEl = document.getElementById("summary_nodes");
|
const summaryNodesEl = document.getElementById("summary_nodes");
|
||||||
if (summaryNodesEl) {
|
if (summaryNodesEl) {
|
||||||
summaryNodesEl.textContent = nodes.length.toLocaleString();
|
summaryNodesEl.textContent = nodes.length.toLocaleString();
|
||||||
}
|
}
|
||||||
|
const summaryGatewaysEl = document.getElementById("summary_gateways");
|
||||||
|
if (summaryGatewaysEl) {
|
||||||
|
summaryGatewaysEl.textContent = gateways.length.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
// Packet types pie
|
// Packet types pie
|
||||||
const packetTypesData = await fetchPacketTypeBreakdown();
|
const packetTypesData = await fetchPacketTypeBreakdown();
|
||||||
@@ -464,6 +507,9 @@ window.addEventListener('resize',()=>{
|
|||||||
chartHwModel,
|
chartHwModel,
|
||||||
chartRole,
|
chartRole,
|
||||||
chartChannel,
|
chartChannel,
|
||||||
|
chartGatewayChannel,
|
||||||
|
chartGatewayRole,
|
||||||
|
chartGatewayFirmware,
|
||||||
chartPacketTypes
|
chartPacketTypes
|
||||||
].forEach(c=>c?.resize());
|
].forEach(c=>c?.resize());
|
||||||
});
|
});
|
||||||
|
|||||||
+145
-188
@@ -15,7 +15,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,16 +38,9 @@
|
|||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
table th { background-color: #333; }
|
table th { background-color: #333; }
|
||||||
table tbody tr:nth-child(odd) { background-color: #272b2f; }
|
|
||||||
|
table tbody tr:nth-child(odd) { background-color: #272b2f; }
|
||||||
table tbody tr:nth-child(even) { background-color: #212529; }
|
table tbody tr:nth-child(even) { background-color: #212529; }
|
||||||
table tbody tr:hover { background-color: #555; cursor: pointer; }
|
table tbody tr:hover { background-color: #555; cursor: pointer; }
|
||||||
|
|
||||||
@@ -58,227 +51,191 @@
|
|||||||
.node-link:hover { text-decoration: underline; }
|
.node-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.good-x { color: #81ff81; font-weight: bold; }
|
.good-x { color: #81ff81; font-weight: bold; }
|
||||||
.ok-x { color: #e8e86d; font-weight: bold; }
|
.ok-x { color: #e8e86d; font-weight: bold; }
|
||||||
.bad-x { color: #ff6464; font-weight: bold; }
|
.bad-x { color: #ff6464; font-weight: bold; }
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<h1>Top Nodes Traffic</h1>
|
<h1 data-translate-lang="top_traffic_nodes">Top Nodes Traffic</h1>
|
||||||
|
|
||||||
<div class="top-container">
|
<div class="top-container">
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<div>
|
<div>
|
||||||
<label for="channelFilter">Channel:</label>
|
<label data-translate-lang="channel">Channel:</label>
|
||||||
<select id="channelFilter" class="form-select form-select-sm" style="width:auto;"></select>
|
<select id="channelFilter"></select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div style="margin-bottom:10px;font-weight:bold;">
|
||||||
<label for="nodeSearch">Search:</label>
|
<span data-translate-lang="showing_nodes">Showing</span>
|
||||||
<input id="nodeSearch" type="text" class="form-control form-control-sm"
|
<span id="node-count">0</span>
|
||||||
placeholder="Search nodes..."
|
<span data-translate-lang="nodes_suffix">nodes</span>
|
||||||
style="width:180px; display:inline-block;">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<table id="nodesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-translate-lang="long_name">Long Name</th>
|
||||||
|
<th data-translate-lang="short_name">Short Name</th>
|
||||||
|
<th data-translate-lang="channel">Channel</th>
|
||||||
|
<th data-translate-lang="packets_sent">Sent (24h)</th>
|
||||||
|
<th data-translate-lang="times_seen">Seen (24h)</th>
|
||||||
|
<th data-translate-lang="avg_gateways">Avg Gateways</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<button id="prevPage" class="btn btn-sm btn-secondary">Prev</button>
|
||||||
<div class="table-responsive">
|
<span id="pageInfo"></span>
|
||||||
<table id="nodesTable">
|
<button id="nextPage" class="btn btn-sm btn-secondary">Next</button>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Long Name</th>
|
|
||||||
<th>Short Name</th>
|
|
||||||
<th>Channel</th>
|
|
||||||
<th>Sent (24h)</th>
|
|
||||||
<th>Seen (24h)</th>
|
|
||||||
<th>Avg Gateways</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let allNodes = [];
|
/* ======================================================
|
||||||
|
TRANSLATIONS
|
||||||
|
====================================================== */
|
||||||
|
let topTranslations = {};
|
||||||
|
|
||||||
async function loadChannels() {
|
function applyTranslationsTop(dict, root=document) {
|
||||||
try {
|
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||||
const res = await fetch("/api/channels");
|
const key = el.dataset.translateLang;
|
||||||
const data = await res.json();
|
if (!dict[key]) return;
|
||||||
const channels = data.channels || [];
|
el.textContent = dict[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const select = document.getElementById("channelFilter");
|
async function loadTranslationsTop() {
|
||||||
|
const cfg = await window._siteConfigPromise;
|
||||||
|
const lang = cfg?.site?.language || "en";
|
||||||
|
const res = await fetch(`/api/lang?lang=${lang}§ion=top`);
|
||||||
|
topTranslations = await res.json();
|
||||||
|
applyTranslationsTop(topTranslations);
|
||||||
|
}
|
||||||
|
|
||||||
// Default LongFast first
|
/* ======================================================
|
||||||
if (channels.includes("LongFast")) {
|
CONFIG
|
||||||
const opt = document.createElement("option");
|
====================================================== */
|
||||||
opt.value = "LongFast";
|
const PAGE_SIZE = 20;
|
||||||
opt.textContent = "LongFast";
|
let currentPage = 0;
|
||||||
select.appendChild(opt);
|
let totalRows = 0;
|
||||||
}
|
|
||||||
|
|
||||||
for (const ch of channels) {
|
/* ======================================================
|
||||||
if (ch === "LongFast") continue;
|
HELPERS
|
||||||
const opt = document.createElement("option");
|
====================================================== */
|
||||||
opt.value = ch;
|
function avgClass(v) {
|
||||||
opt.textContent = ch;
|
if (v >= 10) return "good-x";
|
||||||
select.appendChild(opt);
|
if (v >= 2) return "ok-x";
|
||||||
}
|
return "bad-x";
|
||||||
|
}
|
||||||
|
|
||||||
select.addEventListener("change", renderTable);
|
/* ======================================================
|
||||||
} catch (err) {
|
LOAD CHANNELS
|
||||||
console.error("Error loading channels:", err);
|
====================================================== */
|
||||||
}
|
async function loadChannels() {
|
||||||
|
const res = await fetch("/api/channels");
|
||||||
|
const data = await res.json();
|
||||||
|
const sel = document.getElementById("channelFilter");
|
||||||
|
|
||||||
|
sel.innerHTML = "";
|
||||||
|
for (const ch of data.channels || []) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = ch;
|
||||||
|
opt.textContent = ch;
|
||||||
|
sel.appendChild(opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNodes() {
|
sel.value = "MediumFast";
|
||||||
try {
|
}
|
||||||
const res = await fetch("/api/nodes");
|
|
||||||
const data = await res.json();
|
|
||||||
allNodes = data.nodes || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error loading nodes:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchNodeStats(nodeId) {
|
|
||||||
try {
|
|
||||||
const url = `/api/stats/count?from_node=${nodeId}&period_type=day&length=1`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const sent = data.total_packets || 0;
|
|
||||||
const seen = data.total_seen || 0;
|
|
||||||
const avg = seen / Math.max(sent, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sent,
|
|
||||||
seen,
|
|
||||||
avg: avg
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Stat error", err);
|
|
||||||
return { sent: 0, seen: 0, avg: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function avgClass(v) {
|
|
||||||
if (v >= 10) return "good-x"; // Very strong node
|
|
||||||
if (v >= 2) return "ok-x"; // Normal node
|
|
||||||
return "bad-x"; // Weak node
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
FETCH + RENDER
|
||||||
|
====================================================== */
|
||||||
async function renderTable() {
|
async function renderTable() {
|
||||||
const tbody = document.querySelector("#nodesTable tbody");
|
const tbody = document.querySelector("#nodesTable tbody");
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
const channel = document.getElementById("channelFilter").value;
|
const channel = document.getElementById("channelFilter").value;
|
||||||
const searchText = document.getElementById("nodeSearch").value.trim().toLowerCase();
|
const offset = currentPage * PAGE_SIZE;
|
||||||
|
|
||||||
// Filter nodes by channel FIRST
|
const url = new URL("/api/stats/top", window.location.origin);
|
||||||
let filtered = allNodes.filter(n => n.channel === channel);
|
url.searchParams.set("limit", PAGE_SIZE);
|
||||||
|
url.searchParams.set("offset", offset);
|
||||||
|
if (channel) url.searchParams.set("channel", channel);
|
||||||
|
|
||||||
// Then apply search
|
const res = await fetch(url);
|
||||||
if (searchText !== "") {
|
const data = await res.json();
|
||||||
filtered = filtered.filter(n =>
|
|
||||||
(n.long_name && n.long_name.toLowerCase().includes(searchText)) ||
|
|
||||||
(n.short_name && n.short_name.toLowerCase().includes(searchText)) ||
|
|
||||||
String(n.node_id).includes(searchText)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Create placeholder rows ---
|
totalRows = data.total || 0;
|
||||||
const rowRefs = filtered.map(n => {
|
|
||||||
|
for (const n of data.nodes || []) {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.addEventListener("click", () => {
|
tr.onclick = () => location.href = `/node/${n.node_id}`;
|
||||||
window.location.href = `/node/${n.node_id}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tdLong = document.createElement("td");
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = `/node/${n.node_id}`;
|
|
||||||
a.textContent = n.long_name || n.node_id;
|
|
||||||
a.className = "node-link";
|
|
||||||
a.addEventListener("click", e => e.stopPropagation());
|
|
||||||
tdLong.appendChild(a);
|
|
||||||
|
|
||||||
const tdShort = document.createElement("td");
|
|
||||||
tdShort.textContent = n.short_name || "";
|
|
||||||
|
|
||||||
const tdChannel = document.createElement("td");
|
|
||||||
tdChannel.textContent = n.channel || "";
|
|
||||||
|
|
||||||
const tdSent = document.createElement("td");
|
|
||||||
tdSent.textContent = "Loading...";
|
|
||||||
|
|
||||||
const tdSeen = document.createElement("td");
|
|
||||||
tdSeen.textContent = "Loading...";
|
|
||||||
|
|
||||||
const tdAvg = document.createElement("td");
|
|
||||||
tdAvg.textContent = "Loading...";
|
|
||||||
|
|
||||||
tr.appendChild(tdLong);
|
|
||||||
tr.appendChild(tdShort);
|
|
||||||
tr.appendChild(tdChannel);
|
|
||||||
tr.appendChild(tdSent);
|
|
||||||
tr.appendChild(tdSeen);
|
|
||||||
tr.appendChild(tdAvg);
|
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<a class="node-link" href="/node/${n.node_id}"
|
||||||
|
onclick="event.stopPropagation()">
|
||||||
|
${n.long_name || n.node_id}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>${n.short_name || ""}</td>
|
||||||
|
<td>${n.channel || ""}</td>
|
||||||
|
<td>${n.sent}</td>
|
||||||
|
<td>${n.seen}</td>
|
||||||
|
<td><span class="${avgClass(n.avg)}">${n.avg.toFixed(1)}</span></td>
|
||||||
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
return { node: n, tr, tdSent, tdSeen, tdAvg };
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Stats fetch ---
|
|
||||||
const statsList = await Promise.all(
|
|
||||||
rowRefs.map(ref => fetchNodeStats(ref.node.node_id))
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Update + cleanup empty nodes ---
|
|
||||||
let combined = rowRefs.map((ref, i) => {
|
|
||||||
const stats = statsList[i];
|
|
||||||
|
|
||||||
ref.tdSent.textContent = stats.sent;
|
|
||||||
ref.tdSeen.textContent = stats.seen;
|
|
||||||
ref.tdAvg.innerHTML = `<span class="${avgClass(stats.avg)}">${stats.avg.toFixed(1)}</span>`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tr: ref.tr,
|
|
||||||
sent: stats.sent,
|
|
||||||
seen: stats.seen
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove nodes with no traffic
|
|
||||||
combined = combined.filter(r => !(r.sent === 0 && r.seen === 0));
|
|
||||||
|
|
||||||
// Sort by traffic (seen)
|
|
||||||
combined.sort((a, b) => b.seen - a.seen);
|
|
||||||
|
|
||||||
// Rebuild table
|
|
||||||
tbody.innerHTML = "";
|
|
||||||
for (const r of combined) {
|
|
||||||
tbody.appendChild(r.tr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE));
|
||||||
|
|
||||||
|
document.getElementById("node-count").textContent = totalRows;
|
||||||
|
document.getElementById("pageInfo").textContent =
|
||||||
|
`Page ${currentPage + 1} / ${totalPages}`;
|
||||||
|
|
||||||
|
document.getElementById("prevPage").disabled = currentPage === 0;
|
||||||
|
document.getElementById("nextPage").disabled = currentPage >= totalPages - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================
|
||||||
|
INIT
|
||||||
(async () => {
|
====================================================== */
|
||||||
await loadNodes();
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
await loadTranslationsTop();
|
||||||
await loadChannels();
|
await loadChannels();
|
||||||
document.getElementById("channelFilter").value = "LongFast";
|
await renderTable();
|
||||||
|
|
||||||
document.getElementById("nodeSearch").addEventListener("input", renderTable);
|
channelFilter.onchange = () => {
|
||||||
|
currentPage = 0;
|
||||||
|
renderTable();
|
||||||
|
};
|
||||||
|
|
||||||
renderTable();
|
prevPage.onclick = () => {
|
||||||
})();
|
if (currentPage > 0) {
|
||||||
|
currentPage--;
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nextPage.onclick = () => {
|
||||||
|
currentPage++;
|
||||||
|
renderTable();
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
#traceroute-graph {
|
||||||
|
width: 100%;
|
||||||
|
height: 85vh;
|
||||||
|
border: 1px solid #2a2f36;
|
||||||
|
background: linear-gradient(135deg, #0f1216 0%, #171b22 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#traceroute-meta {
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #c8d0da;
|
||||||
|
}
|
||||||
|
|
||||||
|
#traceroute-error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div id="traceroute-meta">
|
||||||
|
<div><b>Traceroute</b> <span id="traceroute-title"></span></div>
|
||||||
|
<div id="traceroute-error"></div>
|
||||||
|
</div>
|
||||||
|
<div id="traceroute-graph"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const el = document.getElementById("traceroute-graph");
|
||||||
|
const chart = echarts.init(el);
|
||||||
|
|
||||||
|
function packetIdFromPath() {
|
||||||
|
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPathEdges(path, edges, style) {
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
edges.push({
|
||||||
|
source: String(path[i]),
|
||||||
|
target: String(path[i + 1]),
|
||||||
|
lineStyle: style
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTraceroute() {
|
||||||
|
const packetId = packetIdFromPath();
|
||||||
|
document.getElementById("traceroute-title").textContent = `#${packetId}`;
|
||||||
|
|
||||||
|
const [res, nodesRes] = await Promise.all([
|
||||||
|
fetch(`/api/traceroute/${packetId}`),
|
||||||
|
fetch("/api/nodes"),
|
||||||
|
]);
|
||||||
|
if (!res.ok) {
|
||||||
|
document.getElementById("traceroute-error").textContent = "Traceroute not found.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const nodesData = nodesRes.ok ? await nodesRes.json() : { nodes: [] };
|
||||||
|
const nodeShortNameById = new Map(
|
||||||
|
(nodesData.nodes || []).map(n => [String(n.node_id), n.short_name || n.long_name || String(n.node_id)])
|
||||||
|
);
|
||||||
|
const nodeLongNameById = new Map(
|
||||||
|
(nodesData.nodes || []).map(n => [String(n.node_id), n.long_name || n.short_name || String(n.node_id)])
|
||||||
|
);
|
||||||
|
const nodes = new Map();
|
||||||
|
const edges = [];
|
||||||
|
|
||||||
|
const forwardPaths = data?.winning_paths?.forward || [];
|
||||||
|
const reversePaths = data?.winning_paths?.reverse || [];
|
||||||
|
const originId = data?.packet?.from != null ? String(data.packet.from) : null;
|
||||||
|
const targetId = data?.packet?.to != null ? String(data.packet.to) : null;
|
||||||
|
|
||||||
|
forwardPaths.forEach(path => {
|
||||||
|
path.forEach(id => nodes.set(String(id), { name: String(id) }));
|
||||||
|
addPathEdges(path, edges, { color: "#ff5733", width: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
reversePaths.forEach(path => {
|
||||||
|
path.forEach(id => nodes.set(String(id), { name: String(id) }));
|
||||||
|
addPathEdges(path, edges, { color: "#00c3ff", width: 2, type: "dashed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const graphNodes = Array.from(nodes.values()).map(n => {
|
||||||
|
const isOrigin = originId && n.name === originId;
|
||||||
|
const isTarget = targetId && n.name === targetId;
|
||||||
|
const color = isOrigin ? "#ff3b30" : isTarget ? "#34c759" : "#8aa4c8";
|
||||||
|
const size = isOrigin || isTarget ? 44 : 36;
|
||||||
|
return {
|
||||||
|
id: n.name,
|
||||||
|
name: nodeShortNameById.get(n.name) || n.name,
|
||||||
|
symbolSize: size,
|
||||||
|
itemStyle: { color },
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: "#e7eef7",
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
formatter: () => nodeLongNameById.get(n.name) || n.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
tooltip: { trigger: "item" },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "graph",
|
||||||
|
layout: "force",
|
||||||
|
roam: true,
|
||||||
|
zoom: 1.2,
|
||||||
|
draggable: true,
|
||||||
|
force: { repulsion: 200, edgeLength: 80 },
|
||||||
|
data: graphNodes,
|
||||||
|
edges: edges,
|
||||||
|
lineStyle: { opacity: 0.8, curveness: 0.1 },
|
||||||
|
edgeSymbol: ["none", "arrow"],
|
||||||
|
edgeSymbolSize: 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTraceroute();
|
||||||
|
window.addEventListener("resize", () => chart.resize());
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
+62
-32
@@ -1,7 +1,10 @@
|
|||||||
|
"""Main web server routes and page rendering for Meshview."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -18,6 +21,7 @@ from meshview import config, database, decode_payload, migrations, models, store
|
|||||||
from meshview.__version__ import (
|
from meshview.__version__ import (
|
||||||
__version_string__,
|
__version_string__,
|
||||||
)
|
)
|
||||||
|
from meshview.deps import check_optional_deps
|
||||||
from meshview.web_api import api
|
from meshview.web_api import api
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -35,6 +39,7 @@ env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape
|
|||||||
|
|
||||||
# Start Database
|
# Start Database
|
||||||
database.init_database(CONFIG["database"]["connection_string"])
|
database.init_database(CONFIG["database"]["connection_string"])
|
||||||
|
check_optional_deps()
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(__file__)
|
BASE_DIR = os.path.dirname(__file__)
|
||||||
LANG_DIR = os.path.join(BASE_DIR, "lang")
|
LANG_DIR = os.path.join(BASE_DIR, "lang")
|
||||||
@@ -45,22 +50,25 @@ with open(os.path.join(os.path.dirname(__file__), '1x1.png'), 'rb') as png:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Packet:
|
class Packet:
|
||||||
|
"""UI-friendly packet wrapper for templates and API payloads."""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
from_node_id: int
|
from_node_id: int
|
||||||
from_node: models.Node
|
from_node: models.Node
|
||||||
to_node_id: int
|
to_node_id: int
|
||||||
to_node: models.Node
|
to_node: models.Node
|
||||||
|
channel: str
|
||||||
portnum: int
|
portnum: int
|
||||||
data: str
|
data: str
|
||||||
raw_mesh_packet: object
|
raw_mesh_packet: object
|
||||||
raw_payload: object
|
raw_payload: object
|
||||||
payload: str
|
payload: str
|
||||||
pretty_payload: Markup
|
pretty_payload: Markup
|
||||||
import_time: datetime.datetime
|
|
||||||
import_time_us: int
|
import_time_us: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model(cls, packet):
|
def from_model(cls, packet):
|
||||||
|
"""Convert a Packet ORM model into a presentation-friendly Packet."""
|
||||||
mesh_packet, payload = decode_payload.decode(packet)
|
mesh_packet, payload = decode_payload.decode(packet)
|
||||||
pretty_payload = None
|
pretty_payload = None
|
||||||
|
|
||||||
@@ -97,11 +105,11 @@ class Packet:
|
|||||||
from_node_id=packet.from_node_id,
|
from_node_id=packet.from_node_id,
|
||||||
to_node=packet.to_node,
|
to_node=packet.to_node,
|
||||||
to_node_id=packet.to_node_id,
|
to_node_id=packet.to_node_id,
|
||||||
|
channel=packet.channel,
|
||||||
portnum=packet.portnum,
|
portnum=packet.portnum,
|
||||||
data=text_mesh_packet,
|
data=text_mesh_packet,
|
||||||
payload=text_payload, # now always a string
|
payload=text_payload, # now always a string
|
||||||
pretty_payload=pretty_payload,
|
pretty_payload=pretty_payload,
|
||||||
import_time=packet.import_time,
|
|
||||||
import_time_us=packet.import_time_us, # <-- include microseconds
|
import_time_us=packet.import_time_us, # <-- include microseconds
|
||||||
raw_mesh_packet=mesh_packet,
|
raw_mesh_packet=mesh_packet,
|
||||||
raw_payload=payload,
|
raw_payload=payload,
|
||||||
@@ -109,6 +117,7 @@ class Packet:
|
|||||||
|
|
||||||
|
|
||||||
async def build_trace(node_id):
|
async def build_trace(node_id):
|
||||||
|
"""Build a recent GPS trace list for a node using position packets."""
|
||||||
trace = []
|
trace = []
|
||||||
for raw_p in await store.get_packets_from(
|
for raw_p in await store.get_packets_from(
|
||||||
node_id, PortNum.POSITION_APP, since=datetime.timedelta(hours=24)
|
node_id, PortNum.POSITION_APP, since=datetime.timedelta(hours=24)
|
||||||
@@ -130,6 +139,7 @@ async def build_trace(node_id):
|
|||||||
|
|
||||||
|
|
||||||
async def build_neighbors(node_id):
|
async def build_neighbors(node_id):
|
||||||
|
"""Return neighbor node metadata for the given node ID."""
|
||||||
packets = await store.get_packets_from(node_id, PortNum.NEIGHBORINFO_APP, limit=1)
|
packets = await store.get_packets_from(node_id, PortNum.NEIGHBORINFO_APP, limit=1)
|
||||||
packet = packets.first()
|
packet = packets.first()
|
||||||
|
|
||||||
@@ -159,6 +169,7 @@ async def build_neighbors(node_id):
|
|||||||
|
|
||||||
|
|
||||||
def node_id_to_hex(node_id):
|
def node_id_to_hex(node_id):
|
||||||
|
"""Format a node_id in Meshtastic hex notation."""
|
||||||
if node_id is None or isinstance(node_id, Undefined):
|
if node_id is None or isinstance(node_id, Undefined):
|
||||||
return "Invalid node_id" # i... have no clue
|
return "Invalid node_id" # i... have no clue
|
||||||
if node_id == 4294967295:
|
if node_id == 4294967295:
|
||||||
@@ -168,6 +179,7 @@ def node_id_to_hex(node_id):
|
|||||||
|
|
||||||
|
|
||||||
def format_timestamp(timestamp):
|
def format_timestamp(timestamp):
|
||||||
|
"""Normalize timestamps to ISO 8601 strings."""
|
||||||
if isinstance(timestamp, int):
|
if isinstance(timestamp, int):
|
||||||
timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
||||||
return timestamp.isoformat(timespec="milliseconds")
|
return timestamp.isoformat(timespec="milliseconds")
|
||||||
@@ -201,6 +213,38 @@ async def redirect_packet_list(request):
|
|||||||
raise web.HTTPFound(location=f"/node/{packet_id}")
|
raise web.HTTPFound(location=f"/node/{packet_id}")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic static HTML route
|
||||||
|
@routes.get("/{page}")
|
||||||
|
async def serve_page(request):
|
||||||
|
"""Serve static HTML pages from meshview/static."""
|
||||||
|
page = request.match_info["page"]
|
||||||
|
|
||||||
|
# default to index.html if no extension
|
||||||
|
if not page.endswith(".html"):
|
||||||
|
page = f"{page}.html"
|
||||||
|
|
||||||
|
html_file = pathlib.Path(__file__).parent / "static" / page
|
||||||
|
if not html_file.exists():
|
||||||
|
raise web.HTTPNotFound(text=f"Page '{page}' not found")
|
||||||
|
|
||||||
|
content = html_file.read_text(encoding="utf-8")
|
||||||
|
return web.Response(text=content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/docs/{doc}")
|
||||||
|
async def serve_doc(request):
|
||||||
|
"""Serve documentation files from docs/ (markdown)."""
|
||||||
|
doc = request.match_info["doc"]
|
||||||
|
docs_root = pathlib.Path(__file__).parent.parent / "docs"
|
||||||
|
doc_path = (docs_root / doc).resolve()
|
||||||
|
|
||||||
|
if not doc_path.is_file() or docs_root not in doc_path.parents:
|
||||||
|
raise web.HTTPNotFound(text="Document not found")
|
||||||
|
|
||||||
|
content = doc_path.read_text(encoding="utf-8")
|
||||||
|
return web.Response(text=content, content_type="text/markdown")
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/net")
|
@routes.get("/net")
|
||||||
async def net(request):
|
async def net(request):
|
||||||
return web.Response(
|
return web.Response(
|
||||||
@@ -286,6 +330,15 @@ async def stats(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/traceroute/{packet_id}")
|
||||||
|
async def traceroute_page(request):
|
||||||
|
template = env.get_template("traceroute.html")
|
||||||
|
return web.Response(
|
||||||
|
text=template.render(),
|
||||||
|
content_type="text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Keep !!
|
# Keep !!
|
||||||
@routes.get("/graph/traceroute/{packet_id}")
|
@routes.get("/graph/traceroute/{packet_id}")
|
||||||
async def graph_traceroute(request):
|
async def graph_traceroute(request):
|
||||||
@@ -335,8 +388,8 @@ async def graph_traceroute(request):
|
|||||||
# It seems some nodes add them self to the list before uplinking
|
# It seems some nodes add them self to the list before uplinking
|
||||||
path.append(tr.gateway_node_id)
|
path.append(tr.gateway_node_id)
|
||||||
|
|
||||||
if not tr.done and tr.gateway_node_id not in node_seen_time and tr.import_time:
|
if not tr.done and tr.gateway_node_id not in node_seen_time and tr.import_time_us:
|
||||||
node_seen_time[path[-1]] = tr.import_time
|
node_seen_time[path[-1]] = tr.import_time_us
|
||||||
|
|
||||||
mqtt_nodes.add(tr.gateway_node_id)
|
mqtt_nodes.add(tr.gateway_node_id)
|
||||||
node_color[path[-1]] = '#' + hex(hash(tuple(path)))[3:9]
|
node_color[path[-1]] = '#' + hex(hash(tuple(path)))[3:9]
|
||||||
@@ -346,7 +399,7 @@ async def graph_traceroute(request):
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
used_nodes.update(path)
|
used_nodes.update(path)
|
||||||
|
|
||||||
import_times = [tr.import_time for tr in traceroutes if tr.import_time]
|
import_times = [tr.import_time_us for tr in traceroutes if tr.import_time_us]
|
||||||
if import_times:
|
if import_times:
|
||||||
first_time = min(import_times)
|
first_time = min(import_times)
|
||||||
else:
|
else:
|
||||||
@@ -361,7 +414,7 @@ async def graph_traceroute(request):
|
|||||||
f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}\n{node.role}'
|
f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}\n{node.role}'
|
||||||
)
|
)
|
||||||
if node_id in node_seen_time:
|
if node_id in node_seen_time:
|
||||||
ms = (node_seen_time[node_id] - first_time).total_seconds() * 1000
|
ms = (node_seen_time[node_id] - first_time) / 1000
|
||||||
node_name += f'\n {ms:.2f}ms'
|
node_name += f'\n {ms:.2f}ms'
|
||||||
style = 'dashed'
|
style = 'dashed'
|
||||||
if node_id == dest:
|
if node_id == dest:
|
||||||
@@ -379,7 +432,7 @@ async def graph_traceroute(request):
|
|||||||
shape='box',
|
shape='box',
|
||||||
color=node_color.get(node_id, 'black'),
|
color=node_color.get(node_id, 'black'),
|
||||||
style=style,
|
style=style,
|
||||||
href=f"/packet_list/{node_id}",
|
href=f"/node/{node_id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -394,32 +447,8 @@ async def graph_traceroute(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
@routes.get("/stats")
|
|
||||||
async def stats(request):
|
|
||||||
try:
|
|
||||||
total_packets = await store.get_total_packet_count()
|
|
||||||
total_nodes = await store.get_total_node_count()
|
|
||||||
total_packets_seen = await store.get_total_packet_seen_count()
|
|
||||||
template = env.get_template("stats.html")
|
|
||||||
return web.Response(
|
|
||||||
text=template.render(
|
|
||||||
total_packets=total_packets,
|
|
||||||
total_nodes=total_nodes,
|
|
||||||
total_packets_seen=total_packets_seen,
|
|
||||||
),
|
|
||||||
content_type="text/html",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return web.Response(
|
|
||||||
text=f"An error occurred: {str(e)}",
|
|
||||||
status=500,
|
|
||||||
content_type="text/plain",
|
|
||||||
)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
async def run_server():
|
async def run_server():
|
||||||
|
"""Start the aiohttp web server after migrations are complete."""
|
||||||
# Wait for database migrations to complete before starting web server
|
# Wait for database migrations to complete before starting web server
|
||||||
logger.info("Checking database schema status...")
|
logger.info("Checking database schema status...")
|
||||||
database_url = CONFIG["database"]["connection_string"]
|
database_url = CONFIG["database"]["connection_string"]
|
||||||
@@ -436,6 +465,7 @@ async def run_server():
|
|||||||
logger.info("Database schema verified - starting web server")
|
logger.info("Database schema verified - starting web server")
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
|
app.router.add_static("/static/", pathlib.Path(__file__).parent / "static")
|
||||||
app.add_routes(api.routes) # Add API routes
|
app.add_routes(api.routes) # Add API routes
|
||||||
app.add_routes(routes) # Add main web routes
|
app.add_routes(routes) # Add main web routes
|
||||||
|
|
||||||
|
|||||||
+500
-37
@@ -3,15 +3,28 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from sqlalchemy import text
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from meshtastic.protobuf.portnums_pb2 import PortNum
|
from meshtastic.protobuf.portnums_pb2 import PortNum
|
||||||
from meshview import database, decode_payload, store
|
from meshview import database, decode_payload, store
|
||||||
from meshview.__version__ import __version__, _git_revision_short, get_version_info
|
from meshview.__version__ import __version__, _git_revision_short, get_version_info
|
||||||
from meshview.config import CONFIG
|
from meshview.config import CONFIG
|
||||||
|
from meshview.models import Node, NodePublicKey
|
||||||
|
from meshview.models import Packet as PacketModel
|
||||||
|
from meshview.models import PacketSeen as PacketSeenModel
|
||||||
|
from meshview.radio.coverage import (
|
||||||
|
DEFAULT_MAX_DBM,
|
||||||
|
DEFAULT_MIN_DBM,
|
||||||
|
DEFAULT_RELIABILITY,
|
||||||
|
DEFAULT_THRESHOLD_DBM,
|
||||||
|
ITM_AVAILABLE,
|
||||||
|
compute_coverage,
|
||||||
|
compute_perimeter,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,11 +32,35 @@ logger = logging.getLogger(__name__)
|
|||||||
Packet = None
|
Packet = None
|
||||||
SEQ_REGEX = None
|
SEQ_REGEX = None
|
||||||
LANG_DIR = None
|
LANG_DIR = None
|
||||||
|
_LANG_CACHE = {}
|
||||||
|
|
||||||
# Create dedicated route table for API endpoints
|
# Create dedicated route table for API endpoints
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
|
|
||||||
|
def _haversine_km(lat1, lon1, lat2, lon2):
|
||||||
|
r = 6371.0
|
||||||
|
phi1 = math.radians(lat1)
|
||||||
|
phi2 = math.radians(lat2)
|
||||||
|
dphi = math.radians(lat2 - lat1)
|
||||||
|
dlambda = math.radians(lon2 - lon1)
|
||||||
|
a = math.sin(dphi / 2.0) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2.0) ** 2
|
||||||
|
return 2 * r * math.asin(math.sqrt(a))
|
||||||
|
|
||||||
|
|
||||||
|
def _bearing_deg(lat1, lon1, lat2, lon2):
|
||||||
|
phi1 = math.radians(lat1)
|
||||||
|
phi2 = math.radians(lat2)
|
||||||
|
dlambda = math.radians(lon2 - lon1)
|
||||||
|
y = math.sin(dlambda) * math.cos(phi2)
|
||||||
|
x = math.cos(phi1) * math.sin(phi2) - math.sin(phi1) * math.cos(phi2) * math.cos(dlambda)
|
||||||
|
bearing = math.degrees(math.atan2(y, x))
|
||||||
|
return (bearing + 360.0) % 360.0
|
||||||
|
|
||||||
|
|
||||||
|
OBSERVED_MAX_DISTANCE_KM = 50.0
|
||||||
|
|
||||||
|
|
||||||
def init_api_module(packet_class, seq_regex, lang_dir):
|
def init_api_module(packet_class, seq_regex, lang_dir):
|
||||||
"""Initialize API module with dependencies from main web module."""
|
"""Initialize API module with dependencies from main web module."""
|
||||||
global Packet, SEQ_REGEX, LANG_DIR
|
global Packet, SEQ_REGEX, LANG_DIR
|
||||||
@@ -48,6 +85,7 @@ async def api_channels(request: web.Request):
|
|||||||
async def api_nodes(request):
|
async def api_nodes(request):
|
||||||
try:
|
try:
|
||||||
# Optional query parameters
|
# Optional query parameters
|
||||||
|
node_id = request.query.get("node_id")
|
||||||
role = request.query.get("role")
|
role = request.query.get("role")
|
||||||
channel = request.query.get("channel")
|
channel = request.query.get("channel")
|
||||||
hw_model = request.query.get("hw_model")
|
hw_model = request.query.get("hw_model")
|
||||||
@@ -61,7 +99,7 @@ async def api_nodes(request):
|
|||||||
|
|
||||||
# Fetch nodes from database
|
# Fetch nodes from database
|
||||||
nodes = await store.get_nodes(
|
nodes = await store.get_nodes(
|
||||||
role=role, channel=channel, hw_model=hw_model, days_active=days_active
|
node_id=node_id, role=role, channel=channel, hw_model=hw_model, days_active=days_active
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare the JSON response
|
# Prepare the JSON response
|
||||||
@@ -79,7 +117,9 @@ async def api_nodes(request):
|
|||||||
"last_lat": getattr(n, "last_lat", None),
|
"last_lat": getattr(n, "last_lat", None),
|
||||||
"last_long": getattr(n, "last_long", None),
|
"last_long": getattr(n, "last_long", None),
|
||||||
"channel": n.channel,
|
"channel": n.channel,
|
||||||
|
"is_mqtt_gateway": getattr(n, "is_mqtt_gateway", None),
|
||||||
# "last_update": n.last_update.isoformat(),
|
# "last_update": n.last_update.isoformat(),
|
||||||
|
"first_seen_us": n.first_seen_us,
|
||||||
"last_seen_us": n.last_seen_us,
|
"last_seen_us": n.last_seen_us,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -125,15 +165,14 @@ async def api_packets(request):
|
|||||||
"portnum": int(p.portnum) if p.portnum is not None else None,
|
"portnum": int(p.portnum) if p.portnum is not None else None,
|
||||||
"payload": (p.payload or "").strip(),
|
"payload": (p.payload or "").strip(),
|
||||||
"import_time_us": p.import_time_us,
|
"import_time_us": p.import_time_us,
|
||||||
"import_time": p.import_time.isoformat() if p.import_time else None,
|
"channel": p.channel,
|
||||||
"channel": getattr(p.from_node, "channel", ""),
|
|
||||||
"long_name": getattr(p.from_node, "long_name", ""),
|
"long_name": getattr(p.from_node, "long_name", ""),
|
||||||
}
|
}
|
||||||
return web.json_response({"packets": [data]})
|
return web.json_response({"packets": [data]})
|
||||||
|
|
||||||
# --- Parse limit ---
|
# --- Parse limit ---
|
||||||
try:
|
try:
|
||||||
limit = min(max(int(limit_str), 1), 100)
|
limit = min(max(int(limit_str), 1), 1000)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
limit = 50
|
limit = 50
|
||||||
|
|
||||||
@@ -177,13 +216,17 @@ async def api_packets(request):
|
|||||||
logger.warning(f"Invalid node_id: {node_id_str}")
|
logger.warning(f"Invalid node_id: {node_id_str}")
|
||||||
|
|
||||||
# --- Fetch packets using explicit filters ---
|
# --- Fetch packets using explicit filters ---
|
||||||
|
contains_for_query = contains
|
||||||
|
if portnum == PortNum.TEXT_MESSAGE_APP and contains:
|
||||||
|
contains_for_query = None
|
||||||
|
|
||||||
packets = await store.get_packets(
|
packets = await store.get_packets(
|
||||||
from_node_id=from_node_id,
|
from_node_id=from_node_id,
|
||||||
to_node_id=to_node_id,
|
to_node_id=to_node_id,
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
portnum=portnum,
|
portnum=portnum,
|
||||||
after=since,
|
after=since,
|
||||||
contains=contains,
|
contains=contains_for_query,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -207,13 +250,13 @@ async def api_packets(request):
|
|||||||
packet_dict = {
|
packet_dict = {
|
||||||
"id": p.id,
|
"id": p.id,
|
||||||
"import_time_us": p.import_time_us,
|
"import_time_us": p.import_time_us,
|
||||||
"import_time": p.import_time.isoformat() if p.import_time else None,
|
"channel": p.channel,
|
||||||
"channel": getattr(p.from_node, "channel", ""),
|
|
||||||
"from_node_id": p.from_node_id,
|
"from_node_id": p.from_node_id,
|
||||||
"to_node_id": p.to_node_id,
|
"to_node_id": p.to_node_id,
|
||||||
"portnum": int(p.portnum),
|
"portnum": int(p.portnum),
|
||||||
"long_name": getattr(p.from_node, "long_name", ""),
|
"long_name": getattr(p.from_node, "long_name", ""),
|
||||||
"payload": (p.payload or "").strip(),
|
"payload": (p.payload or "").strip(),
|
||||||
|
"to_long_name": getattr(p.to_node, "long_name", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
reply_id = getattr(
|
reply_id = getattr(
|
||||||
@@ -226,20 +269,12 @@ async def api_packets(request):
|
|||||||
|
|
||||||
packets_data.append(packet_dict)
|
packets_data.append(packet_dict)
|
||||||
|
|
||||||
# --- Latest import_time for incremental fetch ---
|
# --- Latest import_time_us for incremental fetch ---
|
||||||
latest_import_time = None
|
latest_import_time = None
|
||||||
if packets_data:
|
if packets_data:
|
||||||
for p in packets_data:
|
for p in packets_data:
|
||||||
if p.get("import_time_us") and p["import_time_us"] > 0:
|
if p.get("import_time_us") and p["import_time_us"] > 0:
|
||||||
latest_import_time = max(latest_import_time or 0, p["import_time_us"])
|
latest_import_time = max(latest_import_time or 0, p["import_time_us"])
|
||||||
elif p.get("import_time") and latest_import_time is None:
|
|
||||||
try:
|
|
||||||
dt = datetime.datetime.fromisoformat(
|
|
||||||
p["import_time"].replace("Z", "+00:00")
|
|
||||||
)
|
|
||||||
latest_import_time = int(dt.timestamp() * 1_000_000)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
response = {"packets": packets_data}
|
response = {"packets": packets_data}
|
||||||
if latest_import_time is not None:
|
if latest_import_time is not None:
|
||||||
@@ -419,44 +454,65 @@ async def api_stats_count(request):
|
|||||||
|
|
||||||
@routes.get("/api/edges")
|
@routes.get("/api/edges")
|
||||||
async def api_edges(request):
|
async def api_edges(request):
|
||||||
since = datetime.datetime.now() - datetime.timedelta(hours=48)
|
since = datetime.datetime.now() - datetime.timedelta(hours=12)
|
||||||
filter_type = request.query.get("type")
|
filter_type = request.query.get("type")
|
||||||
|
|
||||||
edges = {}
|
# NEW → optional single-node filter
|
||||||
|
node_filter_str = request.query.get("node_id")
|
||||||
|
node_filter = None
|
||||||
|
if node_filter_str:
|
||||||
|
try:
|
||||||
|
node_filter = int(node_filter_str)
|
||||||
|
except ValueError:
|
||||||
|
return web.json_response({"error": "node_id must be integer"}, status=400)
|
||||||
|
|
||||||
# Only build traceroute edges if requested
|
edges = {}
|
||||||
|
traceroute_count = 0
|
||||||
|
edges_added_tr = 0
|
||||||
|
edges_added_neighbor = 0
|
||||||
|
|
||||||
|
# --- Traceroute edges ---
|
||||||
if filter_type in (None, "traceroute"):
|
if filter_type in (None, "traceroute"):
|
||||||
async for tr in store.get_traceroutes(since):
|
async for tr in store.get_traceroutes(since):
|
||||||
|
traceroute_count += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error decoding Traceroute {tr.id}: {e}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
path = [tr.packet.from_node_id] + list(route.route)
|
path = [tr.packet.from_node_id] + list(route.route)
|
||||||
path.append(tr.packet.to_node_id if tr.done else tr.gateway_node_id)
|
path.append(tr.packet.to_node_id if tr.done else tr.gateway_node_id)
|
||||||
|
|
||||||
for a, b in zip(path, path[1:], strict=False):
|
for a, b in zip(path, path[1:], strict=False):
|
||||||
edges[(a, b)] = "traceroute"
|
if (a, b) not in edges:
|
||||||
|
edges[(a, b)] = "traceroute"
|
||||||
|
edges_added_tr += 1
|
||||||
|
|
||||||
# Only build neighbor edges if requested
|
# --- Neighbor edges ---
|
||||||
if filter_type in (None, "neighbor"):
|
if filter_type in (None, "neighbor"):
|
||||||
packets = await store.get_packets(portnum=PortNum.NEIGHBORINFO_APP, after=since)
|
packets = await store.get_packets(portnum=71)
|
||||||
for packet in packets:
|
for packet in packets:
|
||||||
try:
|
try:
|
||||||
_, neighbor_info = decode_payload.decode(packet)
|
_, neighbor_info = decode_payload.decode(packet)
|
||||||
for node in neighbor_info.neighbors:
|
except Exception:
|
||||||
edges.setdefault((node.node_id, packet.from_node_id), "neighbor")
|
continue
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error decoding NeighborInfo packet {getattr(packet, 'id', '?')}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert edges dict to list format for JSON response
|
for node in neighbor_info.neighbors:
|
||||||
|
edge = (node.node_id, packet.from_node_id)
|
||||||
|
if edge not in edges:
|
||||||
|
edges[edge] = "neighbor"
|
||||||
|
edges_added_neighbor += 1
|
||||||
|
|
||||||
|
# Convert to list
|
||||||
edges_list = [
|
edges_list = [
|
||||||
{"from": frm, "to": to, "type": edge_type} for (frm, to), edge_type in edges.items()
|
{"from": frm, "to": to, "type": edge_type} for (frm, to), edge_type in edges.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# NEW → apply node_id filtering
|
||||||
|
if node_filter is not None:
|
||||||
|
edges_list = [e for e in edges_list if e["from"] == node_filter or e["to"] == node_filter]
|
||||||
|
|
||||||
return web.json_response({"edges": edges_list})
|
return web.json_response({"edges": edges_list})
|
||||||
|
|
||||||
|
|
||||||
@@ -573,9 +629,20 @@ async def api_lang(request):
|
|||||||
if not os.path.exists(lang_file):
|
if not os.path.exists(lang_file):
|
||||||
lang_file = os.path.join(LANG_DIR, "en.json")
|
lang_file = os.path.join(LANG_DIR, "en.json")
|
||||||
|
|
||||||
# Load JSON translations
|
# Cache by file + mtime to avoid re-reading on every request
|
||||||
with open(lang_file, encoding="utf-8") as f:
|
try:
|
||||||
translations = json.load(f)
|
mtime = os.path.getmtime(lang_file)
|
||||||
|
except OSError:
|
||||||
|
mtime = None
|
||||||
|
|
||||||
|
cache_key = lang_file
|
||||||
|
cached = _LANG_CACHE.get(cache_key)
|
||||||
|
if cached and cached.get("mtime") == mtime:
|
||||||
|
translations = cached["translations"]
|
||||||
|
else:
|
||||||
|
with open(lang_file, encoding="utf-8") as f:
|
||||||
|
translations = json.load(f)
|
||||||
|
_LANG_CACHE[cache_key] = {"mtime": mtime, "translations": translations}
|
||||||
|
|
||||||
if section:
|
if section:
|
||||||
section = section.lower()
|
section = section.lower()
|
||||||
@@ -603,8 +670,14 @@ async def health_check(request):
|
|||||||
# Check database connectivity
|
# Check database connectivity
|
||||||
try:
|
try:
|
||||||
async with database.async_session() as session:
|
async with database.async_session() as session:
|
||||||
await session.execute(text("SELECT 1"))
|
result = await session.execute(select(func.max(PacketModel.import_time_us)))
|
||||||
|
last_import_time_us = result.scalar()
|
||||||
health_status["database"] = "connected"
|
health_status["database"] = "connected"
|
||||||
|
if last_import_time_us is not None:
|
||||||
|
now_us = int(datetime.datetime.now(datetime.UTC).timestamp() * 1_000_000)
|
||||||
|
health_status["seconds_since_last_message"] = round(
|
||||||
|
(now_us - last_import_time_us) / 1_000_000, 1
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Database health check failed: {e}")
|
logger.error(f"Database health check failed: {e}")
|
||||||
health_status["database"] = "disconnected"
|
health_status["database"] = "disconnected"
|
||||||
@@ -677,7 +750,6 @@ async def api_packets_seen(request):
|
|||||||
"rx_snr": row.rx_snr,
|
"rx_snr": row.rx_snr,
|
||||||
"rx_rssi": row.rx_rssi,
|
"rx_rssi": row.rx_rssi,
|
||||||
"topic": row.topic,
|
"topic": row.topic,
|
||||||
"import_time": (row.import_time.isoformat() if row.import_time else None),
|
|
||||||
"import_time_us": row.import_time_us,
|
"import_time_us": row.import_time_us,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -690,3 +762,394 @@ async def api_packets_seen(request):
|
|||||||
{"error": "Internal server error"},
|
{"error": "Internal server error"},
|
||||||
status=500,
|
status=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/api/traceroute/{packet_id}")
|
||||||
|
async def api_traceroute(request):
|
||||||
|
packet_id = int(request.match_info['packet_id'])
|
||||||
|
|
||||||
|
traceroutes = list(await store.get_traceroute(packet_id))
|
||||||
|
packet = await store.get_packet(packet_id)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
return web.json_response({"error": "Packet not found"}, status=404)
|
||||||
|
|
||||||
|
tr_groups = []
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Decode each traceroute entry
|
||||||
|
# --------------------------------------------
|
||||||
|
for idx, tr in enumerate(traceroutes):
|
||||||
|
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
||||||
|
|
||||||
|
forward_list = list(route.route)
|
||||||
|
reverse_list = list(route.route_back)
|
||||||
|
|
||||||
|
tr_groups.append(
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"gateway_node_id": tr.gateway_node_id,
|
||||||
|
"done": tr.done,
|
||||||
|
"forward_hops": forward_list,
|
||||||
|
"reverse_hops": reverse_list,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Compute UNIQUE paths + counts + winning path
|
||||||
|
# --------------------------------------------
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
forward_paths = []
|
||||||
|
reverse_paths = []
|
||||||
|
winning_forward_paths = []
|
||||||
|
winning_reverse_paths = []
|
||||||
|
|
||||||
|
for tr in tr_groups:
|
||||||
|
f = tuple(tr["forward_hops"])
|
||||||
|
r = tuple(tr["reverse_hops"])
|
||||||
|
|
||||||
|
if tr["forward_hops"]:
|
||||||
|
forward_paths.append(f)
|
||||||
|
|
||||||
|
if tr["reverse_hops"]:
|
||||||
|
reverse_paths.append(r)
|
||||||
|
|
||||||
|
if tr["done"]:
|
||||||
|
if tr["forward_hops"]:
|
||||||
|
winning_forward_paths.append(f)
|
||||||
|
if tr["reverse_hops"]:
|
||||||
|
winning_reverse_paths.append(r)
|
||||||
|
|
||||||
|
# Deduplicate
|
||||||
|
unique_forward_paths = sorted(set(forward_paths))
|
||||||
|
unique_reverse_paths = sorted(set(reverse_paths))
|
||||||
|
|
||||||
|
# Count occurrences
|
||||||
|
forward_counts = Counter(forward_paths)
|
||||||
|
|
||||||
|
# Convert for JSON output
|
||||||
|
unique_forward_paths_json = [
|
||||||
|
{"path": list(p), "count": forward_counts[p]} for p in unique_forward_paths
|
||||||
|
]
|
||||||
|
|
||||||
|
unique_reverse_paths_json = [list(p) for p in unique_reverse_paths]
|
||||||
|
|
||||||
|
from_node_id = packet.from_node_id
|
||||||
|
to_node_id = packet.to_node_id
|
||||||
|
winning_forward_with_endpoints = []
|
||||||
|
for path in set(winning_forward_paths):
|
||||||
|
full_path = list(path)
|
||||||
|
if from_node_id is not None and (not full_path or full_path[0] != from_node_id):
|
||||||
|
full_path = [from_node_id, *full_path]
|
||||||
|
if to_node_id is not None and (not full_path or full_path[-1] != to_node_id):
|
||||||
|
full_path = [*full_path, to_node_id]
|
||||||
|
winning_forward_with_endpoints.append(full_path)
|
||||||
|
|
||||||
|
winning_reverse_with_endpoints = []
|
||||||
|
for path in set(winning_reverse_paths):
|
||||||
|
full_path = list(path)
|
||||||
|
if to_node_id is not None and (not full_path or full_path[0] != to_node_id):
|
||||||
|
full_path = [to_node_id, *full_path]
|
||||||
|
if from_node_id is not None and (not full_path or full_path[-1] != from_node_id):
|
||||||
|
full_path = [*full_path, from_node_id]
|
||||||
|
winning_reverse_with_endpoints.append(full_path)
|
||||||
|
|
||||||
|
winning_paths_json = {
|
||||||
|
"forward": winning_forward_with_endpoints,
|
||||||
|
"reverse": winning_reverse_with_endpoints,
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Final API output
|
||||||
|
# --------------------------------------------
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"packet": {
|
||||||
|
"id": packet.id,
|
||||||
|
"from": packet.from_node_id,
|
||||||
|
"to": packet.to_node_id,
|
||||||
|
"channel": packet.channel,
|
||||||
|
},
|
||||||
|
"traceroute_packets": tr_groups,
|
||||||
|
"unique_forward_paths": unique_forward_paths_json,
|
||||||
|
"unique_reverse_paths": unique_reverse_paths_json,
|
||||||
|
"winning_paths": winning_paths_json,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/api/stats/top")
|
||||||
|
async def api_stats_top(request):
|
||||||
|
"""
|
||||||
|
Returns nodes sorted by SEEN (high → low) with pagination.
|
||||||
|
"""
|
||||||
|
|
||||||
|
period_type = request.query.get("period_type", "day")
|
||||||
|
length = int(request.query.get("length", 1))
|
||||||
|
channel = request.query.get("channel")
|
||||||
|
|
||||||
|
limit = min(int(request.query.get("limit", 20)), 100)
|
||||||
|
offset = int(request.query.get("offset", 0))
|
||||||
|
|
||||||
|
multiplier = 3600 if period_type == "hour" else 86400
|
||||||
|
window_us = length * multiplier * 1_000_000
|
||||||
|
|
||||||
|
max_packet_import = select(func.max(PacketModel.import_time_us)).scalar_subquery()
|
||||||
|
max_seen_import = select(func.max(PacketSeenModel.import_time_us)).scalar_subquery()
|
||||||
|
|
||||||
|
sent_cte = (
|
||||||
|
select(PacketModel.from_node_id.label("node_id"), func.count().label("sent"))
|
||||||
|
.where(PacketModel.import_time_us >= max_packet_import - window_us)
|
||||||
|
.group_by(PacketModel.from_node_id)
|
||||||
|
.cte("sent")
|
||||||
|
)
|
||||||
|
|
||||||
|
seen_cte = (
|
||||||
|
select(PacketModel.from_node_id.label("node_id"), func.count().label("seen"))
|
||||||
|
.select_from(PacketSeenModel)
|
||||||
|
.join(PacketModel, PacketModel.id == PacketSeenModel.packet_id)
|
||||||
|
.where(PacketSeenModel.import_time_us >= max_seen_import - window_us)
|
||||||
|
.group_by(PacketModel.from_node_id)
|
||||||
|
.cte("seen")
|
||||||
|
)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
Node.node_id,
|
||||||
|
Node.long_name,
|
||||||
|
Node.short_name,
|
||||||
|
Node.channel,
|
||||||
|
func.coalesce(sent_cte.c.sent, 0).label("sent"),
|
||||||
|
func.coalesce(seen_cte.c.seen, 0).label("seen"),
|
||||||
|
)
|
||||||
|
.select_from(Node)
|
||||||
|
.outerjoin(sent_cte, sent_cte.c.node_id == Node.node_id)
|
||||||
|
.outerjoin(seen_cte, seen_cte.c.node_id == Node.node_id)
|
||||||
|
.order_by(func.coalesce(seen_cte.c.seen, 0).desc())
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(Node)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
query = query.where(Node.channel == channel)
|
||||||
|
count_query = count_query.where(Node.channel == channel)
|
||||||
|
|
||||||
|
async with database.async_session() as session:
|
||||||
|
rows = (await session.execute(query)).all()
|
||||||
|
total = (await session.execute(count_query)).scalar() or 0
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for r in rows:
|
||||||
|
avg = r.seen / max(r.sent, 1)
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"node_id": r.node_id,
|
||||||
|
"long_name": r.long_name,
|
||||||
|
"short_name": r.short_name,
|
||||||
|
"channel": r.channel,
|
||||||
|
"sent": r.sent,
|
||||||
|
"seen": r.seen,
|
||||||
|
"avg": round(avg, 2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"total": total,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"nodes": nodes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/api/node/{node_id}/qr")
|
||||||
|
async def api_node_qr(request):
|
||||||
|
"""
|
||||||
|
Generate a Meshtastic URL for importing the node as a contact.
|
||||||
|
Returns the URL that can be used to generate a QR code.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
node_id_str = request.match_info["node_id"]
|
||||||
|
node_id = int(node_id_str, 0)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return web.json_response({"error": "Invalid node_id"}, status=400)
|
||||||
|
|
||||||
|
node = await store.get_node(node_id)
|
||||||
|
if not node:
|
||||||
|
return web.json_response({"error": "Node not found"}, status=404)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from meshtastic.protobuf.admin_pb2 import SharedContact
|
||||||
|
from meshtastic.protobuf.mesh_pb2 import User
|
||||||
|
|
||||||
|
user = User()
|
||||||
|
user.id = f"!{node_id:08x}"
|
||||||
|
if node.long_name:
|
||||||
|
user.long_name = node.long_name
|
||||||
|
if node.short_name:
|
||||||
|
user.short_name = node.short_name
|
||||||
|
if node.hw_model:
|
||||||
|
try:
|
||||||
|
from meshtastic.protobuf.mesh_pb2 import HardwareModel
|
||||||
|
|
||||||
|
hw_model_value = getattr(HardwareModel, node.hw_model.upper(), None)
|
||||||
|
if hw_model_value is not None:
|
||||||
|
user.hw_model = hw_model_value
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
contact = SharedContact()
|
||||||
|
contact.node_num = node_id
|
||||||
|
contact.user.CopyFrom(user)
|
||||||
|
contact.manually_verified = False
|
||||||
|
|
||||||
|
contact_bytes = contact.SerializeToString()
|
||||||
|
import base64
|
||||||
|
|
||||||
|
contact_b64 = base64.b64encode(contact_bytes).decode("ascii")
|
||||||
|
contact_b64url = contact_b64.replace("+", "-").replace("/", "_").rstrip("=")
|
||||||
|
|
||||||
|
meshtastic_url = f"https://meshtastic.org/v/#{contact_b64url}"
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"node_id": node_id,
|
||||||
|
"long_name": node.long_name,
|
||||||
|
"short_name": node.short_name,
|
||||||
|
"meshtastic_url": meshtastic_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(f"Error generating QR URL for node {node_id}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return web.json_response({"error": f"Failed to generate URL: {str(e)}"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/api/node/{node_id}/impersonation-check")
|
||||||
|
async def api_node_impersonation_check(request):
|
||||||
|
"""
|
||||||
|
Check if a node has multiple different public keys, which could indicate impersonation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
node_id_str = request.match_info["node_id"]
|
||||||
|
node_id = int(node_id_str, 0)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return web.json_response({"error": "Invalid node_id"}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with database.async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(NodePublicKey.public_key).where(NodePublicKey.node_id == node_id).distinct()
|
||||||
|
)
|
||||||
|
public_keys = result.scalars().all()
|
||||||
|
|
||||||
|
unique_key_count = len(public_keys)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"node_id": node_id,
|
||||||
|
"unique_public_key_count": unique_key_count,
|
||||||
|
"potential_impersonation": unique_key_count > 1,
|
||||||
|
"public_keys": public_keys
|
||||||
|
if unique_key_count <= 3
|
||||||
|
else public_keys[:3] + ["..."],
|
||||||
|
"warning": "Multiple different public keys detected. This node may be getting impersonated."
|
||||||
|
if unique_key_count > 1
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking impersonation for node {node_id}: {e}")
|
||||||
|
return web.json_response({"error": "Failed to check impersonation"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/api/coverage/{node_id}")
|
||||||
|
async def api_coverage(request):
|
||||||
|
try:
|
||||||
|
node_id = int(request.match_info["node_id"], 0)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return web.json_response({"error": "Invalid node_id"}, status=400)
|
||||||
|
|
||||||
|
if not ITM_AVAILABLE:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Coverage requires pyitm. Run: pip install -r requirements.txt"},
|
||||||
|
status=503,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_float(name, default):
|
||||||
|
value = request.query.get(name)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise web.HTTPBadRequest(
|
||||||
|
text=json.dumps({"error": f"{name} must be a number"}),
|
||||||
|
content_type="application/json",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
freq_mhz = parse_float("freq_mhz", 907.0)
|
||||||
|
tx_dbm = parse_float("tx_dbm", 20.0)
|
||||||
|
tx_height_m = parse_float("tx_height_m", 5.0)
|
||||||
|
rx_height_m = parse_float("rx_height_m", 1.5)
|
||||||
|
radius_km = parse_float("radius_km", 40.0)
|
||||||
|
step_km = parse_float("step_km", 0.25)
|
||||||
|
reliability = parse_float("reliability", DEFAULT_RELIABILITY)
|
||||||
|
threshold_dbm = parse_float("threshold_dbm", DEFAULT_THRESHOLD_DBM)
|
||||||
|
except web.HTTPBadRequest as exc:
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
node = await store.get_node(node_id)
|
||||||
|
if not node or not node.last_lat or not node.last_long:
|
||||||
|
return web.json_response({"error": "Node not found or missing location"}, status=404)
|
||||||
|
|
||||||
|
lat = node.last_lat * 1e-7
|
||||||
|
lon = node.last_long * 1e-7
|
||||||
|
|
||||||
|
mode = request.query.get("mode", "perimeter")
|
||||||
|
if mode == "perimeter":
|
||||||
|
perimeter = compute_perimeter(
|
||||||
|
lat=round(lat, 7),
|
||||||
|
lon=round(lon, 7),
|
||||||
|
freq_mhz=round(freq_mhz, 3),
|
||||||
|
tx_dbm=round(tx_dbm, 2),
|
||||||
|
tx_height_m=round(tx_height_m, 2),
|
||||||
|
rx_height_m=round(rx_height_m, 2),
|
||||||
|
radius_km=round(radius_km, 2),
|
||||||
|
step_km=round(step_km, 3),
|
||||||
|
reliability=round(reliability, 3),
|
||||||
|
threshold_dbm=round(threshold_dbm, 1),
|
||||||
|
)
|
||||||
|
return web.json_response(
|
||||||
|
{"mode": "perimeter", "threshold_dbm": threshold_dbm, "perimeter": perimeter}
|
||||||
|
)
|
||||||
|
|
||||||
|
points = compute_coverage(
|
||||||
|
lat=round(lat, 7),
|
||||||
|
lon=round(lon, 7),
|
||||||
|
freq_mhz=round(freq_mhz, 3),
|
||||||
|
tx_dbm=round(tx_dbm, 2),
|
||||||
|
tx_height_m=round(tx_height_m, 2),
|
||||||
|
rx_height_m=round(rx_height_m, 2),
|
||||||
|
radius_km=round(radius_km, 2),
|
||||||
|
step_km=round(step_km, 3),
|
||||||
|
reliability=round(reliability, 3),
|
||||||
|
)
|
||||||
|
|
||||||
|
min_dbm = DEFAULT_MIN_DBM
|
||||||
|
max_dbm = DEFAULT_MAX_DBM
|
||||||
|
if points:
|
||||||
|
vals = [p[2] for p in points]
|
||||||
|
min_dbm = min(min_dbm, min(vals))
|
||||||
|
max_dbm = max(max_dbm, max(vals))
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{"mode": "heatmap", "min_dbm": min_dbm, "max_dbm": max_dbm, "points": points}
|
||||||
|
)
|
||||||
|
|||||||
+2
-2
@@ -48,7 +48,7 @@ dev = [
|
|||||||
# Linting
|
# Linting
|
||||||
target-version = "py313"
|
target-version = "py313"
|
||||||
line-length = 100
|
line-length = 100
|
||||||
extend-exclude = ["build", "dist", ".venv"]
|
extend-exclude = ["build", "dist", ".venv", "meshtastic/protobuf", "nanopb_pb2.py"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "UP", "B"] # pick your rulesets
|
select = ["E", "F", "I", "UP", "B"] # pick your rulesets
|
||||||
@@ -56,4 +56,4 @@ ignore = ["E501"] # example; let formatter handle line len
|
|||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "preserve"
|
quote-style = "preserve"
|
||||||
indent-style = "space"
|
indent-style = "space"
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ MarkupSafe~=3.0.2
|
|||||||
|
|
||||||
# Graphs / diagrams
|
# Graphs / diagrams
|
||||||
pydot~=3.0.4
|
pydot~=3.0.4
|
||||||
|
pyitm~=0.3
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -47,4 +48,4 @@ objgraph~=3.6.2
|
|||||||
# Testing
|
# Testing
|
||||||
pytest~=8.3.4
|
pytest~=8.3.4
|
||||||
pytest-aiohttp~=1.0.5
|
pytest-aiohttp~=1.0.5
|
||||||
pytest-asyncio~=0.24.0
|
pytest-asyncio~=0.24.0
|
||||||
|
|||||||
+11
-1
@@ -76,12 +76,22 @@ port = 1883
|
|||||||
username = meshdev
|
username = meshdev
|
||||||
password = large4cats
|
password = large4cats
|
||||||
|
|
||||||
|
# Optional list of node IDs to ignore. Comma-separated.
|
||||||
|
skip_node_ids =
|
||||||
|
|
||||||
|
# Optional list of secondary AES keys (base64), comma-separated.
|
||||||
|
secondary_keys =
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
# -------------------------
|
# -------------------------
|
||||||
[database]
|
[database]
|
||||||
# SQLAlchemy connection string. This one uses SQLite with asyncio support.
|
# SQLAlchemy async connection string.
|
||||||
|
# Examples:
|
||||||
|
# sqlite+aiosqlite:///packets.db
|
||||||
|
# postgresql+asyncpg://user:pass@host:5432/meshview
|
||||||
connection_string = sqlite+aiosqlite:///packets.db
|
connection_string = sqlite+aiosqlite:///packets.db
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, cwd=None):
|
||||||
|
subprocess.run(cmd, cwd=cwd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Update Meshtastic protobufs")
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
default="https://github.com/meshtastic/protobufs.git",
|
||||||
|
help="Meshtastic protobufs repo URL",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ref",
|
||||||
|
default="master",
|
||||||
|
help="Git ref to fetch (branch, tag, or commit)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--check",
|
||||||
|
action="store_true",
|
||||||
|
help="Only check if protobufs are up to date for the given ref",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
out_root = repo_root
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="meshtastic-protobufs-") as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
print(f"Cloning {args.repo} ({args.ref}) into {tmp_path}...")
|
||||||
|
run(["git", "clone", "--depth", "1", "--branch", args.ref, args.repo, str(tmp_path)])
|
||||||
|
upstream_rev = (
|
||||||
|
subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=tmp_path).decode().strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
rev_file = out_root / "meshtastic" / "protobuf" / "UPSTREAM_REV.txt"
|
||||||
|
current_rev = None
|
||||||
|
if rev_file.exists():
|
||||||
|
current_rev = rev_file.read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
if current_rev == upstream_rev:
|
||||||
|
print(f"Up to date: {current_rev}")
|
||||||
|
return 0
|
||||||
|
print(f"Out of date. Local: {current_rev or 'unknown'} / Upstream: {upstream_rev}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
proto_root = None
|
||||||
|
# Common locations in the meshtastic/protobufs repo
|
||||||
|
candidates = [
|
||||||
|
tmp_path / "meshtastic" / "protobuf",
|
||||||
|
tmp_path / "protobufs",
|
||||||
|
tmp_path / "protobuf",
|
||||||
|
tmp_path / "proto",
|
||||||
|
]
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists() and list(candidate.glob("*.proto")):
|
||||||
|
proto_root = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if proto_root is None:
|
||||||
|
# Fallback: search for any directory containing .proto files
|
||||||
|
for candidate in tmp_path.rglob("*.proto"):
|
||||||
|
proto_root = candidate.parent
|
||||||
|
break
|
||||||
|
|
||||||
|
if proto_root is None:
|
||||||
|
print("Proto root not found in cloned repo.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
protos = sorted(proto_root.glob("*.proto"))
|
||||||
|
if not protos:
|
||||||
|
print(f"No .proto files found in {proto_root}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
rel_protos = [str(p.relative_to(tmp_path)) for p in protos]
|
||||||
|
|
||||||
|
protoc = shutil.which("protoc")
|
||||||
|
if protoc:
|
||||||
|
cmd = [
|
||||||
|
protoc,
|
||||||
|
f"-I{tmp_path}",
|
||||||
|
f"--python_out={out_root}",
|
||||||
|
*rel_protos,
|
||||||
|
]
|
||||||
|
print("Running protoc...")
|
||||||
|
run(cmd, cwd=tmp_path)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import grpc_tools.protoc # noqa: F401
|
||||||
|
except Exception:
|
||||||
|
print(
|
||||||
|
"protoc not found. Install it with your package manager, "
|
||||||
|
"or install grpcio-tools and re-run.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"grpc_tools.protoc",
|
||||||
|
f"-I{tmp_path}",
|
||||||
|
f"--python_out={out_root}",
|
||||||
|
*rel_protos,
|
||||||
|
]
|
||||||
|
print("Running grpc_tools.protoc...")
|
||||||
|
run(cmd, cwd=tmp_path)
|
||||||
|
|
||||||
|
rev_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
rev_file.write_text(upstream_rev + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
print("Protobufs updated in meshtastic/protobuf/.")
|
||||||
|
print("Review changes and commit them if desired.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+26
-16
@@ -7,9 +7,11 @@ import shutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
|
|
||||||
from meshview import migrations, models, mqtt_database, mqtt_reader, mqtt_store
|
from meshview import migrations, models, mqtt_database, mqtt_reader, mqtt_store
|
||||||
from meshview.config import CONFIG
|
from meshview.config import CONFIG
|
||||||
|
from meshview.deps import check_optional_deps
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Basic logging configuration
|
# Basic logging configuration
|
||||||
@@ -65,18 +67,16 @@ async def backup_database(database_url: str, backup_dir: str = ".") -> None:
|
|||||||
backup_dir: Directory to store backups (default: current directory)
|
backup_dir: Directory to store backups (default: current directory)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Extract database file path from connection string
|
url = make_url(database_url)
|
||||||
# Format: sqlite+aiosqlite:///path/to/db.db
|
if not url.drivername.startswith("sqlite"):
|
||||||
if not database_url.startswith("sqlite"):
|
|
||||||
cleanup_logger.warning("Backup only supported for SQLite databases")
|
cleanup_logger.warning("Backup only supported for SQLite databases")
|
||||||
return
|
return
|
||||||
|
|
||||||
db_path = database_url.split("///", 1)[1] if "///" in database_url else None
|
if not url.database or url.database == ":memory:":
|
||||||
if not db_path:
|
|
||||||
cleanup_logger.error("Could not extract database path from connection string")
|
cleanup_logger.error("Could not extract database path from connection string")
|
||||||
return
|
return
|
||||||
|
|
||||||
db_file = Path(db_path)
|
db_file = Path(url.database)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
cleanup_logger.error(f"Database file not found: {db_file}")
|
cleanup_logger.error(f"Database file not found: {db_file}")
|
||||||
return
|
return
|
||||||
@@ -153,11 +153,11 @@ async def daily_cleanup_at(
|
|||||||
cleanup_logger.info("Waiting 60 seconds for backup to complete...")
|
cleanup_logger.info("Waiting 60 seconds for backup to complete...")
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
# Local-time cutoff as string for SQLite DATETIME comparison
|
cutoff_dt = (
|
||||||
cutoff = (datetime.datetime.now() - datetime.timedelta(days=days_to_keep)).strftime(
|
datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days_to_keep)
|
||||||
"%Y-%m-%d %H:%M:%S"
|
).replace(tzinfo=None)
|
||||||
)
|
cutoff_us = int(cutoff_dt.timestamp() * 1_000_000)
|
||||||
cleanup_logger.info(f"Running cleanup for records older than {cutoff}...")
|
cleanup_logger.info(f"Running cleanup for records older than {cutoff_dt.isoformat()}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with db_lock: # Pause ingestion
|
async with db_lock: # Pause ingestion
|
||||||
@@ -168,7 +168,7 @@ async def daily_cleanup_at(
|
|||||||
# Packet
|
# Packet
|
||||||
# -------------------------
|
# -------------------------
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
delete(models.Packet).where(models.Packet.import_time < cutoff)
|
delete(models.Packet).where(models.Packet.import_time_us < cutoff_us)
|
||||||
)
|
)
|
||||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from Packet")
|
cleanup_logger.info(f"Deleted {result.rowcount} rows from Packet")
|
||||||
|
|
||||||
@@ -176,7 +176,9 @@ async def daily_cleanup_at(
|
|||||||
# PacketSeen
|
# PacketSeen
|
||||||
# -------------------------
|
# -------------------------
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
delete(models.PacketSeen).where(models.PacketSeen.import_time < cutoff)
|
delete(models.PacketSeen).where(
|
||||||
|
models.PacketSeen.import_time_us < cutoff_us
|
||||||
|
)
|
||||||
)
|
)
|
||||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from PacketSeen")
|
cleanup_logger.info(f"Deleted {result.rowcount} rows from PacketSeen")
|
||||||
|
|
||||||
@@ -184,7 +186,9 @@ async def daily_cleanup_at(
|
|||||||
# Traceroute
|
# Traceroute
|
||||||
# -------------------------
|
# -------------------------
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
delete(models.Traceroute).where(models.Traceroute.import_time < cutoff)
|
delete(models.Traceroute).where(
|
||||||
|
models.Traceroute.import_time_us < cutoff_us
|
||||||
|
)
|
||||||
)
|
)
|
||||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from Traceroute")
|
cleanup_logger.info(f"Deleted {result.rowcount} rows from Traceroute")
|
||||||
|
|
||||||
@@ -192,17 +196,19 @@ async def daily_cleanup_at(
|
|||||||
# Node
|
# Node
|
||||||
# -------------------------
|
# -------------------------
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
delete(models.Node).where(models.Node.last_update < cutoff)
|
delete(models.Node).where(models.Node.last_seen_us < cutoff_us)
|
||||||
)
|
)
|
||||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from Node")
|
cleanup_logger.info(f"Deleted {result.rowcount} rows from Node")
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
if vacuum_db:
|
if vacuum_db and mqtt_database.engine.dialect.name == "sqlite":
|
||||||
cleanup_logger.info("Running VACUUM...")
|
cleanup_logger.info("Running VACUUM...")
|
||||||
async with mqtt_database.engine.begin() as conn:
|
async with mqtt_database.engine.begin() as conn:
|
||||||
await conn.exec_driver_sql("VACUUM;")
|
await conn.exec_driver_sql("VACUUM;")
|
||||||
cleanup_logger.info("VACUUM completed.")
|
cleanup_logger.info("VACUUM completed.")
|
||||||
|
elif vacuum_db:
|
||||||
|
cleanup_logger.info("VACUUM skipped (not supported for this database).")
|
||||||
|
|
||||||
cleanup_logger.info("Cleanup completed successfully.")
|
cleanup_logger.info("Cleanup completed successfully.")
|
||||||
cleanup_logger.info("Ingestion resumed after cleanup.")
|
cleanup_logger.info("Ingestion resumed after cleanup.")
|
||||||
@@ -232,6 +238,7 @@ async def load_database_from_mqtt(
|
|||||||
# Main function
|
# Main function
|
||||||
# -------------------------
|
# -------------------------
|
||||||
async def main():
|
async def main():
|
||||||
|
check_optional_deps()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Initialize database
|
# Initialize database
|
||||||
@@ -260,6 +267,9 @@ async def main():
|
|||||||
await mqtt_database.create_tables()
|
await mqtt_database.create_tables()
|
||||||
logger.info("Database tables created")
|
logger.info("Database tables created")
|
||||||
|
|
||||||
|
# Load MQTT gateway cache after DB init/migrations
|
||||||
|
await mqtt_store.load_gateway_cache()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Clear migration in progress flag
|
# Clear migration in progress flag
|
||||||
logger.info("Clearing migration status...")
|
logger.info("Clearing migration status...")
|
||||||
|
|||||||
Reference in New Issue
Block a user