mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-06-29 14:31:48 +02:00
Merge branch 'pablorevilla-meshtastic:master' into master
This commit is contained in:
+3
-1
@@ -9,7 +9,7 @@ from sqlalchemy import ForeignKey, BigInteger
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# Node
|
||||
class Node(Base):
|
||||
__tablename__ = "node"
|
||||
id: Mapped[str] = mapped_column(primary_key=True)
|
||||
@@ -20,6 +20,7 @@ class Node(Base):
|
||||
role: Mapped[str] = mapped_column(nullable=True)
|
||||
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
channel: Mapped[str]
|
||||
|
||||
|
||||
class Packet(Base):
|
||||
@@ -36,6 +37,7 @@ class Packet(Base):
|
||||
)
|
||||
payload: Mapped[bytes]
|
||||
import_time: Mapped[datetime]
|
||||
channel: Mapped[str]
|
||||
|
||||
|
||||
class PacketSeen(Base):
|
||||
|
||||
+231
-26
@@ -12,30 +12,6 @@ from meshview.models import Packet, PacketSeen, Node, Traceroute
|
||||
from meshview import notify
|
||||
|
||||
|
||||
|
||||
# We count the total amount of packages
|
||||
# This is to be used by /stats in web.py
|
||||
async def get_total_packet_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(Packet.id)) # Use SQLAlchemy's func to count packets
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of packets
|
||||
|
||||
# We count the total amount of nodes
|
||||
async def get_total_node_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(Node.id)) # Use SQLAlchemy's func to count nodes
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of nodes
|
||||
|
||||
# We count the total amount of seen packets
|
||||
async def get_total_packet_seen_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(PacketSeen.node_id)) # Use SQLAlchemy's func to count nodes
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of seen packets
|
||||
|
||||
|
||||
async def process_envelope(topic, env):
|
||||
if not env.packet.id:
|
||||
return
|
||||
@@ -54,6 +30,7 @@ async def process_envelope(topic, env):
|
||||
payload=env.packet.SerializeToString(),
|
||||
# p.r. Here seems to be where the packet is imported on the Database and import time is set.
|
||||
import_time=datetime.datetime.now(),
|
||||
channel=env.channel_id,
|
||||
)
|
||||
session.add(packet)
|
||||
|
||||
@@ -111,7 +88,7 @@ async def process_envelope(topic, env):
|
||||
node.short_name = user.short_name
|
||||
node.hw_model = hw_model
|
||||
node.role = role
|
||||
# if need to update time of last update it may be here
|
||||
# if need to update time of last update it may be here
|
||||
|
||||
else:
|
||||
node = Node(
|
||||
@@ -121,7 +98,8 @@ async def process_envelope(topic, env):
|
||||
short_name=user.short_name,
|
||||
hw_model=hw_model,
|
||||
role=role,
|
||||
# if need to update time of last update it may be here
|
||||
channel=env.channel_id,
|
||||
# if need to update time of last update it may be here
|
||||
)
|
||||
session.add(node)
|
||||
|
||||
@@ -293,6 +271,233 @@ async def get_mqtt_neighbors(since):
|
||||
)
|
||||
return result
|
||||
|
||||
# In order to provide separate network graphs for LongFast and MediumSlow, I am duplicating the procedures.
|
||||
# 3 procedures are needed. These would have to be replicated for any other network that we may need to use graphs.
|
||||
#
|
||||
# get_traceroutes_longfast
|
||||
# get_packets_longfast
|
||||
# get_mqtt_neighbors_longfast
|
||||
#
|
||||
# p.r.
|
||||
#
|
||||
# Get Traceroute for LongFast only
|
||||
async def get_traceroutes_longfast(since):
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Traceroute)
|
||||
.join(Packet)
|
||||
.where(
|
||||
(Traceroute.import_time > (datetime.datetime.now() - since))
|
||||
& (Packet.channel == "LongFast")
|
||||
)
|
||||
.order_by(Traceroute.import_time)
|
||||
)
|
||||
return result.scalars()
|
||||
|
||||
# Get MQTT Neighbors for LongFast only
|
||||
# p.r.
|
||||
async def get_mqtt_neighbors_longfast(since):
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(select(PacketSeen, Packet)
|
||||
.join(Packet)
|
||||
.where(
|
||||
(PacketSeen.hop_limit == PacketSeen.hop_start)
|
||||
& (PacketSeen.hop_start != 0)
|
||||
& (Packet.channel == "LongFast")
|
||||
)
|
||||
|
||||
.options(
|
||||
lazyload(Packet.from_node),
|
||||
lazyload(Packet.to_node),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Get Packets for LongFast only
|
||||
# p.r.
|
||||
async def get_packets_longfast(node_id=None, portnum=None, since=None, limit=500, before=None, after=None):
|
||||
async with database.async_session() as session:
|
||||
q = select(Packet)
|
||||
|
||||
# Add condition for channel being "LongFast"
|
||||
q = q.where(Packet.channel == "LongFast")
|
||||
|
||||
if node_id:
|
||||
q = q.where(
|
||||
(Packet.from_node_id == node_id) | (Packet.to_node_id == node_id)
|
||||
)
|
||||
if portnum:
|
||||
q = q.where(Packet.portnum == portnum)
|
||||
if since:
|
||||
q = q.where(Packet.import_time > (datetime.datetime.now() - since))
|
||||
if before:
|
||||
q = q.where(Packet.import_time < before)
|
||||
if after:
|
||||
q = q.where(Packet.import_time > after)
|
||||
if limit is not None:
|
||||
q = q.limit(limit)
|
||||
|
||||
result = await session.execute(q.order_by(Packet.import_time.desc()))
|
||||
return result.scalars()
|
||||
|
||||
# Get Traceroute for mediumslow only
|
||||
# p.r.
|
||||
async def get_traceroutes_mediumslow(since):
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Traceroute)
|
||||
.join(Packet)
|
||||
.where(
|
||||
(Traceroute.import_time > (datetime.datetime.now() - since))
|
||||
& (Packet.channel == "MediumSlow")
|
||||
)
|
||||
.order_by(Traceroute.import_time)
|
||||
)
|
||||
return result.scalars()
|
||||
|
||||
# Get MQTT Neighbors for mediumslow only
|
||||
# p.r.
|
||||
async def get_mqtt_neighbors_mediumslow(since):
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(select(PacketSeen, Packet)
|
||||
.join(Packet)
|
||||
.where(
|
||||
(PacketSeen.hop_limit == PacketSeen.hop_start)
|
||||
& (PacketSeen.hop_start != 0)
|
||||
& (Packet.channel == "MediumSlow")
|
||||
)
|
||||
|
||||
.options(
|
||||
lazyload(Packet.from_node),
|
||||
lazyload(Packet.to_node),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Get Packets for MediumSlow only
|
||||
# p.r.
|
||||
async def get_packets_mediumslow(node_id=None, portnum=None, since=None, limit=500, before=None, after=None):
|
||||
async with database.async_session() as session:
|
||||
q = select(Packet)
|
||||
|
||||
# Add condition for channel being "MediumSlow"
|
||||
q = q.where(Packet.channel == "MediumSlow")
|
||||
|
||||
if node_id:
|
||||
q = q.where(
|
||||
(Packet.from_node_id == node_id) | (Packet.to_node_id == node_id)
|
||||
)
|
||||
if portnum:
|
||||
q = q.where(Packet.portnum == portnum)
|
||||
if since:
|
||||
q = q.where(Packet.import_time > (datetime.datetime.now() - since))
|
||||
if before:
|
||||
q = q.where(Packet.import_time < before)
|
||||
if after:
|
||||
q = q.where(Packet.import_time > after)
|
||||
if limit is not None:
|
||||
q = q.limit(limit)
|
||||
|
||||
result = await session.execute(q.order_by(Packet.import_time.desc()))
|
||||
return result.scalars()
|
||||
|
||||
|
||||
|
||||
# We count the total amount of packages
|
||||
# This is to be used by /stats in web.py
|
||||
async def get_total_packet_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(Packet.id)) # Use SQLAlchemy's func to count packets
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of packets
|
||||
|
||||
# We count the total amount of nodes
|
||||
async def get_total_node_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(Node.id)) # Use SQLAlchemy's func to count nodes
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of nodes
|
||||
|
||||
# We count the total amount of seen packets
|
||||
async def get_total_packet_seen_count():
|
||||
async with database.async_session() as session:
|
||||
q = select(func.count(PacketSeen.node_id)) # Use SQLAlchemy's func to count nodes
|
||||
result = await session.execute(q)
|
||||
return result.scalar() # Return the total count of seen packets
|
||||
|
||||
|
||||
async def get_total_node_count_longfast() -> int:
|
||||
"""
|
||||
Retrieves the total count of nodes where the channel is equal to 'LongFast'.
|
||||
|
||||
This function queries the database asynchronously to count the number of nodes
|
||||
in the `Node` table that meet the condition `channel == 'LongFast'`. It uses
|
||||
SQLAlchemy's asynchronous session management and query construction.
|
||||
|
||||
Returns:
|
||||
int: The total count of nodes with `channel == 'LongFast'`.
|
||||
|
||||
Raises:
|
||||
Exception: If an error occurs during the database query execution.
|
||||
"""
|
||||
try:
|
||||
# Open an asynchronous session with the database
|
||||
async with database.async_session() as session:
|
||||
# Build the query to count nodes where channel == 'LongFast'
|
||||
q = select(func.count(Node.id)).filter(Node.channel == 'LongFast')
|
||||
|
||||
# Execute the query asynchronously and fetch the result
|
||||
result = await session.execute(q)
|
||||
|
||||
# Return the scalar value (the count of nodes)
|
||||
return result.scalar()
|
||||
except Exception as e:
|
||||
# Log or handle the exception if needed (optional, replace with logging if necessary)
|
||||
print(f"An error occurred: {e}")
|
||||
return 0 # Return 0 or an appropriate fallback value in case of an error
|
||||
|
||||
|
||||
async def get_total_node_count_mediumslow() -> int:
|
||||
"""
|
||||
Retrieves the total count of nodes where the channel is equal to 'MediumSlow'.
|
||||
|
||||
This function queries the database asynchronously to count the number of nodes
|
||||
in the `Node` table that meet the condition `channel == 'MediumSlow'`. It uses
|
||||
SQLAlchemy's asynchronous session management and query construction.
|
||||
|
||||
Returns:
|
||||
int: The total count of nodes with `channel == 'MediumSlow'`.
|
||||
|
||||
Raises:
|
||||
Exception: If an error occurs during the database query execution.
|
||||
"""
|
||||
try:
|
||||
# Open an asynchronous session with the database
|
||||
async with database.async_session() as session:
|
||||
# Build the query to count nodes where channel == 'LongFast'
|
||||
q = select(func.count(Node.id)).filter(Node.channel == 'MediumSlow')
|
||||
|
||||
# Execute the query asynchronously and fetch the result
|
||||
result = await session.execute(q)
|
||||
|
||||
# Return the scalar value (the count of nodes)
|
||||
return result.scalar()
|
||||
except Exception as e:
|
||||
# Log or handle the exception if needed (optional, replace with logging if necessary)
|
||||
print(f"An error occurred: {e}")
|
||||
return 0 # Return 0 or an appropriate fallback value in case of an error
|
||||
|
||||
|
||||
# Get Nodes for mediumslow only
|
||||
# p.r.
|
||||
async def get_nodes_mediumslow():
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Node)
|
||||
.where(
|
||||
(Node.channel == "MediumSlow")
|
||||
)
|
||||
)
|
||||
return result.scalars()
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<title>MeshView - Bay Area Mesh - http://bayme.sh {% if node and node.short_name %}-- {{node.short_name}}{% endif %}</title>
|
||||
<title>MeshView - Bay Area Mesh - http://meshview.bayme.sh {% if node and node.short_name %}-- {{node.short_name}}{% endif %}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script>
|
||||
@@ -34,7 +34,7 @@
|
||||
</head>
|
||||
<body hx-indicator="#spinner">
|
||||
<br><div style="text-align:center"><strong>Bay Area Mesh - http://bayme.sh</strong></div>
|
||||
<div style="text-align:center">Quick Links: <a href="/">Search for a node </a> - <a href="/chat">Conversations</a> - <a href="/firehose">See <strong>everything</strong> </a> - <a href="/graph/network">Mesh Graph </a> - <a href="/stats">Stats </a></div><br>
|
||||
<div style="text-align:center">Quick Links: <a href="/">Search for a node </a> - <a href="/chat">Conversations</a> - <a href="/firehose">See <strong>everything</strong> </a> - Mesh Graph <a href="/graph/longfast">LG</a> - <a href="/graph/mediumslow">MS</a> - <a href="/stats">Stats </a></div><br>
|
||||
<div id="spinner" class="spinner-border secondary-primary htmx-indicator position-absolute top-50 start-50" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,52 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div style="font-family: Arial, sans-serif; color: #333; background-color: #f9f9f9; padding: 15px; border-radius: 8px; max-width: 300px; margin: auto;">
|
||||
<p style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">Total Nodes: <span style="font-weight: normal; color: #007bff;">{{ total_nodes }}</span></p>
|
||||
<p style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">Total Packets: <span style="font-weight: normal; color: #007bff;">{{ total_packets }}</span></p>
|
||||
<p style="font-size: 18px; font-weight: bold;">Total MQTT Reports: <span style="font-weight: normal; color: #007bff;">{{ total_packets_seen }}</span></p>
|
||||
|
||||
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; color: #fff; background: linear-gradient(135deg, #6a11cb, #2575fc); padding: 25px; border-radius: 15px; max-width: 350px; margin: auto; text-align: center; box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);">
|
||||
<h2 style="font-size: 22px; margin-bottom: 20px; font-weight: 600;">
|
||||
Mesh Statistics
|
||||
</h2>
|
||||
<!-- Section for Total Nodes -->
|
||||
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 15px;">
|
||||
<p style="font-size: 16px; margin: 0; font-weight: 500;">
|
||||
Total Nodes:
|
||||
<span style="font-weight: 700; color: #ffeb3b;">{{ total_nodes }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Section for Total Nodes LongFast -->
|
||||
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<p style="font-size: 14px; margin: 0; font-weight: 500;">
|
||||
Total Nodes LongFast:
|
||||
<span style="font-weight: 700; color: #03dac6;">{{ total_nodes_longfast }}</span>
|
||||
<span style="font-size: 12px; color: #ffeb3b; font-weight: 400;">
|
||||
({{ (total_nodes_longfast / total_nodes * 100) | round(2) }}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Section for Total Nodes MediumSlow -->
|
||||
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<p style="font-size: 14px; margin: 0; font-weight: 500;">
|
||||
Total Nodes MediumSlow:
|
||||
<span style="font-weight: 700; color: #03dac6;">{{ total_nodes_mediumslow }}</span>
|
||||
<span style="font-size: 12px; color: #ffeb3b; font-weight: 400;">
|
||||
({{ (total_nodes_mediumslow / total_nodes * 100) | round(2) }}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Section for Total Packets -->
|
||||
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
|
||||
<p style="font-size: 14px; margin: 0; font-weight: 500;">
|
||||
Total Packets:
|
||||
<span style="font-weight: 700; color: #03dac6;">{{ total_packets }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Section for Total MQTT Reports -->
|
||||
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px;">
|
||||
<p style="font-size: 14px; margin: 0; font-weight: 500;">
|
||||
Total MQTT Reports:
|
||||
<span style="font-weight: 700; color: #03dac6;">{{ total_packets_seen }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
+357
-3
@@ -1086,18 +1086,372 @@ async def stats(request):
|
||||
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()
|
||||
total_nodes_longfast = await store.get_total_node_count_longfast()
|
||||
total_nodes_mediumslow = await store.get_total_node_count_mediumslow()
|
||||
|
||||
|
||||
# Render the stats template with the total packet 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 ),
|
||||
text=template.render(total_packets=total_packets, total_nodes=total_nodes,total_packets_seen=total_packets_seen,total_nodes_longfast=total_nodes_longfast, total_nodes_mediumslow=total_nodes_mediumslow ),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
|
||||
|
||||
# In the stats.html template, you would include something like:
|
||||
# <p>Total Packets: {{ total_packets }}</p>
|
||||
@routes.get("/graph/longfast")
|
||||
async def graph_network_longfast(request):
|
||||
try:
|
||||
root = request.query.get("root")
|
||||
depth = int(request.query.get("depth", 5))
|
||||
hours = int(request.query.get("hours", 24))
|
||||
minutes = int(request.query.get("minutes", 0))
|
||||
|
||||
# Validation
|
||||
if root and not root.isdigit():
|
||||
return web.Response(status=400, text="Invalid root node ID.")
|
||||
if depth < 1:
|
||||
return web.Response(status=400, text="Depth must be at least 1.")
|
||||
|
||||
since = datetime.timedelta(hours=hours, minutes=minutes)
|
||||
|
||||
nodes = {}
|
||||
node_ids = set()
|
||||
traceroutes = []
|
||||
|
||||
# Fetch traceroutes
|
||||
for tr in await store.get_traceroutes_longfast(since):
|
||||
node_ids.add(tr.gateway_node_id)
|
||||
node_ids.add(tr.packet.from_node_id)
|
||||
node_ids.add(tr.packet.to_node_id)
|
||||
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
||||
node_ids.update(route.route)
|
||||
|
||||
path = [tr.packet.from_node_id]
|
||||
path.extend(route.route)
|
||||
if tr.done:
|
||||
path.append(tr.packet.to_node_id)
|
||||
else:
|
||||
if path[-1] != tr.gateway_node_id:
|
||||
path.append(tr.gateway_node_id)
|
||||
traceroutes.append((tr, path))
|
||||
|
||||
edges = Counter()
|
||||
edge_type = {}
|
||||
used_nodes = set()
|
||||
|
||||
# Fetch MQTT neighbors
|
||||
for ps, p in await store.get_mqtt_neighbors_longfast(since):
|
||||
node_ids.add(ps.node_id)
|
||||
node_ids.add(p.from_node_id)
|
||||
used_nodes.add(ps.node_id)
|
||||
used_nodes.add(p.from_node_id)
|
||||
edges[(p.from_node_id, ps.node_id)] += 1
|
||||
edge_type[(p.from_node_id, ps.node_id)] = 'sni'
|
||||
|
||||
# Fetch NeighborInfo packets
|
||||
for packet in await store.get_packets_longfast(portnum=PortNum.NEIGHBORINFO_APP, since=since):
|
||||
try:
|
||||
_, neighbor_info = decode_payload.decode(packet)
|
||||
node_ids.add(packet.from_node_id)
|
||||
used_nodes.add(packet.from_node_id)
|
||||
for node in neighbor_info.neighbors:
|
||||
node_ids.add(node.node_id)
|
||||
used_nodes.add(node.node_id)
|
||||
edges[(node.node_id, packet.from_node_id)] += 1
|
||||
edge_type[(node.node_id, packet.from_node_id)] = 'ni'
|
||||
except Exception as e:
|
||||
print(f"Error decoding NeighborInfo packet: {e}")
|
||||
|
||||
# Retrieve node details concurrently
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for node_id in node_ids:
|
||||
nodes[node_id] = tg.create_task(store.get_node(node_id))
|
||||
|
||||
tr_done = set()
|
||||
for tr, path in traceroutes:
|
||||
if tr.done:
|
||||
if tr.packet_id in tr_done:
|
||||
continue
|
||||
else:
|
||||
tr_done.add(tr.packet_id)
|
||||
|
||||
for src, dest in zip(path, path[1:]):
|
||||
used_nodes.add(src)
|
||||
used_nodes.add(dest)
|
||||
edges[(src, dest)] += 1
|
||||
edge_type[(src, dest)] = 'tr'
|
||||
|
||||
# Helper to get node name
|
||||
async def get_node_name(node_id):
|
||||
try:
|
||||
node = await nodes[node_id]
|
||||
if node:
|
||||
return f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}'
|
||||
return node_id_to_hex(node_id)
|
||||
except Exception as e:
|
||||
return f"Error retrieving node: {str(e)}"
|
||||
|
||||
# Filter nodes and edges if root is specified
|
||||
if root:
|
||||
new_used_nodes = set()
|
||||
new_edges = Counter()
|
||||
edge_map = {}
|
||||
for src, dest in edges:
|
||||
edge_map.setdefault(dest, []).append(src)
|
||||
|
||||
queue = [int(root)]
|
||||
for _ in range(depth):
|
||||
next_queue = []
|
||||
for node in queue:
|
||||
new_used_nodes.add(node)
|
||||
for dest in edge_map.get(node, []):
|
||||
new_used_nodes.add(dest)
|
||||
new_edges[(dest, node)] += 1
|
||||
next_queue.append(dest)
|
||||
queue = next_queue
|
||||
|
||||
used_nodes = new_used_nodes
|
||||
edges = new_edges
|
||||
|
||||
# Create graph
|
||||
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
|
||||
for node_id in used_nodes:
|
||||
node = await nodes[node_id]
|
||||
color = '#000000'
|
||||
node_name = await get_node_name(node_id)
|
||||
if node and node.role in ('ROUTER', 'ROUTER_CLIENT', 'REPEATER'):
|
||||
color = '#0000FF'
|
||||
elif node and node.role == 'CLIENT_MUTE':
|
||||
color = '#00FF00'
|
||||
graph.add_node(pydot.Node(
|
||||
str(node_id),
|
||||
label=node_name,
|
||||
shape='box',
|
||||
color=color,
|
||||
href=f"/graph/network?root={node_id}&depth={depth-1}",
|
||||
))
|
||||
|
||||
# Adjust edge visualization
|
||||
if edges:
|
||||
max_edge_count = edges.most_common(1)[0][1]
|
||||
else:
|
||||
max_edge_count = 1
|
||||
|
||||
size_ratio = 2. / max_edge_count
|
||||
edge_added = set()
|
||||
|
||||
for (src, dest), edge_count in edges.items():
|
||||
size = max(size_ratio * edge_count, .25)
|
||||
arrowsize = max(size_ratio * edge_count, .5)
|
||||
if edge_type[(src, dest)] in ('ni'):
|
||||
color = '#FF0000'
|
||||
elif edge_type[(src, dest)] in ('sni'):
|
||||
color = '#00FF00'
|
||||
else:
|
||||
color = '#000000'
|
||||
edge_dir = "forward"
|
||||
if (dest, src) in edges and edge_type[(src, dest)] == edge_type[(dest, src)]:
|
||||
edge_dir = "both"
|
||||
edge_added.add((dest, src))
|
||||
|
||||
if (src, dest) not in edge_added:
|
||||
edge_added.add((src, dest))
|
||||
graph.add_edge(pydot.Edge(
|
||||
str(src),
|
||||
str(dest),
|
||||
color=color,
|
||||
tooltip=f'{await get_node_name(src)} -> {await get_node_name(dest)}',
|
||||
penwidth=1.85,
|
||||
dir=edge_dir,
|
||||
))
|
||||
|
||||
return web.Response(
|
||||
body=graph.create_svg(),
|
||||
content_type="image/svg+xml",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in graph_network_longfast: {e}")
|
||||
return web.Response(status=500, text="Internal Server Error")
|
||||
|
||||
|
||||
|
||||
@routes.get("/graph/mediumslow")
|
||||
async def graph_network_mediumslow(request):
|
||||
try:
|
||||
root = request.query.get("root")
|
||||
depth = int(request.query.get("depth", 5))
|
||||
hours = int(request.query.get("hours", 24))
|
||||
minutes = int(request.query.get("minutes", 0))
|
||||
|
||||
# Validation
|
||||
if root and not root.isdigit():
|
||||
return web.Response(status=400, text="Invalid root node ID.")
|
||||
if depth < 1:
|
||||
return web.Response(status=400, text="Depth must be at least 1.")
|
||||
|
||||
since = datetime.timedelta(hours=hours, minutes=minutes)
|
||||
|
||||
nodes = {}
|
||||
node_ids = set()
|
||||
traceroutes = []
|
||||
|
||||
# Fetch traceroutes
|
||||
for tr in await store.get_traceroutes_mediumslow(since):
|
||||
node_ids.add(tr.gateway_node_id)
|
||||
node_ids.add(tr.packet.from_node_id)
|
||||
node_ids.add(tr.packet.to_node_id)
|
||||
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
||||
node_ids.update(route.route)
|
||||
|
||||
path = [tr.packet.from_node_id]
|
||||
path.extend(route.route)
|
||||
if tr.done:
|
||||
path.append(tr.packet.to_node_id)
|
||||
else:
|
||||
if path[-1] != tr.gateway_node_id:
|
||||
path.append(tr.gateway_node_id)
|
||||
traceroutes.append((tr, path))
|
||||
|
||||
edges = Counter()
|
||||
edge_type = {}
|
||||
used_nodes = set()
|
||||
|
||||
# Fetch MQTT neighbors
|
||||
for ps, p in await store.get_mqtt_neighbors_mediumslow(since):
|
||||
node_ids.add(ps.node_id)
|
||||
node_ids.add(p.from_node_id)
|
||||
used_nodes.add(ps.node_id)
|
||||
used_nodes.add(p.from_node_id)
|
||||
edges[(p.from_node_id, ps.node_id)] += 1
|
||||
edge_type[(p.from_node_id, ps.node_id)] = 'sni'
|
||||
|
||||
# Fetch NeighborInfo packets
|
||||
for packet in await store.get_packets_mediumslow(portnum=PortNum.NEIGHBORINFO_APP, since=since):
|
||||
try:
|
||||
_, neighbor_info = decode_payload.decode(packet)
|
||||
node_ids.add(packet.from_node_id)
|
||||
used_nodes.add(packet.from_node_id)
|
||||
for node in neighbor_info.neighbors:
|
||||
node_ids.add(node.node_id)
|
||||
used_nodes.add(node.node_id)
|
||||
edges[(node.node_id, packet.from_node_id)] += 1
|
||||
edge_type[(node.node_id, packet.from_node_id)] = 'ni'
|
||||
except Exception as e:
|
||||
print(f"Error decoding NeighborInfo packet: {e}")
|
||||
|
||||
# Retrieve node details concurrently
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for node_id in node_ids:
|
||||
nodes[node_id] = tg.create_task(store.get_node(node_id))
|
||||
|
||||
tr_done = set()
|
||||
for tr, path in traceroutes:
|
||||
if tr.done:
|
||||
if tr.packet_id in tr_done:
|
||||
continue
|
||||
else:
|
||||
tr_done.add(tr.packet_id)
|
||||
|
||||
for src, dest in zip(path, path[1:]):
|
||||
used_nodes.add(src)
|
||||
used_nodes.add(dest)
|
||||
edges[(src, dest)] += 1
|
||||
edge_type[(src, dest)] = 'tr'
|
||||
|
||||
# Helper to get node name
|
||||
async def get_node_name(node_id):
|
||||
try:
|
||||
node = await nodes[node_id]
|
||||
if node:
|
||||
return f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}'
|
||||
return node_id_to_hex(node_id)
|
||||
except Exception as e:
|
||||
return f"Error retrieving node: {str(e)}"
|
||||
|
||||
# Filter nodes and edges if root is specified
|
||||
if root:
|
||||
new_used_nodes = set()
|
||||
new_edges = Counter()
|
||||
edge_map = {}
|
||||
for src, dest in edges:
|
||||
edge_map.setdefault(dest, []).append(src)
|
||||
|
||||
queue = [int(root)]
|
||||
for _ in range(depth):
|
||||
next_queue = []
|
||||
for node in queue:
|
||||
new_used_nodes.add(node)
|
||||
for dest in edge_map.get(node, []):
|
||||
new_used_nodes.add(dest)
|
||||
new_edges[(dest, node)] += 1
|
||||
next_queue.append(dest)
|
||||
queue = next_queue
|
||||
|
||||
used_nodes = new_used_nodes
|
||||
edges = new_edges
|
||||
|
||||
# Create graph
|
||||
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
|
||||
for node_id in used_nodes:
|
||||
node = await nodes[node_id]
|
||||
color = '#000000'
|
||||
node_name = await get_node_name(node_id)
|
||||
if node and node.role in ('ROUTER', 'ROUTER_CLIENT', 'REPEATER'):
|
||||
color = '#0000FF'
|
||||
elif node and node.role == 'CLIENT_MUTE':
|
||||
color = '#00FF00'
|
||||
graph.add_node(pydot.Node(
|
||||
str(node_id),
|
||||
label=node_name,
|
||||
shape='box',
|
||||
color=color,
|
||||
href=f"/graph/mediumslow?root={node_id}&depth={depth-1}",
|
||||
))
|
||||
|
||||
# Adjust edge visualization
|
||||
if edges:
|
||||
max_edge_count = edges.most_common(1)[0][1]
|
||||
else:
|
||||
max_edge_count = 1
|
||||
|
||||
size_ratio = 2. / max_edge_count
|
||||
edge_added = set()
|
||||
|
||||
for (src, dest), edge_count in edges.items():
|
||||
size = max(size_ratio * edge_count, .25)
|
||||
arrowsize = max(size_ratio * edge_count, .5)
|
||||
if edge_type[(src, dest)] in ('ni'):
|
||||
color = '#FF0000'
|
||||
elif edge_type[(src, dest)] in ('sni'):
|
||||
color = '#00FF00'
|
||||
else:
|
||||
color = '#000000'
|
||||
edge_dir = "forward"
|
||||
if (dest, src) in edges and edge_type[(src, dest)] == edge_type[(dest, src)]:
|
||||
edge_dir = "both"
|
||||
edge_added.add((dest, src))
|
||||
|
||||
if (src, dest) not in edge_added:
|
||||
edge_added.add((src, dest))
|
||||
graph.add_edge(pydot.Edge(
|
||||
str(src),
|
||||
str(dest),
|
||||
color=color,
|
||||
tooltip=f'{await get_node_name(src)} -> {await get_node_name(dest)}',
|
||||
penwidth=1.85,
|
||||
dir=edge_dir,
|
||||
))
|
||||
|
||||
return web.Response(
|
||||
body=graph.create_svg(),
|
||||
content_type="image/svg+xml",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in graph_network_longfast: {e}")
|
||||
return web.Response(status=500, text="Internal Server Error")
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user