feat(trx-core,trx-protocol,trx-backend-soapysdr): add spectrum data pipeline

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>
This commit is contained in:
2026-02-27 21:35:53 +01:00
parent df79f06ff0
commit 76969b5499
10 changed files with 111 additions and 6 deletions
+1
View File
@@ -32,4 +32,5 @@ pub enum RigCommand {
ResetWsprDecoder,
SetBandwidth(u32),
SetFirTaps(u32),
GetSpectrum,
}
+2 -1
View File
@@ -514,7 +514,8 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
| RigCommand::ResetFt8Decoder
| RigCommand::ResetWsprDecoder
| RigCommand::SetBandwidth(_)
| RigCommand::SetFirTaps(_) => Box::new(GetSnapshotCommand),
| RigCommand::SetFirTaps(_)
| RigCommand::GetSpectrum => Box::new(GetSnapshotCommand),
}
}
+5
View File
@@ -149,6 +149,11 @@ pub trait RigCat: Rig + Send {
fn filter_state(&self) -> Option<state::RigFilterState> {
None
}
/// Return the latest spectrum frame if this backend supports spectrum output.
fn get_spectrum(&self) -> Option<state::SpectrumData> {
None
}
}
/// Snapshot of a rig's status that every backend can expose.
+20
View File
@@ -46,6 +46,10 @@ pub struct RigState {
/// Skipped in serde; flows into RigSnapshot via snapshot().
#[serde(skip)]
pub filter: Option<RigFilterState>,
/// Latest spectrum frame from SDR backends.
/// Skipped in serde (not part of persistent state); flows into RigSnapshot on demand.
#[serde(skip)]
pub spectrum: Option<SpectrumData>,
#[serde(default, skip_serializing)]
pub aprs_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
@@ -132,6 +136,7 @@ impl RigState {
cw_wpm: 15,
cw_tone_hz: 700,
filter: None,
spectrum: None,
aprs_decode_reset_seq: 0,
cw_decode_reset_seq: 0,
ft8_decode_reset_seq: 0,
@@ -192,6 +197,7 @@ impl RigState {
ft8_decode_enabled: snapshot.ft8_decode_enabled,
wspr_decode_enabled: snapshot.wspr_decode_enabled,
filter: snapshot.filter,
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
aprs_decode_reset_seq: 0,
cw_decode_reset_seq: 0,
ft8_decode_reset_seq: 0,
@@ -230,6 +236,7 @@ impl RigState {
ft8_decode_enabled: self.ft8_decode_enabled,
wspr_decode_enabled: self.wspr_decode_enabled,
filter: self.filter.clone(),
spectrum: self.spectrum.clone(),
})
}
@@ -264,6 +271,17 @@ pub struct RigFilterState {
pub cw_center_hz: u32,
}
/// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectrumData {
/// FFT magnitude bins in dBFS, FFT-shifted so DC (centre frequency) is at index N/2.
pub bins: Vec<f32>,
/// Centre frequency of the SDR capture in Hz.
pub center_hz: u64,
/// SDR capture sample rate in Hz; the displayed span is ±sample_rate/2.
pub sample_rate: u32,
}
/// Read-only projection of state shared with clients.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RigSnapshot {
@@ -300,4 +318,6 @@ pub struct RigSnapshot {
pub cw_tone_hz: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<RigFilterState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spectrum: Option<SpectrumData>,
}