mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Add more graphs.
This commit is contained in:
BIN
meshview/1x1.png
Normal file
BIN
meshview/1x1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
@@ -8,8 +8,8 @@
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11/dist/ext/sse.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md mb-3">
|
||||
<div class="card text-white bg-primary" id="node_info">
|
||||
<div class="card" id="node_info">
|
||||
{% if node %}
|
||||
<div class="card-header">
|
||||
{{node.long_name}} ({{node.node_id|node_id_to_hex}})
|
||||
@@ -42,12 +42,7 @@
|
||||
<dt>role</dt>
|
||||
<dd>{{node.role}}</dd>
|
||||
</dl>
|
||||
{% if has_telemetry %}
|
||||
<a href="/graph/power/{{node_id}}"><img src="/graph/power/{{node_id}}" height="200em" width="200em"/></a>
|
||||
{% endif %}
|
||||
{% if neighbors %}
|
||||
<a href="/graph/neighbors2/{{node_id}}"><img src="/graph/neighbors/{{node_id}}" height="200em" width="200em"/></a>
|
||||
{% endif %}
|
||||
{% include "node_graphs.html" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
|
||||
39
meshview/templates/node_graphs.html
Normal file
39
meshview/templates/node_graphs.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% macro graph(name) %}
|
||||
<a href="/graph/{{name}}/{{node_id}}"><img src="/graph/{{name}}/{{node_id}}" height="200em" width="200em"/></a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">Overview</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#neighbors" type="button" role="tab" >Neighbors</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#weather" type="button" role="tab" >Weather</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#power" type="button" role="tab" >Power</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="overview" role="tabpanel">
|
||||
{{ graph("power") }}
|
||||
{{ graph("chutil") }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="neighbors" role="tabpanel">
|
||||
<a href="/graph/neighbors2/{{node_id}}"><img src="/graph/neighbors/{{node_id}}" height="200em" width="200em"/></a>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="weather" role="tabpanel">
|
||||
{{ graph("temperature") }}
|
||||
{{ graph("humidity") }}
|
||||
{{ graph("wind_speed") }}
|
||||
{{ graph("wind_direction") }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="power" role="tabpanel">
|
||||
{{ graph("power_metrics") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
180
meshview/web.py
180
meshview/web.py
@@ -6,6 +6,7 @@ import datetime
|
||||
from aiohttp_sse import sse_response
|
||||
import ssl
|
||||
import re
|
||||
import os
|
||||
|
||||
import pydot
|
||||
from pandas import DataFrame
|
||||
@@ -25,6 +26,11 @@ from meshview import models
|
||||
from meshview import decode_payload
|
||||
from meshview import notify
|
||||
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), '1x1.png'), 'rb') as png:
|
||||
empty_png = png.read()
|
||||
|
||||
|
||||
env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape())
|
||||
|
||||
|
||||
@@ -399,7 +405,7 @@ async def packet_details(request):
|
||||
packet = await store.get_packet(packet_id)
|
||||
|
||||
from_node_cord = None
|
||||
if packet.from_node.last_lat:
|
||||
if packet.from_node and packet.from_node.last_lat:
|
||||
from_node_cord = [packet.from_node.last_lat * 1e-7 , packet.from_node.last_long * 1e-7]
|
||||
|
||||
uplinked_cord = []
|
||||
@@ -470,44 +476,60 @@ async def packet(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/power/{node_id}")
|
||||
async def graph_power(request):
|
||||
date = []
|
||||
battery = []
|
||||
voltage = []
|
||||
for p in await store.get_packets_from(int(request.match_info['node_id']), PortNum.TELEMETRY_APP):
|
||||
async def graph_telemetry(node_id, payload_type, graph_config):
|
||||
data = {'date': []}
|
||||
fields = []
|
||||
for c in graph_config:
|
||||
fields.extend(c['fields'])
|
||||
|
||||
for field in fields:
|
||||
data[field] = []
|
||||
|
||||
for p in await store.get_packets_from(node_id, PortNum.TELEMETRY_APP):
|
||||
_, payload = decode_payload.decode(p)
|
||||
if not payload:
|
||||
continue
|
||||
if not payload.HasField('device_metrics'):
|
||||
if not payload.HasField(payload_type):
|
||||
continue
|
||||
data_field = getattr(payload, payload_type)
|
||||
timestamp = p.import_time
|
||||
date.append(timestamp)
|
||||
battery.append(payload.device_metrics.battery_level)
|
||||
voltage.append(payload.device_metrics.voltage)
|
||||
data['date'].append(timestamp)
|
||||
for field in fields:
|
||||
data[field].append(getattr(data_field, field))
|
||||
|
||||
|
||||
if not date:
|
||||
if not data['date']:
|
||||
return web.Response(
|
||||
body=empty_png,
|
||||
status=404,
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
|
||||
max_time = datetime.timedelta(days=4)
|
||||
newest = date[0]
|
||||
for i, d in enumerate(date):
|
||||
newest = data['date'][0]
|
||||
for i, d in enumerate(data['date']):
|
||||
if d < newest - max_time:
|
||||
break
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(10, 10))
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
fig.autofmt_xdate()
|
||||
ax1.set_xlabel('time')
|
||||
ax1.set_ylabel('battery level', color='tab:blue')
|
||||
ax2 = ax1.twinx()
|
||||
ax2.set_ylabel('voltage', color='tab:red')
|
||||
sns.lineplot(x=date[:i], y=battery[:i], ax=ax1, color='tab:blue')
|
||||
sns.lineplot(x=date[:i], y=voltage[:i], ax=ax2, color='tab:red')
|
||||
ax.set_xlabel('time')
|
||||
axes = {0: ax}
|
||||
|
||||
date = data.pop('date')
|
||||
df = DataFrame(data, index=date)
|
||||
|
||||
for i, ax_config in enumerate(graph_config):
|
||||
args = {}
|
||||
if 'color' in ax_config:
|
||||
args['color'] = 'tab:' + ax_config['color']
|
||||
if i:
|
||||
ax = ax.twinx()
|
||||
ax.set_ylabel(ax_config['label'], **args)
|
||||
ax_df = df[ax_config['fields']]
|
||||
args = {}
|
||||
if 'palette' in ax_config:
|
||||
args['palette'] = ax_config['palette']
|
||||
sns.lineplot(data=ax_df, ax=ax, **args)
|
||||
|
||||
png = io.BytesIO()
|
||||
plt.savefig(png, dpi=100)
|
||||
@@ -519,6 +541,115 @@ async def graph_power(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/power/{node_id}")
|
||||
async def graph_power(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'device_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'battery level',
|
||||
'fields': ['battery_level'],
|
||||
},
|
||||
{
|
||||
'label': 'voltage',
|
||||
'fields': ['voltage'],
|
||||
'palette': 'Set2',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/chutil/{node_id}")
|
||||
async def graph_chutil(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'device_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'utilization',
|
||||
'fields': ['channel_utilization', 'air_util_tx'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@routes.get("/graph/wind_speed/{node_id}")
|
||||
async def graph_wind_speed(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'wind speed m/s',
|
||||
'fields': ['wind_speed'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/wind_direction/{node_id}")
|
||||
async def graph_wind_direction(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'wind direction',
|
||||
'fields': ['wind_direction'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/temperature/{node_id}")
|
||||
async def graph_temperature(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'temperature C',
|
||||
'fields': ['temperature'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/humidity/{node_id}")
|
||||
async def graph_humidity(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'environment_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'humidity',
|
||||
'fields': ['relative_humidity'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/power_metrics/{node_id}")
|
||||
async def graph_power_metrics(request):
|
||||
return await graph_telemetry(
|
||||
int(request.match_info['node_id']),
|
||||
'power_metrics',
|
||||
[
|
||||
{
|
||||
'label': 'voltage',
|
||||
'fields': ['ch1_voltage', 'ch2_voltage', 'ch3_voltage'],
|
||||
},
|
||||
{
|
||||
'label': 'current',
|
||||
'fields': ['ch1_current', 'ch2_current', 'ch3_current'],
|
||||
'palette': 'Set2',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/graph/neighbors/{node_id}")
|
||||
async def graph_neighbors(request):
|
||||
oldest = datetime.datetime.utcnow() - datetime.timedelta(days=4)
|
||||
@@ -601,7 +732,6 @@ async def graph_neighbors2(request):
|
||||
d['node_name'] = node_id_to_hex(node_id)
|
||||
|
||||
df = DataFrame(data)
|
||||
print(df, flush=True)
|
||||
fig = px.line(df, x="time", y="snr", color="node_name", markers=True)
|
||||
html = fig.to_html(full_html=True, include_plotlyjs='cdn')
|
||||
return web.Response(
|
||||
@@ -874,7 +1004,7 @@ async def graph_network(request):
|
||||
|
||||
|
||||
@routes.get("/net")
|
||||
async def graph_net(request):
|
||||
async def net(request):
|
||||
if "date" in request.query:
|
||||
start_date = datetime.date.fromisoformat(request.query["date"])
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user