Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571ef17295 |
@@ -4758,13 +4758,6 @@ function render(update) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof update.filter.sdr_dig_sideband === "string") {
|
||||
sdrDigSidebandSupported = true;
|
||||
if (sdrDigSidebandEl && document.activeElement !== sdrDigSidebandEl) {
|
||||
sdrDigSidebandEl.value = update.filter.sdr_dig_sideband;
|
||||
}
|
||||
updateWfmControls();
|
||||
}
|
||||
}
|
||||
if (typeof update.show_sdr_gain_control === "boolean") {
|
||||
if (sdrSettingsRowEl) sdrSettingsRowEl.style.display = update.show_sdr_gain_control ? "" : "none";
|
||||
@@ -6631,9 +6624,6 @@ var sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-control
|
||||
var sdrNbThresholdEl = document.getElementById("sdr-nb-threshold");
|
||||
var sdrNbThresholdSetBtn = document.getElementById("sdr-nb-threshold-set");
|
||||
var sdrNbSupported = false;
|
||||
var sdrDigSidebandWrapEl = document.getElementById("sdr-dig-sideband-wrap");
|
||||
var sdrDigSidebandEl = document.getElementById("sdr-dig-sideband");
|
||||
var sdrDigSidebandSupported = false;
|
||||
fetch("/audio", { method: "GET" }).then((r) => {
|
||||
if (r.status === 404) audioRow.style.display = "none";
|
||||
}).catch(() => {
|
||||
@@ -7029,26 +7019,11 @@ if (sdrNbThresholdEl) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitSdrDigSideband() {
|
||||
if (!sdrDigSidebandSupported || !sdrDigSidebandEl) return;
|
||||
const policy = sdrDigSidebandEl.value || "auto";
|
||||
if (policy !== "auto" && policy !== "usb" && policy !== "lsb") return;
|
||||
postPath(`/set_sdr_dig_sideband?policy=${encodeURIComponent(policy)}`).catch(() => {
|
||||
});
|
||||
}
|
||||
if (sdrDigSidebandEl) {
|
||||
sdrDigSidebandEl.addEventListener("change", () => {
|
||||
submitSdrDigSideband();
|
||||
});
|
||||
}
|
||||
function updateWfmControls() {
|
||||
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||
if (wfmControlsCol) wfmControlsCol.style.display = mode === "WFM" ? "" : "none";
|
||||
if (samControlsCol) samControlsCol.style.display = mode === "SAM" ? "" : "none";
|
||||
if (modeControlsRow) modeControlsRow.style.display = mode === "WFM" || mode === "SAM" ? "" : "none";
|
||||
if (sdrDigSidebandWrapEl) {
|
||||
sdrDigSidebandWrapEl.style.display = sdrDigSidebandSupported && mode === "DIG" ? "" : "none";
|
||||
}
|
||||
}
|
||||
if (!hasWebCodecs) {
|
||||
rxAudioBtn.disabled = true;
|
||||
|
||||
@@ -214,12 +214,6 @@ function bmBuildDecoderCheckboxes() {
|
||||
container.appendChild(label);
|
||||
});
|
||||
}
|
||||
function bmSyncDigSidebandVisibility() {
|
||||
const label = bmOptionalEl("bm-dig-sideband-label");
|
||||
if (!label) return;
|
||||
const mode = (bmEl("bm-mode").value || "").trim().toUpperCase();
|
||||
label.style.display = mode === "DIG" ? "" : "none";
|
||||
}
|
||||
function bmOpenForm(bm) {
|
||||
const wrap = bmEl("bm-form-wrap");
|
||||
if (!wrap) return;
|
||||
@@ -233,9 +227,7 @@ function bmOpenForm(bm) {
|
||||
bmEl("bm-locator").value = bm ? bm.locator || "" : "";
|
||||
bmEl("bm-category-input").value = bm ? bm.category || "" : "";
|
||||
bmEl("bm-comment").value = bm ? bm.comment || "" : "";
|
||||
bmEl("bm-dig-sideband").value = bm ? bm.dig_sideband || "" : "";
|
||||
bmWriteDecoders(bm?.decoders ?? []);
|
||||
bmSyncDigSidebandVisibility();
|
||||
bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
||||
wrap.style.display = "flex";
|
||||
bmEl("bm-name").focus();
|
||||
@@ -271,7 +263,6 @@ async function bmSave(e) {
|
||||
const category = bmEl("bm-category-input").value.trim();
|
||||
const comment = bmEl("bm-comment").value.trim();
|
||||
const decoders = bmReadDecoders();
|
||||
const dig_sideband = mode.toUpperCase() === "DIG" ? bmEl("bm-dig-sideband").value || null : null;
|
||||
const formError = bmEl("bm-form-error");
|
||||
if (formError) formError.textContent = "";
|
||||
if (!name || !Number.isFinite(freq_hz) || !mode) {
|
||||
@@ -288,8 +279,7 @@ async function bmSave(e) {
|
||||
locator: locator || null,
|
||||
category,
|
||||
comment,
|
||||
decoders,
|
||||
dig_sideband
|
||||
decoders
|
||||
};
|
||||
try {
|
||||
let resp;
|
||||
@@ -354,12 +344,6 @@ function bmApply(bm) {
|
||||
}
|
||||
const tunePromise = (async () => {
|
||||
await bridge.trx.modules.vchan?.takeSchedulerControl();
|
||||
if ((bm.mode || "").toUpperCase() === "DIG" && bm.dig_sideband) {
|
||||
const p = bm.dig_sideband.toLowerCase();
|
||||
if (p === "auto" || p === "usb" || p === "lsb") {
|
||||
await hostCore.postPath("/set_sdr_dig_sideband?policy=" + encodeURIComponent(p));
|
||||
}
|
||||
}
|
||||
const onVirtual = await bridge.trx.modules.vchan?.interceptMode(bm.mode) ?? false;
|
||||
if (!onVirtual) {
|
||||
await hostCore.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
||||
@@ -612,7 +596,6 @@ function bmPopulateScopePicker() {
|
||||
bmEl("bm-form").addEventListener("submit", (event) => {
|
||||
void bmSave(event);
|
||||
});
|
||||
bmEl("bm-mode").addEventListener("input", bmSyncDigSidebandVisibility);
|
||||
bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
|
||||
const formWrap = bmEl("bm-form-wrap");
|
||||
if (formWrap) {
|
||||
|
||||
@@ -385,14 +385,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</label>
|
||||
<button id="sdr-nb-threshold-set" type="button" class="wfm-inline-btn">Set</button>
|
||||
</div>
|
||||
<label class="wfm-control" id="sdr-dig-sideband-wrap" style="display:none;">
|
||||
<span class="wfm-control-label" title="Sideband used to demodulate DIG. Auto = USB ≥ 10 MHz, LSB below.">DIG sideband</span>
|
||||
<select id="sdr-dig-sideband" class="status-input">
|
||||
<option value="auto">Auto (by band)</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full-row label-below-row" id="vchan-row">
|
||||
@@ -729,14 +721,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<label class="bm-label">Bandwidth (Hz)
|
||||
<input type="number" id="bm-bw" class="status-input" min="0" placeholder="optional" />
|
||||
</label>
|
||||
<label class="bm-label" id="bm-dig-sideband-label" style="display:none;">DIG sideband
|
||||
<select id="bm-dig-sideband" class="status-input">
|
||||
<option value="">Global setting</option>
|
||||
<option value="auto">Auto (by band)</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="bm-label">Locator
|
||||
<input type="text" id="bm-locator" class="status-input" maxlength="6" placeholder="e.g. JO93" />
|
||||
</label>
|
||||
|
||||
@@ -12,9 +12,7 @@ use trx_core::rig::{
|
||||
RigAccessMethod, RigCapabilities, RigInfo, RigRxStatus, RigStatus, RigTxStatus, RigVfo,
|
||||
RigVfoEntry,
|
||||
};
|
||||
use trx_core::{
|
||||
DecoderConfig, DigSidebandPolicy, RdsData, RigFilterState, RigMode, RigSnapshot, WfmDenoiseLevel,
|
||||
};
|
||||
use trx_core::{DecoderConfig, RdsData, RigFilterState, RigMode, RigSnapshot, WfmDenoiseLevel};
|
||||
use trx_frontend_http::server::api::rig::{RigListItem, RigListResponse};
|
||||
use trx_frontend_http::server::api::FrontendMeta;
|
||||
use trx_frontend_http::server::auth::AuthRole;
|
||||
@@ -51,7 +49,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
export!(RigStatus);
|
||||
export!(DecoderConfig);
|
||||
export!(WfmDenoiseLevel);
|
||||
export!(DigSidebandPolicy);
|
||||
export!(RigFilterState);
|
||||
export!(RdsData);
|
||||
export!(SpectrumData);
|
||||
|
||||
@@ -55,14 +55,7 @@ export type DecoderConfig = { aprs_decode_enabled: boolean, hf_aprs_decode_enabl
|
||||
|
||||
export type WfmDenoiseLevel = "off" | "auto" | "low" | "medium" | "high";
|
||||
|
||||
export type DigSidebandPolicy = "auto" | "usb" | "lsb";
|
||||
|
||||
export type RigFilterState = { bandwidth_hz: number, cw_center_hz: number, sdr_gain_db?: number | null, sdr_lna_gain_db?: number | null, sdr_agc_enabled?: boolean | null, sdr_squelch_enabled?: boolean | null, sdr_squelch_threshold_db?: number | null, sdr_nb_enabled?: boolean | null, sdr_nb_threshold?: number | null,
|
||||
/**
|
||||
* Current DIG sideband policy (SDR backends only). Surfaces in the UI as
|
||||
* the advanced-controls "DIG sideband" selector.
|
||||
*/
|
||||
sdr_dig_sideband?: DigSidebandPolicy | null, wfm_deemphasis_us: number, wfm_stereo: boolean, wfm_stereo_detected: boolean, wfm_denoise: WfmDenoiseLevel,
|
||||
export type RigFilterState = { bandwidth_hz: number, cw_center_hz: number, sdr_gain_db?: number | null, sdr_lna_gain_db?: number | null, sdr_agc_enabled?: boolean | null, sdr_squelch_enabled?: boolean | null, sdr_squelch_threshold_db?: number | null, sdr_nb_enabled?: boolean | null, sdr_nb_threshold?: number | null, wfm_deemphasis_us: number, wfm_stereo: boolean, wfm_stereo_detected: boolean, wfm_denoise: WfmDenoiseLevel,
|
||||
/**
|
||||
* Co-Channel Interference level (0–100 scale).
|
||||
*/
|
||||
|
||||
@@ -3681,13 +3681,6 @@ function render(update: AppUpdate) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof update.filter.sdr_dig_sideband === "string") {
|
||||
sdrDigSidebandSupported = true;
|
||||
if (sdrDigSidebandEl && document.activeElement !== sdrDigSidebandEl) {
|
||||
sdrDigSidebandEl.value = update.filter.sdr_dig_sideband;
|
||||
}
|
||||
updateWfmControls();
|
||||
}
|
||||
}
|
||||
if (typeof update.show_sdr_gain_control === "boolean") {
|
||||
if (sdrSettingsRowEl) sdrSettingsRowEl.style.display = update.show_sdr_gain_control ? "" : "none";
|
||||
@@ -5618,9 +5611,6 @@ const sdrNbThresholdControlsEl = document.getElementById("sdr-nb-threshold-contr
|
||||
const sdrNbThresholdEl = document.getElementById("sdr-nb-threshold") as HTMLInputElement | null;
|
||||
const sdrNbThresholdSetBtn = document.getElementById("sdr-nb-threshold-set") as HTMLButtonElement | null;
|
||||
let sdrNbSupported = false;
|
||||
const sdrDigSidebandWrapEl = document.getElementById("sdr-dig-sideband-wrap");
|
||||
const sdrDigSidebandEl = document.getElementById("sdr-dig-sideband") as HTMLSelectElement | null;
|
||||
let sdrDigSidebandSupported = false;
|
||||
|
||||
// Hide audio row if audio is not configured on the server
|
||||
fetch("/audio", { method: "GET" }).then((r) => {
|
||||
@@ -6072,17 +6062,6 @@ if (sdrNbThresholdEl) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitSdrDigSideband() {
|
||||
if (!sdrDigSidebandSupported || !sdrDigSidebandEl) return;
|
||||
const policy = sdrDigSidebandEl.value || "auto";
|
||||
if (policy !== "auto" && policy !== "usb" && policy !== "lsb") return;
|
||||
postPath(`/set_sdr_dig_sideband?policy=${encodeURIComponent(policy)}`).catch(() => {});
|
||||
}
|
||||
if (sdrDigSidebandEl) {
|
||||
sdrDigSidebandEl.addEventListener("change", () => {
|
||||
submitSdrDigSideband();
|
||||
});
|
||||
}
|
||||
function updateWfmControls() {
|
||||
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||
if (wfmControlsCol) wfmControlsCol.style.display = mode === "WFM" ? "" : "none";
|
||||
@@ -6090,11 +6069,6 @@ function updateWfmControls() {
|
||||
// The row holds only these two, so it goes with them — an empty one would
|
||||
// still take a track and a gap in the tray, and draw its divider.
|
||||
if (modeControlsRow) modeControlsRow.style.display = (mode === "WFM" || mode === "SAM") ? "" : "none";
|
||||
// DIG sideband selector is only meaningful in DIG mode on an SDR backend.
|
||||
if (sdrDigSidebandWrapEl) {
|
||||
sdrDigSidebandWrapEl.style.display =
|
||||
sdrDigSidebandSupported && mode === "DIG" ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Show compatibility warning for non-Chromium browsers
|
||||
|
||||
@@ -19,8 +19,6 @@ interface Bookmark {
|
||||
category?: string | null;
|
||||
comment?: string | null;
|
||||
decoders?: string[];
|
||||
/** DIG sideband override: "auto" | "usb" | "lsb". Empty/absent = global. */
|
||||
dig_sideband?: string | null;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
@@ -323,14 +321,6 @@ function bmBuildDecoderCheckboxes() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Show the DIG-sideband override selector only when the mode field is DIG. */
|
||||
function bmSyncDigSidebandVisibility(): void {
|
||||
const label = bmOptionalEl("bm-dig-sideband-label");
|
||||
if (!label) return;
|
||||
const mode = (bmEl("bm-mode").value || "").trim().toUpperCase();
|
||||
label.style.display = mode === "DIG" ? "" : "none";
|
||||
}
|
||||
|
||||
function bmOpenForm(bm: Bookmark | null): void {
|
||||
const wrap = bmEl("bm-form-wrap");
|
||||
if (!wrap) return;
|
||||
@@ -348,9 +338,7 @@ function bmOpenForm(bm: Bookmark | null): void {
|
||||
bmEl("bm-locator").value = bm ? (bm.locator || "") : "";
|
||||
bmEl("bm-category-input").value = bm ? (bm.category || "") : "";
|
||||
bmEl("bm-comment").value = bm ? (bm.comment || "") : "";
|
||||
bmEl("bm-dig-sideband").value = bm ? (bm.dig_sideband || "") : "";
|
||||
bmWriteDecoders(bm?.decoders ?? []);
|
||||
bmSyncDigSidebandVisibility();
|
||||
bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
||||
|
||||
wrap.style.display = "flex";
|
||||
@@ -395,9 +383,6 @@ async function bmSave(e: Event): Promise<void> {
|
||||
const category = bmEl("bm-category-input").value.trim();
|
||||
const comment = bmEl("bm-comment").value.trim();
|
||||
const decoders = bmReadDecoders();
|
||||
// Only meaningful for DIG; stored empty otherwise so it never overrides.
|
||||
const dig_sideband =
|
||||
mode.toUpperCase() === "DIG" ? (bmEl("bm-dig-sideband").value || null) : null;
|
||||
|
||||
const formError = bmEl("bm-form-error");
|
||||
if (formError) formError.textContent = "";
|
||||
@@ -418,7 +403,6 @@ async function bmSave(e: Event): Promise<void> {
|
||||
category,
|
||||
comment,
|
||||
decoders,
|
||||
dig_sideband,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -493,16 +477,6 @@ function bmApply(bm: Bookmark): void {
|
||||
const tunePromise = (async () => {
|
||||
await bridge.trx.modules.vchan?.takeSchedulerControl();
|
||||
|
||||
// For a DIG bookmark carrying a sideband override, set the global DIG
|
||||
// policy first so the backend resolves the intended sideband when DIG is
|
||||
// applied. "auto" is honoured explicitly; an empty override is left alone.
|
||||
if ((bm.mode || "").toUpperCase() === "DIG" && bm.dig_sideband) {
|
||||
const p = bm.dig_sideband.toLowerCase();
|
||||
if (p === "auto" || p === "usb" || p === "lsb") {
|
||||
await hostCore.postPath("/set_sdr_dig_sideband?policy=" + encodeURIComponent(p));
|
||||
}
|
||||
}
|
||||
|
||||
const onVirtual = await bridge.trx.modules.vchan?.interceptMode(bm.mode) ?? false;
|
||||
if (!onVirtual) {
|
||||
await hostCore.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
||||
@@ -796,9 +770,6 @@ function bmPopulateScopePicker() {
|
||||
// Form submit
|
||||
bmEl("bm-form").addEventListener("submit", (event) => { void bmSave(event); });
|
||||
|
||||
// Reveal the DIG-sideband override only while the mode field reads DIG.
|
||||
bmEl("bm-mode").addEventListener("input", bmSyncDigSidebandVisibility);
|
||||
|
||||
// Form cancel
|
||||
bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ pub struct BookmarkInput {
|
||||
pub comment: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub decoders: Option<Vec<String>>,
|
||||
pub dig_sideband: Option<String>,
|
||||
}
|
||||
|
||||
/// A bookmark with its owning scope tag for the list response.
|
||||
@@ -89,17 +88,6 @@ fn normalize_bookmark_locator(locator: Option<String>) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Normalize a DIG sideband override to `auto`/`usb`/`lsb`, or `None` when
|
||||
/// absent or unrecognized (leaves the current policy untouched on apply).
|
||||
fn normalize_dig_sideband(value: Option<String>) -> Option<String> {
|
||||
value.and_then(|v| match v.trim().to_lowercase().as_str() {
|
||||
"auto" => Some("auto".to_string()),
|
||||
"usb" => Some("usb".to_string()),
|
||||
"lsb" => Some("lsb".to_string()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Endpoints
|
||||
// ============================================================================
|
||||
@@ -194,7 +182,6 @@ pub async fn create_bookmark(
|
||||
comment: body.comment.clone().unwrap_or_default(),
|
||||
category: body.category.clone().unwrap_or_default(),
|
||||
decoders: body.decoders.clone().unwrap_or_default(),
|
||||
dig_sideband: normalize_dig_sideband(body.dig_sideband.clone()),
|
||||
};
|
||||
if store.insert(&bm) {
|
||||
Ok(HttpResponse::Created().json(bm))
|
||||
@@ -232,7 +219,6 @@ pub async fn update_bookmark(
|
||||
comment: body.comment.clone().unwrap_or_default(),
|
||||
category: body.category.clone().unwrap_or_default(),
|
||||
decoders: body.decoders.clone().unwrap_or_default(),
|
||||
dig_sideband: normalize_dig_sideband(body.dig_sideband.clone()),
|
||||
};
|
||||
if store.upsert(&id, &bm) {
|
||||
Ok(HttpResponse::Ok().json(bm))
|
||||
|
||||
@@ -595,7 +595,6 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(rig::set_sdr_agc)
|
||||
.service(rig::set_sdr_squelch)
|
||||
.service(rig::set_sdr_noise_blanker)
|
||||
.service(rig::set_sdr_dig_sideband)
|
||||
.service(rig::set_wfm_deemphasis)
|
||||
.service(rig::set_wfm_stereo)
|
||||
.service(rig::set_wfm_denoise)
|
||||
|
||||
@@ -14,7 +14,7 @@ use uuid::Uuid;
|
||||
|
||||
use trx_core::radio::freq::Freq;
|
||||
use trx_core::rig::state::WfmDenoiseLevel;
|
||||
use trx_core::{DigSidebandPolicy, RigCommand, RigRequest, RigState};
|
||||
use trx_core::{RigCommand, RigRequest, RigState};
|
||||
use trx_frontend::{FrontendRuntimeContext, RemoteRigEntry};
|
||||
use trx_protocol::parse_mode;
|
||||
|
||||
@@ -299,23 +299,6 @@ pub async fn set_sdr_noise_blanker(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SdrDigSidebandQuery {
|
||||
/// `auto`, `usb`, or `lsb`.
|
||||
pub policy: DigSidebandPolicy,
|
||||
pub remote: Option<String>,
|
||||
}
|
||||
|
||||
/// Set how the SDR backend resolves DIG mode to a sideband.
|
||||
#[post("/set_sdr_dig_sideband")]
|
||||
pub async fn set_sdr_dig_sideband(
|
||||
query: web::Query<SdrDigSidebandQuery>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let q = query.into_inner();
|
||||
send_command(&rig_tx, RigCommand::SetSdrDigSideband(q.policy), q.remote).await
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WFM / SAM settings
|
||||
// ============================================================================
|
||||
|
||||
@@ -21,10 +21,6 @@ pub struct Bookmark {
|
||||
pub comment: String,
|
||||
pub category: String,
|
||||
pub decoders: Vec<String>,
|
||||
/// For `DIG` bookmarks, the sideband policy to apply on tune: `auto`,
|
||||
/// `usb`, or `lsb`. `None` (or non-DIG) leaves the current policy alone.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dig_sideband: Option<String>,
|
||||
}
|
||||
|
||||
pub struct BookmarkStore {
|
||||
|
||||
@@ -592,21 +592,6 @@ async fn apply_scheduler_target(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Apply any DIG sideband override before switching to DIG so the SDR
|
||||
// backend resolves the intended sideband for this bookmark.
|
||||
if let Some(policy) = bookmark
|
||||
.dig_sideband
|
||||
.as_deref()
|
||||
.and_then(trx_core::DigSidebandPolicy::parse)
|
||||
{
|
||||
scheduler_send(
|
||||
rig_tx,
|
||||
RigCommand::SetSdrDigSideband(policy),
|
||||
remote.to_string(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
scheduler_send(
|
||||
rig_tx,
|
||||
RigCommand::SetMode(trx_protocol::parse_mode(&bookmark.mode)),
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::shared::{check_socket_conflicts, validate_log_level, validate_tokens,
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use trx_decode_log::DecodeLogsConfig;
|
||||
|
||||
use trx_core::rig::state::{DigSidebandPolicy, RigMode};
|
||||
use trx_core::rig::state::RigMode;
|
||||
|
||||
/// Every decoder the server knows how to run, by config name.
|
||||
///
|
||||
@@ -393,10 +393,6 @@ pub struct SdrConfig {
|
||||
pub squelch: SdrSquelchConfig,
|
||||
/// Noise blanker for impulse noise suppression on IQ samples.
|
||||
pub noise_blanker: SdrNoiseBlankerConfig,
|
||||
/// How DIG mode picks a sideband: `auto` (USB ≥ 10 MHz, LSB below),
|
||||
/// `usb`, or `lsb`. Overridable at runtime and per-bookmark.
|
||||
#[serde(default)]
|
||||
pub dig_sideband: DigSidebandPolicy,
|
||||
/// Virtual receiver channels (at least one required when SDR backend is active).
|
||||
pub channels: Vec<SdrChannelConfig>,
|
||||
/// Maximum number of simultaneous virtual channels (including the primary).
|
||||
@@ -437,7 +433,6 @@ impl Default for SdrConfig {
|
||||
gain: SdrGainConfig::default(),
|
||||
squelch: SdrSquelchConfig::default(),
|
||||
noise_blanker: SdrNoiseBlankerConfig::default(),
|
||||
dig_sideband: DigSidebandPolicy::default(),
|
||||
channels: Vec::new(),
|
||||
max_virtual_channels: default_max_virtual_channels(),
|
||||
spectrum_fft_size: default_spectrum_fft_size(),
|
||||
|
||||
@@ -17,6 +17,6 @@ pub use rig::request::RigRequest;
|
||||
pub use rig::response::{RigError, RigResult};
|
||||
pub use rig::state::{
|
||||
DecoderConfig, DecoderResetSeqs, RdsData, RigFilterState, RigMode, RigSnapshot, RigState,
|
||||
DigSidebandPolicy, WfmDenoiseLevel,
|
||||
WfmDenoiseLevel,
|
||||
};
|
||||
pub use rig::AudioSource;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
use crate::rig::state::{DigSidebandPolicy, WfmDenoiseLevel};
|
||||
use crate::rig::state::WfmDenoiseLevel;
|
||||
use crate::RigMode;
|
||||
|
||||
/// Internal command handled by the rig task.
|
||||
@@ -50,8 +50,6 @@ pub enum RigCommand {
|
||||
SetSdrAgc(bool),
|
||||
SetSdrSquelch { enabled: bool, threshold_db: f64 },
|
||||
SetSdrNoiseBlanker { enabled: bool, threshold: f64 },
|
||||
/// Set how the SDR backend resolves DIG mode to a sideband (SDR only).
|
||||
SetSdrDigSideband(DigSidebandPolicy),
|
||||
SetWfmDeemphasis(u32),
|
||||
SetWfmStereo(bool),
|
||||
SetWfmDenoise(WfmDenoiseLevel),
|
||||
|
||||
@@ -470,7 +470,6 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
|
||||
| RigCommand::SetSdrAgc(_)
|
||||
| RigCommand::SetSdrSquelch { .. }
|
||||
| RigCommand::SetSdrNoiseBlanker { .. }
|
||||
| RigCommand::SetSdrDigSideband(_)
|
||||
| RigCommand::SetWfmDeemphasis(_)
|
||||
| RigCommand::SetWfmStereo(_)
|
||||
| RigCommand::SetWfmDenoise(_)
|
||||
|
||||
@@ -260,17 +260,6 @@ pub trait RigSdr: Send {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Set how DIG mode resolves to a sideband (SDR backends only).
|
||||
fn set_sdr_dig_sideband<'a>(
|
||||
&'a mut self,
|
||||
_policy: crate::rig::state::DigSidebandPolicy,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(std::future::ready(Err(
|
||||
Box::new(response::RigError::not_supported("set_sdr_dig_sideband"))
|
||||
as Box<dyn std::error::Error + Send + Sync>,
|
||||
)))
|
||||
}
|
||||
|
||||
fn set_wfm_stereo<'a>(
|
||||
&'a mut self,
|
||||
_enabled: bool,
|
||||
|
||||
@@ -338,10 +338,6 @@ pub struct RigFilterState {
|
||||
pub sdr_nb_enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sdr_nb_threshold: Option<f64>,
|
||||
/// Current DIG sideband policy (SDR backends only). Surfaces in the UI as
|
||||
/// the advanced-controls "DIG sideband" selector.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sdr_dig_sideband: Option<DigSidebandPolicy>,
|
||||
#[serde(default = "default_wfm_deemphasis_us")]
|
||||
pub wfm_deemphasis_us: u32,
|
||||
#[serde(default = "default_wfm_stereo")]
|
||||
@@ -374,89 +370,6 @@ pub enum WfmDenoiseLevel {
|
||||
High,
|
||||
}
|
||||
|
||||
/// How the SDR backend resolves the [`RigMode::DIG`] mode to a concrete
|
||||
/// sideband when demodulating.
|
||||
///
|
||||
/// DIG (data / digital) has no inherent sideband; by ham-radio convention it
|
||||
/// follows the SSB convention for the band. `Auto` applies that convention
|
||||
/// (USB at/above 10 MHz, LSB below); `Usb`/`Lsb` force a fixed sideband. The
|
||||
/// setting is a global SDR control (see advanced radio controls) and may be
|
||||
/// overridden per-bookmark.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DigSidebandPolicy {
|
||||
/// Pick USB or LSB from the dial frequency (USB ≥ 10 MHz, LSB below).
|
||||
#[default]
|
||||
Auto,
|
||||
/// Always demodulate DIG as upper sideband.
|
||||
Usb,
|
||||
/// Always demodulate DIG as lower sideband.
|
||||
Lsb,
|
||||
}
|
||||
|
||||
impl DigSidebandPolicy {
|
||||
/// Frequency boundary used by [`DigSidebandPolicy::Auto`]: at or above this
|
||||
/// dial frequency DIG is upper sideband, below it is lower sideband. This is
|
||||
/// the standard amateur SSB/data convention.
|
||||
pub const AUTO_THRESHOLD_HZ: u64 = 10_000_000;
|
||||
|
||||
/// Resolve this policy to a concrete sideband mode ([`RigMode::USB`] or
|
||||
/// [`RigMode::LSB`]) for the given dial frequency.
|
||||
pub fn resolve(self, freq_hz: u64) -> RigMode {
|
||||
match self {
|
||||
DigSidebandPolicy::Usb => RigMode::USB,
|
||||
DigSidebandPolicy::Lsb => RigMode::LSB,
|
||||
DigSidebandPolicy::Auto => {
|
||||
if freq_hz >= Self::AUTO_THRESHOLD_HZ {
|
||||
RigMode::USB
|
||||
} else {
|
||||
RigMode::LSB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact encoding for storage in an atomic (e.g. shared runtime state).
|
||||
pub fn to_u8(self) -> u8 {
|
||||
match self {
|
||||
DigSidebandPolicy::Auto => 0,
|
||||
DigSidebandPolicy::Usb => 1,
|
||||
DigSidebandPolicy::Lsb => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of [`DigSidebandPolicy::to_u8`]; unknown values decode to `Auto`.
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
1 => DigSidebandPolicy::Usb,
|
||||
2 => DigSidebandPolicy::Lsb,
|
||||
_ => DigSidebandPolicy::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a case-insensitive `auto`/`usb`/`lsb` string; `None` if unknown.
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"auto" => Some(DigSidebandPolicy::Auto),
|
||||
"usb" => Some(DigSidebandPolicy::Usb),
|
||||
"lsb" => Some(DigSidebandPolicy::Lsb),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a channel's effective demodulation mode. For [`RigMode::DIG`] this
|
||||
/// applies the sideband `policy` at the given dial frequency; every other mode
|
||||
/// is returned unchanged. Callers hand the result to the DSP while keeping the
|
||||
/// logical `DIG` mode for display and decoder gating.
|
||||
pub fn effective_demod_mode(logical: &RigMode, policy: DigSidebandPolicy, freq_hz: u64) -> RigMode {
|
||||
if *logical == RigMode::DIG {
|
||||
policy.resolve(freq_hz)
|
||||
} else {
|
||||
logical.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_wfm_deemphasis_us() -> u32 {
|
||||
75
|
||||
}
|
||||
@@ -593,61 +506,3 @@ pub struct RigSnapshot {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vchan_rds: Option<Vec<VchanRdsEntry>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dig_sideband_tests {
|
||||
use super::{effective_demod_mode, DigSidebandPolicy, RigMode};
|
||||
|
||||
#[test]
|
||||
fn auto_follows_ssb_band_convention() {
|
||||
// Below 10 MHz → LSB; at/above 10 MHz → USB.
|
||||
assert_eq!(DigSidebandPolicy::Auto.resolve(7_074_000), RigMode::LSB);
|
||||
assert_eq!(DigSidebandPolicy::Auto.resolve(3_580_000), RigMode::LSB);
|
||||
assert_eq!(DigSidebandPolicy::Auto.resolve(9_999_999), RigMode::LSB);
|
||||
assert_eq!(DigSidebandPolicy::Auto.resolve(10_000_000), RigMode::USB);
|
||||
assert_eq!(DigSidebandPolicy::Auto.resolve(14_074_000), RigMode::USB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_policies_ignore_frequency() {
|
||||
assert_eq!(DigSidebandPolicy::Usb.resolve(3_580_000), RigMode::USB);
|
||||
assert_eq!(DigSidebandPolicy::Lsb.resolve(14_074_000), RigMode::LSB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_mode_only_rewrites_dig() {
|
||||
// Non-DIG modes pass through untouched regardless of policy/frequency.
|
||||
assert_eq!(
|
||||
effective_demod_mode(&RigMode::USB, DigSidebandPolicy::Lsb, 3_580_000),
|
||||
RigMode::USB
|
||||
);
|
||||
assert_eq!(
|
||||
effective_demod_mode(&RigMode::FM, DigSidebandPolicy::Usb, 3_580_000),
|
||||
RigMode::FM
|
||||
);
|
||||
// DIG resolves by policy + frequency.
|
||||
assert_eq!(
|
||||
effective_demod_mode(&RigMode::DIG, DigSidebandPolicy::Auto, 7_074_000),
|
||||
RigMode::LSB
|
||||
);
|
||||
assert_eq!(
|
||||
effective_demod_mode(&RigMode::DIG, DigSidebandPolicy::Auto, 14_074_000),
|
||||
RigMode::USB
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u8_roundtrips_and_parses() {
|
||||
for p in [
|
||||
DigSidebandPolicy::Auto,
|
||||
DigSidebandPolicy::Usb,
|
||||
DigSidebandPolicy::Lsb,
|
||||
] {
|
||||
assert_eq!(DigSidebandPolicy::from_u8(p.to_u8()), p);
|
||||
}
|
||||
assert_eq!(DigSidebandPolicy::from_u8(200), DigSidebandPolicy::Auto);
|
||||
assert_eq!(DigSidebandPolicy::parse("USB"), Some(DigSidebandPolicy::Usb));
|
||||
assert_eq!(DigSidebandPolicy::parse(" lsb "), Some(DigSidebandPolicy::Lsb));
|
||||
assert_eq!(DigSidebandPolicy::parse("nonsense"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,6 @@ mod tests {
|
||||
sdr_squelch_threshold_db: None,
|
||||
sdr_nb_enabled: None,
|
||||
sdr_nb_threshold: None,
|
||||
sdr_dig_sideband: None,
|
||||
wfm_deemphasis_us: 75,
|
||||
wfm_stereo: true,
|
||||
wfm_stereo_detected: false,
|
||||
@@ -392,7 +391,6 @@ mod tests {
|
||||
sdr_squelch_threshold_db: None,
|
||||
sdr_nb_enabled: None,
|
||||
sdr_nb_threshold: None,
|
||||
sdr_dig_sideband: None,
|
||||
wfm_deemphasis_us: 50,
|
||||
wfm_stereo: true,
|
||||
wfm_stereo_detected: true,
|
||||
|
||||
@@ -146,7 +146,6 @@ define_command_mapping! {
|
||||
SetSdrGain { gain_db } <=> SetSdrGain,
|
||||
SetSdrLnaGain { gain_db } <=> SetSdrLnaGain,
|
||||
SetSdrAgc { enabled } <=> SetSdrAgc,
|
||||
SetSdrDigSideband { policy } <=> SetSdrDigSideband,
|
||||
SetWfmDeemphasis { deemphasis_us } <=> SetWfmDeemphasis,
|
||||
SetWfmStereo { enabled } <=> SetWfmStereo,
|
||||
SetWfmDenoise { level } <=> SetWfmDenoise,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use trx_core::rig::state::RigSnapshot;
|
||||
use trx_core::{DigSidebandPolicy, WfmDenoiseLevel};
|
||||
use trx_core::WfmDenoiseLevel;
|
||||
|
||||
/// Command received from network clients (JSON).
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -106,9 +106,6 @@ pub enum ClientCommand {
|
||||
enabled: bool,
|
||||
threshold: f64,
|
||||
},
|
||||
SetSdrDigSideband {
|
||||
policy: DigSidebandPolicy,
|
||||
},
|
||||
SetWfmDeemphasis {
|
||||
deemphasis_us: u32,
|
||||
},
|
||||
|
||||
@@ -360,7 +360,6 @@ fn build_sdr_rig_from_instance(rig_cfg: &RigInstanceConfig) -> SdrRigBuildResult
|
||||
max_virtual_channels: rig_cfg.sdr.max_virtual_channels,
|
||||
nb_enabled: rig_cfg.sdr.noise_blanker.enabled,
|
||||
nb_threshold: rig_cfg.sdr.noise_blanker.threshold,
|
||||
dig_sideband: rig_cfg.sdr.dig_sideband,
|
||||
spectrum_fft_size: rig_cfg.sdr.spectrum_fft_size,
|
||||
})?;
|
||||
|
||||
|
||||
@@ -774,18 +774,6 @@ async fn process_command(
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetSdrDigSideband(policy) => {
|
||||
if let Some(sdr) = ctx.rig.as_sdr() {
|
||||
if let Err(e) = sdr.set_sdr_dig_sideband(policy).await {
|
||||
return Err(RigError::communication(format!("set_sdr_dig_sideband: {e}")));
|
||||
}
|
||||
} else {
|
||||
return Err(RigError::not_supported("set_sdr_dig_sideband"));
|
||||
}
|
||||
ctx.state.filter = ctx.rig.as_sdr_ref().and_then(|s| s.filter_state());
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
|
||||
if let Some(sdr) = ctx.rig.as_sdr() {
|
||||
if let Err(e) = sdr.set_wfm_deemphasis(deemphasis_us).await {
|
||||
|
||||
@@ -13,10 +13,7 @@ use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use trx_core::radio::freq::{Band, Freq};
|
||||
use trx_core::rig::response::RigError;
|
||||
use trx_core::rig::state::{
|
||||
effective_demod_mode, DigSidebandPolicy, RigFilterState, SpectrumData, VchanRdsEntry,
|
||||
WfmDenoiseLevel,
|
||||
};
|
||||
use trx_core::rig::state::{RigFilterState, SpectrumData, VchanRdsEntry, WfmDenoiseLevel};
|
||||
use trx_core::rig::{
|
||||
AudioSource, Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigSdr, RigStatusFuture,
|
||||
};
|
||||
@@ -76,8 +73,6 @@ pub struct SoapySdrConfig {
|
||||
pub nb_enabled: bool,
|
||||
/// Noise blanker impulse threshold multiplier.
|
||||
pub nb_threshold: f64,
|
||||
/// How DIG mode resolves to a sideband (auto/usb/lsb).
|
||||
pub dig_sideband: DigSidebandPolicy,
|
||||
/// FFT bin count for the spectrum display; a power of two.
|
||||
///
|
||||
/// Fewer bins cost less DSP and put fewer bytes on the network per frame,
|
||||
@@ -109,7 +104,6 @@ impl Default for SoapySdrConfig {
|
||||
max_virtual_channels: 4,
|
||||
nb_enabled: false,
|
||||
nb_threshold: 10.0,
|
||||
dig_sideband: DigSidebandPolicy::Auto,
|
||||
spectrum_fft_size: 1024,
|
||||
}
|
||||
}
|
||||
@@ -163,12 +157,7 @@ pub struct SoapySdrRig {
|
||||
/// Hidden AIS decoder channels (A and B) when available.
|
||||
ais_channel_indices: Option<(usize, usize)>,
|
||||
/// Virtual channel manager shared with external consumers (e.g. RigHandle).
|
||||
/// Also owns the shared DIG-sideband policy.
|
||||
channel_manager: Arc<vchan_impl::SdrVirtualChannelManager>,
|
||||
/// Concrete demodulation mode last pushed to the primary channel DSP. For
|
||||
/// DIG this is the resolved USB/LSB; used to avoid rebuilding filters on
|
||||
/// every tune step when the resolved sideband hasn't actually changed.
|
||||
applied_primary_mode: RigMode,
|
||||
}
|
||||
|
||||
impl SoapySdrRig {
|
||||
@@ -211,7 +200,6 @@ impl SoapySdrRig {
|
||||
let max_virtual_channels = config.max_virtual_channels;
|
||||
let nb_enabled = config.nb_enabled;
|
||||
let nb_threshold = config.nb_threshold;
|
||||
let dig_sideband = config.dig_sideband;
|
||||
let spectrum_fft_size = config.spectrum_fft_size;
|
||||
tracing::info!(
|
||||
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
|
||||
@@ -376,15 +364,8 @@ impl SoapySdrRig {
|
||||
pipeline.clone(),
|
||||
fixed_slot_count,
|
||||
max_virtual_channels,
|
||||
dig_sideband,
|
||||
));
|
||||
|
||||
// Concrete demod mode the primary channel starts in. For DIG this
|
||||
// resolves the configured sideband policy against the initial dial
|
||||
// frequency so the very first image/audio uses the right sideband.
|
||||
let initial_primary_mode = effective_demod_mode(&initial_mode, dig_sideband, initial_freq.hz);
|
||||
let initial_is_dig = initial_mode == RigMode::DIG;
|
||||
|
||||
let rig = Self {
|
||||
info,
|
||||
freq: initial_freq,
|
||||
@@ -411,17 +392,7 @@ impl SoapySdrRig {
|
||||
nb_threshold,
|
||||
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
||||
channel_manager,
|
||||
applied_primary_mode: initial_primary_mode.clone(),
|
||||
};
|
||||
// If the primary channel starts in DIG, its DSP was created with the
|
||||
// logical DIG demodulator (upper sideband); push the resolved sideband
|
||||
// so an Auto/LSB start is honoured immediately.
|
||||
if initial_is_dig {
|
||||
let dsps = rig.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(rig.primary_channel_idx) {
|
||||
dsp_arc.lock().unwrap().set_mode(&initial_primary_mode);
|
||||
}
|
||||
}
|
||||
rig.apply_ais_channel_activity();
|
||||
Ok(rig)
|
||||
}
|
||||
@@ -477,7 +448,6 @@ impl SoapySdrRig {
|
||||
max_virtual_channels,
|
||||
nb_enabled,
|
||||
nb_threshold,
|
||||
dig_sideband: DigSidebandPolicy::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -649,21 +619,6 @@ impl RigCat for SoapySdrRig {
|
||||
}
|
||||
}
|
||||
}
|
||||
// In DIG/Auto, tuning across the 10 MHz boundary flips the
|
||||
// sideband. Rebuild the demodulator only when it actually changes,
|
||||
// so ordinary tuning stays glitch-free.
|
||||
if self.mode == RigMode::DIG {
|
||||
let effective = self.channel_manager.dig_policy().resolve(self.freq.hz);
|
||||
if effective != self.applied_primary_mode {
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||
let mut dsp = dsp_arc.lock().unwrap();
|
||||
dsp.set_mode(&effective);
|
||||
dsp.set_filter(self.bandwidth_hz);
|
||||
}
|
||||
self.applied_primary_mode = effective;
|
||||
}
|
||||
}
|
||||
self.update_ais_channel_offsets();
|
||||
Ok(())
|
||||
})
|
||||
@@ -677,17 +632,12 @@ impl RigCat for SoapySdrRig {
|
||||
tracing::debug!("SoapySdrRig: set_mode -> {:?}", mode);
|
||||
self.mode = mode.clone();
|
||||
self.bandwidth_hz = Self::default_bandwidth_for_mode(&mode);
|
||||
// DIG carries no inherent sideband: resolve it to a concrete
|
||||
// USB/LSB demodulator from the policy + dial frequency. The logical
|
||||
// DIG mode is kept in `self.mode` (and RigState) for display.
|
||||
let effective = effective_demod_mode(&self.mode, self.channel_manager.dig_policy(), self.freq.hz);
|
||||
self.applied_primary_mode = effective.clone();
|
||||
// Update the primary channel's demodulator in the live pipeline.
|
||||
{
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||
let mut dsp = dsp_arc.lock().unwrap();
|
||||
dsp.set_mode(&effective);
|
||||
dsp.set_mode(&mode);
|
||||
dsp.set_filter(self.bandwidth_hz);
|
||||
}
|
||||
}
|
||||
@@ -1006,30 +956,6 @@ impl RigSdr for SoapySdrRig {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_sdr_dig_sideband<'a>(
|
||||
&'a mut self,
|
||||
policy: DigSidebandPolicy,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
// Update the shared policy (also re-resolves DIG virtual channels).
|
||||
self.channel_manager.set_dig_policy(policy);
|
||||
// Re-apply to the primary channel when it is currently DIG.
|
||||
if self.mode == RigMode::DIG {
|
||||
let effective = policy.resolve(self.freq.hz);
|
||||
if effective != self.applied_primary_mode {
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(self.primary_channel_idx) {
|
||||
let mut dsp = dsp_arc.lock().unwrap();
|
||||
dsp.set_mode(&effective);
|
||||
dsp.set_filter(self.bandwidth_hz);
|
||||
}
|
||||
self.applied_primary_mode = effective;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_wfm_stereo<'a>(
|
||||
&'a mut self,
|
||||
enabled: bool,
|
||||
@@ -1123,7 +1049,6 @@ impl RigSdr for SoapySdrRig {
|
||||
sdr_squelch_threshold_db: Some(self.squelch_threshold_db as f64),
|
||||
sdr_nb_enabled: Some(self.nb_enabled),
|
||||
sdr_nb_threshold: Some(self.nb_threshold),
|
||||
sdr_dig_sideband: Some(self.channel_manager.dig_policy()),
|
||||
wfm_deemphasis_us: self.wfm_deemphasis_us,
|
||||
wfm_stereo: self.wfm_stereo,
|
||||
wfm_stereo_detected,
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
//! updates every `ChannelDsp` in place and pauses out-of-span channels instead
|
||||
//! of destroying them.
|
||||
|
||||
use std::sync::atomic::{AtomicI64, AtomicU8, Ordering};
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use num_complex::Complex;
|
||||
use tokio::sync::broadcast;
|
||||
use trx_core::rig::state::{effective_demod_mode, DigSidebandPolicy, RigMode, VchanRdsEntry};
|
||||
use trx_core::rig::state::{RigMode, VchanRdsEntry};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dsp::SdrPipeline;
|
||||
@@ -59,12 +59,7 @@ fn default_bandwidth_hz(mode: &RigMode) -> u32 {
|
||||
struct ManagedChannel {
|
||||
id: Uuid,
|
||||
freq_hz: u64,
|
||||
/// Logical mode requested by the user (e.g. `DIG`), shown in listings.
|
||||
mode: RigMode,
|
||||
/// Concrete demod mode last pushed to the DSP. For `DIG` this is the
|
||||
/// resolved USB/LSB; tracked so a frequency change only rebuilds the
|
||||
/// demodulator when the resolved sideband actually flips.
|
||||
applied_mode: RigMode,
|
||||
/// `broadcast::Sender` kept alive so new subscribers can join at any time.
|
||||
pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||
/// IQ tap sender (kept alive; external consumers may subscribe).
|
||||
@@ -96,9 +91,6 @@ pub struct SdrVirtualChannelManager {
|
||||
channels: RwLock<Vec<ManagedChannel>>,
|
||||
/// Fires whenever a channel is explicitly destroyed.
|
||||
destroyed_tx: broadcast::Sender<Uuid>,
|
||||
/// Shared DIG-sideband policy ([`DigSidebandPolicy`] encoded via `to_u8`).
|
||||
/// Read when resolving any DIG channel's demodulator.
|
||||
dig_sideband: AtomicU8,
|
||||
}
|
||||
|
||||
impl SdrVirtualChannelManager {
|
||||
@@ -108,12 +100,7 @@ impl SdrVirtualChannelManager {
|
||||
/// - `fixed_slot_count`: number of fixed pipeline slots (primary + AIS),
|
||||
/// i.e. the index of the first slot available for virtual channels.
|
||||
/// - `max_total`: maximum total channels including primary (e.g. 4).
|
||||
pub fn new(
|
||||
pipeline: Arc<SdrPipeline>,
|
||||
fixed_slot_count: usize,
|
||||
max_total: usize,
|
||||
dig_sideband: DigSidebandPolicy,
|
||||
) -> Self {
|
||||
pub fn new(pipeline: Arc<SdrPipeline>, fixed_slot_count: usize, max_total: usize) -> Self {
|
||||
// Seed the channel list with a synthetic primary-channel entry.
|
||||
// We use the first PCM sender from the pipeline (index 0).
|
||||
let primary_pcm_tx = pipeline
|
||||
@@ -131,7 +118,6 @@ impl SdrVirtualChannelManager {
|
||||
id: Uuid::new_v4(),
|
||||
freq_hz: 0, // actual freq kept by SoapySdrRig; manager treats ch-0 as opaque
|
||||
mode: RigMode::USB,
|
||||
applied_mode: RigMode::USB,
|
||||
pcm_tx: primary_pcm_tx,
|
||||
iq_tx: primary_iq_tx,
|
||||
pipeline_slot: 0,
|
||||
@@ -148,7 +134,6 @@ impl SdrVirtualChannelManager {
|
||||
max_total: max_total.max(1),
|
||||
channels: RwLock::new(vec![primary]),
|
||||
destroyed_tx,
|
||||
dig_sideband: AtomicU8::new(dig_sideband.to_u8()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,36 +141,6 @@ impl SdrVirtualChannelManager {
|
||||
self.destroyed_tx.clone()
|
||||
}
|
||||
|
||||
/// Current shared DIG-sideband policy.
|
||||
pub fn dig_policy(&self) -> DigSidebandPolicy {
|
||||
DigSidebandPolicy::from_u8(self.dig_sideband.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Update the shared DIG-sideband policy and re-resolve every DIG virtual
|
||||
/// channel's demodulator (the primary channel is handled by `SoapySdrRig`).
|
||||
pub fn set_dig_policy(&self, policy: DigSidebandPolicy) {
|
||||
self.dig_sideband.store(policy.to_u8(), Ordering::Relaxed);
|
||||
let mut channels = self.channels.write().unwrap();
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
for ch in channels
|
||||
.iter_mut()
|
||||
.filter(|c| !c.permanent && c.mode == RigMode::DIG)
|
||||
{
|
||||
let effective = policy.resolve(ch.freq_hz);
|
||||
if effective != ch.applied_mode {
|
||||
ch.applied_mode = effective.clone();
|
||||
if let Some(dsp_arc) = dsps.get(ch.pipeline_slot) {
|
||||
dsp_arc.lock().unwrap().set_mode(&effective);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a channel's effective demod mode under the current policy.
|
||||
fn resolve_mode(&self, logical: &RigMode, freq_hz: u64) -> RigMode {
|
||||
effective_demod_mode(logical, self.dig_policy(), freq_hz)
|
||||
}
|
||||
|
||||
fn half_span_hz(&self) -> i64 {
|
||||
i64::from(self.pipeline.sdr_sample_rate) / 2
|
||||
}
|
||||
@@ -217,13 +172,10 @@ impl SdrVirtualChannelManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Bandwidth follows the logical mode; the DSP demodulator follows the
|
||||
// resolved sideband (DIG → USB/LSB by policy + frequency).
|
||||
let bandwidth_hz = default_bandwidth_hz(mode);
|
||||
let applied_mode = self.resolve_mode(mode, freq_hz);
|
||||
let (pcm_tx, iq_tx) =
|
||||
self.pipeline
|
||||
.add_virtual_channel(if_hz as f64, &applied_mode, bandwidth_hz);
|
||||
let (pcm_tx, iq_tx) = self
|
||||
.pipeline
|
||||
.add_virtual_channel(if_hz as f64, mode, bandwidth_hz);
|
||||
|
||||
let pipeline_slot = self
|
||||
.pipeline
|
||||
@@ -238,7 +190,6 @@ impl SdrVirtualChannelManager {
|
||||
id,
|
||||
freq_hz,
|
||||
mode: mode.clone(),
|
||||
applied_mode,
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
pipeline_slot,
|
||||
@@ -363,18 +314,9 @@ impl VirtualChannelManager for SdrVirtualChannelManager {
|
||||
.ok_or(VChanError::NotFound)?;
|
||||
|
||||
ch.freq_hz = freq_hz;
|
||||
// A DIG/Auto channel flips sideband across the 10 MHz boundary; rebuild
|
||||
// the demodulator only when the resolved sideband actually changes.
|
||||
let effective = self.resolve_mode(&ch.mode, freq_hz);
|
||||
let mode_changed = effective != ch.applied_mode;
|
||||
ch.applied_mode = effective.clone();
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(ch.pipeline_slot) {
|
||||
let mut dsp = dsp_arc.lock().unwrap();
|
||||
dsp.set_channel_if_hz(if_hz as f64);
|
||||
if mode_changed {
|
||||
dsp.set_mode(&effective);
|
||||
}
|
||||
dsp_arc.lock().unwrap().set_channel_if_hz(if_hz as f64);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -387,11 +329,9 @@ impl VirtualChannelManager for SdrVirtualChannelManager {
|
||||
.ok_or(VChanError::NotFound)?;
|
||||
|
||||
ch.mode = mode.clone();
|
||||
let effective = self.resolve_mode(mode, ch.freq_hz);
|
||||
ch.applied_mode = effective.clone();
|
||||
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||
if let Some(dsp_arc) = dsps.get(ch.pipeline_slot) {
|
||||
dsp_arc.lock().unwrap().set_mode(&effective);
|
||||
dsp_arc.lock().unwrap().set_mode(mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -485,7 +425,6 @@ impl VirtualChannelManager for SdrVirtualChannelManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::demod::Demodulator;
|
||||
use crate::dsp::{MockIqSource, NoiseBlankerConfig, SdrPipeline};
|
||||
|
||||
fn make_pipeline() -> Arc<SdrPipeline> {
|
||||
@@ -507,7 +446,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_and_list() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4, DigSidebandPolicy::Auto);
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
|
||||
// Set center to 14.1 MHz so that 14.074 MHz is within ±960 kHz.
|
||||
mgr.update_center_hz(14_100_000);
|
||||
assert_eq!(mgr.channels().len(), 1); // primary only
|
||||
@@ -523,7 +462,7 @@ mod tests {
|
||||
#[test]
|
||||
fn remove_virtual_channel() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4, DigSidebandPolicy::Auto);
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
|
||||
mgr.update_center_hz(14_100_000);
|
||||
let (id, _) = mgr.add_channel(14_074_000, &RigMode::USB).unwrap();
|
||||
mgr.remove_channel(id).unwrap();
|
||||
@@ -533,7 +472,7 @@ mod tests {
|
||||
#[test]
|
||||
fn cannot_remove_primary() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4, DigSidebandPolicy::Auto);
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
|
||||
let primary_id = mgr.channels()[0].id;
|
||||
let err = mgr.remove_channel(primary_id).unwrap_err();
|
||||
assert!(matches!(err, VChanError::Permanent));
|
||||
@@ -542,7 +481,7 @@ mod tests {
|
||||
#[test]
|
||||
fn cap_enforced() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 2, DigSidebandPolicy::Auto); // primary + 1 virtual max
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 2); // primary + 1 virtual max
|
||||
mgr.update_center_hz(14_100_000);
|
||||
mgr.add_channel(14_074_000, &RigMode::USB).unwrap();
|
||||
let err = mgr.add_channel(14_075_000, &RigMode::USB).unwrap_err();
|
||||
@@ -552,7 +491,7 @@ mod tests {
|
||||
#[test]
|
||||
fn out_of_bandwidth() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4, DigSidebandPolicy::Auto);
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
|
||||
// center_hz = 0, half_span = 960_000 Hz — 10 MHz is way out
|
||||
let err = mgr.add_channel(10_000_000, &RigMode::USB).unwrap_err();
|
||||
assert!(matches!(err, VChanError::OutOfBandwidth { .. }));
|
||||
@@ -561,7 +500,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hidden_background_channels_are_outside_visible_cap() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 2, DigSidebandPolicy::Auto); // primary + 1 visible max
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 2); // primary + 1 visible max
|
||||
mgr.update_center_hz(14_100_000);
|
||||
|
||||
mgr.add_channel(14_074_000, &RigMode::USB).unwrap();
|
||||
@@ -574,46 +513,10 @@ mod tests {
|
||||
assert!(visible.iter().all(|channel| channel.id != hidden_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dig_channel_resolves_sideband_and_tracks_policy() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p.clone(), 1, 6, DigSidebandPolicy::Auto);
|
||||
mgr.update_center_hz(7_100_000);
|
||||
|
||||
// The virtual channel occupies pipeline slot 1 (slot 0 is the primary).
|
||||
let demod_at_slot1 = || {
|
||||
p.channel_dsps.read().unwrap()[1]
|
||||
.lock()
|
||||
.unwrap()
|
||||
.demodulator
|
||||
.clone()
|
||||
};
|
||||
|
||||
// DIG below 10 MHz under Auto → lower sideband.
|
||||
let (_id, _rx) = mgr.add_channel(7_074_000, &RigMode::DIG).unwrap();
|
||||
assert_eq!(demod_at_slot1(), Demodulator::Lsb);
|
||||
|
||||
// Forcing USB re-resolves the live channel.
|
||||
mgr.set_dig_policy(DigSidebandPolicy::Usb);
|
||||
assert_eq!(demod_at_slot1(), Demodulator::Usb);
|
||||
|
||||
// Back to Auto (still below 10 MHz) → lower sideband again.
|
||||
mgr.set_dig_policy(DigSidebandPolicy::Auto);
|
||||
assert_eq!(demod_at_slot1(), Demodulator::Lsb);
|
||||
|
||||
// The listing keeps the logical DIG mode for display.
|
||||
let ch = mgr
|
||||
.channels()
|
||||
.into_iter()
|
||||
.find(|c| !c.permanent)
|
||||
.expect("virtual channel");
|
||||
assert_eq!(ch.mode, "DIG");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retune_keeps_virtual_channel_allocated() {
|
||||
let p = make_pipeline();
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4, DigSidebandPolicy::Auto);
|
||||
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
|
||||
mgr.update_center_hz(14_100_000);
|
||||
let mut destroyed_rx = mgr.subscribe_destroyed();
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ sample_rate = 1920000
|
||||
bandwidth = 1500000
|
||||
wfm_deemphasis_us = 50
|
||||
center_offset_hz = 100000
|
||||
dig_sideband = "auto"
|
||||
channels = []
|
||||
max_virtual_channels = 4
|
||||
spectrum_fft_size = 1024
|
||||
|
||||
Reference in New Issue
Block a user