From 7f222eaf10f40f9a51dee133a34eac6c719f62d2 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Fri, 27 Feb 2026 23:09:21 +0100 Subject: [PATCH] [chore](trx-rs): refine pending SDR frontend and backend changes Signed-off-by: Stan Grams Co-authored-by: OpenAI Codex --- .../trx-frontend-http/assets/web/app.js | 10 +++++-- .../trx-frontend-http/assets/web/style.css | 17 ++++++++--- src/trx-server/src/audio.rs | 11 +++++-- src/trx-server/src/config.rs | 14 ++------- src/trx-server/src/main.rs | 29 +++++++++---------- .../trx-backend-soapysdr/src/dsp.rs | 27 ++++------------- 6 files changed, 50 insertions(+), 58 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 5862930..9f4a3ea 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 @@ -2426,6 +2426,10 @@ let _bwDragEdge = null; // "left" | "right" | null let _bwDragStartX = 0; let _bwDragStartBwHz = 0; +function spectrumBgColor() { + return currentTheme() === "light" ? "#eef3fb" : "#0a0f18"; +} + // Returns { loHz, hiHz, visLoHz, visHiHz, fullSpanHz, visSpanHz } and clamps // panFrac so the view never scrolls past the edges. function spectrumVisibleRange(data) { @@ -2506,7 +2510,7 @@ function stopSpectrumStreaming() { function clearSpectrumCanvas() { if (!spectrumCanvas) return; const ctx = spectrumCanvas.getContext("2d"); - ctx.fillStyle = "#0a0f18"; + ctx.fillStyle = spectrumBgColor(); ctx.fillRect(0, 0, spectrumCanvas.width, spectrumCanvas.height); } @@ -2530,7 +2534,7 @@ function drawSpectrum(data) { const n = bins.length; // Background - ctx.fillStyle = "#0a0f18"; + ctx.fillStyle = spectrumBgColor(); ctx.fillRect(0, 0, W, H); if (!n) return; @@ -2628,7 +2632,7 @@ function drawSpectrum(data) { ctx.closePath(); ctx.fill(); // Tab text - ctx.fillStyle = "#0a0f18"; + ctx.fillStyle = spectrumBgColor(); ctx.textAlign = "left"; ctx.fillText(bwText, tabX + PAD, TAB_H - 4 * dpr); ctx.restore(); 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 dee39a7..d3975e1 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 @@ -25,6 +25,7 @@ --filter-fg: #e7edf9; --filter-border: #385577; --wavelength-fg: #8da3be; + --spectrum-bg: #0a0f18; --jog-wheel-size: 83.2px; } @@ -54,6 +55,7 @@ --filter-fg: #1f2937; --filter-border: #b8c5da; --wavelength-fg: #6b7280; + --spectrum-bg: #eef3fb; } body { font-family: sans-serif; margin: 0; min-height: 100vh; box-sizing: border-box; display: flex; align-items: flex-start; justify-content: center; padding-top: 2em; background: var(--bg); color: var(--text); } @@ -193,7 +195,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; border-right: 1px solid var(--border-light); border-radius: 0; height: 100%; - padding: 0 0.8rem; + padding: 0 0.65rem; font-size: 0.92rem; background: var(--input-bg); color: var(--text-muted); @@ -238,8 +240,9 @@ button:disabled { opacity: 0.6; cursor: not-allowed; } .hint { color: var(--text-muted); font-size: 0.85rem; } .inline { display: flex; gap: 0.5rem; align-items: center; } .freq-inline { + gap: 0.35rem; align-items: flex-start; - flex-wrap: wrap; + flex-wrap: nowrap; } .freq-field { display: grid; @@ -254,6 +257,10 @@ button:disabled { opacity: 0.6; cursor: not-allowed; } flex: 1 1 auto; min-width: 0; } +.center-frequency-col { + flex: 0 1 8.75rem; + min-width: 7.75rem; +} .frequency-col input.status-input { width: 100%; height: 3.35rem; @@ -285,7 +292,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; } order: 1; } .wavelength-display { - min-width: 6.2rem; + min-width: 5.2rem; height: 3.35rem; padding: 0 0.7rem; border: 1px solid var(--border-light); @@ -568,6 +575,8 @@ button:focus-visible, input:focus-visible, select:focus-visible { .card { padding: 1rem; } button { min-height: 2.8rem; font-size: 0.95rem; } input.status-input, select.status-input { font-size: 1.1rem; } + .freq-inline { gap: 0.5rem; } + .freq-inline { flex-wrap: wrap; } .header-text { width: auto; min-width: 0; flex: 0 1 auto; } .header-signal-wrap { display: none; } .header-right { align-items: flex-end; } @@ -599,7 +608,7 @@ button:focus-visible, input:focus-visible, select:focus-visible { display: block; width: 100%; height: 160px; - background: #0a0f18; + background: var(--spectrum-bg); border-radius: 6px 6px 0 0; cursor: crosshair; touch-action: none; diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 191f49f..1771305 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -479,9 +479,14 @@ pub fn spawn_audio_playback( let device_name = cfg.device.clone(); std::thread::spawn(move || { - if let Err(e) = - run_playback(sample_rate, channels, frame_duration_ms, device_name, rx, shutdown_rx) - { + if let Err(e) = run_playback( + sample_rate, + channels, + frame_duration_ms, + device_name, + rx, + shutdown_rx, + ) { error!("Audio playback thread error: {}", e); } }) diff --git a/src/trx-server/src/config.rs b/src/trx-server/src/config.rs index 5d8fa25..888a33b 100644 --- a/src/trx-server/src/config.rs +++ b/src/trx-server/src/config.rs @@ -54,7 +54,7 @@ pub struct RigInstanceConfig { impl Default for RigInstanceConfig { fn default() -> Self { Self { - id: String::new(), // Empty by default so auto-generation triggers in resolved_rigs() + id: String::new(), // Empty by default so auto-generation triggers in resolved_rigs() name: None, rig: RigConfig::default(), behavior: BehaviorConfig::default(), @@ -634,21 +634,13 @@ impl ServerConfig { .map(|(idx, rig)| { let id = if rig.id.trim().is_empty() { // Generate ID from model name with counter. - let model = rig - .rig - .model - .as_deref() - .unwrap_or("unknown") - .to_lowercase(); + let model = rig.rig.model.as_deref().unwrap_or("unknown").to_lowercase(); format!("{}_{}", model, idx) } else { rig.id.clone() }; - RigInstanceConfig { - id, - ..rig.clone() - } + RigInstanceConfig { id, ..rig.clone() } }) .collect(); } diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 07926e8..bf218d4 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -488,11 +488,11 @@ fn spawn_rig_audio_stack( "[{}] using SDR audio source — cpal capture disabled", rig_cfg.id ); - let pcm_tx_clone = pcm_tx.clone(); - let rx_audio_tx_sdr = rx_audio_tx.clone(); - let sdr_sample_rate = rig_cfg.audio.sample_rate; - let sdr_channels = rig_cfg.audio.channels; - let sdr_bitrate_bps = rig_cfg.audio.bitrate_bps; + let pcm_tx_clone = pcm_tx.clone(); + let rx_audio_tx_sdr = rx_audio_tx.clone(); + let sdr_sample_rate = rig_cfg.audio.sample_rate; + let sdr_channels = rig_cfg.audio.channels; + let sdr_bitrate_bps = rig_cfg.audio.bitrate_bps; handles.push(tokio::spawn(async move { let opus_ch = match sdr_channels { 1 => opus::Channels::Mono, @@ -502,17 +502,14 @@ fn spawn_rig_audio_stack( return; } }; - let mut encoder = match opus::Encoder::new( - sdr_sample_rate, - opus_ch, - opus::Application::Audio, - ) { - Ok(e) => e, - Err(e) => { - tracing::error!("SDR audio: Opus encoder init failed: {}", e); - return; - } - }; + let mut encoder = + match opus::Encoder::new(sdr_sample_rate, opus_ch, opus::Application::Audio) { + Ok(e) => e, + Err(e) => { + tracing::error!("SDR audio: Opus encoder init failed: {}", e); + return; + } + }; if let Err(e) = encoder.set_bitrate(opus::Bitrate::Bits(sdr_bitrate_bps as i32)) { tracing::warn!("SDR audio: set_bitrate failed: {}", e); } diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs index d8ab88b..85417ca 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs @@ -172,10 +172,8 @@ impl BlockFirFilter { let ifft = planner.plan_fft_inverse(fft_size); // Pre-compute H(f) = FFT of zero-padded coefficients. - let mut h_buf: Vec> = coeffs - .iter() - .map(|&c| FftComplex::new(c, 0.0)) - .collect(); + let mut h_buf: Vec> = + coeffs.iter().map(|&c| FftComplex::new(c, 0.0)).collect(); h_buf.resize(fft_size, FftComplex::new(0.0, 0.0)); fft.process(&mut h_buf); @@ -384,8 +382,7 @@ impl ChannelDsp { mixed_q[idx] = sample.re * lo_im + sample.im * lo_re; } // Advance phase with wrap to avoid precision loss. - self.mixer_phase = - (phase_start + n as f64 * phase_inc).rem_euclid(std::f64::consts::TAU); + self.mixer_phase = (phase_start + n as f64 * phase_inc).rem_euclid(std::f64::consts::TAU); // --- 2. FFT FIR (overlap-save) -------------------------------------- let filtered_i = self.lpf_i.filter_block(&mixed_i); @@ -471,8 +468,7 @@ impl SdrPipeline { let thread_dsps: Vec>> = channel_dsps.clone(); let spectrum_buf: Arc>>> = Arc::new(Mutex::new(None)); let thread_spectrum_buf = spectrum_buf.clone(); - let retune_cmd: Arc>> = - Arc::new(std::sync::Mutex::new(None)); + let retune_cmd: Arc>> = Arc::new(std::sync::Mutex::new(None)); let thread_retune_cmd = retune_cmd.clone(); std::thread::Builder::new() @@ -529,9 +525,7 @@ fn iq_read_loop( // Pre-compute Hann window coefficients. let hann_window: Vec = (0..SPECTRUM_FFT_SIZE) - .map(|i| { - 0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos()) - }) + .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (SPECTRUM_FFT_SIZE - 1) as f32).cos())) .collect(); let mut planner = FftPlanner::::new(); @@ -683,16 +677,7 @@ mod tests { #[test] fn channel_dsp_processes_silence() { let (pcm_tx, _pcm_rx) = broadcast::channel::>(8); - let mut dsp = ChannelDsp::new( - 0.0, - &RigMode::USB, - 48_000, - 8_000, - 20, - 3000, - 31, - pcm_tx, - ); + let mut dsp = ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 20, 3000, 31, pcm_tx); let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096]; dsp.process_block(&block); }