Multiple changes to the code. Important to mentioned:

- We added another column to the nodes table to report on firmware version

- Also changed the date and time format to be more readable.

- Added a node list report

- Modified the Mesh Graphs
This commit is contained in:
Pablo Revilla
2025-02-07 21:18:02 -08:00
parent 604fc5c284
commit 0831301997
12 changed files with 160 additions and 26 deletions
+2 -1
View File
@@ -7,7 +7,8 @@ def init_database(database_connection_string):
if not database_connection_string.startswith('sqlite'):
kwargs['pool_size'] = 20
kwargs['max_overflow'] = 50
engine = create_async_engine(database_connection_string, echo=False, **kwargs)
print (**kwargs)
engine = create_async_engine(database_connection_string, echo=False, connect_args={"timeout": 15})
async_session = async_sessionmaker(engine, expire_on_commit=False)
async def create_tables():
+2
View File
@@ -1,3 +1,4 @@
from meshtastic.protobuf.mqtt_pb2 import MapReport
from meshtastic.protobuf.portnums_pb2 import PortNum
from meshtastic.protobuf.mesh_pb2 import (
Position,
@@ -24,6 +25,7 @@ DECODE_MAP = {
PortNum.TRACEROUTE_APP: RouteDiscovery.FromString,
PortNum.ROUTING_APP: Routing.FromString,
PortNum.TEXT_MESSAGE_APP: text_message,
PortNum.MAP_REPORT_APP: MapReport.FromString
}
+1
View File
@@ -17,6 +17,7 @@ class Node(Base):
long_name: Mapped[str]
short_name: Mapped[str]
hw_model: Mapped[str]
firmware: Mapped[str]
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)
-2
View File
@@ -1,11 +1,9 @@
import base64
import asyncio
import random
import aiomqtt
from google.protobuf.message import DecodeError
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from meshtastic.protobuf.mqtt_pb2 import ServiceEnvelope
KEY = base64.b64decode("1PG7OiApB1nwvP+rz05pAQ==")
+43 -2
View File
@@ -2,7 +2,7 @@ import datetime
from sqlalchemy import select, func
from sqlalchemy.orm import lazyload
from sqlalchemy import update
from meshtastic.protobuf.config_pb2 import Config
from meshtastic.protobuf.portnums_pb2 import PortNum
from meshtastic.protobuf.mesh_pb2 import User, HardwareModel
@@ -12,7 +12,34 @@ from meshview.models import Packet, PacketSeen, Node, Traceroute
from meshview import notify
async def process_envelope(topic, env):
# Checking if the received packet is a MAP_REPORT
# Update the node table with the firmware version
if env.packet.decoded.portnum == PortNum.MAP_REPORT_APP:
# Extract the node ID from the packet (renamed from 'id' to 'node_id' to avoid conflicts with Python's built-in id function)
node_id = getattr(env.packet, "from")
# Decode the MAP report payload to extract the firmware version
map_report = decode_payload.decode_payload(PortNum.MAP_REPORT_APP, env.packet.decoded.payload)
# Establish an asynchronous database session
async with database.async_session() as session:
# Construct an SQLAlchemy update statement
stmt = (
update(Node)
.where(Node.node_id == node_id) # Ensure correct column reference
.values(firmware=map_report.firmware_version) # Assign new firmware value
)
# Execute the update statement asynchronously
await session.execute(stmt)
# Commit the changes to the database
await session.commit()
# This ignores any packet that does not have a ID
if not env.packet.id:
return
@@ -58,6 +85,8 @@ async def process_envelope(topic, env):
)
session.add(seen)
if env.packet.decoded.portnum == PortNum.NODEINFO_APP:
user = decode_payload.decode_payload(
PortNum.NODEINFO_APP, env.packet.decoded.payload
@@ -89,7 +118,6 @@ async def process_envelope(topic, env):
node.hw_model = hw_model
node.role = role
node.last_update =datetime.datetime.now()
# if need to update time of last update it may be here
else:
node = Node(
@@ -481,3 +509,16 @@ async def get_nodes_mediumslow():
return result.scalars()
async def get_nodes():
async with database.async_session() as session:
result = await session.execute(
select(Node)
.where(Node.last_update != "")
.order_by(Node.long_name) # Sorting by long_name
)
return result.scalars()
+1 -1
View File
@@ -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:&nbsp;&nbsp;<a href="/">Search for a node </a>&nbsp;-&nbsp;<a href="/chat">Conversations</a>&nbsp;-&nbsp;<a href="/firehose">See <strong>everything</strong> </a>&nbsp;-&nbsp;Mesh Graph <a href="/graph/longfast">LG</a>&nbsp;-&nbsp;<a href="/graph/mediumslow">MS </a>&nbsp;-&nbsp;<a href="/stats">Stats </a></div><br>
<div style="text-align:center">Quick Links:&nbsp;&nbsp;<a href="/">Search for a node </a>&nbsp;-&nbsp;<a href="/chat">Conversations</a>&nbsp;-&nbsp;<a href="/firehose">See <strong>everything</strong> </a>&nbsp;-&nbsp;Mesh Graph <a href="/graph/longfast">LG</a>&nbsp;-&nbsp;<a href="/graph/mediumslow">MS </a>&nbsp;-&nbsp;<a href="/nodelist">Nodes</a>&nbsp;-&nbsp;<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>
+5 -4
View File
@@ -1,5 +1,6 @@
<div class="row chat-packet">
<span class="col-3 timestamp">{{packet.import_time | format_timestamp}} <a href="/packet/{{packet.id}}">✉️</a></span>
<span class="col-3 username"><a href="/packet_list/{{packet.from_node_id}}">{{packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}</a></span>
<span class="col message">{{packet.payload}}</span>
</div>
<span class="col-2 timestamp">{{packet.import_time.strftime('%-I:%M:%S %p - %d-%m-%Y')}} </span>
<span class="col-1 timestamp"><a href="/packet/{{packet.id}}">✉️</a> {{packet.from_node.channel}}</span>
<span class="col-2 username"><a href="/packet_list/{{packet.from_node_id}}">{{packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}</a></span>
<span class="col-6 message">{{packet.payload}}</span>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
</div>
<div class="card-text text-start">
<dl>
<div>{{packet.import_time | format_timestamp}}</div>
<div>{{packet.import_time.strftime('%-I:%M:%S %p - %d-%m-%Y')}}</div>
<div>{{packet.payload}}</div>
</dl>
</div>
+69
View File
@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block css %}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1em;
}
th, td {
padding: 10px;
border: 1px solid #333;
text-align: left;
}
th {
background-color: #1f1f1f;
color: white;
}
tr:nth-child(even) {
background-color: #181818;
}
tr:nth-child(odd) {
background-color: #222;
}
{% endblock %}
{% block body %}
<div>
{% if nodes %}
<table>
<thead>
<tr>
<th>Node ID</th>
<th>Long Name</th>
<th>Short Name</th>
<th>HW Model</th>
<th>Firmware</th>
<th>Role</th>
<th>Last Latitude</th>
<th>Last Longitude</th>
<th>Channel</th>
<th>Last Update</th>
</tr>
</thead>
<tbody>
{% for node in nodes %}
<tr>
<td> <a href="/packet_list/{{node.node_id }}">{{node.node_id }}</a></td>
<td>{{ node.long_name }}</td>
<td>{{ node.short_name }}</td>
<td>{{ node.hw_model }}</td>
<td>{{ node.firmware }}</td>
<td>{{ node.role if node.role else "N/A" }}</td>
<td>{{ node.last_lat if node.last_lat else "N/A" }}</td>
<td>{{ node.last_long if node.last_long else "N/A" }}</td>
<td>{{ node.channel }}</td>
<td>{{ node.last_update.strftime('%-I:%M:%S %p - %d-%m-%Y') if node.last_update else "N/A" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No nodes found.</p>
{% endif %}
</div>
{% endblock %}
+2 -2
View File
@@ -34,8 +34,8 @@
</div>
<div class="card-text text-start">
<dl>
<dt>import_time</dt>
<dd>{{packet.import_time | format_timestamp}}</dd>
<dt>Import Time</dt>
<dd>{{packet.import_time.strftime('%-I:%M:%S %p - %d-%m-%Y')}}</dd>
<dt>packet</dt>
<dd><pre>{{packet.data}}</pre></dd>
<dt>payload</dt>
+2 -2
View File
@@ -12,8 +12,8 @@
<div class="card-body">
<div class="card-text text-start">
<dl>
<dt>import_time</dt>
<dd>{{seen.import_time|format_timestamp}}</dd>
<dt>Import Time</dt>
<dd>{{seen.import_time.strftime('%-I:%M:%S %p - %d-%m-%Y')}}</dd>
<dt>rx_time</dt>
<dd>{{seen.rx_time|format_timestamp}}</dd>
<dt>hop_limit</dt>
+32 -11
View File
@@ -474,14 +474,13 @@ async def packet_details(request):
content_type="text/html",
)
@routes.get("/chat")
async def chat(request):
try:
# Fetch packets for the given node ID and port number
#print("Fetching packets...")
packets = await store.get_packets(
node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP
node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, limit=100
)
#print(f"Fetched {len(packets)} packets.")
@@ -625,9 +624,6 @@ async def graph_chutil(request):
],
)
@routes.get("/graph/wind_speed/{node_id}")
async def graph_wind_speed(request):
return await graph_telemetry(
@@ -1022,7 +1018,10 @@ async def graph_network(request):
#graph = pydot.Dot('network', graph_type="digraph", layout="sfdp", overlap="prism", quadtree="2", repulsiveforce="1.5", k="1", overlap_scaling="1.5", concentrate=True)
#graph = pydot.Dot('network', graph_type="digraph", layout="sfdp", overlap="prism1000", overlap_scaling="-4", sep="1000", pack="true")
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
#graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
graph = pydot.Dot('network', graph_type="digraph", layout="sfdp", overlap="prism", esep="+10", nodesep="0.5",
ranksep="1")
for node_id in used_nodes:
node = await nodes[node_id]
color = '#000000'
@@ -1229,20 +1228,21 @@ async def graph_network_longfast(request):
edges = new_edges
# Create graph
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
graph = pydot.Dot('network', graph_type="digraph", layout="sfdp", overlap="scale", model='subset', splines="true")
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'
#elif node and node.role == 'CLIENT_MUTE':
# color = '#00FF00'
graph.add_node(pydot.Node(
str(node_id),
label=node_name,
shape='box',
color=color,
fontsize="10", width="0", height="0",
href=f"/graph/network?root={node_id}&amp;depth={depth-1}",
))
@@ -1276,8 +1276,9 @@ async def graph_network_longfast(request):
str(dest),
color=color,
tooltip=f'{await get_node_name(src)} -> {await get_node_name(dest)}',
penwidth=1.85,
penwidth=.5,
dir=edge_dir,
arrowsize=".5",
))
return web.Response(
@@ -1407,7 +1408,8 @@ async def graph_network_mediumslow(request):
edges = new_edges
# Create graph
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
graph = pydot.Dot('network', graph_type="digraph", layout="sfdp", overlap="scale", model='subset', esep="+5", splines="true", nodesep="2", ranksep="2")
for node_id in used_nodes:
node = await nodes[node_id]
color = '#000000'
@@ -1467,6 +1469,25 @@ async def graph_network_mediumslow(request):
print(f"Error in graph_network_longfast: {e}")
return web.Response(status=500, text="Internal Server Error")
@routes.get("/nodelist")
async def nodelist(request):
try:
nodes= await store.get_nodes()
template = env.get_template("nodelist.html")
return web.Response(
text=template.render(nodes=nodes),
content_type="text/html",
)
except Exception as e:
return web.Response(
text="An error occurred while processing your request.",
status=500,
content_type="text/plain",
)
async def run_server(bind, port, tls_cert):