mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-06 08:53:20 +02:00
Modified the Neighbours graphs to eliminate the errors.
This commit is contained in:
@@ -1,68 +1,170 @@
|
||||
{% macro graph(name) %}
|
||||
<a href="/graph/{{name}}/{{node_id}}"><img src="/graph/{{name}}/{{node_id}}" height="200em" width="200em"/></a>
|
||||
<a href="/graph/{{name}}/{{node_id}}">
|
||||
<img src="/graph/{{name}}/{{node_id}}" height="200" width="200" alt="{{ name }} graph"/>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Tabs -->
|
||||
<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>
|
||||
<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 class="nav-item" role="presentation">
|
||||
<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" >Environment</button>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#weather" type="button" role="tab">Environment</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#power" type="button" role="tab" >Power</button>
|
||||
<li class="nav-item" role="presentation">
|
||||
<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">
|
||||
<dl>
|
||||
<dt>Battery</dt>
|
||||
<dd>{{ graph("power") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>ChUtil</dt>
|
||||
<dd>{{ graph("chutil") }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="neighbors" role="tabpanel">
|
||||
<a style="display: block;" 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">
|
||||
<dl>
|
||||
<dt>Temperature</dt>
|
||||
<dd>{{ graph("temperature") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Humidity</dt>
|
||||
<dd>{{ graph("humidity") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Pressure</dt>
|
||||
<dd>{{ graph("pressure") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Indoor Air Quality</dt>
|
||||
<dd>{{ graph("iaq") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Wind Speed</dt>
|
||||
<dd>{{ graph("wind_speed") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Wind Direction</dt>
|
||||
<dd>{{ graph("wind_direction") }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="power" role="tabpanel">
|
||||
<dl>
|
||||
<dt>Power Metrics</dt>
|
||||
<dd>{{ graph("power_metrics") }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<div class="tab-pane fade show active" id="overview" role="tabpanel">
|
||||
<dl>
|
||||
<dt>Battery</dt>
|
||||
<dd>{{ graph("power") }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>ChUtil</dt>
|
||||
<dd>{{ graph("chutil") }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="neighbors" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="text-white">Neighbor SNR</h5>
|
||||
<button class="btn btn-sm btn-outline-light" data-bs-toggle="modal" data-bs-target="#fullChartModal">
|
||||
Expand
|
||||
</button>
|
||||
</div>
|
||||
<div id="neighborChart" style="width: 100%; height: 400px;"></div>
|
||||
<noscript>
|
||||
<a href="/graph/neighbors2/{{node_id}}">
|
||||
<img src="/graph/neighbors/{{node_id}}" width="200" height="200" alt="Neighbor SNR Graph"/>
|
||||
</a>
|
||||
</noscript>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="weather" role="tabpanel">
|
||||
<dl><dt>Temperature</dt><dd>{{ graph("temperature") }}</dd></dl>
|
||||
<dl><dt>Humidity</dt><dd>{{ graph("humidity") }}</dd></dl>
|
||||
<dl><dt>Pressure</dt><dd>{{ graph("pressure") }}</dd></dl>
|
||||
<dl><dt>Indoor Air Quality</dt><dd>{{ graph("iaq") }}</dd></dl>
|
||||
<dl><dt>Wind Speed</dt><dd>{{ graph("wind_speed") }}</dd></dl>
|
||||
<dl><dt>Wind Direction</dt><dd>{{ graph("wind_direction") }}</dd></dl>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="power" role="tabpanel">
|
||||
<dl><dt>Power Metrics</dt><dd>{{ graph("power_metrics") }}</dd></dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen Modal for Neighbor Chart -->
|
||||
<div class="modal fade" id="fullChartModal" tabindex="-1" aria-labelledby="fullChartModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-white" id="fullChartModalLabel">Full Neighbor SNR Chart</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="fullNeighborChart" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ECharts Library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const neighborTab = document.querySelector('button[data-bs-target="#neighbors"]');
|
||||
neighborTab.addEventListener('shown.bs.tab', function () {
|
||||
const chartContainer = document.getElementById('neighborChart');
|
||||
if (chartContainer.dataset.loaded) return;
|
||||
|
||||
fetch("/graph/neighbors_json/{{ node_id }}")
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
const chart = echarts.init(chartContainer);
|
||||
chart.setOption({
|
||||
title: { text: 'Neighbor SNR over Time', textStyle: { color: '#ffffff' }},
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { type: 'scroll', top: 30, textStyle: { color: '#ffffff' }},
|
||||
grid: { left: 60, right: 30, bottom: 80, top: 60, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: json.timestamps,
|
||||
name: 'Time',
|
||||
axisLabel: { rotate: 45, color: '#ffffff' },
|
||||
nameTextStyle: { color: '#ffffff' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: 'SNR',
|
||||
axisLabel: { color: '#ffffff' },
|
||||
nameTextStyle: { color: '#ffffff' }
|
||||
},
|
||||
series: json.series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'line',
|
||||
data: s.data,
|
||||
connectNulls: true,
|
||||
showSymbol: false,
|
||||
smooth: true
|
||||
}))
|
||||
});
|
||||
chartContainer.dataset.loaded = "true";
|
||||
})
|
||||
.catch(err => {
|
||||
chartContainer.innerText = "Failed to load graph.";
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Fullscreen chart loader
|
||||
document.getElementById('fullChartModal').addEventListener('shown.bs.modal', function () {
|
||||
const container = document.getElementById('fullNeighborChart');
|
||||
container.innerHTML = ''; // clear
|
||||
|
||||
fetch("/graph/neighbors_json/{{ node_id }}")
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
const chart = echarts.init(container);
|
||||
chart.setOption({
|
||||
title: { text: 'Neighbor SNR over Time', textStyle: { color: '#ffffff' }},
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { type: 'scroll', top: 30, textStyle: { color: '#ffffff' }},
|
||||
grid: { left: 60, right: 30, bottom: 80, top: 60, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: json.timestamps,
|
||||
name: 'Time',
|
||||
axisLabel: { rotate: 45, color: '#ffffff' },
|
||||
nameTextStyle: { color: '#ffffff' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: 'SNR',
|
||||
axisLabel: { color: '#ffffff' },
|
||||
nameTextStyle: { color: '#ffffff' }
|
||||
},
|
||||
series: json.series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'line',
|
||||
data: s.data,
|
||||
connectNulls: true,
|
||||
showSymbol: false,
|
||||
smooth: true
|
||||
}))
|
||||
});
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
+21
-87
@@ -29,13 +29,13 @@ SOFTWARE_RELEASE= "2.0.1"
|
||||
CONFIG = config.CONFIG
|
||||
|
||||
env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape())
|
||||
|
||||
# Start Database
|
||||
database.init_database(CONFIG["database"]["connection_string"])
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), '1x1.png'), 'rb') as png:
|
||||
empty_png = png.read()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Packet:
|
||||
id: int
|
||||
@@ -160,7 +160,6 @@ async def build_neighbors(node_id):
|
||||
return list(neighbors.values()) # Return a list of dictionaries
|
||||
|
||||
|
||||
|
||||
def node_id_to_hex(node_id):
|
||||
if node_id is None or isinstance(node_id, Undefined):
|
||||
return "Invalid node_id" # i... have no clue
|
||||
@@ -242,7 +241,6 @@ async def node_search(request):
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@routes.get("/node_match")
|
||||
async def node_match(request):
|
||||
if not "q" in request.query or not request.query["q"]:
|
||||
@@ -381,7 +379,6 @@ async def packet_details(request):
|
||||
if portnum:
|
||||
portnum = int(portnum)
|
||||
packets = await store.get_packets(portnum=portnum, limit=20)
|
||||
print_memory_usage()
|
||||
template = env.get_template("firehose.html")
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
@@ -392,7 +389,6 @@ async def packet_details(request):
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/packet/{packet_id}")
|
||||
async def packet(request):
|
||||
packet = await store.get_packet(int(request.match_info["packet_id"]))
|
||||
@@ -400,7 +396,6 @@ async def packet(request):
|
||||
return web.Response(status=404)
|
||||
|
||||
node = await store.get_node(packet.from_node_id)
|
||||
print_memory_usage()
|
||||
template = env.get_template("packet_index.html")
|
||||
|
||||
return web.Response(
|
||||
@@ -409,7 +404,6 @@ async def packet(request):
|
||||
)
|
||||
|
||||
|
||||
|
||||
async def graph_telemetry(node_id, payload_type, graph_config):
|
||||
data = {'date': []}
|
||||
fields = []
|
||||
@@ -475,7 +469,6 @@ async def graph_telemetry(node_id, payload_type, graph_config):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@routes.get("/graph/power/{node_id}")
|
||||
async def graph_power(request):
|
||||
return await graph_telemetry(
|
||||
@@ -606,95 +599,46 @@ async def graph_power_metrics(request):
|
||||
],
|
||||
)
|
||||
|
||||
@routes.get("/graph/neighbors_json/{node_id}")
|
||||
async def graph_neighbors_json(request):
|
||||
import datetime
|
||||
from pandas import DataFrame
|
||||
|
||||
@routes.get("/graph/neighbors/{node_id}")
|
||||
async def graph_neighbors(request):
|
||||
node_id = int(request.match_info['node_id'])
|
||||
oldest = datetime.datetime.now() - datetime.timedelta(days=4)
|
||||
|
||||
data = {}
|
||||
dates =[]
|
||||
for p in await store.get_packets_from(int(request.match_info['node_id']), PortNum.NEIGHBORINFO_APP):
|
||||
dates = []
|
||||
for p in await store.get_packets_from(node_id, PortNum.NEIGHBORINFO_APP):
|
||||
_, payload = decode_payload.decode(p)
|
||||
if not payload:
|
||||
continue
|
||||
if p.import_time < oldest:
|
||||
break
|
||||
|
||||
dates.append(p.import_time)
|
||||
dates.append(p.import_time.isoformat()) # format for JSON
|
||||
for v in data.values():
|
||||
v.append(None)
|
||||
|
||||
for n in payload.neighbors:
|
||||
data.setdefault(n.node_id, [None] * len(dates))[-1] = n.snr
|
||||
|
||||
# Resolve node short names
|
||||
nodes = {}
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for node_id in data:
|
||||
nodes[node_id] = tg.create_task(store.get_node(node_id))
|
||||
for nid in data:
|
||||
nodes[nid] = tg.create_task(store.get_node(nid))
|
||||
|
||||
data_by_short_name = {}
|
||||
for node_id, data in data.items():
|
||||
series = []
|
||||
for node_id, snrs in data.items():
|
||||
node = await nodes[node_id]
|
||||
if node:
|
||||
data_by_short_name[node.short_name] = data
|
||||
else:
|
||||
data_by_short_name[node_id_to_hex(node_id)] = data
|
||||
name = node.short_name if node else node_id_to_hex(node_id)
|
||||
series.append({"name": name, "data": snrs})
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(5, 5))
|
||||
ax1.set_xlabel('time')
|
||||
ax1.set_ylabel('SNR')
|
||||
df = DataFrame(data_by_short_name, index=dates)
|
||||
sns.lineplot(data=df)
|
||||
|
||||
png = io.BytesIO()
|
||||
plt.savefig(png, dpi=100)
|
||||
plt.close()
|
||||
return web.Response(
|
||||
body=png.getvalue(),
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
@routes.get("/graph/neighbors2/{node_id}")
|
||||
async def graph_neighbors2(request):
|
||||
oldest = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||
|
||||
data = []
|
||||
node_ids = set()
|
||||
for p in await store.get_packets_from(int(request.match_info['node_id']), PortNum.NEIGHBORINFO_APP):
|
||||
_, payload = decode_payload.decode(p)
|
||||
if not payload:
|
||||
continue
|
||||
if p.import_time < oldest:
|
||||
break
|
||||
|
||||
for n in payload.neighbors:
|
||||
node_ids.add(n.node_id)
|
||||
data.append({
|
||||
'time': p.import_time,
|
||||
'snr': n.snr,
|
||||
'node_id': n.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))
|
||||
|
||||
for d in data:
|
||||
node = await nodes[d['node_id']]
|
||||
if node:
|
||||
d['node_name'] = node.short_name
|
||||
else:
|
||||
d['node_name'] = node_id_to_hex(node_id)
|
||||
|
||||
df = DataFrame(data)
|
||||
fig = px.line(df, x="time", y="snr", color="node_name", markers=True)
|
||||
html = fig.to_html(full_html=True, include_plotlyjs='cdn')
|
||||
print_memory_usage()
|
||||
return web.Response(
|
||||
text=html,
|
||||
content_type="text/html",
|
||||
)
|
||||
return web.json_response({
|
||||
"timestamps": dates,
|
||||
"series": series,
|
||||
})
|
||||
|
||||
@routes.get("/graph/traceroute/{packet_id}")
|
||||
async def graph_traceroute(request):
|
||||
@@ -1078,7 +1022,6 @@ async def graph_network(request):
|
||||
penwidth=1.85,
|
||||
dir=edge_dir,
|
||||
))
|
||||
print_memory_usage()
|
||||
return web.Response(
|
||||
body=graph.create_svg(),
|
||||
content_type="image/svg+xml",
|
||||
@@ -1136,7 +1079,6 @@ async def api(request):
|
||||
@routes.get("/net")
|
||||
async def net(request):
|
||||
try:
|
||||
print_memory_usage()
|
||||
# Fetch packets for the given node ID and port number
|
||||
packets = await store.get_packets(
|
||||
node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, limit=1000
|
||||
@@ -1200,13 +1142,6 @@ async def map(request):
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Print memory usage
|
||||
def print_memory_usage():
|
||||
process = psutil.Process(os.getpid())
|
||||
print(f"Memory Usage: {process.memory_info().rss / (1024 * 1024):.2f} MB")
|
||||
|
||||
@routes.get("/stats")
|
||||
async def stats(request):
|
||||
try:
|
||||
@@ -1215,7 +1150,6 @@ async def stats(request):
|
||||
total_packets_seen = await store.get_total_packet_seen_count()
|
||||
total_nodes_longfast = await get_total_node_count("LongFast")
|
||||
total_nodes_mediumslow = await get_total_node_count("MediumSlow")
|
||||
print_memory_usage()
|
||||
template = env.get_template("stats.html")
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
@@ -1378,6 +1312,7 @@ async def get_config(request):
|
||||
"Server": site.get("domain", ""),
|
||||
"Title": site.get("title", ""),
|
||||
"Message": site.get("message", ""),
|
||||
"MQTT Server": mqtt.get("server", ""),
|
||||
"Topics": json.loads(mqtt.get("topics", "[]")),
|
||||
"Release": SOFTWARE_RELEASE
|
||||
})
|
||||
@@ -1400,4 +1335,3 @@ async def run_server():
|
||||
await site.start()
|
||||
while True:
|
||||
await asyncio.sleep(3600) # sleep forever
|
||||
|
||||
|
||||
Reference in New Issue
Block a user