From 4c5e04bfc39f331f70d59283b4c9d8d447cf8586 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Sun, 8 Feb 2026 14:55:52 +0100 Subject: [PATCH] [fix](trx-frontend-http): add energy gate and show CRC-failing APRS frames Add silence detection that resets the demodulator state when RMS drops below threshold, so burst-mode APRS packets after squelch activation start with a clean correlator and clock recovery. Display CRC-failing frames at reduced opacity with a [CRC] tag to aid debugging. Enhanced CRC-fail console logging now includes attempted address decode, bit count, and hex dump. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Stanislaw Grams --- .../trx-frontend-http/assets/web/app.js | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index b66e7da..5eef02b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -698,6 +698,12 @@ function createDemodulator(sampleRate) { let dbgFramesOk = 0; let dbgLastLog = 0; + // Energy gate — reset demodulator when signal is absent + let energyAcc = 0; + let energyCount = 0; + const ENERGY_WINDOW = Math.round(sampleRate * 0.05); // 50ms window + const ENERGY_THRESHOLD = 0.001; // silence threshold + // Correlation buffers let markI = 0, markQ = 0, spaceI = 0, spaceQ = 0; const ringLen = windowLen; @@ -722,7 +728,32 @@ function createDemodulator(sampleRate) { const frames = []; + function resetState() { + markI = markQ = spaceI = spaceQ = 0; + ringMarkI.fill(0); ringMarkQ.fill(0); + ringSpaceI.fill(0); ringSpaceQ.fill(0); + ringIdx = 0; + lastTone = 0; + bitPhase = 0; + prevSampledBit = 0; + ones = 0; + frameBits = []; + inFrame = false; + } + function processSample(s) { + // Energy gate: detect silence and reset state + energyAcc += s * s; + energyCount++; + if (energyCount >= ENERGY_WINDOW) { + const rms = Math.sqrt(energyAcc / energyCount); + if (rms < ENERGY_THRESHOLD) { + resetState(); + } + energyAcc = 0; + energyCount = 0; + } + const t = sampleCount / sampleRate; const mI = s * Math.cos(2 * Math.PI * MARK * t); const mQ = s * Math.sin(2 * Math.PI * MARK * t); @@ -788,8 +819,11 @@ function createDemodulator(sampleRate) { dbgFlags++; if (inFrame && frameBits.length >= 136) { dbgFrameAttempts++; - const frame = bitsToBytes(frameBits); - if (frame) { dbgFramesOk++; frames.push(frame); } + const result = bitsToBytes(frameBits); + if (result) { + if (result.crcOk) dbgFramesOk++; + frames.push(result); + } } frameBits = []; inFrame = true; @@ -831,12 +865,21 @@ function createDemodulator(sampleRate) { const computed = crc16ccitt(payload); if (computed !== fcs) { dbgCrcFails++; - console.debug("[APRS-DBG] CRC fail:", byteLen, "bytes, fcs=0x" + fcs.toString(16), "computed=0x" + computed.toString(16), - "first6:", Array.from(payload.subarray(0, 6)).map(b => b.toString(16).padStart(2, "0")).join(" ")); - return null; + // Try to decode addresses for diagnostics + let addrInfo = ""; + if (payload.length >= 14) { + const dstCall = Array.from(payload.subarray(0, 6)).map(b => String.fromCharCode(b >> 1)).join("").trim(); + const srcCall = Array.from(payload.subarray(7, 13)).map(b => String.fromCharCode(b >> 1)).join("").trim(); + addrInfo = ` dst="${dstCall}" src="${srcCall}"`; + } + console.debug("[APRS-DBG] CRC fail:", byteLen, "bytes, fcs=0x" + fcs.toString(16), + "computed=0x" + computed.toString(16), "bits:", bits.length, addrInfo, + "hex:", Array.from(bytes.subarray(0, Math.min(20, byteLen))).map(b => b.toString(16).padStart(2, "0")).join(" ")); + // Return as suspect frame for display + return { payload, crcOk: false }; } - return payload; + return { payload, crcOk: true }; } function processBuffer(samples) { @@ -915,12 +958,15 @@ function parseAPRS(ax25) { } function addAprsPacket(pkt) { - console.log("[APRS]", `${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`, pkt); + const tag = pkt.crcOk ? "[APRS]" : "[APRS-CRC-FAIL]"; + console.log(tag, `${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`, pkt); const row = document.createElement("div"); row.className = "aprs-packet"; + if (!pkt.crcOk) row.style.opacity = "0.5"; const now = new Date(); const ts = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); - row.innerHTML = `${ts}${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`; + const crcTag = pkt.crcOk ? "" : ' [CRC]'; + row.innerHTML = `${ts}${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}${crcTag}`; aprsPacketsEl.prepend(row); while (aprsPacketsEl.children.length > APRS_MAX_PACKETS) { aprsPacketsEl.removeChild(aprsPacketsEl.lastChild); @@ -973,10 +1019,11 @@ function startAprs() { } } const frames = demodulator.processBuffer(mono); - for (const f of frames) { - const ax25 = parseAX25(f); + for (const result of frames) { + const ax25 = parseAX25(result.payload); if (!ax25) continue; const pkt = parseAPRS(ax25); + pkt.crcOk = result.crcOk; addAprsPacket(pkt); } frame.close();