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 11162ba..8e6547d 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
@@ -1,3 +1,15 @@
+// --- Persistent settings (localStorage) ---
+const STORAGE_PREFIX = "trx_";
+function saveSetting(key, value) {
+ try { localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value)); } catch(e) {}
+}
+function loadSetting(key, fallback) {
+ try {
+ const v = localStorage.getItem(STORAGE_PREFIX + key);
+ return v !== null ? JSON.parse(v) : fallback;
+ } catch(e) { return fallback; }
+}
+
const freqEl = document.getElementById("freq");
const modeEl = document.getElementById("mode");
const bandLabel = document.getElementById("band-label");
@@ -32,7 +44,7 @@ let hintTimer = null;
let sigMeasuring = false;
let sigSamples = [];
let lastFreqHz = null;
-let jogStep = 1000; // default 1 kHz
+let jogStep = loadSetting("jogStep", 1000);
const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"];
function vfoColor(idx) {
if (idx < VFO_COLORS.length) return VFO_COLORS[idx];
@@ -562,6 +574,12 @@ jogStepEl.addEventListener("click", (e) => {
jogStep = parseInt(btn.dataset.step, 10);
jogStepEl.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
+ saveSetting("jogStep", jogStep);
+});
+
+// Restore active jog step button from saved setting
+jogStepEl.querySelectorAll("button").forEach((b) => {
+ b.classList.toggle("active", parseInt(b.dataset.step, 10) === jogStep);
});
modeBtn.addEventListener("click", async () => {
@@ -1076,24 +1094,31 @@ txAudioBtn.addEventListener("click", startTxAudio);
const rxVolPct = document.getElementById("rx-vol-pct");
const txVolPct = document.getElementById("tx-vol-pct");
+// Restore saved volumes
+rxVolSlider.value = loadSetting("rxVol", 80);
+txVolSlider.value = loadSetting("txVol", 80);
+rxVolPct.textContent = `${rxVolSlider.value}%`;
+txVolPct.textContent = `${txVolSlider.value}%`;
+
function updateVolSlider(slider, pctEl, gainNode) {
pctEl.textContent = `${slider.value}%`;
if (gainNode) gainNode.gain.value = slider.value / 100;
}
-rxVolSlider.addEventListener("input", () => updateVolSlider(rxVolSlider, rxVolPct, rxGainNode));
-txVolSlider.addEventListener("input", () => updateVolSlider(txVolSlider, txVolPct, txGainNode));
+rxVolSlider.addEventListener("input", () => { updateVolSlider(rxVolSlider, rxVolPct, rxGainNode); saveSetting("rxVol", Number(rxVolSlider.value)); });
+txVolSlider.addEventListener("input", () => { updateVolSlider(txVolSlider, txVolPct, txGainNode); saveSetting("txVol", Number(txVolSlider.value)); });
-function volWheel(slider, pctEl, getGain) {
+function volWheel(slider, pctEl, getGain, storageKey) {
slider.addEventListener("wheel", (e) => {
e.preventDefault();
const step = e.deltaY < 0 ? 2 : -2;
slider.value = Math.max(0, Math.min(100, Number(slider.value) + step));
updateVolSlider(slider, pctEl, getGain());
+ saveSetting(storageKey, Number(slider.value));
}, { passive: false });
}
-volWheel(rxVolSlider, rxVolPct, () => rxGainNode);
-volWheel(txVolSlider, txVolPct, () => txGainNode);
+volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
+volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
document.getElementById("copyright-year").textContent = new Date().getFullYear();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
index cae8edd..adeda69 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
@@ -9,6 +9,9 @@ let aprsWs = null;
let aprsAudioCtx = null;
let aprsDecoder = null;
+// Persistent packet history
+let aprsPacketHistory = loadSetting("aprsPackets", []);
+
// CRC-16-CCITT lookup table
const CRC_CCITT_TABLE = new Uint16Array(256);
(function initCrc() {
@@ -448,14 +451,11 @@ function escapeAprsInfo(str) {
return out;
}
-function addAprsPacket(pkt) {
- const tag = pkt.crcOk ? "[APRS]" : "[APRS-CRC-FAIL]";
- console.log(tag, `${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`, pkt);
+function renderAprsRow(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" });
+ const ts = pkt._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
const crcTag = pkt.crcOk ? "" : ' [CRC]';
let symbolHtml = "";
if (pkt.symbolTable && pkt.symbolCode) {
@@ -473,6 +473,22 @@ function addAprsPacket(pkt) {
posHtml = ` ${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`;
}
row.innerHTML = `${ts}${symbolHtml}${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${escapeAprsInfo(pkt.info)}${posHtml}${crcTag}`;
+ return row;
+}
+
+function addAprsPacket(pkt) {
+ const tag = pkt.crcOk ? "[APRS]" : "[APRS-CRC-FAIL]";
+ console.log(tag, `${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`, pkt);
+
+ // Stamp timestamp for persistence
+ pkt._ts = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+
+ // Persist to history
+ aprsPacketHistory.unshift(pkt);
+ if (aprsPacketHistory.length > APRS_MAX_PACKETS) aprsPacketHistory.length = APRS_MAX_PACKETS;
+ saveSetting("aprsPackets", aprsPacketHistory);
+
+ const row = renderAprsRow(pkt);
if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode);
}
@@ -483,7 +499,7 @@ function addAprsPacket(pkt) {
}
function startAprs() {
- if (aprsActive) { stopAprs(); return; }
+ if (aprsActive) return;
if (!hasWebCodecs) {
aprsStatus.textContent = "Requires Chrome/Edge";
return;
@@ -565,6 +581,7 @@ function startAprs() {
});
aprsActive = true;
+ saveSetting("aprsRunning", true);
aprsToggleBtn.style.borderColor = "#00d17f";
aprsToggleBtn.style.color = "#00d17f";
aprsToggleBtn.textContent = "Stop APRS";
@@ -590,7 +607,7 @@ function startAprs() {
};
aprsWs.onclose = () => {
- stopAprs();
+ stopAprs(false);
};
aprsWs.onerror = () => {
@@ -598,8 +615,9 @@ function startAprs() {
};
}
-function stopAprs() {
+function stopAprs(explicit) {
aprsActive = false;
+ if (explicit) saveSetting("aprsRunning", false);
if (aprsWs) { aprsWs.close(); aprsWs = null; }
if (aprsAudioCtx) { aprsAudioCtx.close(); aprsAudioCtx = null; }
if (aprsDecoder) {
@@ -612,4 +630,20 @@ function stopAprs() {
aprsStatus.textContent = "Stopped";
}
-aprsToggleBtn.addEventListener("click", startAprs);
+aprsToggleBtn.addEventListener("click", () => {
+ if (aprsActive) { stopAprs(true); } else { startAprs(); }
+});
+
+// Restore saved packets and map markers on page load
+for (let i = aprsPacketHistory.length - 1; i >= 0; i--) {
+ const pkt = aprsPacketHistory[i];
+ aprsPacketsEl.prepend(renderAprsRow(pkt));
+ if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
+ window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode);
+ }
+}
+
+// Auto-start APRS if it was running before page refresh
+if (loadSetting("aprsRunning", false) && hasWebCodecs) {
+ startAprs();
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
index 59493ce..bba38fe 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
@@ -11,6 +11,16 @@ const cwWpmAutoCheck = document.getElementById("cw-wpm-auto");
const cwToneAutoCheck = document.getElementById("cw-tone-auto");
const CW_MAX_LINES = 200;
+// Restore saved CW settings
+cwWpmInput.value = loadSetting("cwWpm", 15);
+cwToneInput.value = loadSetting("cwTone", 700);
+cwThresholdInput.value = loadSetting("cwThreshold", 5);
+cwThresholdVal.textContent = (cwThresholdInput.value / 100).toFixed(2);
+cwWpmAutoCheck.checked = loadSetting("cwWpmAuto", true);
+cwToneAutoCheck.checked = loadSetting("cwToneAuto", true);
+cwWpmInput.readOnly = cwWpmAutoCheck.checked;
+cwToneInput.readOnly = cwToneAutoCheck.checked;
+
let cwActive = false;
let cwWs = null;
let cwAudioCtx = null;
@@ -36,18 +46,25 @@ const MORSE_TABLE = {
// Update threshold display
cwThresholdInput.addEventListener("input", () => {
cwThresholdVal.textContent = (cwThresholdInput.value / 100).toFixed(2);
+ saveSetting("cwThreshold", Number(cwThresholdInput.value));
});
// Toggle readonly on WPM input based on Auto checkbox
cwWpmAutoCheck.addEventListener("change", () => {
cwWpmInput.readOnly = cwWpmAutoCheck.checked;
+ saveSetting("cwWpmAuto", cwWpmAutoCheck.checked);
});
// Toggle readonly on Tone input based on Auto checkbox
cwToneAutoCheck.addEventListener("change", () => {
cwToneInput.readOnly = cwToneAutoCheck.checked;
+ saveSetting("cwToneAuto", cwToneAutoCheck.checked);
});
+// Save WPM/Tone when manually changed
+cwWpmInput.addEventListener("change", () => { saveSetting("cwWpm", Number(cwWpmInput.value)); });
+cwToneInput.addEventListener("change", () => { saveSetting("cwTone", Number(cwToneInput.value)); });
+
function createCwDecoder(sampleRate) {
let wpm = parseInt(cwWpmInput.value, 10) || 15;
let toneFreq = parseInt(cwToneInput.value, 10) || 700;
@@ -163,6 +180,7 @@ function createCwDecoder(sampleRate) {
if (Math.abs(detectedFreq - toneFreq) > TONE_SCAN_STEP) {
recomputeGoertzel(detectedFreq);
cwToneInput.value = detectedFreq;
+ saveSetting("cwTone", detectedFreq);
}
}
}
@@ -201,6 +219,7 @@ function createCwDecoder(sampleRate) {
if (newWpm !== wpm) {
wpm = newWpm;
cwWpmInput.value = wpm;
+ saveSetting("cwWpm", wpm);
}
}