From 551852e38740372b0e207d89f3177d52356aacd0 Mon Sep 17 00:00:00 2001 From: Jason Michalski Date: Sun, 9 Jun 2024 21:39:36 -0700 Subject: [PATCH] Graph traceroutes. --- meshview/models.py | 10 ++++ meshview/store.py | 23 ++++++++- meshview/templates/packet.html | 3 ++ meshview/web.py | 85 ++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/meshview/models.py b/meshview/models.py index ab62e2e..4c2215b 100644 --- a/meshview/models.py +++ b/meshview/models.py @@ -51,3 +51,13 @@ class PacketSeen(Base): rx_rssi: Mapped[int] = mapped_column(nullable=True) topic: Mapped[str] import_time: Mapped[datetime] + + +class Traceroute(Base): + __tablename__ = "traceroute" + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + packet_id = mapped_column(ForeignKey("packet.id")) + gateway_node_id: Mapped[int] = mapped_column(BigInteger) + done: Mapped[bool] + route: Mapped[bytes] + diff --git a/meshview/store.py b/meshview/store.py index fb67463..c1d51d0 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -6,7 +6,7 @@ from meshtastic.portnums_pb2 import PortNum from meshtastic.mesh_pb2 import User, HardwareModel from meshview import database from meshview import decode_payload -from meshview.models import Packet, PacketSeen, Node +from meshview.models import Packet, PacketSeen, Node, Traceroute from meshview import notify @@ -99,6 +99,18 @@ async def process_envelope(topic, env): node.last_long = position.longitude_i session.add(node) + if env.packet.decoded.portnum == PortNum.TRACEROUTE_APP: + if env.packet.decoded.want_response: + packet_id = env.packet.id + else: + packet_id = env.packet.decoded.request_id + session.add(Traceroute( + packet_id=packet_id, + route=env.packet.decoded.payload, + done=not env.packet.decoded.want_response, + gateway_node_id=int(env.gateway_id[1:], 16), + )) + await session.commit() if new_packet: await packet.awaitable_attrs.to_node @@ -188,3 +200,12 @@ async def has_packets(node_id, portnum): select(Packet.id).where(Packet.from_node_id == node_id).limit(1) )).scalar() ) + + +async def get_traceroute(packet_id): + async with database.async_session() as session: + result = await session.execute( + select(Traceroute) + .where(Traceroute.packet_id == packet_id) + ) + return result.scalars() diff --git a/meshview/templates/packet.html b/meshview/templates/packet.html index 57bdfea..3ef01f5 100644 --- a/meshview/templates/packet.html +++ b/meshview/templates/packet.html @@ -53,6 +53,9 @@ {% endfor %} + {% if packet.raw_mesh_packet.decoded.want_response %} + graph + {% endif %} {% endif %}
{{packet.payload}}
diff --git a/meshview/web.py b/meshview/web.py index c842aad..3aa1058 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -7,6 +7,7 @@ from aiohttp_sse import sse_response import ssl import re +import pydot from pandas import DataFrame import seaborn as sns import matplotlib.pyplot as plt @@ -528,6 +529,90 @@ async def graph_neighbors(request): ) +@routes.get("/graph/traceroute/{packet_id}") +async def graph_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) + + node_ids = set() + for tr in traceroutes: + route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route) + node_ids.add(tr.gateway_node_id) + for node_id in route.route: + node_ids.add(node_id) + node_ids.add(packet.from_node_id) + node_ids.add(packet.to_node_id) + + nodes = {} + async with asyncio.TaskGroup() as tg: + for node_id in node_ids: + nodes[node_id] = tg.create_task(store.get_node(node_id)) + + graph = pydot.Dot('traceroute', graph_type="digraph") + + paths = set() + node_color = {} + mqtt_nodes = set() + saw_reply = set() + dest = None + for tr in traceroutes: + if tr.done: + saw_reply.add(tr.gateway_node_id) + if tr.done and dest: + continue + route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route) + path = [packet.from_node_id] + path.extend(route.route) + if tr.done: + dest = packet.to_node_id + path.append(packet.to_node_id) + elif path[-1] != tr.gateway_node_id: + # It seems some nodes add them self to the list before uplinking + path.append(tr.gateway_node_id) + mqtt_nodes.add(tr.gateway_node_id) + node_color[path[-1]] = '#' + hex(hash(tuple(path)))[3:9] + paths.add(tuple(path)) + + used_nodes = set() + for path in paths: + used_nodes.update(path) + + for node_id in used_nodes: + node = await nodes[node_id] + if not node: + node_name = node_id_to_hex(node_id) + else: + node_name = f'[{node.short_name}] {node.long_name} - {node_id_to_hex(node_id)}' + style = 'dashed' + if node_id == dest: + style = 'filled' + elif node_id in mqtt_nodes: + style = 'solid' + + if node_id in saw_reply: + style += ', diagonals' + + graph.add_node(pydot.Node( + str(node_id), + label=node_name, + shape='box', + color=node_color.get(node_id, 'black'), + style=style, + )) + + for path in paths: + color = '#' + hex(hash(tuple(path)))[3:9] + for src, dest in zip(path, path[1:]): + graph.add_edge(pydot.Edge(src, dest, color=color)) + + return web.Response( + body=graph.create_svg(), + content_type="image/svg+xml", + ) + + async def run_server(bind, port, tls_cert): app = web.Application() app.add_routes(routes) diff --git a/requirements.txt b/requirements.txt index 6bf4d90..f6f308a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ Jinja2 aiohttp-sse asyncpg seaborn +pydot