added more fields

This commit is contained in:
2026-06-16 12:53:43 +03:00
parent e71b6eed2f
commit 40a1ccab1e
8 changed files with 319 additions and 31 deletions
@@ -19,7 +19,7 @@ public final class RadioSnapshot {
public final String frame;
public final Double frequencyMhz;
public final Integer sf;
public final Integer bwKhz;
public final Double bwKhz;
public final Double powerDbm;
public final Double rssiDbm;
public final Double snrDb;
@@ -30,6 +30,18 @@ public final class RadioSnapshot {
public final Double rxPktPerS;
public final Double perPercent;
public final Double rxQualityPercent;
public final String codeRate;
public final Integer preambleLength;
public final String lowDataRateOpt;
public final Boolean crcEnabled;
public final Integer payloadLengthBytes;
public final Double txTimeoutMs;
public final Integer packetReceive;
public final Integer packetTotal;
public final Integer packetError;
public final Integer crcError;
public final Integer preambleDetected;
public final Integer headerValid;
public final Map<String, String> extraFields;
public RadioSnapshot(
@@ -37,7 +49,7 @@ public final class RadioSnapshot {
String frame,
Double frequencyMhz,
Integer sf,
Integer bwKhz,
Double bwKhz,
Double powerDbm,
Double rssiDbm,
Double snrDb,
@@ -48,6 +60,18 @@ public final class RadioSnapshot {
Double rxPktPerS,
Double perPercent,
Double rxQualityPercent,
String codeRate,
Integer preambleLength,
String lowDataRateOpt,
Boolean crcEnabled,
Integer payloadLengthBytes,
Double txTimeoutMs,
Integer packetReceive,
Integer packetTotal,
Integer packetError,
Integer crcError,
Integer preambleDetected,
Integer headerValid,
Map<String, String> extraFields
) {
this.role = role;
@@ -65,12 +89,26 @@ public final class RadioSnapshot {
this.rxPktPerS = rxPktPerS;
this.perPercent = perPercent;
this.rxQualityPercent = rxQualityPercent;
this.codeRate = codeRate;
this.preambleLength = preambleLength;
this.lowDataRateOpt = lowDataRateOpt;
this.crcEnabled = crcEnabled;
this.payloadLengthBytes = payloadLengthBytes;
this.txTimeoutMs = txTimeoutMs;
this.packetReceive = packetReceive;
this.packetTotal = packetTotal;
this.packetError = packetError;
this.crcError = crcError;
this.preambleDetected = preambleDetected;
this.headerValid = headerValid;
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, null, Map.of());
null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
Map.of());
}
public static RadioSnapshot fromMeta(String metaJson, String roleFallback, Double rssiFallback) {
@@ -78,7 +116,9 @@ 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, null, Map.of());
rssiFallback, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
Map.of());
}
return snap;
}
@@ -108,7 +148,7 @@ public final class RadioSnapshot {
text(o, "frame"),
hzToMhz(lng(o, "frequency_hz")),
integer(o, "spreading_factor"),
integer(o, "bandwidth_khz"),
dbl(o, "bandwidth_khz"),
dbl(o, "power_dbm"),
rssi,
dbl(o, "snr_db"),
@@ -119,11 +159,25 @@ public final class RadioSnapshot {
dbl(o, "rx_pkt_per_s"),
dbl(o, "per_percent"),
dbl(o, "rx_quality_percent"),
text(o, "code_rate"),
integer(o, "preamble_length"),
text(o, "low_data_rate_opt"),
bool(o, "crc_enabled"),
integer(o, "payload_length_bytes"),
dbl(o, "tx_timeout_ms"),
integer(o, "packet_receive"),
integer(o, "packet_total"),
integer(o, "packet_error"),
integer(o, "crc_error"),
integer(o, "preamble_detected"),
integer(o, "header_valid"),
extra
);
} catch (Exception ignored) {
return new RadioSnapshot(roleFallback, null, null, null, null, null,
rssiFallback, null, null, null, null, null, null, null, null, Map.of());
rssiFallback, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
Map.of());
}
}
@@ -152,6 +206,14 @@ public final class RadioSnapshot {
cmp(changed, "sf", sf, prev.sf);
cmp(changed, "bw", bwKhz, prev.bwKhz);
cmp(changed, "power", powerDbm, prev.powerDbm);
cmp(changed, "packetReceive", packetReceive, prev.packetReceive);
cmp(changed, "packetTotal", packetTotal, prev.packetTotal);
cmp(changed, "packetError", packetError, prev.packetError);
cmp(changed, "crcError", crcError, prev.crcError);
cmp(changed, "preambleDetected", preambleDetected, prev.preambleDetected);
cmp(changed, "headerValid", headerValid, prev.headerValid);
cmp(changed, "codeRate", codeRate, prev.codeRate);
cmp(changed, "crc", crcEnabled, prev.crcEnabled);
return changed;
}
@@ -167,8 +229,12 @@ public final class RadioSnapshot {
|| n.contains("frequency") || n.equals("power") || n.equals("rssi")
|| n.equals("snr") || n.contains("spreading") || n.contains("bandwidth")
|| n.equals("packet") || n.contains("packet number") || n.equals("payload")
|| n.contains("packet receive") || n.contains("packet total") || n.contains("packet error")
|| n.contains("crc error") || n.contains("preamble detected") || n.contains("header valid")
|| n.contains("on air") || n.contains("tx speed") || n.contains("rx speed")
|| n.equals("per") || n.contains("rx quality");
|| n.equals("per") || n.contains("rx quality") || n.contains("tx timeout")
|| n.contains("code rate") || n.contains("preamble length")
|| n.contains("low data rate") || n.equals("crc") || n.contains("payload length");
}
private static String text(JsonObject o, String key) {
@@ -186,6 +252,11 @@ public final class RadioSnapshot {
return e != null && e.isJsonPrimitive() ? e.getAsDouble() : null;
}
private static Boolean bool(JsonObject o, String key) {
JsonElement e = o.get(key);
return e != null && e.isJsonPrimitive() ? e.getAsBoolean() : null;
}
private static Long lng(JsonObject o, String key) {
JsonElement e = o.get(key);
return e != null && e.isJsonPrimitive() ? e.getAsLong() : null;
@@ -40,6 +40,12 @@ public final class LoraStatsFormatter {
appendLine(sb, "Пакет", fmtInt(s.packet), "packet", changed);
appendLine(sb, "Payload", s.payload, "payload", changed);
appendLine(sb, "PER", fmtSuffix(s.perPercent, " %"), "per", changed);
appendLine(sb, "Принято", fmtInt(s.packetReceive), "packetReceive", changed);
appendLine(sb, "Всего пакетов", fmtInt(s.packetTotal), "packetTotal", changed);
appendLine(sb, "Ошибки пакетов", fmtInt(s.packetError), "packetError", changed);
appendLine(sb, "CRC Error", fmtInt(s.crcError), "crcError", changed);
appendLine(sb, "Preamble Det.", fmtInt(s.preambleDetected), "preambleDetected", changed);
appendLine(sb, "Header Valid", fmtInt(s.headerValid), "headerValid", changed);
appendLine(sb, "TX Speed", fmtSuffix(s.txPktPerS, " pkt/s"), "txSpeed", changed);
appendLine(sb, "RX Speed", fmtSuffix(s.rxPktPerS, " pkt/s"), "rxSpeed", changed);
for (Map.Entry<String, String> e : s.extraFields.entrySet()) {
@@ -58,8 +64,14 @@ public final class LoraStatsFormatter {
}
appendLine(sb, "Частота", fmtSuffix(s.frequencyMhz, " MHz"), "frequency", changed);
appendLine(sb, "SF", fmtInt(s.sf), "sf", changed);
appendLine(sb, "BW", fmtSuffix(s.bwKhz, " kHz"), "bw", changed);
appendLine(sb, "BW", fmtBw(s.bwKhz), "bw", changed);
appendLine(sb, "Мощность TX", fmtDbm(s.powerDbm), "power", changed);
appendLine(sb, "Code Rate", s.codeRate, "codeRate", changed);
appendLine(sb, "Preamble Len", fmtInt(s.preambleLength), "preambleLength", changed);
appendLine(sb, "Low DR Opt", s.lowDataRateOpt, "lowDataRateOpt", changed);
appendLine(sb, "CRC", fmtCrc(s.crcEnabled), "crc", changed);
appendLine(sb, "Payload len", fmtSuffix(s.payloadLengthBytes, " byte"), "payloadLength", changed);
appendLine(sb, "TX Timeout", fmtSuffix(s.txTimeoutMs, " ms"), "txTimeout", changed);
appendLine(sb, "On Air", fmtSuffix(s.onAirMs, " ms"), "onAir", changed);
return sb.toString().trim();
}
@@ -111,11 +123,22 @@ public final class LoraStatsFormatter {
return v != null ? String.valueOf(v) : null;
}
private static String fmtSuffix(Double v, String suffix) {
return v != null ? String.format(Locale.US, "%s%s", v, suffix) : null;
private static String fmtBw(Double v) {
return v != null ? String.format(Locale.US, "%.2f kHz", v) : null;
}
private static String fmtCrc(Boolean enabled) {
if (enabled == null) {
return null;
}
return enabled ? "On" : "Off";
}
private static String fmtSuffix(Integer v, String suffix) {
return v != null ? v + suffix : null;
}
private static String fmtSuffix(Double v, String suffix) {
return v != null ? String.format(Locale.US, "%s%s", v, suffix) : null;
}
}
@@ -25,9 +25,26 @@ public class StatsExtractor {
private static final Pattern SNR = Pattern.compile("SNR\\s*:\\s*(-?\\d+(?:\\.\\d+)?)", Pattern.CASE_INSENSITIVE);
private static final Pattern FREQUENCY = Pattern.compile("Frequency\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern SPREADING = Pattern.compile("Spreading Factor\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern BANDWIDTH = Pattern.compile("Bandwidth\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET = Pattern.compile("Packet\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern BANDWIDTH = Pattern.compile("Bandwidth\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET_TX = Pattern.compile("(?m)^\\s*Packet\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET_NUMBER = Pattern.compile("Packet Number\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET_RECEIVE = Pattern.compile("Packet Receive\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET_TOTAL = Pattern.compile("Packet Total\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PACKET_ERROR = Pattern.compile("Packet Error\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern CRC_ERROR = Pattern.compile("CRC Error\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PREAMBLE_DETECTED = Pattern.compile(
"Preamble Detected\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern HEADER_VALID = Pattern.compile("Header Valid\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern CODE_RATE = Pattern.compile("Code Rate\\s*:\\s*(\\S+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PREAMBLE_LENGTH = Pattern.compile(
"Preamble Length\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern LOW_DATA_RATE = Pattern.compile(
"Low Data Rate Opt\\s*:\\s*(\\S+)", Pattern.CASE_INSENSITIVE);
private static final Pattern CRC = Pattern.compile("CRC\\s*:\\s*(On|Off)", Pattern.CASE_INSENSITIVE);
private static final Pattern PAYLOAD_LENGTH = Pattern.compile(
"Payload length\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern TX_TIMEOUT = Pattern.compile(
"TX Timeout\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern PAYLOAD = Pattern.compile("Payload\\s*:\\s*(.+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ON_AIR = Pattern.compile("On Air\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern TX_SPEED = Pattern.compile("TX Speed\\s*:\\s*([\\d.]+)", Pattern.CASE_INSENSITIVE);
@@ -89,10 +106,10 @@ public class StatsExtractor {
putLong(meta, "frequency_hz", matchLong(FREQUENCY, normalized));
putInt(meta, "spreading_factor", matchInt(SPREADING, normalized));
putInt(meta, "bandwidth_khz", matchInt(BANDWIDTH, normalized));
putDouble(meta, "bandwidth_khz", matchDouble(BANDWIDTH, normalized));
Integer packet = matchInt(PACKET_NUMBER, normalized);
if (packet == null) {
packet = matchInt(PACKET, normalized);
packet = matchInt(PACKET_TX, normalized);
}
putInt(meta, "packet", packet);
putString(meta, "payload", matchString(PAYLOAD, normalized));
@@ -101,6 +118,18 @@ public class StatsExtractor {
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));
putString(meta, "code_rate", matchString(CODE_RATE, normalized));
putInt(meta, "preamble_length", matchInt(PREAMBLE_LENGTH, normalized));
putString(meta, "low_data_rate_opt", matchString(LOW_DATA_RATE, normalized));
putBool(meta, "crc_enabled", matchBool(CRC, normalized));
putInt(meta, "payload_length_bytes", matchInt(PAYLOAD_LENGTH, normalized));
putDouble(meta, "tx_timeout_ms", matchDouble(TX_TIMEOUT, normalized));
putInt(meta, "packet_receive", matchInt(PACKET_RECEIVE, normalized));
putInt(meta, "packet_total", matchInt(PACKET_TOTAL, normalized));
putInt(meta, "packet_error", matchInt(PACKET_ERROR, normalized));
putInt(meta, "crc_error", matchInt(CRC_ERROR, normalized));
putInt(meta, "preamble_detected", matchInt(PREAMBLE_DETECTED, normalized));
putInt(meta, "header_valid", matchInt(HEADER_VALID, normalized));
if (!fields.isEmpty()) {
meta.put("fields", fields);
@@ -144,8 +173,12 @@ public class StatsExtractor {
return n.equals("frequency") || n.equals("power") || n.equals("rssi")
|| n.equals("snr") || n.contains("spreading factor") || n.equals("bandwidth")
|| n.equals("packet") || n.contains("packet number") || n.equals("payload")
|| n.contains("packet receive") || n.contains("packet total") || n.contains("packet error")
|| n.contains("crc error") || n.contains("preamble detected") || n.contains("header valid")
|| n.contains("on air") || n.contains("tx speed") || n.contains("rx speed")
|| n.equals("per") || n.contains("rx quality") || n.equals("timeout");
|| n.equals("per") || n.contains("rx quality") || n.contains("tx timeout")
|| n.contains("code rate") || n.contains("preamble length")
|| n.contains("low data rate") || n.equals("crc") || n.contains("payload length");
}
private static ExtractedStats empty(String frame) {
@@ -208,6 +241,20 @@ public class StatsExtractor {
}
}
private static void putBool(Map<String, Object> meta, String key, Boolean value) {
if (value != null) {
meta.put(key, value);
}
}
private static Boolean matchBool(Pattern pattern, String text) {
Matcher m = pattern.matcher(text);
if (!m.find()) {
return null;
}
return "on".equalsIgnoreCase(m.group(1).trim());
}
private static Double matchDouble(Pattern pattern, String text) {
Matcher m = pattern.matcher(text);
if (m.find()) {
@@ -92,14 +92,30 @@ public class RadioComparePanel extends LinearLayout {
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);
addRow(table, "Принято", fmtInt(tx.packetReceive), fmtInt(rx.packetReceive), "packetReceive", changedTx, changedRx);
addRow(table, "Всего", fmtInt(tx.packetTotal), fmtInt(rx.packetTotal), "packetTotal", changedTx, changedRx);
addRow(table, "Ошибки", fmtInt(tx.packetError), fmtInt(rx.packetError), "packetError", changedTx, changedRx);
addRow(table, "CRC err", fmtInt(tx.crcError), fmtInt(rx.crcError), "crcError", changedTx, changedRx);
addRow(table, "Preamble", fmtInt(tx.preambleDetected), fmtInt(rx.preambleDetected),
"preambleDetected", changedTx, changedRx);
addRow(table, "Header OK", fmtInt(tx.headerValid), fmtInt(rx.headerValid), "headerValid", changedTx, changedRx);
addRow(table, "TX spd", fmtSuffix(tx.txPktPerS, " p/s"), fmtSuffix(rx.txPktPerS, " p/s"), "txSpeed", changedTx, changedRx);
addRow(table, "RX spd", fmtSuffix(tx.rxPktPerS, " p/s"), fmtSuffix(rx.rxPktPerS, " p/s"), "rxSpeed", changedTx, changedRx);
} else {
addRow(table, "Роль", LoraStatsFormatter.roleLabel(tx.role), LoraStatsFormatter.roleLabel(rx.role), "role", changedTx, changedRx);
addRow(table, "Частота", fmtMhz(tx.frequencyMhz), fmtMhz(rx.frequencyMhz), "frequency", changedTx, changedRx);
addRow(table, "SF", fmtInt(tx.sf), fmtInt(rx.sf), "sf", changedTx, changedRx);
addRow(table, "BW", fmtSuffixInt(tx.bwKhz, " kHz"), fmtSuffixInt(rx.bwKhz, " kHz"), "bw", changedTx, changedRx);
addRow(table, "BW", fmtBw(tx.bwKhz), fmtBw(rx.bwKhz), "bw", changedTx, changedRx);
addRow(table, "Мощн.", fmtDbm(tx.powerDbm), fmtDbm(rx.powerDbm), "power", changedTx, changedRx);
addRow(table, "Code Rate", str(tx.codeRate), str(rx.codeRate), "codeRate", changedTx, changedRx);
addRow(table, "Preamble", fmtInt(tx.preambleLength), fmtInt(rx.preambleLength), "preambleLength", changedTx, changedRx);
addRow(table, "LDR", str(tx.lowDataRateOpt), str(rx.lowDataRateOpt), "lowDataRateOpt", changedTx, changedRx);
addRow(table, "CRC", fmtCrc(tx.crcEnabled), fmtCrc(rx.crcEnabled), "crc", changedTx, changedRx);
addRow(table, "Payl.len", fmtSuffixInt(tx.payloadLengthBytes, " B"), fmtSuffixInt(rx.payloadLengthBytes, " B"),
"payloadLength", changedTx, changedRx);
addRow(table, "TX Timeout", fmtSuffix(tx.txTimeoutMs, " ms"), fmtSuffix(rx.txTimeoutMs, " ms"),
"txTimeout", changedTx, changedRx);
addRow(table, "On Air", fmtSuffix(tx.onAirMs, " ms"), fmtSuffix(rx.onAirMs, " ms"), "onAir", changedTx, changedRx);
}
}
@@ -183,6 +199,17 @@ public class RadioComparePanel extends LinearLayout {
return v != null ? v + suffix : "";
}
private static String fmtBw(Double v) {
return v != null ? String.format(Locale.US, "%.2f kHz", v) : "";
}
private static String fmtCrc(Boolean enabled) {
if (enabled == null) {
return "";
}
return enabled ? "On" : "Off";
}
private static String fmtSuffixInt(Integer v, String suffix) {
return v != null ? v + suffix : "";
}
@@ -54,11 +54,10 @@ public class LoraFrameExtractTest {
public void parsesAllLabeledLinesFromSendScreen() {
StatsExtractor extractor = StatsExtractor.withDefaults();
StatsExtractor.ExtractedStats stats = extractor.extract(FULL_SEND);
assertTrue(stats.metaJson.contains("\"fields\""));
assertTrue(stats.metaJson.contains("Frequency"));
assertTrue(stats.metaJson.contains("Spreading Factor"));
assertTrue(stats.metaJson.contains("Packet"));
assertTrue(stats.metaJson.contains("Payload"));
assertTrue(stats.metaJson.contains("\"code_rate\":\"4/5\""));
assertTrue(stats.metaJson.contains("\"spreading_factor\":12"));
assertTrue(stats.metaJson.contains("\"packet\":304"));
assertTrue(stats.metaJson.contains("Test TX!"));
}
@Test
@@ -83,7 +82,8 @@ public class LoraFrameExtractTest {
assertEquals(StatsExtractor.ROLE_RX, stats.role);
assertEquals(-78.0, stats.rssi, 0.01);
assertEquals(10.5, stats.snrDb, 0.01);
assertTrue(stats.metaJson.contains("\"fields\""));
assertTrue(stats.metaJson.contains("\"packet\":0"));
assertTrue(stats.metaJson.contains("\"rx_pkt_per_s\":0.45"));
}
@Test
@@ -96,6 +96,73 @@ public class LoraFrameExtractTest {
assertTrue(!stats.metaJson.contains("RX Quality"));
}
@Test
public void parsesRxCountersAndCrcErrors() {
StatsExtractor extractor = StatsExtractor.withDefaults();
String frame = """
RECEIVE
Frequency: 433500000 Hz
Power: 0 dBm
Spreading Factor: 5
Bandwidth: 7.81 kHz
Code Rate: 4/6
Preamble Length: 8
Low Data Rate Opt: Off
CRC: Off
Payload length: 32 byte
On Air: 427.14 ms, 2.34 pkt/c
Packet Number: 0
Payload: test
RSSI: -78
SNR: 10.5
RX Speed: 0.45 pkt/s, 120 bit/s
Packet Receive: 12
Packet Total: 100
Packet Error: 3
CRC Error: 2
PER: 3.00 %
Preamble Detected: 2
Header Valid: 1
RX Quality: 87 %
""";
StatsExtractor.ExtractedStats stats = extractor.extract(frame);
assertTrue(stats.metaJson.contains("\"crc_error\":2"));
assertTrue(stats.metaJson.contains("\"packet_error\":3"));
assertTrue(stats.metaJson.contains("\"packet_total\":100"));
assertTrue(stats.metaJson.contains("\"packet_receive\":12"));
assertTrue(stats.metaJson.contains("\"bandwidth_khz\":7.81"));
assertTrue(stats.metaJson.contains("\"crc_enabled\":false"));
assertTrue(stats.metaJson.contains("\"code_rate\":\"4/6\""));
}
@Test
public void parsesSendCrcAndConfig() {
StatsExtractor extractor = StatsExtractor.withDefaults();
String frame = """
SEND
Frequency: 433500000 Hz
Power: 0 dBm
Spreading Factor: 5
Bandwidth: 125 kHz
Code Rate: 4/5
Preamble Length: 8
Low Data Rate Opt: Off
CRC: On
Payload length: 32 byte
On Air: 23.10 ms, 43.28 pkt/c
Packet: 3816
Payload: Test TX!
TX Timeout: 0 ms
TX Speed: 32.06 pkt/s, 8206 bit/s
""";
StatsExtractor.ExtractedStats stats = extractor.extract(frame);
assertTrue(stats.metaJson.contains("\"packet\":3816"));
assertTrue(stats.metaJson.contains("\"crc_enabled\":true"));
assertTrue(stats.metaJson.contains("\"payload_length_bytes\":32"));
assertTrue(stats.metaJson.contains("\"tx_timeout_ms\":0"));
assertTrue(stats.metaJson.contains("\"preamble_length\":8"));
}
@Test
public void splitsTwoFramesByReceiveHeaderWithoutEsc() {
List<String> frames = new ArrayList<>();
+1 -1
View File
@@ -379,7 +379,7 @@ def health():
return {
"ok": status["db_ok"],
"ts": time.time(),
"api_build": "2026-06-16f",
"api_build": "2026-06-16g",
**status,
**elevation_status(),
}
+1 -1
View File
@@ -361,7 +361,7 @@
{ position: 'topright', collapsed: true }
).addTo(map);
const API_BUILD = '2026-06-16f';
const API_BUILD = '2026-06-16g';
const markers = {};
let selectedId = null;
+60 -7
View File
@@ -5,7 +5,10 @@
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', 'rx quality'
'packet receive', 'packet total', 'packet error', 'crc error',
'preamble detected', 'header valid',
'on air', 'tx speed', 'rx speed', 'per', 'rx quality',
'code rate', 'preamble length', 'low data rate', 'crc', 'payload length', 'tx timeout'
]);
function roleLabel(role) {
@@ -22,6 +25,15 @@
return false;
}
function fmtCrc(enabled) {
if (enabled == null) return '—';
return enabled ? 'On' : 'Off';
}
function fmtBw(khz) {
return khz != null ? `${Number(khz).toFixed(2)} kHz` : '—';
}
function parseRadioSnapshot(meta, roleFallback, rssiFallback) {
const snap = {
role: roleFallback || null,
@@ -39,6 +51,18 @@
rxPktPerS: null,
perPercent: null,
rxQualityPercent: null,
codeRate: null,
preambleLength: null,
lowDataRateOpt: null,
crcEnabled: null,
payloadLengthBytes: null,
txTimeoutMs: null,
packetReceive: null,
packetTotal: null,
packetError: null,
crcError: null,
preambleDetected: null,
headerValid: null,
extraFields: {}
};
if (!meta) return snap;
@@ -62,6 +86,18 @@
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.stats_at != null) snap.statsAt = Number(o.stats_at);
if (o.code_rate != null) snap.codeRate = String(o.code_rate);
if (o.preamble_length != null) snap.preambleLength = Number(o.preamble_length);
if (o.low_data_rate_opt != null) snap.lowDataRateOpt = String(o.low_data_rate_opt);
if (o.crc_enabled != null) snap.crcEnabled = Boolean(o.crc_enabled);
if (o.payload_length_bytes != null) snap.payloadLengthBytes = Number(o.payload_length_bytes);
if (o.tx_timeout_ms != null) snap.txTimeoutMs = Number(o.tx_timeout_ms);
if (o.packet_receive != null) snap.packetReceive = Number(o.packet_receive);
if (o.packet_total != null) snap.packetTotal = Number(o.packet_total);
if (o.packet_error != null) snap.packetError = Number(o.packet_error);
if (o.crc_error != null) snap.crcError = Number(o.crc_error);
if (o.preamble_detected != null) snap.preambleDetected = Number(o.preamble_detected);
if (o.header_valid != null) snap.headerValid = Number(o.header_valid);
if (o.fields && typeof o.fields === 'object') {
for (const [k, v] of Object.entries(o.fields)) {
if (!isKnownLabel(k)) snap.extraFields[k] = String(v);
@@ -79,11 +115,16 @@
const changed = new Set();
if (!a || !b) return changed;
const keys = ['gps', 'packetTime', 'role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent',
'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm'];
const map = { gps: 'gps', packetTime: 'packetTime', 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' };
'packetReceive', 'packetTotal', 'packetError', 'crcError', 'preambleDetected', 'headerValid',
'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm', 'codeRate', 'crcEnabled'];
const map = {
gps: 'gps', packetTime: 'packetTime', role: 'role', rssiDbm: 'rssi', snrDb: 'snr',
rxQualityPercent: 'rxQuality', packet: 'packet', payload: 'payload', perPercent: 'per',
packetReceive: 'packetReceive', packetTotal: 'packetTotal', packetError: 'packetError',
crcError: 'crcError', preambleDetected: 'preambleDetected', headerValid: 'headerValid',
txPktPerS: 'txSpeed', rxPktPerS: 'rxSpeed',
frequencyMhz: 'frequency', sf: 'sf', bwKhz: 'bw', powerDbm: 'power', codeRate: 'codeRate', crcEnabled: 'crc'
};
for (const k of keys) {
if (a[k] !== b[k] && !(a[k] == null && b[k] == null)) changed.add(map[k]);
}
@@ -99,6 +140,12 @@
{ 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} %` : '—' },
{ key: 'packetReceive', label: 'Принято', fmt: s => s.packetReceive != null ? String(s.packetReceive) : '—' },
{ key: 'packetTotal', label: 'Всего', fmt: s => s.packetTotal != null ? String(s.packetTotal) : '—' },
{ key: 'packetError', label: 'Ошибки', fmt: s => s.packetError != null ? String(s.packetError) : '—' },
{ key: 'crcError', label: 'CRC err', fmt: s => s.crcError != null ? String(s.crcError) : '—' },
{ key: 'preambleDetected', label: 'Preamble', fmt: s => s.preambleDetected != null ? String(s.preambleDetected) : '—' },
{ key: 'headerValid', label: 'Header OK', fmt: s => s.headerValid != null ? String(s.headerValid) : '—' },
{ key: 'txSpeed', label: 'TX Speed', fmt: s => s.txPktPerS != null ? `${s.txPktPerS} pkt/s` : '—' },
{ key: 'rxSpeed', label: 'RX Speed', fmt: s => s.rxPktPerS != null ? `${s.rxPktPerS} pkt/s` : '—' }
];
@@ -107,8 +154,14 @@
{ key: 'role', label: 'Роль', fmt: s => roleLabel(s.role) },
{ key: 'frequency', label: 'Частота', fmt: s => s.frequencyMhz != null ? `${s.frequencyMhz.toFixed(3)} MHz` : '—' },
{ key: 'sf', label: 'SF', fmt: s => s.sf != null ? String(s.sf) : '—' },
{ key: 'bw', label: 'BW', fmt: s => s.bwKhz != null ? `${s.bwKhz} kHz` : '—' },
{ key: 'bw', label: 'BW', fmt: s => fmtBw(s.bwKhz) },
{ key: 'power', label: 'Мощность', fmt: s => s.powerDbm != null ? `${s.powerDbm} dBm` : '—' },
{ key: 'codeRate', label: 'Code Rate', fmt: s => s.codeRate || '—' },
{ key: 'preambleLength', label: 'Preamble', fmt: s => s.preambleLength != null ? String(s.preambleLength) : '—' },
{ key: 'lowDataRateOpt', label: 'LDR', fmt: s => s.lowDataRateOpt || '—' },
{ key: 'crc', label: 'CRC', fmt: s => fmtCrc(s.crcEnabled) },
{ key: 'payloadLength', label: 'Payl.len', fmt: s => s.payloadLengthBytes != null ? `${s.payloadLengthBytes} B` : '—' },
{ key: 'txTimeout', label: 'TX Timeout', fmt: s => s.txTimeoutMs != null ? `${s.txTimeoutMs} ms` : '—' },
{ key: 'onAir', label: 'On Air', fmt: s => s.onAirMs != null ? `${s.onAirMs} ms` : '—' }
];