diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 018859f..65fa99c 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -274,6 +274,9 @@ async fn async_init() -> DynResult { server_longitude: None, aprs_decode_enabled: false, cw_decode_enabled: false, + cw_auto: true, + cw_wpm: 15, + cw_tone_hz: 700, aprs_decode_reset_seq: 0, cw_decode_reset_seq: 0, }; diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 6fa0891..2ff3099 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -148,6 +148,9 @@ fn map_rig_command(cmd: trx_core::RigCommand) -> ClientCommand { trx_core::RigCommand::Unlock => ClientCommand::Unlock, trx_core::RigCommand::SetAprsDecodeEnabled(enabled) => ClientCommand::SetAprsDecodeEnabled { enabled }, trx_core::RigCommand::SetCwDecodeEnabled(enabled) => ClientCommand::SetCwDecodeEnabled { enabled }, + trx_core::RigCommand::SetCwAuto(enabled) => ClientCommand::SetCwAuto { enabled }, + trx_core::RigCommand::SetCwWpm(wpm) => ClientCommand::SetCwWpm { wpm }, + trx_core::RigCommand::SetCwToneHz(tone_hz) => ClientCommand::SetCwToneHz { tone_hz }, trx_core::RigCommand::ResetAprsDecoder => ClientCommand::ResetAprsDecoder, trx_core::RigCommand::ResetCwDecoder => ClientCommand::ResetCwDecoder, } @@ -190,6 +193,9 @@ pub fn state_from_snapshot(snapshot: trx_core::RigSnapshot) -> RigState { server_longitude: snapshot.server_longitude, aprs_decode_enabled: snapshot.aprs_decode_enabled, cw_decode_enabled: snapshot.cw_decode_enabled, + cw_auto: snapshot.cw_auto, + cw_wpm: snapshot.cw_wpm, + cw_tone_hz: snapshot.cw_tone_hz, aprs_decode_reset_seq: 0, cw_decode_reset_seq: 0, } 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 8afc95b..a60e44d 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 @@ -267,6 +267,25 @@ function render(update) { pttBtn.style.color = ""; } } + const cwAutoEl = document.getElementById("cw-auto"); + const cwWpmEl = document.getElementById("cw-wpm"); + const cwToneEl = document.getElementById("cw-tone"); + if (cwAutoEl && typeof update.cw_auto === "boolean") { + cwAutoEl.checked = update.cw_auto; + } + if (cwWpmEl && typeof update.cw_wpm === "number") { + cwWpmEl.value = update.cw_wpm; + } + if (cwToneEl && typeof update.cw_tone_hz === "number") { + cwToneEl.value = update.cw_tone_hz; + } + if (cwWpmEl && cwToneEl && typeof update.cw_auto === "boolean") { + const disabled = update.cw_auto; + cwWpmEl.disabled = disabled; + cwWpmEl.readOnly = disabled; + cwToneEl.disabled = disabled; + cwToneEl.readOnly = disabled; + } if (update.status && update.status.vfo && Array.isArray(update.status.vfo.entries)) { const entries = update.status.vfo.entries; const activeIdx = Number.isInteger(update.status.vfo.active) ? update.status.vfo.active : null; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 091e7e1..455667b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -157,9 +157,9 @@
- - - + + +
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 eeaf544..ee79533 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 @@ -1,11 +1,53 @@ // --- CW (Morse) Decoder Plugin (server-side decode) --- const cwStatusEl = document.getElementById("cw-status"); const cwOutputEl = document.getElementById("cw-output"); +const cwAutoInput = document.getElementById("cw-auto"); const cwWpmInput = document.getElementById("cw-wpm"); const cwToneInput = document.getElementById("cw-tone"); const cwSignalIndicator = document.getElementById("cw-signal-indicator"); const CW_MAX_LINES = 200; +function applyCwAutoUi(enabled) { + if (cwAutoInput) cwAutoInput.checked = enabled; + if (cwWpmInput) { + cwWpmInput.disabled = enabled; + cwWpmInput.readOnly = enabled; + } + if (cwToneInput) { + cwToneInput.disabled = enabled; + cwToneInput.readOnly = enabled; + } +} + +if (cwAutoInput) { + cwAutoInput.addEventListener("change", async () => { + const enabled = cwAutoInput.checked; + applyCwAutoUi(enabled); + try { await postPath(`/set_cw_auto?enabled=${enabled ? 1 : 0}`); } + catch (e) { console.error("CW auto toggle failed", e); } + }); +} + +if (cwWpmInput) { + cwWpmInput.addEventListener("change", async () => { + if (cwAutoInput && cwAutoInput.checked) return; + const wpm = Math.max(5, Math.min(40, Number(cwWpmInput.value))); + cwWpmInput.value = wpm; + try { await postPath(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`); } + catch (e) { console.error("CW WPM set failed", e); } + }); +} + +if (cwToneInput) { + cwToneInput.addEventListener("change", async () => { + if (cwAutoInput && cwAutoInput.checked) return; + const tone = Math.max(300, Math.min(1200, Number(cwToneInput.value))); + cwToneInput.value = tone; + try { await postPath(`/set_cw_tone?tone_hz=${encodeURIComponent(tone)}`); } + catch (e) { console.error("CW tone set failed", e); } + }); +} + document.getElementById("cw-clear-btn").addEventListener("click", async () => { cwOutputEl.innerHTML = ""; try { await postPath("/clear_cw_decode"); } catch (e) { console.error("CW clear failed", e); } @@ -34,6 +76,8 @@ window.onServerCw = function(evt) { cwOutputEl.scrollTop = cwOutputEl.scrollHeight; } cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off"; - cwWpmInput.value = evt.wpm; - cwToneInput.value = evt.tone_hz; + if (!cwAutoInput || cwAutoInput.checked) { + cwWpmInput.value = evt.wpm; + cwToneInput.value = evt.tone_hz; + } }; 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 83bc48d..cb008c3 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 @@ -232,7 +232,7 @@ small { color: var(--text-muted); } .cw-line { line-height: 1.5; } .cw-signal-on { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green); flex-shrink: 0; } .cw-signal-off { width: 10px; height: 10px; border-radius: 50%; background: var(--border-light); flex-shrink: 0; } -.cw-auto-label { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; } +.cw-config .cw-auto-label { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; flex-direction: row; } .cw-auto-label input[type="checkbox"] { margin: 0; cursor: pointer; } .cw-config input[type="number"][readonly] { opacity: 0.6; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 222ec52..9aaa48c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -282,6 +282,45 @@ pub async fn toggle_cw_decode( send_command(&rig_tx, RigCommand::SetCwDecodeEnabled(!enabled)).await } +#[derive(serde::Deserialize)] +pub struct CwAutoQuery { + pub enabled: bool, +} + +#[post("/set_cw_auto")] +pub async fn set_cw_auto( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetCwAuto(query.enabled)).await +} + +#[derive(serde::Deserialize)] +pub struct CwWpmQuery { + pub wpm: u32, +} + +#[post("/set_cw_wpm")] +pub async fn set_cw_wpm( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetCwWpm(query.wpm)).await +} + +#[derive(serde::Deserialize)] +pub struct CwToneQuery { + pub tone_hz: u32, +} + +#[post("/set_cw_tone")] +pub async fn set_cw_tone( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetCwToneHz(query.tone_hz)).await +} + #[post("/clear_aprs_decode")] pub async fn clear_aprs_decode( rig_tx: web::Data>, @@ -311,6 +350,9 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(set_tx_limit) .service(toggle_aprs_decode) .service(toggle_cw_decode) + .service(set_cw_auto) + .service(set_cw_wpm) + .service(set_cw_tone) .service(clear_aprs_decode) .service(clear_cw_decode) .service(crate::server::audio::audio_ws) @@ -443,6 +485,9 @@ async fn wait_for_view(mut rx: watch::Receiver) -> Result { let state = state_rx.borrow(); + if state.cw_auto != last_auto { + last_auto = state.cw_auto; + decoder.set_auto(last_auto); + } + if state.cw_wpm != last_wpm { + last_wpm = state.cw_wpm; + decoder.set_wpm(last_wpm); + } + if state.cw_tone_hz != last_tone { + last_tone = state.cw_tone_hz; + decoder.set_tone_hz(last_tone); + } if state.cw_decode_reset_seq != last_reset_seq { last_reset_seq = state.cw_decode_reset_seq; decoder.reset(); @@ -477,6 +507,18 @@ pub async fn run_cw_decoder( Ok(()) => { let state = state_rx.borrow(); active = matches!(state.status.mode, RigMode::CW | RigMode::CWR); + if state.cw_auto != last_auto { + last_auto = state.cw_auto; + decoder.set_auto(last_auto); + } + if state.cw_wpm != last_wpm { + last_wpm = state.cw_wpm; + decoder.set_wpm(last_wpm); + } + if state.cw_tone_hz != last_tone { + last_tone = state.cw_tone_hz; + decoder.set_tone_hz(last_tone); + } if state.cw_decode_reset_seq != last_reset_seq { last_reset_seq = state.cw_decode_reset_seq; decoder.reset(); diff --git a/src/trx-server/src/decode/cw.rs b/src/trx-server/src/decode/cw.rs index 5231097..b6a6899 100644 --- a/src/trx-server/src/decode/cw.rs +++ b/src/trx-server/src/decode/cw.rs @@ -124,6 +124,10 @@ pub struct CwDecoder { // WPM wpm: u32, + // Auto control + auto_tone: bool, + auto_wpm: bool, + // Auto tone detection tone_scan_bins: Vec, tone_stable_bin: i32, @@ -172,6 +176,8 @@ impl CwDecoder { current_symbol: String::new(), sample_counter: 0, wpm: 15, + auto_tone: true, + auto_wpm: true, tone_scan_bins, tone_stable_bin: -1, tone_stable_count: 0, @@ -180,6 +186,20 @@ impl CwDecoder { } } + pub fn set_auto(&mut self, enabled: bool) { + self.auto_tone = enabled; + self.auto_wpm = enabled; + } + + pub fn set_wpm(&mut self, wpm: u32) { + self.wpm = wpm.clamp(5, 40); + } + + pub fn set_tone_hz(&mut self, tone_hz: u32) { + let tone_hz = tone_hz.clamp(TONE_SCAN_LOW, TONE_SCAN_HIGH); + self.recompute_goertzel(tone_hz); + } + fn recompute_goertzel(&mut self, new_freq: u32) { self.tone_freq = new_freq; let k = (new_freq as f32 * self.window_size as f32 / self.sample_rate as f32) @@ -300,8 +320,9 @@ impl CwDecoder { } fn process_window(&mut self) { - // Auto tone detection - self.auto_detect_tone(); + if self.auto_tone { + self.auto_detect_tone(); + } let detected = self.goertzel_detect(); let now = self.now_ms(); @@ -345,12 +366,14 @@ impl CwDecoder { } self.tone_off_at = now; - // Collect for auto WPM - self.on_durations.push(on_duration); - if self.on_durations.len() > 30 { - self.on_durations.remove(0); + if self.auto_wpm { + // Collect for auto WPM + self.on_durations.push(on_duration); + if self.on_durations.len() > 30 { + self.on_durations.remove(0); + } + self.auto_detect_wpm(); } - self.auto_detect_wpm(); } // Flush pending character after long silence @@ -387,6 +410,10 @@ impl CwDecoder { } pub fn reset(&mut self) { + let tone = self.tone_freq; + let wpm = self.wpm; + let auto_tone = self.auto_tone; + let auto_wpm = self.auto_wpm; self.sample_buf.fill(0.0); self.sample_idx = 0; self.tone_on = false; @@ -394,9 +421,11 @@ impl CwDecoder { self.tone_off_at = 0.0; self.current_symbol.clear(); self.sample_counter = 0; - self.wpm = 15; - self.tone_freq = 700; - self.recompute_goertzel(700); + self.wpm = wpm; + self.tone_freq = tone; + self.auto_tone = auto_tone; + self.auto_wpm = auto_wpm; + self.recompute_goertzel(tone); self.tone_stable_bin = -1; self.tone_stable_count = 0; self.on_durations.clear(); diff --git a/src/trx-server/src/listener.rs b/src/trx-server/src/listener.rs index 1cc1653..cdfd60a 100644 --- a/src/trx-server/src/listener.rs +++ b/src/trx-server/src/listener.rs @@ -190,6 +190,9 @@ fn map_command(cmd: ClientCommand) -> RigCommand { ClientCommand::SetTxLimit { limit } => RigCommand::SetTxLimit(limit), ClientCommand::SetAprsDecodeEnabled { enabled } => RigCommand::SetAprsDecodeEnabled(enabled), ClientCommand::SetCwDecodeEnabled { enabled } => RigCommand::SetCwDecodeEnabled(enabled), + ClientCommand::SetCwAuto { enabled } => RigCommand::SetCwAuto(enabled), + ClientCommand::SetCwWpm { wpm } => RigCommand::SetCwWpm(wpm), + ClientCommand::SetCwToneHz { tone_hz } => RigCommand::SetCwToneHz(tone_hz), ClientCommand::ResetAprsDecoder => RigCommand::ResetAprsDecoder, ClientCommand::ResetCwDecoder => RigCommand::ResetCwDecoder, } diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index b2021f8..da171b3 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -224,6 +224,9 @@ fn build_initial_state(cfg: &ServerConfig, resolved: &ResolvedConfig) -> RigStat server_longitude: resolved.longitude, aprs_decode_enabled: false, cw_decode_enabled: false, + cw_auto: true, + cw_wpm: 15, + cw_tone_hz: 700, aprs_decode_reset_seq: 0, cw_decode_reset_seq: 0, } diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 8c8c27a..9565a98 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -125,6 +125,9 @@ pub async fn run_rig_task( server_longitude, aprs_decode_enabled: false, cw_decode_enabled: false, + cw_auto: true, + cw_wpm: 15, + cw_tone_hz: 700, aprs_decode_reset_seq: 0, cw_decode_reset_seq: 0, }; @@ -360,6 +363,21 @@ async fn process_command( let _ = ctx.state_tx.send(ctx.state.clone()); return snapshot_from(ctx.state); } + RigCommand::SetCwAuto(en) => { + ctx.state.cw_auto = en; + let _ = ctx.state_tx.send(ctx.state.clone()); + return snapshot_from(ctx.state); + } + RigCommand::SetCwWpm(wpm) => { + ctx.state.cw_wpm = wpm.clamp(5, 40); + let _ = ctx.state_tx.send(ctx.state.clone()); + return snapshot_from(ctx.state); + } + RigCommand::SetCwToneHz(tone_hz) => { + ctx.state.cw_tone_hz = tone_hz.clamp(300, 1200); + let _ = ctx.state_tx.send(ctx.state.clone()); + return snapshot_from(ctx.state); + } RigCommand::ResetAprsDecoder => { audio::clear_aprs_history(); ctx.state.aprs_decode_reset_seq += 1;