[feat](trx-rs): configurable DIG sideband for SDR (auto/USB/LSB)
DIG has no inherent sideband; on the SDR backend it was always demodulated
as USB. Make it resolve to USB or LSB via a policy that defaults to the
amateur SSB/data convention (USB at/above 10 MHz, LSB below) and can be
overridden globally from the advanced radio controls or per-bookmark.
Design: the logical DIG mode is kept in RigState (display, decoder gating)
while the SDR pipeline is handed a concrete USB/LSB demodulator resolved from
(policy, dial frequency). Resolution happens at the boundary — the rig for
the primary channel and the virtual-channel manager for vchans — so the hot
DSP/demod path is untouched. The resolved sideband is only re-pushed when it
actually changes (e.g. tuning DIG/Auto across 10 MHz), keeping ordinary
tuning glitch-free.
Core/protocol:
- New `DigSidebandPolicy { Auto, Usb, Lsb }` with `resolve(freq)` and an
`effective_demod_mode()` helper (trx-core), re-exported at the crate root.
- `RigCommand::SetSdrDigSideband`, `RigSdr::set_sdr_dig_sideband`, and a
`RigFilterState.sdr_dig_sideband` field for state sync; wired through the
ClientCommand mapping.
Config: `[rig.sdr] dig_sideband = "auto"` (regenerated trx-rs.toml.example).
SDR backend: the vchan manager owns the shared policy (atomic); the rig
applies it to the primary channel and, on `set_sdr_dig_sideband`, re-resolves
all DIG virtual channels.
Frontend: a mode-gated "DIG sideband" selector in the SDR advanced controls
(POST /set_sdr_dig_sideband), reflecting server state; bookmarks gain an
optional `dig_sideband` field (form selector shown only for DIG) that, on
apply, sets the global policy before switching to DIG. The scheduler honours
it for automated bookmark activation too.
Tests: policy resolution / effective-mode / u8+parse round-trips (trx-core);
a vchan integration test asserting a DIG channel resolves to LSB below
10 MHz, flips with the policy, and still lists as DIG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UiK871ht2uPFBHtMbxy3wD
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -4758,6 +4758,13 @@ 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";
|
||||
@@ -6624,6 +6631,9 @@ 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(() => {
|
||||
@@ -7019,11 +7029,26 @@ 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,6 +214,12 @@ 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;
|
||||
@@ -227,7 +233,9 @@ 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();
|
||||
@@ -263,6 +271,7 @@ 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) {
|
||||
@@ -279,7 +288,8 @@ async function bmSave(e) {
|
||||
locator: locator || null,
|
||||
category,
|
||||
comment,
|
||||
decoders
|
||||
decoders,
|
||||
dig_sideband
|
||||
};
|
||||
try {
|
||||
let resp;
|
||||
@@ -344,6 +354,12 @@ 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));
|
||||
@@ -596,6 +612,7 @@ 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,6 +385,14 @@ 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">
|
||||
@@ -721,6 +729,14 @@ 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,7 +12,9 @@ use trx_core::rig::{
|
||||
RigAccessMethod, RigCapabilities, RigInfo, RigRxStatus, RigStatus, RigTxStatus, RigVfo,
|
||||
RigVfoEntry,
|
||||
};
|
||||
use trx_core::{DecoderConfig, RdsData, RigFilterState, RigMode, RigSnapshot, WfmDenoiseLevel};
|
||||
use trx_core::{
|
||||
DecoderConfig, DigSidebandPolicy, 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;
|
||||
@@ -49,6 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
export!(RigStatus);
|
||||
export!(DecoderConfig);
|
||||
export!(WfmDenoiseLevel);
|
||||
export!(DigSidebandPolicy);
|
||||
export!(RigFilterState);
|
||||
export!(RdsData);
|
||||
export!(SpectrumData);
|
||||
|
||||
@@ -55,7 +55,14 @@ export type DecoderConfig = { aprs_decode_enabled: boolean, hf_aprs_decode_enabl
|
||||
|
||||
export type WfmDenoiseLevel = "off" | "auto" | "low" | "medium" | "high";
|
||||
|
||||
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,
|
||||
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,
|
||||
/**
|
||||
* Co-Channel Interference level (0–100 scale).
|
||||
*/
|
||||
|
||||
@@ -3681,6 +3681,13 @@ 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";
|
||||
@@ -5611,6 +5618,9 @@ 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) => {
|
||||
@@ -6062,6 +6072,17 @@ 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";
|
||||
@@ -6069,6 +6090,11 @@ 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,6 +19,8 @@ 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;
|
||||
}
|
||||
|
||||
@@ -321,6 +323,14 @@ 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;
|
||||
@@ -338,7 +348,9 @@ 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";
|
||||
@@ -383,6 +395,9 @@ 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 = "";
|
||||
@@ -403,6 +418,7 @@ async function bmSave(e: Event): Promise<void> {
|
||||
category,
|
||||
comment,
|
||||
decoders,
|
||||
dig_sideband,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -477,6 +493,16 @@ 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));
|
||||
@@ -770,6 +796,9 @@ 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,6 +37,7 @@ 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.
|
||||
@@ -88,6 +89,17 @@ 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
|
||||
// ============================================================================
|
||||
@@ -182,6 +194,7 @@ 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))
|
||||
@@ -219,6 +232,7 @@ 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,6 +595,7 @@ 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::{RigCommand, RigRequest, RigState};
|
||||
use trx_core::{DigSidebandPolicy, RigCommand, RigRequest, RigState};
|
||||
use trx_frontend::{FrontendRuntimeContext, RemoteRigEntry};
|
||||
use trx_protocol::parse_mode;
|
||||
|
||||
@@ -299,6 +299,23 @@ 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,6 +21,10 @@ 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,6 +592,21 @@ 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)),
|
||||
|
||||
Reference in New Issue
Block a user