added rx Quality

This commit is contained in:
2026-06-15 11:17:10 +03:00
parent e20b81c817
commit 23eb7ffb91
7 changed files with 100 additions and 24 deletions
+55 -12
View File
@@ -474,6 +474,36 @@
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
const RADIO_STATIC_KEY = 'radioStaticOpen';
function isRadioStaticOpen(container) {
if (container) {
const d = container.querySelector('details');
if (d) return d.open;
}
try {
return sessionStorage.getItem(RADIO_STATIC_KEY) === '1';
} catch (e) {
return false;
}
}
function setPanelHtml(container, html) {
if (!container) return;
const wasOpen = isRadioStaticOpen(container);
container.innerHTML = html;
const d = container.querySelector('details');
if (d && wasOpen) d.open = true;
}
document.addEventListener('toggle', e => {
if (!(e.target instanceof HTMLDetailsElement)) return;
if (!e.target.closest('#stats, #mapModalBody, #timelineStats, #cmdCurrentValues')) return;
try {
sessionStorage.setItem(RADIO_STATIC_KEY, e.target.open ? '1' : '0');
} catch (err) {}
}, true);
function telemetryToSnap(r) {
return RadioUI.parseRadioSnapshot(r.meta, r.role, r.rssi);
}
@@ -495,14 +525,21 @@
const chRx = RadioUI.diffSnapshots(prevTimelineRxSnap, rxSnap);
prevTimelineTxSnap = txSnap;
prevTimelineRxSnap = rxSnap;
return RadioUI.renderCompareGrid(txSnap, rxSnap, txId, rxId, chTx, chRx);
return RadioUI.renderCompareGrid(
txSnap, rxSnap, txId, rxId, chTx, chRx,
isRadioStaticOpen(document.getElementById('timelineStats'))
);
}
function fillCmdFormFromDevice(d) {
if (!d) return;
const snap = RadioUI.parseRadioSnapshot(d.meta, d.role, d.rssi);
document.getElementById('cmdCurrentValues').innerHTML =
RadioUI.formatRadioPanel(snap, new Set());
RadioUI.formatRadioPanel(
snap,
new Set(),
isRadioStaticOpen(document.getElementById('cmdCurrentValues'))
);
if (snap.frequencyMhz != null) {
document.getElementById('cmdFq').value = snap.frequencyMhz.toFixed(3);
}
@@ -1474,14 +1511,14 @@
function openMapModal(html, mode) {
if (mode) modalMode = mode;
mapModalBody.innerHTML = html;
setPanelHtml(mapModalBody, html);
mapModal.classList.add('open');
loadModalPosition();
}
function syncModalHtml(html) {
if (!isModalOpen()) return;
mapModalBody.innerHTML = html;
setPanelHtml(mapModalBody, html);
}
function closeMapModal() {
@@ -1703,12 +1740,13 @@
const txTel = nearestTelemetry(telemetryTx, t);
const rxTel = nearestTelemetry(telemetryRx, t);
document.getElementById('timelineStats').innerHTML = renderTimelineCompare(
const timelineStatsEl = document.getElementById('timelineStats');
setPanelHtml(timelineStatsEl, renderTimelineCompare(
txTel,
rxTel,
loadedTxTrack?.device_id,
loadedRxTrack?.device_id
);
));
drawElevationChart({
tx: trackDistanceAtTime(loadedTxTrack, t),
rx: trackDistanceAtTime(loadedRxTrack, t)
@@ -1735,7 +1773,7 @@
html += `${pos.lat.toFixed(5)}, ${pos.lon.toFixed(5)}<br>`;
const tel = nearestTelemetry(telemetrySingle, t);
const snap = tel ? telemetryToSnap(tel) : RadioUI.parseRadioSnapshot(pos.meta);
html += RadioUI.formatRadioPanel(snap, new Set());
html += RadioUI.formatRadioPanel(snap, new Set(), isRadioStaticOpen(mapModalBody));
if (tel) html += '<br>' + formatTelemetryRow(tel, new Set());
if (openModal || (isModalOpen() && modalMode === 'timeline')) {
openMapModal(html, 'timeline');
@@ -1743,9 +1781,13 @@
}
const tel = nearestTelemetry(telemetrySingle, t);
const snap = tel ? telemetryToSnap(tel) : RadioUI.parseRadioSnapshot(null);
document.getElementById('timelineStats').innerHTML = tel
? RadioUI.formatRadioPanel(snap, new Set())
: '<span class="muted">нет данных</span>';
const timelineStatsEl = document.getElementById('timelineStats');
setPanelHtml(
timelineStatsEl,
tel
? RadioUI.formatRadioPanel(snap, new Set(), isRadioStaticOpen(timelineStatsEl))
: '<span class="muted">нет данных</span>'
);
drawElevationChart({ single: trackDistanceAtTime(track, t) });
}
@@ -2147,7 +2189,8 @@
const snap = RadioUI.parseRadioSnapshot(d.meta, d.role, d.rssi);
const changed = RadioUI.diffSnapshots(prevDeviceSnap, snap);
prevDeviceSnap = snap;
let html = RadioUI.formatRadioPanel(snap, changed);
const statsEl = document.getElementById('stats');
let html = RadioUI.formatRadioPanel(snap, changed, isRadioStaticOpen(statsEl));
html += `<b>${escapeHtml(d.device_id)}</b><br>Range: ${d.range_m ?? '—'} m<br>`;
if (d.lat != null && d.lon != null && !isNullIsland(d.lat, d.lon)) {
html += `GPS: ${d.lat.toFixed(5)}, ${d.lon.toFixed(5)}<br>`;
@@ -2165,7 +2208,7 @@
function updateStatsPanel(d, openModal) {
const html = buildDeviceStatsHtml(d);
document.getElementById('stats').innerHTML = html;
setPanelHtml(document.getElementById('stats'), html);
if (openModal) {
openMapModal(html, 'device');
} else if (isModalOpen() && modalMode === 'device' && selectedId === d.device_id) {
+19 -7
View File
@@ -5,7 +5,7 @@
const KNOWN_LABELS = new Set([
'send', 'receive', 'frequency', 'power', 'rssi', 'snr',
'spreading factor', 'bandwidth', 'packet', 'packet number', 'payload',
'on air', 'tx speed', 'rx speed', 'per'
'on air', 'tx speed', 'rx speed', 'per', 'rx quality'
]);
function roleLabel(role) {
@@ -38,6 +38,7 @@
txPktPerS: null,
rxPktPerS: null,
perPercent: null,
rxQualityPercent: null,
extraFields: {}
};
if (!meta) return snap;
@@ -59,9 +60,15 @@
if (o.tx_pkt_per_s != null) snap.txPktPerS = Number(o.tx_pkt_per_s);
if (o.rx_pkt_per_s != null) snap.rxPktPerS = Number(o.rx_pkt_per_s);
if (o.per_percent != null) snap.perPercent = Number(o.per_percent);
if (o.rx_quality_percent != null) snap.rxQualityPercent = Number(o.rx_quality_percent);
if (o.fields && typeof o.fields === 'object') {
for (const [k, v] of Object.entries(o.fields)) {
if (!isKnownLabel(k)) snap.extraFields[k] = String(v);
const nk = String(k).toLowerCase().trim();
if (snap.rxQualityPercent == null && nk.includes('rx quality')) {
const n = parseFloat(String(v).replace('%', '').trim());
if (!Number.isNaN(n)) snap.rxQualityPercent = n;
}
}
}
return snap;
@@ -70,9 +77,10 @@
function diffSnapshots(a, b) {
const changed = new Set();
if (!a || !b) return changed;
const keys = ['role', 'rssiDbm', 'snrDb', 'packet', 'payload', 'perPercent',
const keys = ['role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent',
'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm'];
const map = { role: 'role', rssiDbm: 'rssi', snrDb: 'snr', packet: 'packet',
const map = { role: 'role', rssiDbm: 'rssi', snrDb: 'snr', rxQualityPercent: 'rxQuality',
packet: 'packet',
payload: 'payload', perPercent: 'per', txPktPerS: 'txSpeed', rxPktPerS: 'rxSpeed',
frequencyMhz: 'frequency', sf: 'sf', bwKhz: 'bw', powerDbm: 'power' };
for (const k of keys) {
@@ -84,6 +92,7 @@
const DYNAMIC_ROWS = [
{ key: 'rssi', label: 'RSSI', fmt: s => s.rssiDbm != null ? `${s.rssiDbm} dBm` : '—' },
{ key: 'snr', label: 'SNR', fmt: s => s.snrDb != null ? `${s.snrDb} dB` : '—' },
{ key: 'rxQuality', label: 'RX Quality', fmt: s => s.rxQualityPercent != null ? `${s.rxQualityPercent} %` : '—' },
{ key: 'packet', label: 'Пакет', fmt: s => s.packet != null ? String(s.packet) : '—' },
{ key: 'payload', label: 'Payload', fmt: s => s.payload || '—' },
{ key: 'per', label: 'PER', fmt: s => s.perPercent != null ? `${s.perPercent} %` : '—' },
@@ -105,7 +114,7 @@
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderCompareGrid(txSnap, rxSnap, txId, rxId, changedTx, changedRx) {
function renderCompareGrid(txSnap, rxSnap, txId, rxId, changedTx, changedRx, staticOpen) {
let html = '<div class="radio-compare-grid">';
html += `<div class="radio-compare-head"><span class="legend-tx">TX</span> ${escapeHtml(txId || '—')}`;
html += `<span class="legend-rx">RX</span> ${escapeHtml(rxId || '—')}</div>`;
@@ -116,7 +125,7 @@
html += `<span class="radio-tx${txCls}">${escapeHtml(row.fmt(txSnap))}</span>`;
html += `<span class="radio-rx${rxCls}">${escapeHtml(row.fmt(rxSnap))}</span></div>`;
}
html += '<details class="radio-static"><summary>Статика</summary>';
html += `<details class="radio-static"${staticOpen ? ' open' : ''}><summary>Статика</summary>`;
for (const row of STATIC_ROWS) {
const txCls = changedTx && changedTx.has(row.key) ? ' changed' : '';
const rxCls = changedRx && changedRx.has(row.key) ? ' changed' : '';
@@ -128,7 +137,7 @@
return html;
}
function formatRadioPanel(snap, changed) {
function formatRadioPanel(snap, changed, staticOpen) {
if (!snap) return '—';
const ch = changed || new Set();
let html = '';
@@ -136,7 +145,10 @@
const cls = ch.has(row.key) ? ' class="changed"' : '';
html += `<div${cls}><b>${row.label}:</b> ${escapeHtml(row.fmt(snap))}</div>`;
}
html += '<details><summary>Статика</summary>';
for (const [label, value] of Object.entries(snap.extraFields || {})) {
html += `<div><b>${escapeHtml(label)}:</b> ${escapeHtml(value)}</div>`;
}
html += `<details class="radio-static"${staticOpen ? ' open' : ''}><summary>Статика</summary>`;
for (const row of STATIC_ROWS) {
const cls = ch.has(row.key) ? ' class="changed"' : '';
html += `<div${cls}><b>${row.label}:</b> ${escapeHtml(row.fmt(snap))}</div>`;