Modified the Neighbours graphs to eliminate the errors.

This commit is contained in:
Pablo Revilla
2025-05-21 09:14:17 -07:00
parent f9319472b1
commit a6c9970054
2 changed files with 178 additions and 142 deletions
+157 -55
View File
@@ -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
View File
@@ -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