generated from Grigo/AndroidTemplate
added rx Quality
This commit is contained in:
@@ -29,6 +29,7 @@ public final class RadioSnapshot {
|
||||
public final Double txPktPerS;
|
||||
public final Double rxPktPerS;
|
||||
public final Double perPercent;
|
||||
public final Double rxQualityPercent;
|
||||
public final Map<String, String> extraFields;
|
||||
|
||||
public RadioSnapshot(
|
||||
@@ -46,6 +47,7 @@ public final class RadioSnapshot {
|
||||
Double txPktPerS,
|
||||
Double rxPktPerS,
|
||||
Double perPercent,
|
||||
Double rxQualityPercent,
|
||||
Map<String, String> extraFields
|
||||
) {
|
||||
this.role = role;
|
||||
@@ -62,12 +64,13 @@ public final class RadioSnapshot {
|
||||
this.txPktPerS = txPktPerS;
|
||||
this.rxPktPerS = rxPktPerS;
|
||||
this.perPercent = perPercent;
|
||||
this.rxQualityPercent = rxQualityPercent;
|
||||
this.extraFields = extraFields != null ? extraFields : Map.of();
|
||||
}
|
||||
|
||||
public static RadioSnapshot empty() {
|
||||
return new RadioSnapshot(null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, Map.of());
|
||||
null, null, null, null, null, null, null, Map.of());
|
||||
}
|
||||
|
||||
public static RadioSnapshot fromMeta(String metaJson, String roleFallback, Double rssiFallback) {
|
||||
@@ -75,7 +78,7 @@ public final class RadioSnapshot {
|
||||
RadioSnapshot snap = empty();
|
||||
if (roleFallback != null || rssiFallback != null) {
|
||||
return new RadioSnapshot(roleFallback, null, null, null, null, null,
|
||||
rssiFallback, null, null, null, null, null, null, null, Map.of());
|
||||
rssiFallback, null, null, null, null, null, null, null, null, Map.of());
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
@@ -115,11 +118,12 @@ public final class RadioSnapshot {
|
||||
dbl(o, "tx_pkt_per_s"),
|
||||
dbl(o, "rx_pkt_per_s"),
|
||||
dbl(o, "per_percent"),
|
||||
dbl(o, "rx_quality_percent"),
|
||||
extra
|
||||
);
|
||||
} catch (Exception ignored) {
|
||||
return new RadioSnapshot(roleFallback, null, null, null, null, null,
|
||||
rssiFallback, null, null, null, null, null, null, null, Map.of());
|
||||
rssiFallback, null, null, null, null, null, null, null, null, Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +145,7 @@ public final class RadioSnapshot {
|
||||
cmp(changed, "packet", packet, prev.packet);
|
||||
cmp(changed, "payload", payload, prev.payload);
|
||||
cmp(changed, "per", perPercent, prev.perPercent);
|
||||
cmp(changed, "rxQuality", rxQualityPercent, prev.rxQualityPercent);
|
||||
cmp(changed, "txSpeed", txPktPerS, prev.txPktPerS);
|
||||
cmp(changed, "rxSpeed", rxPktPerS, prev.rxPktPerS);
|
||||
cmp(changed, "frequency", frequencyMhz, prev.frequencyMhz);
|
||||
@@ -163,7 +168,7 @@ public final class RadioSnapshot {
|
||||
|| n.equals("snr") || n.contains("spreading") || n.contains("bandwidth")
|
||||
|| n.equals("packet") || n.contains("packet number") || n.equals("payload")
|
||||
|| n.contains("on air") || n.contains("tx speed") || n.contains("rx speed")
|
||||
|| n.equals("per");
|
||||
|| n.equals("per") || n.contains("rx quality");
|
||||
}
|
||||
|
||||
private static String text(JsonObject o, String key) {
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class LoraStatsFormatter {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendLine(sb, "RSSI", fmtDbm(s.rssiDbm), "rssi", changed);
|
||||
appendLine(sb, "SNR", fmtSuffix(s.snrDb, " dB"), "snr", changed);
|
||||
appendLine(sb, "RX Quality", fmtSuffix(s.rxQualityPercent, " %"), "rxQuality", changed);
|
||||
appendLine(sb, "Пакет", fmtInt(s.packet), "packet", changed);
|
||||
appendLine(sb, "Payload", s.payload, "payload", changed);
|
||||
appendLine(sb, "PER", fmtSuffix(s.perPercent, " %"), "per", changed);
|
||||
|
||||
@@ -33,6 +33,8 @@ public class StatsExtractor {
|
||||
private static final Pattern TX_SPEED = Pattern.compile("TX Speed\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RX_SPEED = Pattern.compile("RX Speed\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern PER = Pattern.compile("PER\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern RX_QUALITY = Pattern.compile(
|
||||
"RX Quality\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final Pattern rssiPattern;
|
||||
private final Pattern rangePattern;
|
||||
@@ -98,6 +100,7 @@ public class StatsExtractor {
|
||||
putDouble(meta, "tx_pkt_per_s", matchDouble(TX_SPEED, normalized));
|
||||
putDouble(meta, "rx_pkt_per_s", matchDouble(RX_SPEED, normalized));
|
||||
putDouble(meta, "per_percent", matchDouble(PER, normalized));
|
||||
putDouble(meta, "rx_quality_percent", matchDouble(RX_QUALITY, normalized));
|
||||
|
||||
if (!fields.isEmpty()) {
|
||||
meta.put("fields", fields);
|
||||
@@ -140,7 +143,7 @@ public class StatsExtractor {
|
||||
|| n.equals("snr") || n.contains("spreading factor") || n.equals("bandwidth")
|
||||
|| n.equals("packet") || n.contains("packet number") || n.equals("payload")
|
||||
|| n.contains("on air") || n.contains("tx speed") || n.contains("rx speed")
|
||||
|| n.equals("per");
|
||||
|| n.equals("per") || n.contains("rx quality");
|
||||
}
|
||||
|
||||
private static ExtractedStats empty(String frame) {
|
||||
|
||||
@@ -87,6 +87,8 @@ public class RadioComparePanel extends LinearLayout {
|
||||
if (dynamic) {
|
||||
addRow(table, "RSSI", fmtDbm(tx.rssiDbm), fmtDbm(rx.rssiDbm), "rssi", changedTx, changedRx);
|
||||
addRow(table, "SNR", fmtSuffix(tx.snrDb, " dB"), fmtSuffix(rx.snrDb, " dB"), "snr", changedTx, changedRx);
|
||||
addRow(table, "RX Quality", fmtSuffix(tx.rxQualityPercent, " %"), fmtSuffix(rx.rxQualityPercent, " %"),
|
||||
"rxQuality", changedTx, changedRx);
|
||||
addRow(table, "Пакет", fmtInt(tx.packet), fmtInt(rx.packet), "packet", changedTx, changedRx);
|
||||
addRow(table, "Payload", str(tx.payload), str(rx.payload), "payload", changedTx, changedRx);
|
||||
addRow(table, "PER", fmtSuffix(tx.perPercent, " %"), fmtSuffix(rx.perPercent, " %"), "per", changedTx, changedRx);
|
||||
|
||||
@@ -86,6 +86,16 @@ public class LoraFrameExtractTest {
|
||||
assertTrue(stats.metaJson.contains("\"fields\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsesRxQualityPercent() {
|
||||
StatsExtractor extractor = StatsExtractor.withDefaults();
|
||||
String frame = RECEIVE_FRAME + " RX Quality: 87 %\n";
|
||||
StatsExtractor.ExtractedStats stats = extractor.extract(frame);
|
||||
|
||||
assertTrue(stats.metaJson.contains("\"rx_quality_percent\":87"));
|
||||
assertTrue(!stats.metaJson.contains("RX Quality"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitsTwoFramesByReceiveHeaderWithoutEsc() {
|
||||
List<String> frames = new ArrayList<>();
|
||||
|
||||
+55
-12
@@ -474,6 +474,36 @@
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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>`;
|
||||
|
||||
Reference in New Issue
Block a user