[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 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -698,6 +698,12 @@ function createDemodulator(sampleRate) {
|
|||||||
let dbgFramesOk = 0;
|
let dbgFramesOk = 0;
|
||||||
let dbgLastLog = 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
|
// Correlation buffers
|
||||||
let markI = 0, markQ = 0, spaceI = 0, spaceQ = 0;
|
let markI = 0, markQ = 0, spaceI = 0, spaceQ = 0;
|
||||||
const ringLen = windowLen;
|
const ringLen = windowLen;
|
||||||
@@ -722,7 +728,32 @@ function createDemodulator(sampleRate) {
|
|||||||
|
|
||||||
const frames = [];
|
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) {
|
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 t = sampleCount / sampleRate;
|
||||||
const mI = s * Math.cos(2 * Math.PI * MARK * t);
|
const mI = s * Math.cos(2 * Math.PI * MARK * t);
|
||||||
const mQ = s * Math.sin(2 * Math.PI * MARK * t);
|
const mQ = s * Math.sin(2 * Math.PI * MARK * t);
|
||||||
@@ -788,8 +819,11 @@ function createDemodulator(sampleRate) {
|
|||||||
dbgFlags++;
|
dbgFlags++;
|
||||||
if (inFrame && frameBits.length >= 136) {
|
if (inFrame && frameBits.length >= 136) {
|
||||||
dbgFrameAttempts++;
|
dbgFrameAttempts++;
|
||||||
const frame = bitsToBytes(frameBits);
|
const result = bitsToBytes(frameBits);
|
||||||
if (frame) { dbgFramesOk++; frames.push(frame); }
|
if (result) {
|
||||||
|
if (result.crcOk) dbgFramesOk++;
|
||||||
|
frames.push(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frameBits = [];
|
frameBits = [];
|
||||||
inFrame = true;
|
inFrame = true;
|
||||||
@@ -831,12 +865,21 @@ function createDemodulator(sampleRate) {
|
|||||||
const computed = crc16ccitt(payload);
|
const computed = crc16ccitt(payload);
|
||||||
if (computed !== fcs) {
|
if (computed !== fcs) {
|
||||||
dbgCrcFails++;
|
dbgCrcFails++;
|
||||||
console.debug("[APRS-DBG] CRC fail:", byteLen, "bytes, fcs=0x" + fcs.toString(16), "computed=0x" + computed.toString(16),
|
// Try to decode addresses for diagnostics
|
||||||
"first6:", Array.from(payload.subarray(0, 6)).map(b => b.toString(16).padStart(2, "0")).join(" "));
|
let addrInfo = "";
|
||||||
return null;
|
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) {
|
function processBuffer(samples) {
|
||||||
@@ -915,12 +958,15 @@ function parseAPRS(ax25) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addAprsPacket(pkt) {
|
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");
|
const row = document.createElement("div");
|
||||||
row.className = "aprs-packet";
|
row.className = "aprs-packet";
|
||||||
|
if (!pkt.crcOk) row.style.opacity = "0.5";
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const ts = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
const ts = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
row.innerHTML = `<span class="aprs-time">${ts}</span><span class="aprs-call">${pkt.srcCall}</span>>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: <span title="${pkt.type}">${pkt.info}</span>`;
|
const crcTag = pkt.crcOk ? "" : ' <span style="color:var(--accent-red);">[CRC]</span>';
|
||||||
|
row.innerHTML = `<span class="aprs-time">${ts}</span><span class="aprs-call">${pkt.srcCall}</span>>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: <span title="${pkt.type}">${pkt.info}</span>${crcTag}`;
|
||||||
aprsPacketsEl.prepend(row);
|
aprsPacketsEl.prepend(row);
|
||||||
while (aprsPacketsEl.children.length > APRS_MAX_PACKETS) {
|
while (aprsPacketsEl.children.length > APRS_MAX_PACKETS) {
|
||||||
aprsPacketsEl.removeChild(aprsPacketsEl.lastChild);
|
aprsPacketsEl.removeChild(aprsPacketsEl.lastChild);
|
||||||
@@ -973,10 +1019,11 @@ function startAprs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const frames = demodulator.processBuffer(mono);
|
const frames = demodulator.processBuffer(mono);
|
||||||
for (const f of frames) {
|
for (const result of frames) {
|
||||||
const ax25 = parseAX25(f);
|
const ax25 = parseAX25(result.payload);
|
||||||
if (!ax25) continue;
|
if (!ax25) continue;
|
||||||
const pkt = parseAPRS(ax25);
|
const pkt = parseAPRS(ax25);
|
||||||
|
pkt.crcOk = result.crcOk;
|
||||||
addAprsPacket(pkt);
|
addAprsPacket(pkt);
|
||||||
}
|
}
|
||||||
frame.close();
|
frame.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user