diff --git a/app/src/main/java/com/grigowashere/loratester/model/RadioSnapshot.java b/app/src/main/java/com/grigowashere/loratester/model/RadioSnapshot.java index f4adcf5..d460aa0 100644 --- a/app/src/main/java/com/grigowashere/loratester/model/RadioSnapshot.java +++ b/app/src/main/java/com/grigowashere/loratester/model/RadioSnapshot.java @@ -48,6 +48,7 @@ public final class RadioSnapshot { public final Double modulationIndex; public final Integer rxBandwidthHz; public final Integer syncWordLengthBits; + public final Boolean goldCodesEnabled; public final Map extraFields; public RadioSnapshot( @@ -84,6 +85,7 @@ public final class RadioSnapshot { Double modulationIndex, Integer rxBandwidthHz, Integer syncWordLengthBits, + Boolean goldCodesEnabled, Map extraFields ) { this.role = role; @@ -119,6 +121,7 @@ public final class RadioSnapshot { this.modulationIndex = modulationIndex; this.rxBandwidthHz = rxBandwidthHz; this.syncWordLengthBits = syncWordLengthBits; + this.goldCodesEnabled = goldCodesEnabled; this.extraFields = extraFields != null ? extraFields : Map.of(); } @@ -126,7 +129,7 @@ public final class RadioSnapshot { return new RadioSnapshot(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, Map.of()); } @@ -137,7 +140,7 @@ public final class RadioSnapshot { return new RadioSnapshot(roleFallback, null, null, null, null, null, null, rssiFallback, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, Map.of()); } return snap; @@ -197,13 +200,14 @@ public final class RadioSnapshot { dbl(o, "modulation_index"), integer(o, "rx_bandwidth_hz"), integer(o, "sync_word_length_bits"), + bool(o, "gold_codes_enabled"), extra ); } catch (Exception ignored) { return new RadioSnapshot(roleFallback, null, null, null, null, null, null, rssiFallback, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, Map.of()); } } @@ -247,6 +251,7 @@ public final class RadioSnapshot { cmp(changed, "modIndex", modulationIndex, prev.modulationIndex); cmp(changed, "rxBw", rxBandwidthHz, prev.rxBandwidthHz); cmp(changed, "syncWord", syncWordLengthBits, prev.syncWordLengthBits); + cmp(changed, "goldCodes", goldCodesEnabled, prev.goldCodesEnabled); return changed; } @@ -269,7 +274,8 @@ public final class RadioSnapshot { || n.contains("code rate") || n.contains("preamble length") || n.contains("low data rate") || n.equals("crc") || n.contains("payload length") || n.contains("bitrate") || n.equals("fdev") || n.contains("modulation index") - || n.contains("rx bandwidth") || n.contains("sync word"); + || n.contains("rx bandwidth") || n.contains("sync word") + || n.contains("gold codes"); } private static String text(JsonObject o, String key) { diff --git a/app/src/main/java/com/grigowashere/loratester/telnet/AtCommands.java b/app/src/main/java/com/grigowashere/loratester/telnet/AtCommands.java index e2a7b8c..d8a4c13 100644 --- a/app/src/main/java/com/grigowashere/loratester/telnet/AtCommands.java +++ b/app/src/main/java/com/grigowashere/loratester/telnet/AtCommands.java @@ -17,6 +17,7 @@ public final class AtCommands { public static final String MODEM_MODE = "AT+MD="; public static final String BITRATE_BPS = "AT+BR="; public static final String FDEV_HZ = "AT+FD="; + public static final String GOLD_CODES = "AT+GD="; /** Legacy / bridge helpers (if supported by firmware). */ public static final String HELP = "AT+H"; diff --git a/app/src/main/java/com/grigowashere/loratester/telnet/LoraStatsFormatter.java b/app/src/main/java/com/grigowashere/loratester/telnet/LoraStatsFormatter.java index a087975..a3e5da8 100644 --- a/app/src/main/java/com/grigowashere/loratester/telnet/LoraStatsFormatter.java +++ b/app/src/main/java/com/grigowashere/loratester/telnet/LoraStatsFormatter.java @@ -70,6 +70,7 @@ public final class LoraStatsFormatter { appendLine(sb, "Mod Index", fmtSuffix(s.modulationIndex, ""), "modIndex", changed); appendLine(sb, "Rx BW", fmtSuffix(s.rxBandwidthHz, " Hz"), "rxBw", changed); appendLine(sb, "Sync Word", fmtSuffix(s.syncWordLengthBits, " bit"), "syncWord", changed); + appendLine(sb, "Gold Codes", fmtOnOff(s.goldCodesEnabled), "goldCodes", changed); } else { appendLine(sb, "SF", fmtInt(s.sf), "sf", changed); appendLine(sb, "BW", fmtBw(s.bwKhz), "bw", changed); @@ -147,6 +148,10 @@ public final class LoraStatsFormatter { } private static String fmtCrc(Boolean enabled) { + return fmtOnOff(enabled); + } + + private static String fmtOnOff(Boolean enabled) { if (enabled == null) { return null; } diff --git a/app/src/main/java/com/grigowashere/loratester/telnet/RadioMacroBuilder.java b/app/src/main/java/com/grigowashere/loratester/telnet/RadioMacroBuilder.java index 35279b7..5638a01 100644 --- a/app/src/main/java/com/grigowashere/loratester/telnet/RadioMacroBuilder.java +++ b/app/src/main/java/com/grigowashere/loratester/telnet/RadioMacroBuilder.java @@ -24,6 +24,7 @@ public final class RadioMacroBuilder { public Integer sendTimeoutMs; public Integer bitrateBps; public Integer fdevHz; + public Boolean goldCodesEnabled; public String role; } @@ -61,6 +62,9 @@ public final class RadioMacroBuilder { if (p.fdevHz != null && p.fdevHz >= 400 && p.fdevHz <= 100_000) { lines.add(AtCommands.FDEV_HZ + p.fdevHz); } + if (p.goldCodesEnabled != null) { + lines.add(AtCommands.GOLD_CODES + (p.goldCodesEnabled ? "ON" : "OFF")); + } if (p.preamble != null && p.preamble >= 1 && p.preamble <= 64) { lines.add(AtCommands.PREAMBLE + p.preamble); } diff --git a/app/src/main/java/com/grigowashere/loratester/telnet/StatsExtractor.java b/app/src/main/java/com/grigowashere/loratester/telnet/StatsExtractor.java index 898926c..807845f 100644 --- a/app/src/main/java/com/grigowashere/loratester/telnet/StatsExtractor.java +++ b/app/src/main/java/com/grigowashere/loratester/telnet/StatsExtractor.java @@ -71,6 +71,8 @@ public class StatsExtractor { "Rx Bandwidth\\s*:?\\s*(\\d+)", Pattern.CASE_INSENSITIVE); private static final Pattern SYNC_WORD_LENGTH = Pattern.compile( "Sync Word Length\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE); + private static final Pattern GOLD_CODES = Pattern.compile( + "Gold Codes\\s*:\\s*(On|Off)", Pattern.CASE_INSENSITIVE); private final Pattern rssiPattern; private final Pattern rangePattern; @@ -158,6 +160,7 @@ public class StatsExtractor { putDouble(meta, "modulation_index", matchDouble(MODULATION_INDEX, normalized)); putInt(meta, "rx_bandwidth_hz", matchInt(RX_BANDWIDTH, normalized)); putInt(meta, "sync_word_length_bits", matchInt(SYNC_WORD_LENGTH, normalized)); + putBool(meta, "gold_codes_enabled", matchBool(GOLD_CODES, normalized)); if (!fields.isEmpty()) { meta.put("fields", fields); @@ -209,7 +212,8 @@ public class StatsExtractor { || n.contains("code rate") || n.contains("preamble length") || n.contains("low data rate") || n.equals("crc") || n.contains("payload length") || n.contains("bitrate") || n.equals("fdev") || n.contains("modulation index") - || n.contains("rx bandwidth") || n.contains("sync word"); + || n.contains("rx bandwidth") || n.contains("sync word") + || n.contains("gold codes"); } private static ExtractedStats empty(String frame) { diff --git a/app/src/main/java/com/grigowashere/loratester/ui/AtFragment.java b/app/src/main/java/com/grigowashere/loratester/ui/AtFragment.java index ae163a6..9d9da5d 100644 --- a/app/src/main/java/com/grigowashere/loratester/ui/AtFragment.java +++ b/app/src/main/java/com/grigowashere/loratester/ui/AtFragment.java @@ -19,6 +19,7 @@ import androidx.fragment.app.Fragment; import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButtonToggleGroup; +import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.android.material.textfield.TextInputEditText; import com.grigowashere.loratester.CommandPoller; import com.grigowashere.loratester.LoraApp; @@ -62,6 +63,7 @@ public class AtFragment extends Fragment { private TextInputEditText atInputTm; private View atLoraFields; private View atGfskFields; + private SwitchMaterial atGoldCodesSwitch; private Spinner atModemModeSpinner; private Spinner atBwSpinner; private Spinner atCrSpinner; @@ -108,6 +110,7 @@ public class AtFragment extends Fragment { atInputTm = view.findViewById(R.id.atInputTm); atLoraFields = view.findViewById(R.id.atLoraFields); atGfskFields = view.findViewById(R.id.atGfskFields); + atGoldCodesSwitch = view.findViewById(R.id.atGoldCodesSwitch); atModemModeSpinner = view.findViewById(R.id.atModemModeSpinner); atBwSpinner = view.findViewById(R.id.atBwSpinner); atCrSpinner = view.findViewById(R.id.atCrSpinner); @@ -266,6 +269,9 @@ public class AtFragment extends Fragment { if (ModemMode.isGfsk(p.modemMode)) { p.bitrateBps = parseInt(atInputBr); p.fdevHz = parseInt(atInputFd); + if (atGoldCodesSwitch != null) { + p.goldCodesEnabled = atGoldCodesSwitch.isChecked(); + } } else { p.sf = parseInt(atInputSf); int bwPos = atBwSpinner != null ? atBwSpinner.getSelectedItemPosition() : -1; @@ -334,6 +340,7 @@ public class AtFragment extends Fragment { if (ModemMode.isGfsk(p.modemMode)) { if (p.bitrateBps != null) map.put("bitrate_bps", p.bitrateBps); if (p.fdevHz != null) map.put("fdev_hz", p.fdevHz); + if (p.goldCodesEnabled != null) map.put("gold_codes_enabled", p.goldCodesEnabled); } else { if (p.sf != null) map.put("sf", p.sf); if (p.bwKhz != null) map.put("bw_khz", p.bwKhz); @@ -433,6 +440,9 @@ public class AtFragment extends Fragment { if (snap.fdevHz != null && isEmpty(atInputFd)) { atInputFd.setText(String.valueOf(snap.fdevHz)); } + if (snap.goldCodesEnabled != null && atGoldCodesSwitch != null) { + atGoldCodesSwitch.setChecked(snap.goldCodesEnabled); + } } else { if (snap.sf != null && isEmpty(atInputSf)) { atInputSf.setText(String.valueOf(snap.sf)); @@ -484,6 +494,10 @@ public class AtFragment extends Fragment { if (bitrate != null && atInputBr != null) atInputBr.setText(String.valueOf(bitrate)); Integer fdev = intValue(p.get("fdev_hz")); if (fdev != null && atInputFd != null) atInputFd.setText(String.valueOf(fdev)); + Boolean goldCodes = boolValue(p.get("gold_codes_enabled")); + if (goldCodes != null && atGoldCodesSwitch != null) { + atGoldCodesSwitch.setChecked(goldCodes); + } } else { Integer sf = intValue(p.get("sf")); if (sf != null && atInputSf != null) atInputSf.setText(String.valueOf(sf)); @@ -540,6 +554,23 @@ public class AtFragment extends Fragment { } } + private static Boolean boolValue(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value == null) { + return null; + } + String s = String.valueOf(value).trim(); + if ("true".equalsIgnoreCase(s) || "on".equalsIgnoreCase(s) || "1".equals(s)) { + return true; + } + if ("false".equalsIgnoreCase(s) || "off".equalsIgnoreCase(s) || "0".equals(s)) { + return false; + } + return null; + } + private static Long longValue(Object value) { if (value instanceof Number) { return ((Number) value).longValue(); diff --git a/app/src/main/java/com/grigowashere/loratester/ui/RadioComparePanel.java b/app/src/main/java/com/grigowashere/loratester/ui/RadioComparePanel.java index 5ffd27f..f951218 100644 --- a/app/src/main/java/com/grigowashere/loratester/ui/RadioComparePanel.java +++ b/app/src/main/java/com/grigowashere/loratester/ui/RadioComparePanel.java @@ -118,6 +118,8 @@ public class RadioComparePanel extends LinearLayout { "rxBw", changedTx, changedRx); addRow(table, "Sync Word", fmtSuffixInt(tx.syncWordLengthBits, " bit"), fmtSuffixInt(rx.syncWordLengthBits, " bit"), "syncWord", changedTx, changedRx); + addRow(table, "Gold Codes", fmtCrc(tx.goldCodesEnabled), fmtCrc(rx.goldCodesEnabled), + "goldCodes", changedTx, changedRx); } if (!ModemMode.isGfsk(tx.modemMode) || !ModemMode.isGfsk(rx.modemMode)) { addRow(table, "SF", fmtInt(tx.sf), fmtInt(rx.sf), "sf", changedTx, changedRx); diff --git a/app/src/main/res/layout/fragment_at.xml b/app/src/main/res/layout/fragment_at.xml index 03b733b..e8a77ae 100644 --- a/app/src/main/res/layout/fragment_at.xml +++ b/app/src/main/res/layout/fragment_at.xml @@ -156,6 +156,14 @@ android:inputType="number" /> + + Режим модема Bitrate bit/s (600–300000) FDev Hz (400–100000) + Gold Codes Мощность dBm (-9…22) Bandwidth kHz Code rate diff --git a/app/src/test/java/com/grigowashere/loratester/LoraFrameExtractTest.java b/app/src/test/java/com/grigowashere/loratester/LoraFrameExtractTest.java index f4dd810..6c0ddcb 100644 --- a/app/src/test/java/com/grigowashere/loratester/LoraFrameExtractTest.java +++ b/app/src/test/java/com/grigowashere/loratester/LoraFrameExtractTest.java @@ -4,6 +4,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.grigowashere.loratester.telnet.ModemMode; +import com.grigowashere.loratester.telnet.RadioMacroBuilder; import com.grigowashere.loratester.telnet.StatsExtractor; import com.grigowashere.loratester.telnet.TelnetFrameParser; @@ -266,6 +268,7 @@ public class LoraFrameExtractTest { Preamble Length: 48 bit Sync Word Length: 32 bit Payload length: 224 byte + Gold Codes: On On Air: 93.60 ms, 10.68 pkt/s TX Timeout: 0 ms """; @@ -276,6 +279,16 @@ public class LoraFrameExtractTest { assertTrue(stats.metaJson.contains("\"modem_mode\":\"GFSK\"")); assertTrue(stats.metaJson.contains("\"bitrate_bps\":20000")); assertTrue(stats.metaJson.contains("\"fdev_hz\":10000")); + assertTrue(stats.metaJson.contains("\"gold_codes_enabled\":true")); + } + + @Test + public void radioMacroBuilder_emitsGoldCodesCommand() { + RadioMacroBuilder.Params p = new RadioMacroBuilder.Params(); + p.modemMode = ModemMode.GFSK; + p.goldCodesEnabled = false; + List lines = RadioMacroBuilder.apply(p); + assertTrue(lines.contains("AT+GD=OFF")); } @Test diff --git a/server/core/__pycache__/storage.cpython-313.pyc b/server/core/__pycache__/storage.cpython-313.pyc index 260a6a8..b128ccb 100644 Binary files a/server/core/__pycache__/storage.cpython-313.pyc and b/server/core/__pycache__/storage.cpython-313.pyc differ diff --git a/server/core/storage.py b/server/core/storage.py index 62c9d8b..ede5b06 100644 --- a/server/core/storage.py +++ b/server/core/storage.py @@ -579,6 +579,7 @@ _TX_PARAM_KEYS = ( "modulation_index", "rx_bandwidth_hz", "sync_word_length_bits", + "gold_codes_enabled", ) diff --git a/server/static/app.js b/server/static/app.js index 08e5f28..1443ad1 100644 --- a/server/static/app.js +++ b/server/static/app.js @@ -632,6 +632,8 @@ function fillCmdInputsFromDevice(d) { if (RadioUI.isGfskMode(snap.modemMode)) { if (snap.bitrateBps != null) document.getElementById('cmdBr').value = snap.bitrateBps; if (snap.fdevHz != null) document.getElementById('cmdFd').value = snap.fdevHz; + const gd = document.getElementById('cmdGd'); + if (gd && snap.goldCodesEnabled != null) gd.checked = snap.goldCodesEnabled; } else { if (snap.sf != null) document.getElementById('cmdSf').value = snap.sf; if (snap.bwKhz != null) document.getElementById('cmdBw').value = String(snap.bwKhz); @@ -658,6 +660,9 @@ function macroLinesFromParams(params) { if (RadioUI.isGfskMode(mode)) { if (params.bitrate_bps != null) lines.push(`AT+BR=${params.bitrate_bps}`); if (params.fdev_hz != null) lines.push(`AT+FD=${params.fdev_hz}`); + if (params.gold_codes_enabled != null) { + lines.push(`AT+GD=${params.gold_codes_enabled ? 'ON' : 'OFF'}`); + } if (params.preamble != null) lines.push(`AT+PL=${params.preamble}`); } else { if (params.sf != null) lines.push(`AT+SF=${params.sf}`); @@ -690,6 +695,8 @@ function buildMacroParams() { const fd = document.getElementById('cmdFd').value.trim(); if (br) params.bitrate_bps = Number(br); if (fd) params.fdev_hz = Number(fd); + const gd = document.getElementById('cmdGd'); + if (gd) params.gold_codes_enabled = gd.checked; } else { const sf = document.getElementById('cmdSf').value.trim(); const bw = document.getElementById('cmdBw').value.trim(); @@ -714,6 +721,8 @@ function applyMacroParams(params) { if (RadioUI.isGfskMode(params.modem_mode)) { if (params.bitrate_bps != null) document.getElementById('cmdBr').value = params.bitrate_bps; if (params.fdev_hz != null) document.getElementById('cmdFd').value = params.fdev_hz; + const gd = document.getElementById('cmdGd'); + if (gd && params.gold_codes_enabled != null) gd.checked = Boolean(params.gold_codes_enabled); } else { if (params.sf != null) document.getElementById('cmdSf').value = params.sf; if (params.bw_khz != null) document.getElementById('cmdBw').value = String(params.bw_khz); @@ -737,6 +746,7 @@ function paramsFromMacroLines(lines) { else if (s.startsWith('AT+CR=')) params.code_rate = s.slice(6); else if (s.startsWith('AT+BR=')) params.bitrate_bps = Number(s.slice(6)); else if (s.startsWith('AT+FD=')) params.fdev_hz = Number(s.slice(6)); + else if (s.startsWith('AT+GD=')) params.gold_codes_enabled = s.slice(6).toUpperCase() === 'ON'; else if (s.startsWith('AT+PL=')) params.preamble = Number(s.slice(6)); else if (s.startsWith('AT+TM=')) params.send_timeout_ms = Number(s.slice(6)); else if (s === 'AT+TX') params.role = 'TX'; @@ -773,6 +783,8 @@ function buildPresetEditParams() { if (br != null) params.bitrate_bps = br; const fd = numOrNull('presetEditFd'); if (fd != null) params.fdev_hz = fd; + const gd = document.getElementById('presetEditGd'); + if (gd) params.gold_codes_enabled = gd.checked; } else { const sf = numOrNull('presetEditSf'); if (sf != null) params.sf = sf; @@ -798,6 +810,8 @@ function fillPresetModal(preset) { if (RadioUI.isGfskMode(params.modem_mode)) { document.getElementById('presetEditBr').value = params.bitrate_bps ?? ''; document.getElementById('presetEditFd').value = params.fdev_hz ?? ''; + const gd = document.getElementById('presetEditGd'); + if (gd) gd.checked = params.gold_codes_enabled == null ? true : Boolean(params.gold_codes_enabled); } else { document.getElementById('presetEditSf').value = params.sf ?? ''; document.getElementById('presetEditBw').value = params.bw_khz ?? ''; @@ -975,6 +989,9 @@ function formatReferenceTxParams(params) { if (RadioUI.isGfskMode(params.modem_mode)) { if (params.bitrate_bps != null) parts.push(`BR ${params.bitrate_bps} bit/s`); if (params.fdev_hz != null) parts.push(`FD ${params.fdev_hz} Hz`); + if (params.gold_codes_enabled != null) { + parts.push(`Gold ${params.gold_codes_enabled ? 'On' : 'Off'}`); + } if (params.modulation_index != null) parts.push(`Mod ${params.modulation_index}`); if (params.rx_bandwidth_hz != null) parts.push(`RxBW ${params.rx_bandwidth_hz} Hz`); if (params.sync_word_length_bits != null) parts.push(`Sync ${params.sync_word_length_bits} bit`); diff --git a/server/static/index.html b/server/static/index.html index 381664d..ac6bd51 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -198,6 +198,10 @@ + @@ -351,6 +355,10 @@ diff --git a/server/static/radio-ui.js b/server/static/radio-ui.js index 1abe1a1..46e2edd 100644 --- a/server/static/radio-ui.js +++ b/server/static/radio-ui.js @@ -12,7 +12,7 @@ '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', - 'bitrate', 'fdev', 'modulation index', 'rx bandwidth', 'sync word', 'modem mode' + 'bitrate', 'fdev', 'modulation index', 'rx bandwidth', 'sync word', 'modem mode', 'gold codes' ]); function normalizeModemMode(mode) { @@ -93,6 +93,7 @@ modulationIndex: null, rxBandwidthHz: null, syncWordLengthBits: null, + goldCodesEnabled: null, extraFields: {} }; if (!meta) return snap; @@ -134,6 +135,7 @@ if (o.modulation_index != null) snap.modulationIndex = Number(o.modulation_index); if (o.rx_bandwidth_hz != null) snap.rxBandwidthHz = Number(o.rx_bandwidth_hz); if (o.sync_word_length_bits != null) snap.syncWordLengthBits = Number(o.sync_word_length_bits); + if (o.gold_codes_enabled != null) snap.goldCodesEnabled = Boolean(o.gold_codes_enabled); if (o.fields && typeof o.fields === 'object') { for (const [k, v] of Object.entries(o.fields)) { if (!isKnownLabel(k)) snap.extraFields[k] = String(v); @@ -153,7 +155,7 @@ const keys = ['gps', 'packetTime', 'role', 'modemMode', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent', 'packetReceive', 'packetTotal', 'packetError', 'crcError', 'preambleDetected', 'headerValid', 'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm', 'codeRate', 'crcEnabled', - 'bitrateBps', 'fdevHz', 'modulationIndex', 'rxBandwidthHz', 'syncWordLengthBits']; + 'bitrateBps', 'fdevHz', 'modulationIndex', 'rxBandwidthHz', 'syncWordLengthBits', 'goldCodesEnabled']; const map = { gps: 'gps', packetTime: 'packetTime', role: 'role', modemMode: 'modemMode', rssiDbm: 'rssi', snrDb: 'snr', rxQualityPercent: 'rxQuality', packet: 'packet', payload: 'payload', perPercent: 'per', @@ -162,7 +164,7 @@ txPktPerS: 'txSpeed', rxPktPerS: 'rxSpeed', frequencyMhz: 'frequency', sf: 'sf', bwKhz: 'bw', powerDbm: 'power', codeRate: 'codeRate', crcEnabled: 'crc', bitrateBps: 'bitrate', fdevHz: 'fdev', modulationIndex: 'modIndex', - rxBandwidthHz: 'rxBw', syncWordLengthBits: 'syncWord' + rxBandwidthHz: 'rxBw', syncWordLengthBits: 'syncWord', goldCodesEnabled: 'goldCodes' }; for (const k of keys) { if (a[k] !== b[k] && !(a[k] == null && b[k] == null)) changed.add(map[k]); @@ -213,7 +215,8 @@ { key: 'fdev', label: 'FDev', fmt: s => s.fdevHz != null ? `${s.fdevHz} Hz` : '—' }, { key: 'modIndex', label: 'Mod Idx', fmt: s => s.modulationIndex != null ? String(s.modulationIndex) : '—' }, { key: 'rxBw', label: 'Rx BW', fmt: s => s.rxBandwidthHz != null ? `${s.rxBandwidthHz} Hz` : '—' }, - { key: 'syncWord', label: 'Sync Word', fmt: s => s.syncWordLengthBits != null ? `${s.syncWordLengthBits} bit` : '—' } + { key: 'syncWord', label: 'Sync Word', fmt: s => s.syncWordLengthBits != null ? `${s.syncWordLengthBits} bit` : '—' }, + { key: 'goldCodes', label: 'Gold Codes', fmt: s => fmtCrc(s.goldCodesEnabled) } ]; function staticRowsForCompare(txSnap, rxSnap) {