Add channel utilization gauges

This commit is contained in:
Anton Roslund
2025-09-20 10:38:17 +02:00
parent 457ececec7
commit 9fae44a7b5
2 changed files with 223 additions and 0 deletions
+24
View File
@@ -32,6 +32,29 @@ Antalet meddelanden per timme senaste 7 dygnen. Grafen visar meddelanden som ski
</div>
## Kanalutnyttjande
Mätarna visar genomsnittligt kanalutnyttjande för de 10 noder som har högst kanalutnyttjande. Kanalutnyttjande (Channel Utilization) visar hur mycket av radiokanalen som är upptagen. Under 1520% är allt normalt, över 25% får Telemetri lägre prioritet, vilket kan ge längre tid mellan uppdateringar. Runt 40% stryps positionsuppdateringar. Här i EU gäller dessutom max 10% sändtid per timme som kan pausa sändningar helt.
<div class="container my-3 mx-0" style="max-width: 1000px;">
<div class="row text-center px-0">
<div class="col-md-6">
<h5 class="text-muted mb-3">LongFast</h5>
<div id="longfastGaugeContainer" class="stats-chart-container" style="height: 300px;">
<canvas id="longfastGauge"></canvas>
</div>
<div id="longfastLegend" class="mt-3" style="font-size: 0.85rem;"></div>
</div>
<div class="col-md-6">
<h5 class="text-muted mb-3">MediumFast</h5>
<div id="mediumfastGaugeContainer" class="stats-chart-container" style="height: 300px;">
<canvas id="mediumfastGauge"></canvas>
</div>
<div id="mediumfastLegend" class="mt-3" style="font-size: 0.85rem;"></div>
</div>
</div>
</div>
## Skapade paket per nod
Grafen nedan visar vilka noder som har skickat flest paket under det senaste dygnet.
Graferna bygger på MQTT-data från ett begränsat antal noder.
@@ -143,6 +166,7 @@ Graferna baseras på data från [map.sthlm-mesh.se](https://map.sthlm-mesh.se),
<script src="/js/status/firmware-versions.js"></script>
<script src="/js/status/battery-stats.js"></script>
<script src="/js/status/channel-utilization.js"></script>
<script src="/js/status/channel-utilization-gauges.js"></script>
<script src="/js/status/is-unmessagable-chart.js"></script>
<script src="/js/status/is-ok-to-mqtt-chart.js"></script>
<script src="/js/status/max-hops-chart.js"></script>
@@ -0,0 +1,199 @@
async function channelUtilizationGauges() {
try {
await fetchNodes();
// Create gauges for both channels
await createChannelUtilizationGauge('LongFast', 'longfastGauge');
await createChannelUtilizationGauge('MediumFast', 'mediumfastGauge');
} catch (err) {
console.error('Error creating channel utilization gauges:', err);
// Show error on both canvases
showGaugeError('longfastGauge');
showGaugeError('mediumfastGauge');
}
}
async function createChannelUtilizationGauge(channelName, canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
console.warn(`Canvas ${canvasId} not found`);
return;
}
const ctx = canvas.getContext('2d');
// Show loading
ctx.font = '16px Arial';
ctx.fillStyle = 'gray';
ctx.textAlign = 'center';
ctx.fillText('Loading...', canvas.width/2, canvas.height/2);
// Filter nodes by channel_id, recent updates (last 24 hours), and valid channel utilization
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const channelNodes = nodes.filter(node =>
node.channel_id === channelName &&
node.channel_utilization != null &&
node.channel_utilization !== '' &&
node.updated_at &&
new Date(node.updated_at) >= twentyFourHoursAgo
);
if (channelNodes.length === 0) {
showNoDataMessage(ctx, canvas, `No ${channelName} data`);
return;
}
// Get top 10 nodes with highest channel utilization for this channel
const top10Nodes = channelNodes
.map(node => ({
...node,
utilizationValue: parseFloat(node.channel_utilization)
}))
.sort((a, b) => b.utilizationValue - a.utilizationValue)
.slice(0, 10);
// Calculate average channel utilization for top 10 nodes only
const avgUtilization = top10Nodes.length > 0
? top10Nodes.reduce((sum, node) => sum + node.utilizationValue, 0) / top10Nodes.length
: 0;
// Create gauge chart using doughnut chart (0-30% scale)
const maxScale = 30;
const displayValue = Math.min(avgUtilization, maxScale); // Cap at 30%
new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [displayValue, maxScale - displayValue],
backgroundColor: [
getUtilizationColor(avgUtilization),
'rgba(220, 220, 220, 0.3)'
],
borderWidth: 0,
cutout: '70%'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
rotation: -90, // Start from top
circumference: 180, // Half circle
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
}
},
onComplete: function() {
// Add center text showing the percentage
const centerX = canvas.width / 2;
const centerY = canvas.height / 2 + 20; // Adjust for half circle
ctx.save();
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText(`${avgUtilization.toFixed(1)}%`, centerX, centerY);
ctx.font = '12px Arial';
ctx.fillStyle = '#666';
ctx.fillText(`Top ${top10Nodes.length} noder (24h)`, centerX, centerY + 25);
ctx.restore();
}
},
plugins: [{
afterDraw: function(chart) {
// Add center text showing the percentage
const centerX = chart.width / 2;
const centerY = chart.height / 2 + 20; // Adjust for half circle
ctx.save();
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText(`${avgUtilization.toFixed(1)}%`, centerX, centerY);
ctx.font = '12px Arial';
ctx.fillStyle = '#666';
ctx.fillText(`Top ${top10Nodes.length} noder (24h)`, centerX, centerY + 25);
ctx.restore();
}
}]
});
// Show top 10 nodes legend after chart is created
showTop10NodesLegend(channelName, top10Nodes);
}
function getUtilizationColor(utilization) {
if (utilization >= 25) {
return 'rgba(255, 0, 0, 0.8)';
} else if (utilization >= 20) {
return 'rgba(255, 69, 0, 0.8)';
} else if (utilization >= 15) {
return 'rgba(255, 140, 0, 0.8)';
} else if (utilization >= 10) {
return 'rgba(234, 184, 57, 0.8)';
} else {
return 'rgba(1, 163, 23, 0.8)';
}
}
function showNoDataMessage(ctx, canvas, message) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = '16px Arial';
ctx.fillStyle = 'gray';
ctx.textAlign = 'center';
ctx.fillText(message, canvas.width/2, canvas.height/2);
}
function showTop10NodesLegend(channelName, top10Nodes) {
const legendId = channelName.toLowerCase() + 'Legend';
const legendContainer = document.getElementById(legendId);
if (!legendContainer || top10Nodes.length === 0) return;
let legendHTML = '<h6 class="text-muted mb-2">Top 10 Nodes:</h6>';
legendHTML += '<div class="row">';
top10Nodes.forEach((node, index) => {
const utilizationColor = getUtilizationColor(node.utilizationValue);
const shortName = node.short_name || node.node_id_hex || `Node ${node.node_id}`;
legendHTML += `
<div class="col-6 mb-1 d-flex align-items-center">
<span class="badge me-2" style="background-color: ${utilizationColor}; color: white; font-size: 0.7rem; min-width: 35px;">
${node.utilizationValue.toFixed(1)}%
</span>
<span class="text-truncate" style="font-size: 0.75rem;" title="${node.long_name || shortName}">
${shortName}
</span>
</div>
`;
});
legendHTML += '</div>';
legendContainer.innerHTML = legendHTML;
}
function showGaugeError(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = '16px Arial';
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.fillText('Error loading data', canvas.width/2, canvas.height/2);
}
// Initialize the gauges when the page loads
channelUtilizationGauges();