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 String frame;
public final Double frequencyMhz; public final Double frequencyMhz;
public final Integer sf; public final Integer sf;
public final Integer bwKhz; public final Double bwKhz;
public final Double powerDbm; public final Double powerDbm;
public final Double rssiDbm; public final Double rssiDbm;
public final Double snrDb; public final Double snrDb;
@@ -30,6 +30,18 @@ public final class RadioSnapshot {
public final Double rxPktPerS; public final Double rxPktPerS;
public final Double perPercent; public final Double perPercent;
public final Double rxQualityPercent; 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 final Map<String, String> extraFields;
public RadioSnapshot( public RadioSnapshot(
@@ -37,7 +49,7 @@ public final class RadioSnapshot {
String frame, String frame,
Double frequencyMhz, Double frequencyMhz,
Integer sf, Integer sf,
Integer bwKhz, Double bwKhz,
Double powerDbm, Double powerDbm,
Double rssiDbm, Double rssiDbm,
Double snrDb, Double snrDb,
@@ -48,6 +60,18 @@ public final class RadioSnapshot {
Double rxPktPerS, Double rxPktPerS,
Double perPercent, Double perPercent,
Double rxQualityPercent, 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 Map<String, String> extraFields
) { ) {
this.role = role; this.role = role;
@@ -65,12 +89,26 @@ public final class RadioSnapshot {
this.rxPktPerS = rxPktPerS; this.rxPktPerS = rxPktPerS;
this.perPercent = perPercent; this.perPercent = perPercent;
this.rxQualityPercent = rxQualityPercent; 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(); this.extraFields = extraFields != null ? extraFields : Map.of();
} }
public static RadioSnapshot empty() { public static RadioSnapshot empty() {
return new RadioSnapshot(null, null, null, null, null, null, null, null, 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) { public static RadioSnapshot fromMeta(String metaJson, String roleFallback, Double rssiFallback) {
@@ -78,7 +116,9 @@ public final class RadioSnapshot {
RadioSnapshot snap = empty(); RadioSnapshot snap = empty();
if (roleFallback != null || rssiFallback != null) { if (roleFallback != null || rssiFallback != null) {
return new RadioSnapshot(roleFallback, null, null, null, null, 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; return snap;
} }
@@ -108,7 +148,7 @@ public final class RadioSnapshot {
text(o, "frame"), text(o, "frame"),
hzToMhz(lng(o, "frequency_hz")), hzToMhz(lng(o, "frequency_hz")),
integer(o, "spreading_factor"), integer(o, "spreading_factor"),
integer(o, "bandwidth_khz"), dbl(o, "bandwidth_khz"),
dbl(o, "power_dbm"), dbl(o, "power_dbm"),
rssi, rssi,
dbl(o, "snr_db"), dbl(o, "snr_db"),
@@ -119,11 +159,25 @@ public final class RadioSnapshot {
dbl(o, "rx_pkt_per_s"), dbl(o, "rx_pkt_per_s"),
dbl(o, "per_percent"), dbl(o, "per_percent"),
dbl(o, "rx_quality_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 extra
); );
} catch (Exception ignored) { } catch (Exception ignored) {
return new RadioSnapshot(roleFallback, null, null, null, null, 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());
} }
} }
@@ -152,6 +206,14 @@ public final class RadioSnapshot {
cmp(changed, "sf", sf, prev.sf); cmp(changed, "sf", sf, prev.sf);
cmp(changed, "bw", bwKhz, prev.bwKhz); cmp(changed, "bw", bwKhz, prev.bwKhz);
cmp(changed, "power", powerDbm, prev.powerDbm); 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; return changed;
} }
@@ -167,8 +229,12 @@ public final class RadioSnapshot {
|| n.contains("frequency") || n.equals("power") || n.equals("rssi") || n.contains("frequency") || n.equals("power") || n.equals("rssi")
|| n.equals("snr") || n.contains("spreading") || n.contains("bandwidth") || n.equals("snr") || n.contains("spreading") || n.contains("bandwidth")
|| n.equals("packet") || n.contains("packet number") || n.equals("payload") || 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.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) { private static String text(JsonObject o, String key) {
@@ -186,6 +252,11 @@ public final class RadioSnapshot {
return e != null && e.isJsonPrimitive() ? e.getAsDouble() : null; 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) { private static Long lng(JsonObject o, String key) {
JsonElement e = o.get(key); JsonElement e = o.get(key);
return e != null && e.isJsonPrimitive() ? e.getAsLong() : null; 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, "Пакет", fmtInt(s.packet), "packet", changed);
appendLine(sb, "Payload", s.payload, "payload", changed); appendLine(sb, "Payload", s.payload, "payload", changed);
appendLine(sb, "PER", fmtSuffix(s.perPercent, " %"), "per", 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, "TX Speed", fmtSuffix(s.txPktPerS, " pkt/s"), "txSpeed", changed);
appendLine(sb, "RX Speed", fmtSuffix(s.rxPktPerS, " pkt/s"), "rxSpeed", changed); appendLine(sb, "RX Speed", fmtSuffix(s.rxPktPerS, " pkt/s"), "rxSpeed", changed);
for (Map.Entry<String, String> e : s.extraFields.entrySet()) { 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, "Частота", fmtSuffix(s.frequencyMhz, " MHz"), "frequency", changed);
appendLine(sb, "SF", fmtInt(s.sf), "sf", 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, "Мощность 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); appendLine(sb, "On Air", fmtSuffix(s.onAirMs, " ms"), "onAir", changed);
return sb.toString().trim(); return sb.toString().trim();
} }
@@ -111,11 +123,22 @@ public final class LoraStatsFormatter {
return v != null ? String.valueOf(v) : null; return v != null ? String.valueOf(v) : null;
} }
private static String fmtSuffix(Double v, String suffix) { private static String fmtBw(Double v) {
return v != null ? String.format(Locale.US, "%s%s", v, suffix) : null; 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) { private static String fmtSuffix(Integer v, String suffix) {
return v != null ? v + suffix : null; 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 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 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 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 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 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_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 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 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); 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)); putLong(meta, "frequency_hz", matchLong(FREQUENCY, normalized));
putInt(meta, "spreading_factor", matchInt(SPREADING, 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); Integer packet = matchInt(PACKET_NUMBER, normalized);
if (packet == null) { if (packet == null) {
packet = matchInt(PACKET, normalized); packet = matchInt(PACKET_TX, normalized);
} }
putInt(meta, "packet", packet); putInt(meta, "packet", packet);
putString(meta, "payload", matchString(PAYLOAD, normalized)); 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, "rx_pkt_per_s", matchDouble(RX_SPEED, normalized));
putDouble(meta, "per_percent", matchDouble(PER, normalized)); putDouble(meta, "per_percent", matchDouble(PER, normalized));
putDouble(meta, "rx_quality_percent", matchDouble(RX_QUALITY, 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()) { if (!fields.isEmpty()) {
meta.put("fields", fields); meta.put("fields", fields);
@@ -144,8 +173,12 @@ public class StatsExtractor {
return n.equals("frequency") || n.equals("power") || n.equals("rssi") return n.equals("frequency") || n.equals("power") || n.equals("rssi")
|| n.equals("snr") || n.contains("spreading factor") || n.equals("bandwidth") || n.equals("snr") || n.contains("spreading factor") || n.equals("bandwidth")
|| n.equals("packet") || n.contains("packet number") || n.equals("payload") || 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.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) { 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) { private static Double matchDouble(Pattern pattern, String text) {
Matcher m = pattern.matcher(text); Matcher m = pattern.matcher(text);
if (m.find()) { 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, "Пакет", fmtInt(tx.packet), fmtInt(rx.packet), "packet", changedTx, changedRx);
addRow(table, "Payload", str(tx.payload), str(rx.payload), "payload", 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, "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, "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); addRow(table, "RX spd", fmtSuffix(tx.rxPktPerS, " p/s"), fmtSuffix(rx.rxPktPerS, " p/s"), "rxSpeed", changedTx, changedRx);
} else { } else {
addRow(table, "Роль", LoraStatsFormatter.roleLabel(tx.role), LoraStatsFormatter.roleLabel(rx.role), "role", changedTx, changedRx); 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, "Частота", fmtMhz(tx.frequencyMhz), fmtMhz(rx.frequencyMhz), "frequency", changedTx, changedRx);
addRow(table, "SF", fmtInt(tx.sf), fmtInt(rx.sf), "sf", 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, "Мощн.", 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 : ""; 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) { private static String fmtSuffixInt(Integer v, String suffix) {
return v != null ? v + suffix : ""; return v != null ? v + suffix : "";
} }
@@ -54,11 +54,10 @@ public class LoraFrameExtractTest {
public void parsesAllLabeledLinesFromSendScreen() { public void parsesAllLabeledLinesFromSendScreen() {
StatsExtractor extractor = StatsExtractor.withDefaults(); StatsExtractor extractor = StatsExtractor.withDefaults();
StatsExtractor.ExtractedStats stats = extractor.extract(FULL_SEND); StatsExtractor.ExtractedStats stats = extractor.extract(FULL_SEND);
assertTrue(stats.metaJson.contains("\"fields\"")); assertTrue(stats.metaJson.contains("\"code_rate\":\"4/5\""));
assertTrue(stats.metaJson.contains("Frequency")); assertTrue(stats.metaJson.contains("\"spreading_factor\":12"));
assertTrue(stats.metaJson.contains("Spreading Factor")); assertTrue(stats.metaJson.contains("\"packet\":304"));
assertTrue(stats.metaJson.contains("Packet")); assertTrue(stats.metaJson.contains("Test TX!"));
assertTrue(stats.metaJson.contains("Payload"));
} }
@Test @Test
@@ -83,7 +82,8 @@ public class LoraFrameExtractTest {
assertEquals(StatsExtractor.ROLE_RX, stats.role); assertEquals(StatsExtractor.ROLE_RX, stats.role);
assertEquals(-78.0, stats.rssi, 0.01); assertEquals(-78.0, stats.rssi, 0.01);
assertEquals(10.5, stats.snrDb, 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 @Test
@@ -96,6 +96,73 @@ public class LoraFrameExtractTest {
assertTrue(!stats.metaJson.contains("RX Quality")); 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 @Test
public void splitsTwoFramesByReceiveHeaderWithoutEsc() { public void splitsTwoFramesByReceiveHeaderWithoutEsc() {
List<String> frames = new ArrayList<>(); List<String> frames = new ArrayList<>();
+1 -1
View File
@@ -379,7 +379,7 @@ def health():
return { return {
"ok": status["db_ok"], "ok": status["db_ok"],
"ts": time.time(), "ts": time.time(),
"api_build": "2026-06-16f", "api_build": "2026-06-16g",
**status, **status,
**elevation_status(), **elevation_status(),
} }
+1 -1
View File
@@ -361,7 +361,7 @@
{ position: 'topright', collapsed: true } { position: 'topright', collapsed: true }
).addTo(map); ).addTo(map);
const API_BUILD = '2026-06-16f'; const API_BUILD = '2026-06-16g';
const markers = {}; const markers = {};
let selectedId = null; let selectedId = null;
+60 -7
View File
@@ -5,7 +5,10 @@
const KNOWN_LABELS = new Set([ const KNOWN_LABELS = new Set([
'send', 'receive', 'frequency', 'power', 'rssi', 'snr', 'send', 'receive', 'frequency', 'power', 'rssi', 'snr',
'spreading factor', 'bandwidth', 'packet', 'packet number', 'payload', '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) { function roleLabel(role) {
@@ -22,6 +25,15 @@
return false; 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) { function parseRadioSnapshot(meta, roleFallback, rssiFallback) {
const snap = { const snap = {
role: roleFallback || null, role: roleFallback || null,
@@ -39,6 +51,18 @@
rxPktPerS: null, rxPktPerS: null,
perPercent: null, perPercent: null,
rxQualityPercent: 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: {} extraFields: {}
}; };
if (!meta) return snap; if (!meta) return snap;
@@ -62,6 +86,18 @@
if (o.per_percent != null) snap.perPercent = Number(o.per_percent); 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.rx_quality_percent != null) snap.rxQualityPercent = Number(o.rx_quality_percent);
if (o.stats_at != null) snap.statsAt = Number(o.stats_at); 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') { if (o.fields && typeof o.fields === 'object') {
for (const [k, v] of Object.entries(o.fields)) { for (const [k, v] of Object.entries(o.fields)) {
if (!isKnownLabel(k)) snap.extraFields[k] = String(v); if (!isKnownLabel(k)) snap.extraFields[k] = String(v);
@@ -79,11 +115,16 @@
const changed = new Set(); const changed = new Set();
if (!a || !b) return changed; if (!a || !b) return changed;
const keys = ['gps', 'packetTime', 'role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent', const keys = ['gps', 'packetTime', 'role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent',
'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm']; 'packetReceive', 'packetTotal', 'packetError', 'crcError', 'preambleDetected', 'headerValid',
const map = { gps: 'gps', packetTime: 'packetTime', role: 'role', rssiDbm: 'rssi', snrDb: 'snr', rxQualityPercent: 'rxQuality', 'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm', 'codeRate', 'crcEnabled'];
packet: 'packet', const map = {
payload: 'payload', perPercent: 'per', txPktPerS: 'txSpeed', rxPktPerS: 'rxSpeed', gps: 'gps', packetTime: 'packetTime', role: 'role', rssiDbm: 'rssi', snrDb: 'snr',
frequencyMhz: 'frequency', sf: 'sf', bwKhz: 'bw', powerDbm: 'power' }; 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) { for (const k of keys) {
if (a[k] !== b[k] && !(a[k] == null && b[k] == null)) changed.add(map[k]); 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: 'packet', label: 'Пакет', fmt: s => s.packet != null ? String(s.packet) : '—' },
{ key: 'payload', label: 'Payload', fmt: s => s.payload || '—' }, { key: 'payload', label: 'Payload', fmt: s => s.payload || '—' },
{ key: 'per', label: 'PER', fmt: s => s.perPercent != null ? `${s.perPercent} %` : '—' }, { 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: '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` : '—' } { 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: 'role', label: 'Роль', fmt: s => roleLabel(s.role) },
{ key: 'frequency', label: 'Частота', fmt: s => s.frequencyMhz != null ? `${s.frequencyMhz.toFixed(3)} MHz` : '—' }, { 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: '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: '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` : '—' } { key: 'onAir', label: 'On Air', fmt: s => s.onAirMs != null ? `${s.onAirMs} ms` : '—' }
]; ];