diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/cw.js
deleted file mode 100644
index 2b44b13..0000000
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/cw.js
+++ /dev/null
@@ -1,287 +0,0 @@
-// --- CW (Morse) Decoder Plugin ---
-const cwToggleBtn = document.getElementById("cw-toggle-btn");
-const cwStatusEl = document.getElementById("cw-status");
-const cwOutputEl = document.getElementById("cw-output");
-const cwWpmInput = document.getElementById("cw-wpm");
-const cwToneInput = document.getElementById("cw-tone");
-const cwThresholdInput = document.getElementById("cw-threshold");
-const cwThresholdVal = document.getElementById("cw-threshold-val");
-const cwSignalIndicator = document.getElementById("cw-signal-indicator");
-const CW_MAX_LINES = 200;
-
-let cwActive = false;
-let cwWs = null;
-let cwAudioCtx = null;
-let cwDecoder = null;
-
-// ITU Morse code lookup
-const MORSE_TABLE = {
- ".-": "A", "-...": "B", "-.-.": "C", "-..": "D", ".": "E",
- "..-.": "F", "--.": "G", "....": "H", "..": "I", ".---": "J",
- "-.-": "K", ".-..": "L", "--": "M", "-.": "N", "---": "O",
- ".--.": "P", "--.-": "Q", ".-.": "R", "...": "S", "-": "T",
- "..-": "U", "...-": "V", ".--": "W", "-..-": "X", "-.--": "Y",
- "--..": "Z",
- "-----": "0", ".----": "1", "..---": "2", "...--": "3", "....-": "4",
- ".....": "5", "-....": "6", "--...": "7", "---..": "8", "----.": "9",
- ".-.-.-": ".", "--..--": ",", "..--..": "?", ".----.": "'",
- "-.-.--": "!", "-..-.": "/", "-.--.": "(", "-.--.-": ")",
- ".-...": "&", "---...": ":", "-.-.-.": ";", "-...-": "=",
- ".-.-.": "+", "-....-": "-", "..--.-": "_", ".-..-.": "\"",
- "...-..-": "$", ".--.-.": "@",
-};
-
-// Update threshold display
-cwThresholdInput.addEventListener("input", () => {
- cwThresholdVal.textContent = (cwThresholdInput.value / 100).toFixed(2);
-});
-
-function createCwDecoder(sampleRate) {
- let wpm = parseInt(cwWpmInput.value, 10) || 15;
- let toneFreq = parseInt(cwToneInput.value, 10) || 700;
- let threshold = (parseInt(cwThresholdInput.value, 10) || 5) / 100;
-
- // Goertzel parameters
- const windowMs = 50; // 50ms analysis window
- const windowSize = Math.round(sampleRate * windowMs / 1000);
- const k = Math.round(toneFreq * windowSize / sampleRate);
- const omega = (2 * Math.PI * k) / windowSize;
- const coeff = 2 * Math.cos(omega);
-
- let sampleBuf = new Float32Array(windowSize);
- let sampleIdx = 0;
-
- // Tone state tracking
- let toneOn = false;
- let toneOnAt = 0;
- let toneOffAt = 0;
- let currentSymbol = ""; // accumulates dits/dahs for current character
- let decoded = "";
- let lastAppendTime = 0;
-
- // Timing: 1 unit = 1200/WPM ms
- function unitMs() { return 1200 / wpm; }
-
- function goertzelDetect(buf) {
- let s0 = 0, s1 = 0, s2 = 0;
- let totalEnergy = 0;
- for (let i = 0; i < buf.length; i++) {
- s0 = coeff * s1 - s2 + buf[i];
- s2 = s1;
- s1 = s0;
- totalEnergy += buf[i] * buf[i];
- }
- const toneEnergy = (s1 * s1 + s2 * s2 - coeff * s1 * s2) / (buf.length * buf.length);
- const avgEnergy = totalEnergy / buf.length;
- if (avgEnergy < 1e-10) return false;
- return (toneEnergy / avgEnergy) > threshold;
- }
-
- function processWindow() {
- const detected = goertzelDetect(sampleBuf);
- const now = performance.now();
-
- // Update signal indicator
- if (detected) {
- cwSignalIndicator.className = "cw-signal-on";
- } else {
- cwSignalIndicator.className = "cw-signal-off";
- }
-
- if (detected && !toneOn) {
- // Tone just turned on
- toneOn = true;
- const offDuration = now - toneOffAt;
- if (toneOffAt > 0) {
- const u = unitMs();
- if (offDuration > u * 5) {
- // Word gap (7 units, use 5 as threshold)
- if (currentSymbol) {
- const ch = MORSE_TABLE[currentSymbol] || "?";
- appendChar(ch);
- currentSymbol = "";
- }
- appendChar(" ");
- } else if (offDuration > u * 2) {
- // Character gap (3 units, use 2 as threshold)
- if (currentSymbol) {
- const ch = MORSE_TABLE[currentSymbol] || "?";
- appendChar(ch);
- currentSymbol = "";
- }
- }
- // else: inter-element gap, do nothing
- }
- toneOnAt = now;
- } else if (!detected && toneOn) {
- // Tone just turned off
- toneOn = false;
- const onDuration = now - toneOnAt;
- const u = unitMs();
- if (onDuration > u * 2) {
- currentSymbol += "-"; // dah (3 units, use 2 as threshold)
- } else {
- currentSymbol += "."; // dit
- }
- toneOffAt = now;
- }
-
- // Flush pending character after long silence
- if (!toneOn && currentSymbol && toneOffAt > 0) {
- const silenceDuration = now - toneOffAt;
- if (silenceDuration > unitMs() * 5) {
- const ch = MORSE_TABLE[currentSymbol] || "?";
- appendChar(ch);
- currentSymbol = "";
- }
- }
- }
-
- function appendChar(ch) {
- decoded += ch;
- // Append to output element
- const now = Date.now();
- if (!cwOutputEl.lastElementChild || now - lastAppendTime > 10000 || ch === "\n") {
- const line = document.createElement("div");
- line.className = "cw-line";
- cwOutputEl.appendChild(line);
- }
- lastAppendTime = now;
- const lastLine = cwOutputEl.lastElementChild;
- if (lastLine) {
- lastLine.textContent += ch;
- }
- // Cap lines
- while (cwOutputEl.children.length > CW_MAX_LINES) {
- cwOutputEl.removeChild(cwOutputEl.firstChild);
- }
- cwOutputEl.scrollTop = cwOutputEl.scrollHeight;
- }
-
- function processSamples(mono) {
- for (let i = 0; i < mono.length; i++) {
- sampleBuf[sampleIdx++] = mono[i];
- if (sampleIdx >= windowSize) {
- processWindow();
- sampleIdx = 0;
- }
- }
- }
-
- function updateConfig() {
- wpm = parseInt(cwWpmInput.value, 10) || 15;
- threshold = (parseInt(cwThresholdInput.value, 10) || 5) / 100;
- }
-
- return { processSamples, updateConfig };
-}
-
-function startCw() {
- if (cwActive) { stopCw(); return; }
- if (!hasWebCodecs) {
- cwStatusEl.textContent = "Requires Chrome/Edge";
- return;
- }
-
- const proto = location.protocol === "https:" ? "wss:" : "ws:";
- cwWs = new WebSocket(`${proto}//${location.host}/audio`);
- cwWs.binaryType = "arraybuffer";
- cwStatusEl.textContent = "Connecting…";
-
- let decoderEngine = null;
-
- cwWs.onopen = () => {
- cwStatusEl.textContent = "Waiting for stream info…";
- };
-
- cwWs.onmessage = (evt) => {
- if (typeof evt.data === "string") {
- try {
- const info = JSON.parse(evt.data);
- const sr = info.sample_rate || 48000;
- const ch = info.channels || 1;
- cwAudioCtx = new AudioContext({ sampleRate: sr });
- decoderEngine = createCwDecoder(sr);
-
- let cwFrameCount = 0;
- cwDecoder = new AudioDecoder({
- output: (frame) => {
- if (cwFrameCount++ === 0) {
- console.log("[CW-DBG] First PCM frame:", frame.numberOfFrames, "samples,", frame.numberOfChannels, "ch, format:", frame.format, "sr:", frame.sampleRate);
- }
- const buf = new Float32Array(frame.numberOfFrames * frame.numberOfChannels);
- frame.copyTo(buf, { planeIndex: 0 });
- let mono;
- if (frame.numberOfChannels === 1) {
- mono = buf;
- } else {
- mono = new Float32Array(frame.numberOfFrames);
- for (let i = 0; i < frame.numberOfFrames; i++) {
- mono[i] = buf[i * frame.numberOfChannels];
- }
- }
- decoderEngine.processSamples(mono);
- frame.close();
- },
- error: (e) => { console.error("CW AudioDecoder error", e); }
- });
- cwDecoder.configure({
- codec: "opus",
- sampleRate: sr,
- numberOfChannels: ch,
- });
-
- cwActive = true;
- cwToggleBtn.style.borderColor = "#00d17f";
- cwToggleBtn.style.color = "#00d17f";
- cwToggleBtn.textContent = "Stop CW";
- cwStatusEl.textContent = "Listening…";
-
- // Allow live config updates
- cwWpmInput.addEventListener("change", decoderEngine.updateConfig);
- cwThresholdInput.addEventListener("input", decoderEngine.updateConfig);
- } catch (e) {
- console.error("CW stream info error", e);
- cwStatusEl.textContent = "Error";
- }
- return;
- }
-
- // Binary Opus data
- if (!cwDecoder) return;
- try {
- cwDecoder.decode(new EncodedAudioChunk({
- type: "key",
- timestamp: performance.now() * 1000,
- data: new Uint8Array(evt.data),
- }));
- } catch (e) {
- // Ignore individual decode errors
- }
- };
-
- cwWs.onclose = () => {
- stopCw();
- };
-
- cwWs.onerror = () => {
- cwStatusEl.textContent = "Connection error";
- };
-}
-
-function stopCw() {
- cwActive = false;
- if (cwWs) { cwWs.close(); cwWs = null; }
- if (cwAudioCtx) { cwAudioCtx.close(); cwAudioCtx = null; }
- if (cwDecoder) {
- try { cwDecoder.close(); } catch (e) {}
- cwDecoder = null;
- }
- cwToggleBtn.style.borderColor = "";
- cwToggleBtn.style.color = "";
- cwToggleBtn.textContent = "Start CW";
- cwStatusEl.textContent = "Stopped";
- cwSignalIndicator.className = "cw-signal-off";
-}
-
-cwToggleBtn.addEventListener("click", startCw);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
similarity index 64%
rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/aprs.js
rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
index b77ea95..2cf1563 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
@@ -30,7 +30,7 @@ function crc16ccitt(bytes) {
}
// AFSK Bell 202 Demodulator (1200 baud, mark=1200Hz, space=2200Hz)
-// Uses delay-and-multiply frequency discriminator for robust non-coherent decoding.
+// Uses mark/space correlation detector (non-coherent FSK matched filter).
function createDemodulator(sampleRate) {
const BAUD = 1200;
const MARK = 1200;
@@ -52,35 +52,27 @@ function createDemodulator(sampleRate) {
const ENERGY_WINDOW = Math.round(sampleRate * 0.05);
const ENERGY_THRESHOLD = 0.001;
- // Bandpass pre-filter: biquad centered at 1700 Hz (AFSK midpoint), Q=1.2
- // Passes ~1000-2500 Hz, removes out-of-band noise
- const bpfW0 = 2 * Math.PI * ((MARK + SPACE) / 2) / sampleRate;
- const bpfAlpha = Math.sin(bpfW0) / (2 * 1.2);
- const bpfA0 = 1 + bpfAlpha;
- const bpfCoeffs = {
- b0: bpfAlpha / bpfA0, b1: 0, b2: -bpfAlpha / bpfA0,
- a1: (-2 * Math.cos(bpfW0)) / bpfA0, a2: (1 - bpfAlpha) / bpfA0,
- };
- let bpfState = [0, 0, 0, 0]; // x1, x2, y1, y2
+ // Mark/space correlation detector
+ // Mix input with cos/sin reference oscillators at mark and space frequencies,
+ // then integrate over one bit period to get I/Q energy at each frequency.
+ const markPhaseInc = 2 * Math.PI * MARK / sampleRate;
+ const spacePhaseInc = 2 * Math.PI * SPACE / sampleRate;
+ let markPhase = 0;
+ let spacePhase = 0;
- // Delay-and-multiply discriminator
- // Delay = Fs / (2 * Δf) where Δf = space - mark = 1000 Hz
- // At this delay: mark tone → negative product, space tone → positive product
- const DELAY = Math.round(sampleRate / (2 * (SPACE - MARK)));
- const delayBuf = new Float32Array(DELAY);
- let delayIdx = 0;
-
- // Moving-average LPF over half a bit period
- // First null at 2*mark freq (2400 Hz), cleanly removing discriminator artifacts
- const avgLen = Math.round(samplesPerBit / 2);
- const avgBuf = new Float32Array(avgLen);
- let avgIdx = 0;
- let avgSum = 0;
+ // Sliding-window matched filter (1 bit period)
+ const corrLen = Math.round(samplesPerBit);
+ const markIBuf = new Float32Array(corrLen);
+ const markQBuf = new Float32Array(corrLen);
+ const spaceIBuf = new Float32Array(corrLen);
+ const spaceQBuf = new Float32Array(corrLen);
+ let corrIdx = 0;
+ let markISum = 0, markQSum = 0, spaceISum = 0, spaceQSum = 0;
// Clock recovery (PLL)
- let lastTone = 0;
+ let lastBit = 0;
let bitPhase = 0;
- const PLL_GAIN = 0.7;
+ const PLL_GAIN = 0.4;
// NRZI state
let prevSampledBit = 0;
@@ -93,13 +85,14 @@ function createDemodulator(sampleRate) {
const frames = [];
function resetState() {
- bpfState = [0, 0, 0, 0];
- delayBuf.fill(0);
- delayIdx = 0;
- avgBuf.fill(0);
- avgIdx = 0;
- avgSum = 0;
- lastTone = 0;
+ markPhase = 0;
+ spacePhase = 0;
+ markIBuf.fill(0); markQBuf.fill(0);
+ spaceIBuf.fill(0); spaceQBuf.fill(0);
+ corrIdx = 0;
+ markISum = 0; markQSum = 0;
+ spaceISum = 0; spaceQSum = 0;
+ lastBit = 0;
bitPhase = 0;
prevSampledBit = 0;
ones = 0;
@@ -119,31 +112,35 @@ function createDemodulator(sampleRate) {
energyCount = 0;
}
- // Bandpass pre-filter
- const c = bpfCoeffs;
- const filtered = c.b0 * s + c.b1 * bpfState[0] + c.b2 * bpfState[1]
- - c.a1 * bpfState[2] - c.a2 * bpfState[3];
- bpfState[1] = bpfState[0]; bpfState[0] = s;
- bpfState[3] = bpfState[2]; bpfState[2] = filtered;
+ // Mix with mark/space reference oscillators
+ const mI = s * Math.cos(markPhase);
+ const mQ = s * Math.sin(markPhase);
+ const sI = s * Math.cos(spacePhase);
+ const sQ = s * Math.sin(spacePhase);
+ markPhase += markPhaseInc;
+ spacePhase += spacePhaseInc;
+ if (markPhase > 6.283185307) markPhase -= 6.283185307;
+ if (spacePhase > 6.283185307) spacePhase -= 6.283185307;
- // Delay-and-multiply frequency discriminator
- const delayed = delayBuf[delayIdx];
- delayBuf[delayIdx] = filtered;
- delayIdx = (delayIdx + 1) % DELAY;
+ // Sliding-window integration (matched filter over 1 bit period)
+ markISum += mI - markIBuf[corrIdx];
+ markQSum += mQ - markQBuf[corrIdx];
+ spaceISum += sI - spaceIBuf[corrIdx];
+ spaceQSum += sQ - spaceQBuf[corrIdx];
+ markIBuf[corrIdx] = mI;
+ markQBuf[corrIdx] = mQ;
+ spaceIBuf[corrIdx] = sI;
+ spaceQBuf[corrIdx] = sQ;
+ corrIdx = (corrIdx + 1) % corrLen;
- const disc = filtered * delayed;
-
- // Moving-average LPF
- avgSum += disc - avgBuf[avgIdx];
- avgBuf[avgIdx] = disc;
- avgIdx = (avgIdx + 1) % avgLen;
-
- // mark (1200 Hz) → negative, space (2200 Hz) → positive
- const bit = avgSum < 0 ? 1 : 0;
+ // Compare mark vs space energy (I²+Q²)
+ const markEnergy = markISum * markISum + markQSum * markQSum;
+ const spaceEnergy = spaceISum * spaceISum + spaceQSum * spaceQSum;
+ const bit = markEnergy > spaceEnergy ? 1 : 0;
// PLL clock recovery
- if (bit !== lastTone) {
- lastTone = bit;
+ if (bit !== lastBit) {
+ lastBit = bit;
const error = bitPhase - samplesPerBit / 2;
bitPhase -= PLL_GAIN * error;
}
@@ -318,7 +315,117 @@ function parseAPRS(ax25) {
else if (dt === "`" || dt === "'") type = "Mic-E";
}
- return { srcCall, destCall, path, info: infoStr, type };
+ const result = { srcCall, destCall, path, info: infoStr, type };
+
+ if (type === "Position") {
+ const pos = parseAprsPosition(infoStr);
+ if (pos) {
+ result.lat = pos.lat;
+ result.lon = pos.lon;
+ result.symbolTable = pos.symbolTable;
+ result.symbolCode = pos.symbolCode;
+ }
+ }
+
+ return result;
+}
+
+function parseAprsPosition(infoStr) {
+ if (infoStr.length < 1) return null;
+ const dt = infoStr[0];
+ let posStr;
+
+ if (dt === "!" || dt === "=") {
+ posStr = infoStr.substring(1);
+ } else if (dt === "/" || dt === "@") {
+ if (infoStr.length < 8) return null;
+ posStr = infoStr.substring(8);
+ } else {
+ return null;
+ }
+
+ if (posStr.length < 1) return null;
+
+ // Compressed format: first char is symbol table (not a digit)
+ // Layout: T YYYY XXXX C [cs T] — 10 chars minimum
+ const firstChar = posStr[0];
+ if (firstChar < "0" || firstChar > "9") {
+ return parseAprsCompressed(posStr);
+ }
+
+ // Uncompressed: DDMM.MMN/DDDMM.MMEsYYY...
+ // Need at least: 8 lat + 1 table + 9 lon + 1 code = 19 chars
+ if (posStr.length < 19) return null;
+
+ const latStr = posStr.substring(0, 8); // DDMM.MMN
+ const symbolTable = posStr[8];
+ const lonStr = posStr.substring(9, 18); // DDDMM.MME
+ const symbolCode = posStr[18];
+
+ const lat = parseAprsLat(latStr);
+ const lon = parseAprsLon(lonStr);
+ if (lat === null || lon === null) return null;
+
+ return { lat, lon, symbolTable, symbolCode };
+}
+
+function parseAprsCompressed(posStr) {
+ // Compressed position: SymTable(1) Lat(4) Lon(4) SymCode(1) = 10 chars min
+ if (posStr.length < 10) return null;
+
+ const symbolTable = posStr[0];
+ const latChars = posStr.substring(1, 5);
+ const lonChars = posStr.substring(5, 9);
+ const symbolCode = posStr[9];
+
+ // Base-91 decode: each char value = (ASCII - 33)
+ let latVal = 0;
+ let lonVal = 0;
+ for (let i = 0; i < 4; i++) {
+ const lc = latChars.charCodeAt(i) - 33;
+ const xc = lonChars.charCodeAt(i) - 33;
+ if (lc < 0 || lc > 90 || xc < 0 || xc > 90) return null;
+ latVal = latVal * 91 + lc;
+ lonVal = lonVal * 91 + xc;
+ }
+
+ const lat = 90 - latVal / 380926;
+ const lon = -180 + lonVal / 190463;
+
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
+
+ return {
+ lat: Math.round(lat * 1e6) / 1e6,
+ lon: Math.round(lon * 1e6) / 1e6,
+ symbolTable,
+ symbolCode,
+ };
+}
+
+function parseAprsLat(s) {
+ // DDMM.MMN
+ if (s.length < 8) return null;
+ const deg = parseInt(s.substring(0, 2), 10);
+ const min = parseFloat(s.substring(2, 7));
+ const ns = s[7];
+ if (isNaN(deg) || isNaN(min)) return null;
+ let lat = deg + min / 60;
+ if (ns === "S" || ns === "s") lat = -lat;
+ else if (ns !== "N" && ns !== "n") return null;
+ return Math.round(lat * 1e6) / 1e6;
+}
+
+function parseAprsLon(s) {
+ // DDDMM.MME
+ if (s.length < 9) return null;
+ const deg = parseInt(s.substring(0, 3), 10);
+ const min = parseFloat(s.substring(3, 8));
+ const ew = s[8];
+ if (isNaN(deg) || isNaN(min)) return null;
+ let lon = deg + min / 60;
+ if (ew === "W" || ew === "w") lon = -lon;
+ else if (ew !== "E" && ew !== "e") return null;
+ return Math.round(lon * 1e6) / 1e6;
}
function addAprsPacket(pkt) {
@@ -330,7 +437,22 @@ function addAprsPacket(pkt) {
const now = new Date();
const ts = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
const crcTag = pkt.crcOk ? "" : ' [CRC]';
- row.innerHTML = `${ts}${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}${crcTag}`;
+ let symbolHtml = "";
+ if (pkt.symbolTable && pkt.symbolCode) {
+ const sheet = pkt.symbolTable === "/" ? 0 : 1;
+ const code = pkt.symbolCode.charCodeAt(0) - 33;
+ const col = code % 16;
+ const row2 = Math.floor(code / 16);
+ const bgX = -(col * 24);
+ const bgY = -(row2 * 24);
+ symbolHtml = ``;
+ }
+ let posHtml = "";
+ if (pkt.lat != null && pkt.lon != null) {
+ const osmUrl = `https://www.openstreetmap.org/?mlat=${pkt.lat}&mlon=${pkt.lon}#map=15/${pkt.lat}/${pkt.lon}`;
+ posHtml = ` ${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`;
+ }
+ row.innerHTML = `${ts}${symbolHtml}${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}${posHtml}${crcTag}`;
aprsPacketsEl.prepend(row);
while (aprsPacketsEl.children.length > APRS_MAX_PACKETS) {
aprsPacketsEl.removeChild(aprsPacketsEl.lastChild);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index 4d694e8..479ce43 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -218,6 +218,9 @@ small { color: var(--text-muted); }
.aprs-packet:last-child { border-bottom: none; }
.aprs-call { color: var(--accent-green); font-weight: 600; }
.aprs-time { color: var(--text-muted); margin-right: 0.5rem; }
+.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-size: 384px 192px; vertical-align: middle; margin-right: 0.3rem; }
+.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
+.aprs-pos:hover { text-decoration: underline; }
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.cw-config { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin-bottom: 0.75rem; }
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs
index 31a3d22..f6e7a6f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs
@@ -8,8 +8,8 @@ const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const INDEX_HTML: &str = include_str!("../assets/web/index.html");
pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
pub const APP_JS: &str = include_str!("../assets/web/app.js");
-pub const APRS_JS: &str = include_str!("../assets/web/aprs.js");
-pub const CW_JS: &str = include_str!("../assets/web/cw.js");
+pub const APRS_JS: &str = include_str!("../assets/web/plugins/aprs.js");
+pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
pub fn index_html() -> String {
INDEX_HTML