[feat](trx-backend): VirtualChannelManager trait + SdrVirtualChannelManager impl

Add VirtualChannelManager trait in trx-core::vchan with types VChannelInfo,
VChanError, and SharedVChanManager alias. Re-export from trx-backend::vchan.

Implement SdrVirtualChannelManager in trx-backend-soapysdr:
- Wraps Arc<SdrPipeline> + shared AtomicI64 center_hz
- add_channel / remove_channel / set_channel_freq / set_channel_mode
- Slot-stability: on remove, shifts pipeline_slot for surviving channels
- update_center_hz: recomputes IF offsets for all virtual channels on retune
- update_primary_meta: keeps channel-0 freq/mode in sync for API consumers

Wire into SoapySdrRig (holds Arc<SdrVirtualChannelManager>, exposes
channel_manager()), SdrPipeline (shared_center_hz AtomicI64), and RigHandle
(vchan_manager: Option<SharedVChanManager>). main.rs extracts the manager
before boxing the SDR rig and stores it in the handle.

Add max_virtual_channels to SdrConfig (default 4, TOML-configurable).
Add 5 unit tests: add, remove, permanent guard, cap, out-of-bandwidth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-11 00:48:31 +01:00
parent 05169912b1
commit dda5ec17bb
16 changed files with 779 additions and 82 deletions
+1
View File
@@ -13,3 +13,4 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tracing = { workspace = true }
flate2 = { workspace = true }
uuid = { workspace = true }
+1
View File
@@ -7,6 +7,7 @@ pub mod decode;
pub mod math;
pub mod radio;
pub mod rig;
pub mod vchan;
pub type DynResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
+110
View File
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
//! Virtual channel management trait and shared types.
//!
//! A *virtual channel* is an independent DSP slice within the capture bandwidth
//! of an SDR rig. Each has its own frequency offset, demodulation mode, and
//! PCM audio broadcast. Traditional (non-SDR) rigs do not support virtual
//! channels; their `RigHandle::vchan_manager` field will be `None`.
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use uuid::Uuid;
use crate::rig::state::RigMode;
// ---------------------------------------------------------------------------
// Shared types
// ---------------------------------------------------------------------------
/// Snapshot of one virtual channel's state (HTTP-serialisable).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VChannelInfo {
/// Stable UUID identifier.
pub id: Uuid,
/// Display index in the ordered channel list (0 = primary).
pub index: usize,
/// Dial frequency in Hz.
pub freq_hz: u64,
/// Demodulation mode name (e.g. "USB", "FM").
pub mode: String,
/// `true` for the primary channel (index 0), which cannot be removed.
pub permanent: bool,
}
/// Errors returned by virtual channel management operations.
#[derive(Debug, Clone)]
pub enum VChanError {
/// The configured channel cap would be exceeded.
CapReached { max: usize },
/// The requested frequency lies outside the current SDR capture bandwidth.
OutOfBandwidth { half_span_hz: i64 },
/// No channel with the given UUID exists.
NotFound,
/// Attempted to remove the permanent primary channel.
Permanent,
}
impl std::fmt::Display for VChanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VChanError::CapReached { max } => {
write!(f, "virtual channel cap reached (max {})", max)
}
VChanError::OutOfBandwidth { half_span_hz } => write!(
f,
"frequency outside SDR capture bandwidth (±{} Hz)",
half_span_hz
),
VChanError::NotFound => write!(f, "virtual channel not found"),
VChanError::Permanent => write!(f, "cannot remove the primary channel"),
}
}
}
// ---------------------------------------------------------------------------
// Trait
// ---------------------------------------------------------------------------
/// Manages virtual DSP channels for an SDR rig.
///
/// Implementations are `Send + Sync` so the manager can be shared across
/// tokio tasks and actix-web handlers.
pub trait VirtualChannelManager: Send + Sync {
/// Add a new virtual channel tuned to `freq_hz` with `mode`.
///
/// Returns the new channel UUID and a PCM broadcast receiver that delivers
/// decoded audio frames for this channel.
fn add_channel(
&self,
freq_hz: u64,
mode: &RigMode,
) -> Result<(Uuid, broadcast::Receiver<Vec<f32>>), VChanError>;
/// Remove a virtual channel by UUID. The primary channel (index 0) cannot
/// be removed and returns `VChanError::Permanent`.
fn remove_channel(&self, id: Uuid) -> Result<(), VChanError>;
/// Update the dial frequency of an existing channel.
fn set_channel_freq(&self, id: Uuid, freq_hz: u64) -> Result<(), VChanError>;
/// Update the demodulation mode of an existing channel.
fn set_channel_mode(&self, id: Uuid, mode: &RigMode) -> Result<(), VChanError>;
/// Subscribe to decoded PCM audio from a channel.
/// Returns `None` if the channel UUID does not exist.
fn subscribe_pcm(&self, id: Uuid) -> Option<broadcast::Receiver<Vec<f32>>>;
/// Return a snapshot of all channels in display order.
fn channels(&self) -> Vec<VChannelInfo>;
/// Maximum number of channels (including the primary channel).
fn max_channels(&self) -> usize;
}
/// Convenience alias used in `RigHandle`.
pub type SharedVChanManager = Arc<dyn VirtualChannelManager>;