mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-11-10 07:42:22 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57f7f0d8a | ||
|
|
d934f511aa | ||
|
|
e17d10099a | ||
|
|
549bdf5404 |
No files matched your search
@@ -14,6 +14,7 @@ The project serves as a real-time monitoring and diagnostic tool for the Meshtas
|
||||
- Browser assets: serve pinned JavaScript and CSS libraries locally, including Leaflet, ECharts, Bootstrap, and QRCode.js, instead of loading them from CDNs. Map tiles still require access to their providers.
|
||||
- Reliability and privacy: improved handling of malformed packets, unknown protobuf enums, invalid numeric API parameters, and secondary MQTT decryption keys; bounded traceroute rendering; escaped chart and telemetry content; and hardened CSV exports.
|
||||
- Meshtastic and API: refreshed the protobuf definitions, included available public keys in shared-contact QR codes, and updated the API documentation and generated browser reference.
|
||||
- Contributors: thanks to Nathan ([nullrouten0](https://github.com/nullrouten0)) for packet handling, security, CSV export, API validation, logging, and traceroute improvements ([#176](https://github.com/pablorevilla-meshtastic/meshview/pull/176)–[#182](https://github.com/pablorevilla-meshtastic/meshview/pull/182)), and Mike Benner for shared-contact public-key support ([#185](https://github.com/pablorevilla-meshtastic/meshview/pull/185)) and the Leaflet heatmap compatibility fix ([#188](https://github.com/pablorevilla-meshtastic/meshview/pull/188)).
|
||||
|
||||
**Upgrading:** Back up your database, update the application and dependencies, then restart both the database writer and web server (or use `mvrun.py`). The writer applies the new altitude-column and packet-index migrations automatically. Reload browser pages to load the updated assets. See [the migration guide](docs/Database-Changes-With-Alembic.md) and [Docker instructions](README-Docker.md) for your installation.
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
"battery_voltage": "Battery & Voltage",
|
||||
"air_channel": "Air & Channel Utilization",
|
||||
"environment": "Environment Metrics",
|
||||
"power": "Power Metrics",
|
||||
"neighbors_chart": "Neighbors (Signal-to-Noise)",
|
||||
"neighbors_chart_subtitle": "Last {days} days — {n} neighbors",
|
||||
"neighbors_chart_subtitle_capped": "Last {days} days — the {n} neighbors with the most data points, of {total}",
|
||||
@@ -444,6 +445,8 @@
|
||||
"Channel:": "Channel:",
|
||||
"Chat messages API": "Chat messages API",
|
||||
"Chat uses": "Chat uses",
|
||||
"Ch{n} current (mA)": "Ch{n} current (mA)",
|
||||
"Ch{n} voltage (V)": "Ch{n} voltage (V)",
|
||||
"Click to view text packets": "Click to view text packets",
|
||||
"Close": "Close",
|
||||
"Close expanded chart": "Close expanded chart",
|
||||
@@ -471,6 +474,8 @@
|
||||
"Drag nodes to rearrange · Scroll to zoom": "Drag nodes to rearrange · Scroll to zoom",
|
||||
"Elevated": "Elevated",
|
||||
"Enter a valid node ID or select a node.": "Enter a valid node ID or select a node.",
|
||||
"Environment current (mA)": "Environment current (mA)",
|
||||
"Environment voltage (V)": "Environment voltage (V)",
|
||||
"Error": "Error",
|
||||
"Error Details": "Error Details",
|
||||
"Error loading nodes": "Error loading nodes",
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
"battery_voltage": "Batería y voltaje",
|
||||
"air_channel": "Utilización del aire y del canal",
|
||||
"environment": "Métricas Ambientales",
|
||||
"power": "Métricas de potencia",
|
||||
"neighbors_chart": "Vecinos (Relación Señal/Ruido)",
|
||||
"neighbors_chart_subtitle": "Últimos {days} días — {n} vecinos",
|
||||
"neighbors_chart_subtitle_capped": "Últimos {days} días — los {n} vecinos con más datos, de {total}",
|
||||
@@ -452,6 +453,8 @@
|
||||
"Channel:": "Canal:",
|
||||
"Chat messages API": "API de mensajes de chat",
|
||||
"Chat uses": "El chat usa",
|
||||
"Ch{n} current (mA)": "Corriente Ch{n} (mA)",
|
||||
"Ch{n} voltage (V)": "Voltaje Ch{n} (V)",
|
||||
"Click to view text packets": "Pulse para ver los paquetes de texto",
|
||||
"Close": "Cerrar",
|
||||
"Close expanded chart": "Cerrar gráfico ampliado",
|
||||
@@ -479,6 +482,8 @@
|
||||
"Drag nodes to rearrange · Scroll to zoom": "Arrastre los nodos para reorganizar · Desplace para ampliar",
|
||||
"Elevated": "Elevada",
|
||||
"Enter a valid node ID or select a node.": "Introduzca un ID válido o seleccione un nodo.",
|
||||
"Environment current (mA)": "Corriente ambiental (mA)",
|
||||
"Environment voltage (V)": "Voltaje ambiental (V)",
|
||||
"Error": "Error",
|
||||
"Error Details": "Detalles del error",
|
||||
"Error loading nodes": "Error al cargar nodos",
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
"battery_voltage": "Батарея и напряжение",
|
||||
"air_channel": "Использование сети и каналов",
|
||||
"environment": "Метрики среды",
|
||||
"power": "Метрики питания",
|
||||
"neighbors_chart": "Соседи (Соотношение сигнал/шум)",
|
||||
"neighbors_chart_subtitle": "Последние {days} дн. — {n} соседей",
|
||||
"neighbors_chart_subtitle_capped": "Последние {days} дн. — {n} соседей с наибольшим числом точек, из {total}",
|
||||
@@ -444,6 +445,8 @@
|
||||
"Channel:": "Канал:",
|
||||
"Chat messages API": "API сообщений чата",
|
||||
"Chat uses": "Чат использует",
|
||||
"Ch{n} current (mA)": "Ток Ch{n} (мА)",
|
||||
"Ch{n} voltage (V)": "Напряжение Ch{n} (В)",
|
||||
"Click to view text packets": "Нажмите для просмотра текстовых пакетов",
|
||||
"Close": "Закрыть",
|
||||
"Close expanded chart": "Закрыть развёрнутый график",
|
||||
@@ -471,6 +474,8 @@
|
||||
"Drag nodes to rearrange · Scroll to zoom": "Перетаскивайте узлы · Масштабируйте прокруткой",
|
||||
"Elevated": "Повышенная",
|
||||
"Enter a valid node ID or select a node.": "Введите корректный ID или выберите узел.",
|
||||
"Environment current (mA)": "Ток (среда) (мА)",
|
||||
"Environment voltage (V)": "Напряжение (среда) (В)",
|
||||
"Error": "Ошибка",
|
||||
"Error Details": "Подробности ошибки",
|
||||
"Error loading nodes": "Ошибка загрузки нод",
|
||||
|
||||
+124
-24
@@ -139,6 +139,18 @@
|
||||
<div id="chart_environment" style="height:380px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Power Metrics -->
|
||||
<div id="power_chart_container" class="chart-container" style="display:none;">
|
||||
<div class="chart-header">
|
||||
⚡ <span data-translate-lang="power">Power Metrics</span>
|
||||
<div class="chart-actions">
|
||||
<button onclick="expandChart('power')" data-translate-lang="expand">Expand</button>
|
||||
<button onclick="exportCSV('power')" data-translate-lang="export_csv">Export CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart_power" style="height:380px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Neighbor chart -->
|
||||
<!-- Neighbor Time-Series Chart -->
|
||||
<div id="neighbor_chart_container" class="chart-container">
|
||||
@@ -874,7 +886,30 @@ async function loadTelemetryCharts(){
|
||||
times: [],
|
||||
battery: [], voltage: [],
|
||||
airUtil: [], chanUtil: [],
|
||||
temperature: [], humidity: [], pressure: []
|
||||
temperature: [], humidity: [], pressure: [],
|
||||
// power_metrics channels (ch1..ch8) plus the legacy voltage/current
|
||||
// that INA sensors report inside environment_metrics.
|
||||
power: {}, powerSeries: []
|
||||
};
|
||||
const POWER_CHANNELS = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
for (const ch of POWER_CHANNELS) {
|
||||
chartData.power[`ch${ch}_voltage`] = [];
|
||||
chartData.power[`ch${ch}_current`] = [];
|
||||
}
|
||||
chartData.power.env_voltage = [];
|
||||
chartData.power.env_current = [];
|
||||
|
||||
// Return the body of a "name { ... }" block from the text-format payload.
|
||||
// Several telemetry variants share field names (voltage, temperature...),
|
||||
// so each value must be read only from its own block.
|
||||
const metricsBlock = (pl, name) =>
|
||||
pl.match(new RegExp(`(?:^|\\n)\\s*${name}\\s*\\{([^}]*)\\}`))?.[1] || "";
|
||||
|
||||
// Read a numeric field by its exact name (line-anchored, so "voltage"
|
||||
// does not match "ch1_voltage").
|
||||
const metricField = (block, name) => {
|
||||
const m = block.match(new RegExp(`(?:^|\\n)\\s*${name}:\\s*(-?[\\d.]+(?:[eE][-+]?\\d+)?)`));
|
||||
return m ? parseFloat(m[1]) : NaN;
|
||||
};
|
||||
|
||||
for (const pkt of packets.reverse()) {
|
||||
@@ -884,28 +919,24 @@ async function loadTelemetryCharts(){
|
||||
t.toLocaleString(MeshI18n.language, { month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit" })
|
||||
);
|
||||
|
||||
// Matches your payload exactly (inside device_metrics {})
|
||||
chartData.battery.push(
|
||||
parseFloat(pl.match(/battery_level:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.voltage.push(
|
||||
parseFloat(pl.match(/voltage:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.airUtil.push(
|
||||
parseFloat(pl.match(/air_util_tx:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.chanUtil.push(
|
||||
parseFloat(pl.match(/channel_utilization:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.temperature.push(
|
||||
parseFloat(pl.match(/temperature:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.humidity.push(
|
||||
parseFloat(pl.match(/relative_humidity:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
chartData.pressure.push(
|
||||
parseFloat(pl.match(/barometric_pressure:\s*([\d.]+)/)?.[1] || NaN)
|
||||
);
|
||||
const dev = metricsBlock(pl, "device_metrics");
|
||||
const env = metricsBlock(pl, "environment_metrics");
|
||||
const pwr = metricsBlock(pl, "power_metrics");
|
||||
|
||||
for (const ch of POWER_CHANNELS) {
|
||||
chartData.power[`ch${ch}_voltage`].push(metricField(pwr, `ch${ch}_voltage`));
|
||||
chartData.power[`ch${ch}_current`].push(metricField(pwr, `ch${ch}_current`));
|
||||
}
|
||||
chartData.power.env_voltage.push(metricField(env, "voltage"));
|
||||
chartData.power.env_current.push(metricField(env, "current"));
|
||||
|
||||
chartData.battery.push(metricField(dev, "battery_level"));
|
||||
chartData.voltage.push(metricField(dev, "voltage"));
|
||||
chartData.airUtil.push(metricField(dev, "air_util_tx"));
|
||||
chartData.chanUtil.push(metricField(dev, "channel_utilization"));
|
||||
chartData.temperature.push(metricField(env, "temperature"));
|
||||
chartData.humidity.push(metricField(env, "relative_humidity"));
|
||||
chartData.pressure.push(metricField(env, "barometric_pressure"));
|
||||
}
|
||||
|
||||
const hasBattery = chartData.battery.some(v => !isNaN(v));
|
||||
@@ -1015,9 +1046,73 @@ async function loadTelemetryCharts(){
|
||||
envContainer.style.display = "none";
|
||||
}
|
||||
|
||||
// Power chart: one voltage (left axis) and one current (right axis) series
|
||||
// per channel that reported data. Current is dashed and unfilled so up to
|
||||
// 18 series stay readable.
|
||||
const POWER_COLORS = [
|
||||
'rgba(79,155,255,1)', 'rgba(255,214,82,1)', 'rgba(138,255,108,1)',
|
||||
'rgba(255,102,204,1)', 'rgba(255,138,82,1)', 'rgba(0,212,212,1)',
|
||||
'rgba(178,132,255,1)', 'rgba(200,200,200,1)'
|
||||
];
|
||||
const ENV_POWER_COLOR = 'rgba(255,80,80,1)';
|
||||
const powerSources = [
|
||||
...POWER_CHANNELS.map(ch => ({
|
||||
key: `ch${ch}`,
|
||||
color: POWER_COLORS[ch - 1],
|
||||
voltageLabel: MeshI18n.t("Ch{n} voltage (V)", {n: ch}),
|
||||
currentLabel: MeshI18n.t("Ch{n} current (mA)", {n: ch})
|
||||
})),
|
||||
{
|
||||
key: "env",
|
||||
color: ENV_POWER_COLOR,
|
||||
voltageLabel: MeshI18n.t("Environment voltage (V)"),
|
||||
currentLabel: MeshI18n.t("Environment current (mA)")
|
||||
}
|
||||
];
|
||||
const hasData = values => values.some(v => !isNaN(v));
|
||||
for (const src of powerSources) {
|
||||
const voltage = chartData.power[`${src.key}_voltage`];
|
||||
const current = chartData.power[`${src.key}_current`];
|
||||
if (hasData(voltage))
|
||||
chartData.powerSeries.push({ label: src.voltageLabel, color: src.color, data: voltage, axis: 0 });
|
||||
if (hasData(current))
|
||||
chartData.powerSeries.push({ label: src.currentLabel, color: src.color, data: current, axis: 1 });
|
||||
}
|
||||
|
||||
const powerContainer = document.getElementById("power_chart_container");
|
||||
let chart4 = null;
|
||||
if (chartData.powerSeries.length) {
|
||||
powerContainer.style.display = "block";
|
||||
chart4 = initMeshChart(document.getElementById('chart_power'));
|
||||
chart4.setOption({
|
||||
tooltip: { trigger:'axis' },
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
data: chartData.powerSeries.map(s => s.label),
|
||||
textStyle:{ color:'#64748b' }
|
||||
},
|
||||
xAxis: { type:'category', data:chartData.times, axisLabel:{ color:'#64748b' } },
|
||||
yAxis: [
|
||||
{ type:'value', name:'Voltage (V)', axisLabel:{ color:'#64748b' } },
|
||||
{ type:'value', name:'Current (mA)', axisLabel:{ color:'#64748b' } }
|
||||
],
|
||||
series: chartData.powerSeries.map(s => {
|
||||
const line = makeLine(s.label, s.color, s.data, s.axis);
|
||||
if (s.axis === 1) {
|
||||
delete line.areaStyle;
|
||||
line.lineStyle = { ...line.lineStyle, type: 'dashed' };
|
||||
line.symbol = 'triangle';
|
||||
}
|
||||
return line;
|
||||
})
|
||||
});
|
||||
} else {
|
||||
powerContainer.style.display = "none";
|
||||
}
|
||||
|
||||
// Resize charts that exist
|
||||
window.addEventListener("resize", () => {
|
||||
[chart1, chart2, chart3].forEach(c => { if (c) c.resize(); });
|
||||
[chart1, chart2, chart3, chart4].forEach(c => { if (c) c.resize(); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1390,6 +1485,11 @@ function exportCSV(type){
|
||||
chartData.pressure[i]
|
||||
]);
|
||||
}
|
||||
else if (type === "power") {
|
||||
rows[0].push(...chartData.powerSeries.map(s => s.label));
|
||||
for (let i = 0; i < chartData.times.length; i++)
|
||||
rows.push([chartData.times[i], ...chartData.powerSeries.map(s => isNaN(s.data[i]) ? null : s.data[i])]);
|
||||
}
|
||||
else if (type === "neighbors") {
|
||||
rows[0] = [MeshI18n.t("Neighbor Node ID"), MeshI18n.t("Neighbor Name"), "SNR (dB)"];
|
||||
for (let i = 0; i < neighborData.ids.length; i++) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 804 KiB After Width: | Height: | Size: 1.8 MiB |
Reference in new issue
Block a user