Both trx-server and trx-client now look for a combined trx-rs.toml
with [trx-server] and [trx-client] section headers respectively,
falling back to per-binary config files as before.
Search order per tier: combined trx-rs.toml → flat per-binary file,
checked in CWD, ~/.config/trx-rs/, and /etc/trx-rs/.
--print-config now outputs the config under the appropriate section
header so the combined file can be generated directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
JS: fix stereo AudioData channel copy — frame.copyTo with planeIndex:0
only fills the left plane; reading it as interleaved data caused every
other sample to be zero, making WFM stereo play at half speed. Now
calls copyTo per channel with the correct planeIndex.
Rust WFM: replace integer output_decim/output_counter in WfmStereoDecoder
with a fractional phase accumulator (output_phase_inc = audio_rate /
composite_rate). Integer division caused the effective output rate to
drift from audio_sample_rate when the SDR rate is not an exact multiple
(e.g. 2 MHz SDR → 250 kHz composite → ~50 kHz output instead of 48 kHz,
making audio play 4% slow).
Rust non-WFM: add resample_phase/resample_phase_inc to ChannelDsp and
use a fractional-phase resampler in process_block for non-WFM paths,
ensuring exactly audio_sample_rate samples/sec regardless of SDR rate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix updateJogStepSupport to snap jogUnit (not jogStep) to nearest
supported unit, then recompute jogStep = jogUnit * jogMult so the
multiplier is preserved across rig connect/reconnect.
Rename "Mult" label to "Unit Multiplier" for clarity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix waterfall overview freezing at steady state by tracking a monotonic
push counter instead of row array length — the array size stays constant
once the waterfall is full, so the previous row-count comparison never
triggered the incremental draw path.
Fix WFM RDS not decoding when switching to WFM from a narrowband mode:
set_mode now resets audio_bandwidth_hz to the mode-appropriate default
(180 kHz for WFM) before rebuilding the FIR, preventing the 57 kHz RDS
subcarrier from being filtered out.
Add 1×/10×/100× multiplier button group next to the jog unit selector.
jogUnit × jogMult gives the effective jog step; both are persisted to
localStorage independently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Lower SPECTRUM_POLL_INTERVAL and SSE tick from 100 ms to 200 ms to halve
the number of spectrum frames pushed to the browser.
Introduce an OffscreenCanvas cache for the overview waterfall: at steady
state only the new row is painted and the existing image is scrolled up,
reducing per-frame work from O(rows × cols) to O(cols).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When a WFM/SDR spectrum stream carries RDS data, show the Program
Service name (8-char station name) centered on the visible waveform
area in DSEG14 monospace — the same font as the frequency display.
- Add #rds-ps-overlay div inside .overview-strip (pointer-events: none,
z-index: 2, absolutely positioned at the center of the visible canvas)
- updateRdsPsOverlay(rds) shows/hides and updates text on every spectrum
frame; trims trailing spaces common in RDS PS strings
- Overlay cleared on spectrum disconnect / null frame
- text-shadow uses CSS color-mix against --bg for legibility across all
style/theme combinations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When in dark mode the button has a light appearance; when in light mode
it has a dark appearance. This makes the button a preview of what
clicking will switch to, rather than mirroring the current theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Same root cause as the overviewDrawPending TDZ: setStyle() references
lastSpectrumData at module init time but it was declared far below.
Hoist let lastSpectrumData = null to the top variable block and remove
the now-duplicate declaration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
setStyle() was called at module init time (to restore the saved style
from localStorage) before the let overviewDrawPending declaration, which
caused a ReferenceError: Cannot access 'overviewDrawPending' before
initialization.
Fix: hoist let overviewDrawPending = false to the top of the variable
declarations block, before any top-level code that may call
scheduleOverviewDraw(). Remove the now-duplicate declaration from its
original location.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a style picker dropdown to the tab bar (right of rig picker) with
four styles — Original, Nord, Monokai, Contrast — each with full
light/dark variants.
CSS: define data-style attribute overrides for all CSS custom properties
(bg, card-bg, borders, text, accents, jog, audio level, filter, spectrum
background) for each of the three new styles × two themes (6 new blocks).
JS: introduce CANVAS_PALETTE lookup table covering spectrum/waveform/
waterfall colors for all style×theme combinations. Add currentStyle(),
canvasPalette(), setStyle() helpers. Persist selection to localStorage.
Replace all isLight ternaries in drawing code with palette lookups:
- drawOverviewWaterfall, drawOverviewSignalHistory, waterfallColor
signatures changed from isLight flag to pal object
- drawSpectrum uses canvasPalette() for grid lines, labels, fill, line
- spectrumBgColor() now delegates to canvasPalette().bg
Theme toggle also triggers a spectrum redraw so canvas colors update
immediately when switching light/dark.
Also fix light-theme spectrum rendering broken since canvas drawing used
hardcoded dark-only colors (white grid lines invisible on light bg).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Add viewport meta tag so mobile browsers use device width instead of
the default 980px desktop viewport
- Add 520px breakpoint: remove controls-tray forced min-width (was
52–58rem, causing horizontal scroll on phones), stack controls-row
into a single column with jog wheel first (order: -1)
- Scale frequency DSEG14 display with clamp() on narrow screens
- Make volume sliders responsive width with larger 20px thumb
- Raise spectrum Set/Auto button and input heights to 2.2rem (overrides
the min-height: 0 that blocked the existing 760px touch-size rule)
- Add overflow-x: auto + flex-shrink: 0 to sub-tab-bar so all six
plugin tabs are reachable on small screens without clipping
- Swap spectrum hint text based on input device: mouse/keyboard hint
hidden on touch devices, touch-specific hint shown instead
- Offset S0/S9/S9+ signal history labels 6px below their grid lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The SDR audio bridge was forwarding PCM to server-side decoders but
never encoding it to Opus for rx_audio_tx, so TCP audio clients
(browser RX button) received nothing. Add Opus encoder initialised
from the rig's audio config and encode each PCM frame alongside the
pcm_tx broadcast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the BW slider + FIR taps filter panel with a visual bandwidth
bookmark drawn directly on the spectrum canvas:
- Semi-transparent amber gradient strip spanning dialFreq ± BW/2
- Rounded-top bookmark tab at the top of the strip showing the current BW
- Draggable left/right edge handles (cursor: ew-resize) that adjust bandwidth
live and send set_bandwidth on mouse-up; range clamped per-mode defaults
- Y-axis now labeled with dB values (floor to ceiling) drawn on canvas
- Configurable floor level via number input below spectrum (default -100 dB)
- Auto button fits floor/range to current noise floor and peak level
- Remove FIR taps selector (internal DSP implementation detail)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add MODE_BW_DEFAULTS table mapping each mode to [default, min, max, step]
in Hz. When mode changes (via picker or SSE), applyBwDefaultForMode
updates the filter bandwidth slider range and sends set_bandwidth to the
server, so the DSP filter is rebuilt automatically for the new mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Compute hardware center as dial - center_offset_hz (was ignoring offset)
- Pass configured bandwidth_hz to device instead of hardcoded 1.5 MHz
- Add retune_cmd channel so set_freq repoints SDR hardware in real time
- Auto-add default channel with mode-appropriate bandwidth when [[sdr.channels]]
is empty, preventing silent audio with minimal config
- Add ChannelDsp::set_filter to rebuild FIR LPFs at runtime; wire
set_bandwidth and set_fir_taps to call it so UI filter changes take effect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Spectrum panel is now placed above the freq/mode/jog row, spanning the
full card width. Key improvements:
- Scroll wheel zooms in/out at the cursor position (up to 64x); double-
click resets to full bandwidth view.
- Mouse drag pans the visible window; click-to-tune is suppressed when a
drag has occurred.
- Touch pinch-to-zoom and single-finger drag-to-pan supported.
- Hover tooltip shows the frequency under the cursor, formatted to the
currently selected unit (MHz/kHz/Hz, matching the jog-step selection).
- Frequency axis labels update to reflect the zoomed visible range.
- Canvas height increased to 160 px; axis bar styled with card bg.
- A small hint line below the panel explains the controls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Poll GetSpectrum every 200 ms in remote_client via a dedicated timer that
bypasses the main state-watch channel (no SSE noise). The resulting
SpectrumData is stored in FrontendRuntimeContext::spectrum and served by
a new GET /spectrum endpoint (JSON or 204 when unavailable).
HTTP frontend shows a spectrum panel (canvas + frequency axis) only when
the rig reports filter_controls=true (i.e. SoapySDR). The canvas renders:
- dark background with dBFS grid lines
- green FFT spectrum line with semi-transparent fill
- red dashed vertical marker at the currently tuned frequency
- frequency axis labels (MHz/kHz) below the canvas
Clicking the canvas tunes the rig to the clicked frequency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SpectrumData struct (bins, center_hz, sample_rate) to RigState and
RigSnapshot. Add GetSpectrum RigCommand and ClientCommand plumbed through
the protocol layer. SoapySDR DSP pipeline now computes a 1024-bin FFT
(Hann window, FFT-shifted, dBFS) every 4 IQ blocks (~10 Hz update rate)
and exposes it via RigCat::get_spectrum(). The rig_task handles
GetSpectrum without persisting spectrum data in ongoing state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three root causes prevented APRS decoding at 144.800 MHz with PKT/FM mode:
1. `RealIqSource::read_into` returned zeros — the SoapySDR streaming API
was never wired up. `RxStream<Complex<f32>>` is `Send` and
`StreamSample` is implemented for `num_complex::Complex<f32>` in the
soapysdr 0.3 crate, so the stream can read directly into the IQ buffer.
Now creates and activates an `RxStream` in `new()` and calls
`stream.read` in `read_into`.
2. PKT mode used `Passthrough` (take `.re`) demodulation. VHF/UHF packet
radio (APRS, AX.25) is FM-encoded AFSK — it must be FM-demodulated
before the APRS decoder sees the audio tones. Changed PKT to `Fm`.
3. `iq_read_loop` always slept `block_duration_ms` after each read. Real
hardware already blocks inside `read_into`; the extra sleep doubled
latency. Added `IqSource::is_blocking()` (default `false`; `true` for
`RealIqSource`) and skip the throttle sleep for blocking sources.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Pass `watch::Receiver<bool>` into `run_capture` and `run_playback` so
both threads check for shutdown at the top of their outer recovery loop
and inner monitoring loop. Without this, restarting the process (e.g.
via systemd after an ALSA error) left the old threads stalled forever
while new ones were created alongside them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the per-sample ring-buffer FIR convolution with block-level
overlap-save convolution using rustfft. For a block of M samples and
N taps the old approach costs O(N·M); the new one costs O(M log M),
with rustfft using SIMD (AVX2/SSE4) internally.
Key changes:
- Add rustfft = "6" dependency
- Add BlockFirFilter: overlap-save filter with pre-computed H(f) and
a single forward+inverse FFT pair per block (no per-sample multiply)
- ChannelDsp.process_block() now:
1. Batch-mixes entire block to baseband in one vectorisable loop
2. Applies BlockFirFilter to I and Q (one FFT pair each)
3. Decimates and demodulates as before
- Keep the old FirFilter for unit tests (sample-by-sample interface)
- Add BlockFirFilter unit tests (DC passthrough, length preservation)
- IQ_BLOCK_SIZE promoted to pub const for use in filter sizing
For the default config (4096-sample blocks, 64 taps, decim=40):
Old: ~262144 multiply-adds per FIR × 2 components = ~524k per block
New: ~2 × (3 × 8192 × log2(8192)) ops, all SIMD-vectorised by rustfft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix compiler warning about discarded const qualifier in strchr() call.
The result of strchr(const char*, ...) should be assigned to const char*,
not char*.
This resolves:
warning: initialization discards 'const' qualifier from pointer target type
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add .cargo/config.toml to enable native CPU features (AVX2, SSE4.2, etc.)
for maximum performance during compilation. This allows rustc and
dependencies to use SIMD instructions and other CPU optimizations.
This should reduce CPU usage of the DSP backend by allowing:
- Vectorized floating-point operations
- Better compiler optimizations for complex number math
- SIMD acceleration in dependencies like num-complex
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Hide the header rig picker when showing the authentication gate,
since no rigs are accessible until the user authenticates.
Show the picker again when auth gate is hidden (after login).
This improves UX by not showing an empty picker to unauthenticated users.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When rigs are configured without explicit IDs in TOML, they now
receive auto-generated IDs based on model name and index (e.g.,
"ft817_0", "soapysdr_1"). Previously, the default empty ID prevented
auto-generation from triggering.
Changes:
- Set RigInstanceConfig default id to empty string (was "default")
- This allows resolved_rigs() auto-generation to trigger for all rigs
- Legacy single-rig path still gets explicit "default" ID
- Auto-generated IDs now appear in HTTP API /rigs endpoint
- Rig picker now displays auto-IDs correctly
The fix ensures that rigs without explicit ids get proper identifiers
that appear in the protocol and frontend selectors.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Enhance error handling and messaging for SoapySDR device initialization:
- Add fallback logic to try empty args if device-specific args fail
- Provide detailed multi-line error messages with troubleshooting guidance
- Include suggestions to check SoapySDR plugins and run SoapySDRUtil --probe
- Clarify device lifetime management in struct
- Document why actual streaming not yet implemented (soapysdr 0.3 limitations)
- Note that sdr 0.3 requires FFI or upgrade for streaming support
This helps users diagnose device initialization failures and understand
the current architectural limitations.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove direct dependency on trx-backend-soapysdr from server Cargo.toml.
Re-export SoapySdrRig from trx-backend instead so server accesses it
through the proper facade. This keeps sub-backends internal to trx-backend
and maintains proper module organization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop optional feature gating - SoapySDR hardware support is now required.
Make soapysdr a required dependency in Cargo.toml instead of optional.
Update server to always enable soapysdr backend and its dependencies.
Simplify initialization to always use RealIqSource instead of conditional
fallback to MockIqSource.
This assumes SoapySDR library is installed on the system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow rigs to have empty IDs in config; auto-generate from backend model
name with numeric suffix (e.g., 'ft817_0', 'ft817_1', 'soapysdr_0').
This makes config more concise when using multiple instances of the same
model without explicit ID assignment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add RealIqSource that connects to actual SoapySDR devices when soapysdr-sys
feature is enabled. Implements proper device initialization, frequency/bandwidth
configuration, gain control, and RX stream management.
When soapysdr-sys feature is disabled (default), falls back to MockIqSource
for testing. Update feature flags in Cargo.toml dependencies to support both
real hardware and mock operation.
- Device initialization with proper error handling
- RX frequency, bandwidth, and gain configuration
- IQ sample streaming via broadcast channel
- Proper resource cleanup via Drop trait
- Throttled MockIqSource to prevent 100% CPU
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sleep proportional to block duration in iq_read_loop to simulate
real hardware timing. MockIqSource immediately returns samples without
any delay, causing busy-looping. Throttling prevents excessive CPU usage
when using the mock source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use header rig selector as the single point for rig switching. Remove
redundant about page rig selector controls and associated JavaScript
event listeners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add display_name field to RemoteRigEntry and propagate from GetRigs
response. Update http-json frontend to include display_name in rig
entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include optional display_name field in GetRigs response to allow clients
to show friendly rig names instead of just identifiers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add optional `name` field to RigInstanceConfig to allow long-form rig names
(e.g., "HF Transceiver"). The name defaults to rig id if not configured.
Add display_name() method to get display name with fallback to id.
Propagate display_name through RigHandle to listener for GetRigs response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix useless vec! allocation in demod tests, improve loop iterator usage,
and reformat code for better readability across dummy and soapysdr
backends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reformat assertions, multi-line function calls, and error handling for
better readability. Remove unused soapysdr feature import in main.rs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable SoapySDR backend by default to make multi-device setups work out
of the box without requiring explicit feature flags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reformat filter state methods and error handling for better consistency.
Improve readability of complex return type expressions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Improve assertion and struct literal formatting across test cases
for better code clarity and consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the /frontend-design skill (project-scoped, in
.claude/commands/frontend-design.md) and explains how to add new
skills. Lists global skills available across all projects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Prevent Claude Code session files, worktrees, and settings from
being accidentally committed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add /set_bandwidth and /set_fir_taps HTTP endpoints to api.rs.
Add applyCapabilities(caps) function to app.js that shows/hides:
- PTT button and TX meters: capabilities.tx
- TX limit row: capabilities.tx_limit
- VFO row: capabilities.vfo_switch
- Signal meter row: capabilities.signal_meter
- Filters panel: capabilities.filter_controls
Called from render() whenever capabilities are present; runs on both
initial /status response and every SSE event.
Add a Filters panel to index.html with bandwidth slider (1..500 kHz)
and FIR taps select (16/32/64/128/256); hidden by default, revealed by
applyCapabilities when filter_controls is set. Each control dispatches
to the corresponding HTTP endpoint on change.
Sync filter state from update.filter in render() to keep slider/select
in sync with server-side DSP state.
Fix missing struct fields in test helpers across remote_client.rs,
trx-frontend-http-json/server.rs, trx-frontend-rigctl/server.rs, and
trx-core controller tests (handlers.rs, machine.rs).
Update aidocs/UI-CAPS.md: all tasks UC-01..UC-09 marked [x].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Handle SetBandwidth(hz) and SetFirTaps(taps) in process_command:
call the RigCat methods, update filter state in-place, broadcast
the updated state, and return the new snapshot.
Fix missing capability fields in listener.rs test helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Set tx/tx_limit/vfo_switch/filter_controls/signal_meter on all backends:
- FT-817, FT-450D, dummy: tx=true, tx_limit=true, vfo_switch=true,
filter_controls=false, signal_meter=true
- SoapySDR: tx=false, tx_limit=false, vfo_switch=false,
filter_controls=true, signal_meter=true
SoapySDR backend now stores bandwidth_hz and fir_taps fields; overrides
set_bandwidth, set_fir_taps, and filter_state on RigCat to expose live
DSP state in the snapshot.
Add UC-08 unit tests on dummy backend asserting tx capabilities present
and filter_controls absent, and that filter_state returns None.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>