| Zeit | +Von | +An | +Typ | +Ch | +SNR | +RSSI | +Hops | +Info | +
|---|
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac86ee..bfe021c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.6.10] - 2026-02-18 + +### Added +- **Paket-Log** (`/packets`): neue öffentliche Seite zeigt alle empfangenen Meshtastic-Pakete + in Echtzeit via WebSocket – mit Tabelle (Zeit, Von, An, Typ, Kanal, SNR, RSSI, Hops, Info), + Typ-Filterleiste, Pause- und Löschen-Funktion, max. 300 Einträge im Browser. +- **DB**: `packets`-Tabelle + `insert_packet()` / `get_recent_packets()` in `database.py`. +- **bot.py**: `_handle_packet()` loggt alle empfangenen Pakete mit Payload-Zusammenfassung + (Text, Position, Telemetrie, NodeInfo) und broadcasted sie über WebSocket an alle Clients. +- **webserver.py**: Route `/packets` + API `/api/packets` + `initial_packets` im WS-Initial-Payload. +- **Sidebar**: Eintrag „Pakete" (öffentlich) zwischen Karte und Einstellungen. + +### Changed +- **Node-Modal**: von `modal-lg` auf `modal-xl` + `modal-dialog-scrollable` vergrößert, + Karten-Höhe von 250 px auf 300 px erhöht. + ## [0.6.9] - 2026-02-18 ### Fixed diff --git a/config.yaml b/config.yaml index 1e8d116..03afb09 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.6.9" +version: "0.6.10" bot: name: "MeshDD-Bot" diff --git a/meshbot/bot.py b/meshbot/bot.py index f44cd05..ad303d8 100644 --- a/meshbot/bot.py +++ b/meshbot/bot.py @@ -241,8 +241,36 @@ class MeshBot: try: from_id = packet.get("fromId", str(packet.get("from", ""))) to_id = packet.get("toId", str(packet.get("to", ""))) - portnum = packet.get("decoded", {}).get("portnum", "") + decoded = packet.get("decoded", {}) + portnum = decoded.get("portnum", "") channel = packet.get("channel", 0) + snr = packet.get("snr") or packet.get("rxSnr") + rssi = packet.get("rssi") or packet.get("rxRssi") + hop_limit = packet.get("hopLimit") + hop_start = packet.get("hopStart") + packet_id = packet.get("id") + + # Build payload summary + payload_summary: dict = {} + if portnum == "TEXT_MESSAGE_APP": + payload_summary = {"text": decoded.get("text", "")} + elif portnum == "POSITION_APP": + pos = decoded.get("position", {}) + payload_summary = {"lat": pos.get("latitude"), "lon": pos.get("longitude")} + elif portnum == "TELEMETRY_APP": + dm = decoded.get("telemetry", {}).get("deviceMetrics", {}) + payload_summary = {"battery": dm.get("batteryLevel"), "voltage": dm.get("voltage")} + elif portnum == "NODEINFO_APP": + u = decoded.get("user", {}) + payload_summary = {"long_name": u.get("longName"), "short_name": u.get("shortName")} + + pkt_record = await self.db.insert_packet( + str(from_id), str(to_id), portnum, channel, + snr, rssi, hop_limit, hop_start, packet_id, + json.dumps(payload_summary), + ) + if self.ws_manager: + await self.ws_manager.broadcast("packet", pkt_record) # Update node info from packet node_data = {"snr": packet.get("snr"), "rssi": packet.get("rssi"), @@ -252,7 +280,7 @@ class MeshBot: # Handle nodeinfo if portnum == "NODEINFO_APP": - user = packet.get("decoded", {}).get("user", {}) + user = decoded.get("user", {}) if user and from_id: await self.db.upsert_node(str(from_id), long_name=user.get("longName"), @@ -264,7 +292,7 @@ class MeshBot: # Handle position updates if portnum == "POSITION_APP": - pos = packet.get("decoded", {}).get("position", {}) + pos = decoded.get("position", {}) if pos and from_id: await self.db.upsert_node(str(from_id), lat=pos.get("latitude"), @@ -276,7 +304,7 @@ class MeshBot: # Handle telemetry if portnum == "TELEMETRY_APP": - telemetry = packet.get("decoded", {}).get("telemetry", {}) + telemetry = decoded.get("telemetry", {}) metrics = telemetry.get("deviceMetrics", {}) if metrics and from_id: await self.db.upsert_node(str(from_id), @@ -288,7 +316,7 @@ class MeshBot: # Handle text messages if portnum == "TEXT_MESSAGE_APP": - text = packet.get("decoded", {}).get("text", "") + text = decoded.get("text", "") my_id = self.get_my_node_id() is_own = my_id and str(from_id) == my_id diff --git a/meshbot/database.py b/meshbot/database.py index c710dbd..61a9bf0 100644 --- a/meshbot/database.py +++ b/meshbot/database.py @@ -83,6 +83,21 @@ class Database: FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS packets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL, + from_id TEXT, + to_id TEXT, + portnum TEXT, + channel INTEGER, + snr REAL, + rssi INTEGER, + hop_limit INTEGER, + hop_start INTEGER, + packet_id INTEGER, + payload TEXT + ); + CREATE TABLE IF NOT EXISTS email_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipient TEXT NOT NULL, @@ -288,3 +303,24 @@ class Database: (recipient, subject, status, error_message, now), ) await self.db.commit() + + # ── Packet log methods ──────────────────────────── + + async def insert_packet(self, from_id: str, to_id: str, portnum: str, channel: int, + snr, rssi, hop_limit, hop_start, packet_id, payload: str) -> dict: + now = time.time() + cursor = await self.db.execute( + "INSERT INTO packets (timestamp, from_id, to_id, portnum, channel, snr, rssi, " + "hop_limit, hop_start, packet_id, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (now, from_id, to_id, portnum, channel, snr, rssi, hop_limit, hop_start, packet_id, payload), + ) + await self.db.commit() + async with self.db.execute("SELECT * FROM packets WHERE id = ?", (cursor.lastrowid,)) as c: + row = await c.fetchone() + return dict(row) if row else {} + + async def get_recent_packets(self, limit: int = 200) -> list[dict]: + async with self.db.execute( + "SELECT * FROM packets ORDER BY timestamp DESC LIMIT ?", (limit,) + ) as cursor: + return [dict(row) async for row in cursor] diff --git a/meshbot/webserver.py b/meshbot/webserver.py index d26a39a..079dc24 100644 --- a/meshbot/webserver.py +++ b/meshbot/webserver.py @@ -59,6 +59,7 @@ class WebServer: self.app.router.add_get("/ws", self._ws_handler) self.app.router.add_get("/api/nodes", self._api_nodes) self.app.router.add_get("/api/messages", self._api_messages) + self.app.router.add_get("/api/packets", self._api_packets) self.app.router.add_get("/api/stats", self._api_stats) self.app.router.add_get("/api/scheduler/jobs", self._api_scheduler_get) self.app.router.add_post("/api/scheduler/jobs", self._api_scheduler_add) @@ -72,6 +73,7 @@ class WebServer: self.app.router.add_get("/settings", self._serve_settings) self.app.router.add_get("/scheduler", self._serve_scheduler) self.app.router.add_get("/map", self._serve_map) + self.app.router.add_get("/packets", self._serve_packets) self.app.router.add_get("/", self._serve_index) self.app.router.add_static("/static", STATIC_DIR) @@ -107,6 +109,9 @@ class WebServer: "uptime": self.bot.get_uptime(), }})) + packets = await self.db.get_recent_packets(200) + await ws.send_str(json.dumps({"type": "initial_packets", "data": packets})) + if user: messages = await self.db.get_recent_messages(50) await ws.send_str(json.dumps({"type": "initial_messages", "data": messages})) @@ -129,6 +134,11 @@ class WebServer: messages = await self.db.get_recent_messages(limit) return web.json_response(messages) + async def _api_packets(self, request: web.Request) -> web.Response: + limit = int(request.query.get("limit", "200")) + packets = await self.db.get_recent_packets(limit) + return web.json_response(packets) + async def _api_stats(self, request: web.Request) -> web.Response: stats = await self.db.get_stats() stats["version"] = config.get("version", "0.0.0") @@ -175,6 +185,9 @@ class WebServer: async def _serve_map(self, request: web.Request) -> web.Response: return web.FileResponse(os.path.join(STATIC_DIR, "map.html")) + async def _serve_packets(self, request: web.Request) -> web.Response: + return web.FileResponse(os.path.join(STATIC_DIR, "packets.html")) + async def _serve_scheduler(self, request: web.Request) -> web.Response: return web.FileResponse(os.path.join(STATIC_DIR, "scheduler.html")) diff --git a/static/index.html b/static/index.html index 30698cd..885f3d8 100644 --- a/static/index.html +++ b/static/index.html @@ -191,7 +191,7 @@