commit c691339ca12a19dc604f0322bea9a0704bdc34cc
parent 2d159453f5503dd13bd74a002e967d4ef1ca19b8
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Tue, 8 Sep 2026 14:45:28 -0400
Add mode and filter bandwidth syncing
Diffstat:
| M | shim/CMakeLists.txt | | | 1 | + |
| M | shim/src/main.cpp | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| M | src/civ.rs | | | 268 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | src/lib.rs | | | 18 | +++++++++++++++++- |
| M | src/sync.rs | | | 126 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
5 files changed, 452 insertions(+), 17 deletions(-)
diff --git a/shim/CMakeLists.txt b/shim/CMakeLists.txt
@@ -29,6 +29,7 @@ add_library(${PROJECT_NAME} SHARED ${SRC})
target_include_directories(${PROJECT_NAME} PRIVATE
"${SDRPP_DIR}/core/src"
"${SDRPP_DIR}/core/src/imgui"
+ "${SDRPP_DIR}/decoder_modules/radio/src"
${OPENGL_INCLUDE_DIRS}
)
diff --git a/shim/src/main.cpp b/shim/src/main.cpp
@@ -1,9 +1,11 @@
#include <imgui.h>
#include <module.h>
+#include <core.h>
#include <gui/gui.h>
#include <gui/tuner.h>
#include <signal_path/signal_path.h>
#include <utils/flog.h>
+#include <radio_interface.h>
#include <cstring>
#include <cstdint>
@@ -20,19 +22,31 @@ extern "C" {
void civ_status(void* ctx, char* out, size_t out_len);
void civ_set_serial_port(void* ctx, const char* port);
void civ_set_serial_baud(void* ctx, uint32_t baud);
+ void civ_set_sync_mode(void* ctx, bool sync_mode);
}
SDRPP_MOD_INFO{
/* Name: */ "civ_sync",
- /* Description: */ "Sync an Icom IC-7300 (CI-V) with an SDR++ VFO",
+ /* Description: */ "Sync an Icom IC-7300 (CI-V) with an SDR++ VFO (freq/mode/filter)",
/* Author: */ "vfo-sync",
- /* Version: */ 0, 1, 0,
+ /* Version: */ 0, 2, 0,
/* Max instances */ 1
};
// Guard the GUI-side state accessors (waterfall/VFO) called off-thread.
static std::mutex guiMtx;
+// Returns the selected VFO name if it is bound to the "radio" demodulator,
+// or "" otherwise. Caller must hold guiMtx.
+static std::string selectedRadioVfo() {
+ std::string vfo = gui::waterfall.selectedVFO;
+ if (!core::modComManager.interfaceExists(vfo) ||
+ core::modComManager.getModuleName(vfo) != "radio") {
+ return "";
+ }
+ return vfo;
+}
+
extern "C" void civ_bridge_tune(double freq) {
std::lock_guard<std::mutex> lck(guiMtx);
tuner::tune(tuner::TUNER_MODE_NORMAL, gui::waterfall.selectedVFO, freq);
@@ -47,6 +61,39 @@ extern "C" double civ_bridge_get_vfo_freq() {
return freq;
}
+extern "C" int civ_bridge_get_mode() {
+ std::lock_guard<std::mutex> lck(guiMtx);
+ std::string vfo = selectedRadioVfo();
+ if (vfo.empty()) { return -1; }
+ int mode = -1;
+ core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_GET_MODE, NULL, &mode);
+ return mode;
+}
+
+extern "C" void civ_bridge_set_mode(int mode) {
+ std::lock_guard<std::mutex> lck(guiMtx);
+ std::string vfo = selectedRadioVfo();
+ if (vfo.empty()) { return; }
+ core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_SET_MODE, &mode, NULL);
+}
+
+extern "C" double civ_bridge_get_bandwidth() {
+ std::lock_guard<std::mutex> lck(guiMtx);
+ std::string vfo = selectedRadioVfo();
+ if (vfo.empty()) { return 0.0; }
+ float bw = 0.0f;
+ core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_GET_BANDWIDTH, NULL, &bw);
+ return (double)bw;
+}
+
+extern "C" void civ_bridge_set_bandwidth(double hz) {
+ std::lock_guard<std::mutex> lck(guiMtx);
+ std::string vfo = selectedRadioVfo();
+ if (vfo.empty()) { return; }
+ float bw = (float)hz;
+ core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_SET_BANDWIDTH, &bw, NULL);
+}
+
extern "C" void civ_bridge_log(int level, const char* msg) {
if (msg == nullptr) { return; }
switch (level) {
@@ -136,6 +183,10 @@ private:
}
}
+ if (ImGui::Checkbox("Sync mode & filter", &_this->syncMode)) {
+ civ_set_sync_mode(_this->rustCtx, _this->syncMode);
+ }
+
if (ImGui::Button(_this->isEnabled() ? "Disconnect" : "Connect")) {
if (_this->isEnabled()) {
_this->disable();
@@ -152,6 +203,7 @@ private:
uint32_t baud = 0;
bool customBaudMode = false;
int customBaud = 115200;
+ bool syncMode = true;
};
MOD_EXPORT void _INIT_() {
diff --git a/src/civ.rs b/src/civ.rs
@@ -26,6 +26,18 @@ const CMD_BROADCAST_FREQ: u8 = 0x00;
/// the radio during baud-rate auto-detection.
const CMD_READ_FREQ: u8 = 0x03;
+/// "Sets and outputs the current operating mode" — what the radio broadcasts
+/// on a mode change when CI-V Transceive is enabled.
+const CMD_BROADCAST_MODE: u8 = 0x01;
+/// "Reads the current operating mode".
+const CMD_READ_MODE: u8 = 0x04;
+/// "Sets the current operating mode" (set-only, no echo).
+const CMD_SET_MODE: u8 = 0x06;
+/// "Misc memory/bank/rig control" — carrier for the filter-width subcommand.
+const CMD_CTL_MEM: u8 = 0x1a;
+/// `1A 03` subcommand: current passband (IF) filter width.
+const SUB_FILT_WDTH: u8 = 0x03;
+
fn to_bcd(n: u8) -> u8 {
((n / 10) * 16) + (n % 10)
}
@@ -91,9 +103,175 @@ pub fn encode_read_freq() -> Vec<u8> {
]
}
+/// IC-7300 operating-mode data byte values (for the `0x01`/`0x04`/`0x06`
+/// commands). See "Operating mode" in the CI-V reference guide.
+pub mod icmode {
+ pub const LSB: u8 = 0x00;
+ pub const USB: u8 = 0x01;
+ pub const AM: u8 = 0x02;
+ pub const CW: u8 = 0x03;
+ pub const RTTY: u8 = 0x04;
+ pub const FM: u8 = 0x05;
+ pub const CW_R: u8 = 0x07;
+ pub const RTTY_R: u8 = 0x08;
+}
+
+/// SDR++ radio demodulator IDs. These mirror the `RADIO_IFACE_MODE_*` / `DemodID`
+/// enum in SDR++'s `radio` decoder module (`decoder_modules/radio/src`).
+/// A few constants are unused here but kept for completeness/documentation.
+#[allow(dead_code)]
+pub mod sdrmode {
+ pub const NFM: i32 = 0;
+ pub const WFM: i32 = 1;
+ pub const AM: i32 = 2;
+ pub const DSB: i32 = 3;
+ pub const USB: i32 = 4;
+ pub const CW: i32 = 5;
+ pub const LSB: i32 = 6;
+ pub const RAW: i32 = 7;
+}
+
+/// "Sets the current operating mode" (`0x06`, set-only, no echo).
+pub fn encode_set_mode(mode: u8) -> Vec<u8> {
+ vec![
+ PREAMBLE[0],
+ PREAMBLE[1],
+ RADIO_ADDR,
+ CONTROLLER_ADDR,
+ CMD_SET_MODE,
+ mode,
+ TERMINATOR,
+ ]
+}
+
+/// "Sets the current passband filter width" (`1A 03 <index>`) to one of the
+/// IC-7300's discrete filter steps.
+pub fn encode_set_filter(index: u8) -> Vec<u8> {
+ vec![
+ PREAMBLE[0],
+ PREAMBLE[1],
+ RADIO_ADDR,
+ CONTROLLER_ADDR,
+ CMD_CTL_MEM,
+ SUB_FILT_WDTH,
+ to_bcd(index),
+ TERMINATOR,
+ ]
+}
+
+/// "Reads the current passband filter width" (`1A 03`, no data). The radio
+/// replies with a `1A 03 <index>` frame.
+pub fn encode_read_filter() -> Vec<u8> {
+ vec![
+ PREAMBLE[0],
+ PREAMBLE[1],
+ RADIO_ADDR,
+ CONTROLLER_ADDR,
+ CMD_CTL_MEM,
+ SUB_FILT_WDTH,
+ TERMINATOR,
+ ]
+}
+
+/// Translate an IC-7300 operating-mode byte to an SDR++ demodulator ID.
+///
+/// Best-effort for modes with no exact SDR++ equivalent: `RTTY` → `USB`,
+/// `CW-R` → `CW`, `RTTY-R` → `LSB`, `FM` → `NFM`.
+pub fn icmode_to_sdrmode(mode: u8) -> Option<i32> {
+ use icmode as r;
+ use sdrmode as s;
+ Some(match mode {
+ r::LSB => s::LSB,
+ r::USB => s::USB,
+ r::AM => s::AM,
+ r::CW => s::CW,
+ r::RTTY => s::USB,
+ r::FM => s::NFM,
+ r::CW_R => s::CW,
+ r::RTTY_R => s::LSB,
+ _ => return None,
+ })
+}
+
+/// Translate an SDR++ demodulator ID to an IC-7300 operating-mode byte.
+///
+/// `WFM`/`DSB`/`RAW` have no IC-7300 equivalent and return `None`.
+pub fn sdrmode_to_icmode(mode: i32) -> Option<u8> {
+ use icmode as r;
+ use sdrmode as s;
+ Some(match mode {
+ s::LSB => r::LSB,
+ s::USB => r::USB,
+ s::AM => r::AM,
+ s::CW => r::CW,
+ s::NFM => r::FM,
+ _ => return None,
+ })
+}
+
+/// Whether bandwidth sync applies to a given SDR++ mode. SSB/CW/AM have a
+/// `1A 03` filter width; FM/NFM uses fixed FIL1/2/3 presets instead.
+pub fn bandwidth_applicable(sdr_mode: i32) -> bool {
+ matches!(
+ sdr_mode,
+ sdrmode::AM | sdrmode::USB | sdrmode::LSB | sdrmode::CW
+ )
+}
+
+/// SSB/CW/RTTY filter width in Hz for a filter index (0..=40).
+/// Indices 0..=9 are 50..500 Hz (50 Hz steps); 10..=40 are 600..3600 Hz
+/// (100 Hz steps). See "IF filter width settings" in the CI-V reference guide.
+fn ssb_filter_hz(index: u8) -> Option<u32> {
+ match index {
+ 0..=9 => Some(50 + u32::from(index) * 50),
+ 10..=40 => Some(600 + u32::from(index - 10) * 100),
+ _ => None,
+ }
+}
+
+/// AM filter width in Hz for a filter index (0..=49): 200 Hz..10 kHz in 200 Hz
+/// steps.
+fn am_filter_hz(index: u8) -> Option<u32> {
+ if index <= 49 {
+ Some(200 + u32::from(index) * 200)
+ } else {
+ None
+ }
+}
+
+/// Map a filter index to Hz for the given mode class.
+pub fn filter_index_to_hz(is_am: bool, index: u8) -> Option<u32> {
+ if is_am {
+ am_filter_hz(index)
+ } else {
+ ssb_filter_hz(index)
+ }
+}
+
+/// Map a bandwidth in Hz to the nearest IC-7300 filter index for the given mode
+/// class.
+pub fn hz_to_filter_index(is_am: bool, hz: u32) -> Option<u8> {
+ let max = if is_am { 49u8 } else { 40u8 };
+ let mut best: Option<u8> = None;
+ let mut best_diff = u32::MAX;
+ for i in 0..=max {
+ let h = filter_index_to_hz(is_am, i)?;
+ let d = h.abs_diff(hz);
+ if d < best_diff {
+ best_diff = d;
+ best = Some(i);
+ }
+ }
+ best
+}
+
#[derive(Debug, PartialEq, Eq)]
pub enum CivMsg {
Freq(Freq),
+ /// Operating mode byte (IC-7300 `icmode::*`).
+ Mode(u8),
+ /// Filter-width index (0..=49, BCD-decoded).
+ Filter(u8),
}
#[derive(Debug, PartialEq, Eq)]
@@ -130,6 +308,17 @@ impl TryFrom<&[u8]> for CivMsg {
+ from_bcd(frame[8]) as Freq * 1_000_000;
return Ok(CivMsg::Freq(freq));
}
+ // `0x01` (transceive broadcast) and `0x04` (read reply) carry the
+ // operating mode as the first data byte; an optional FIL1/2/3 filter
+ // byte follows and is ignored.
+ if (frame[4] == CMD_BROADCAST_MODE || frame[4] == CMD_READ_MODE) && frame.len() >= 7 {
+ return Ok(CivMsg::Mode(frame[5]));
+ }
+ // `1A 03 <index>` — current passband filter width (read reply), a
+ // single BCD-encoded filter index.
+ if frame[4] == CMD_CTL_MEM && frame[5] == SUB_FILT_WDTH && frame.len() >= 8 {
+ return Ok(CivMsg::Filter(from_bcd(frame[6])));
+ }
Err(CivMsgError::WrongCommand)
}
}
@@ -269,7 +458,7 @@ mod tests {
0xfe, 0xfe, 0xe1, 0x55, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd,
];
let wrong_cmd = [
- 0xfe, 0xfe, 0xe1, 0x94, 0x04, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd,
+ 0xfe, 0xfe, 0xe1, 0x94, 0x09, 0x00, 0x00, 0x10, 0x07, 0x00, 0xfd,
];
let short = [0xfe, 0xfe, 0xe1, 0x94];
let bad_end = [
@@ -332,4 +521,81 @@ mod tests {
let out = f.push(&rest);
assert_eq!(out, vec![frame.to_vec()]);
}
+
+ #[test]
+ fn encode_set_mode_uses_command_06() {
+ let frame = encode_set_mode(icmode::USB);
+ assert_eq!(frame, [0xfe, 0xfe, 0x94, 0xe1, 0x06, icmode::USB, 0xfd]);
+ }
+
+ #[test]
+ fn encode_set_filter_uses_1a_03() {
+ // index 28 -> BCD 0x28
+ let frame = encode_set_filter(28);
+ assert_eq!(frame, [0xfe, 0xfe, 0x94, 0xe1, 0x1a, 0x03, 0x28, 0xfd]);
+ }
+
+ #[test]
+ fn parse_mode_broadcast() {
+ // Radio transceive mode broadcast: FE FE 00 94 01 <mode> FD
+ let frame = [0xfe, 0xfe, 0x00, 0x94, 0x01, icmode::USB, 0xfd];
+ assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Mode(icmode::USB)));
+ }
+
+ #[test]
+ fn parse_mode_read_reply() {
+ // Reply to 0x04 read: FE FE E1 94 04 <mode> <filter> FD
+ let frame = [0xfe, 0xfe, 0xe1, 0x94, 0x04, icmode::LSB, 0x01, 0xfd];
+ assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Mode(icmode::LSB)));
+ }
+
+ #[test]
+ fn parse_filter_read_reply() {
+ // Reply to 1A 03 read: FE FE E1 94 1A 03 <bcd index> FD
+ let frame = [0xfe, 0xfe, 0xe1, 0x94, 0x1a, 0x03, 0x28, 0xfd];
+ assert_eq!(CivMsg::try_from(&frame[..]), Ok(CivMsg::Filter(28)));
+ }
+
+ #[test]
+ fn mode_mapping_exact_and_best_effort() {
+ assert_eq!(icmode_to_sdrmode(icmode::LSB), Some(sdrmode::LSB));
+ assert_eq!(icmode_to_sdrmode(icmode::USB), Some(sdrmode::USB));
+ assert_eq!(icmode_to_sdrmode(icmode::AM), Some(sdrmode::AM));
+ assert_eq!(icmode_to_sdrmode(icmode::CW), Some(sdrmode::CW));
+ // best effort
+ assert_eq!(icmode_to_sdrmode(icmode::RTTY), Some(sdrmode::USB));
+ assert_eq!(icmode_to_sdrmode(icmode::CW_R), Some(sdrmode::CW));
+ assert_eq!(icmode_to_sdrmode(icmode::RTTY_R), Some(sdrmode::LSB));
+ assert_eq!(icmode_to_sdrmode(icmode::FM), Some(sdrmode::NFM));
+
+ assert_eq!(sdrmode_to_icmode(sdrmode::LSB), Some(icmode::LSB));
+ assert_eq!(sdrmode_to_icmode(sdrmode::USB), Some(icmode::USB));
+ assert_eq!(sdrmode_to_icmode(sdrmode::AM), Some(icmode::AM));
+ assert_eq!(sdrmode_to_icmode(sdrmode::CW), Some(icmode::CW));
+ assert_eq!(sdrmode_to_icmode(sdrmode::NFM), Some(icmode::FM));
+ assert_eq!(sdrmode_to_icmode(sdrmode::WFM), None);
+ assert_eq!(sdrmode_to_icmode(sdrmode::DSB), None);
+ assert_eq!(sdrmode_to_icmode(sdrmode::RAW), None);
+ }
+
+ #[test]
+ fn filter_width_mapping() {
+ // SSB/CW table: index 0 = 50 Hz, 9 = 500 Hz, 10 = 600 Hz, 40 = 3600 Hz.
+ assert_eq!(filter_index_to_hz(false, 0), Some(50));
+ assert_eq!(filter_index_to_hz(false, 9), Some(500));
+ assert_eq!(filter_index_to_hz(false, 10), Some(600));
+ assert_eq!(filter_index_to_hz(false, 40), Some(3600));
+ assert_eq!(filter_index_to_hz(false, 41), None);
+ // AM table: 0 = 200 Hz, 49 = 10000 Hz.
+ assert_eq!(filter_index_to_hz(true, 0), Some(200));
+ assert_eq!(filter_index_to_hz(true, 49), Some(10_000));
+ assert_eq!(filter_index_to_hz(true, 50), None);
+
+ // Nearest-index round trips.
+ assert_eq!(hz_to_filter_index(false, 3600), Some(40));
+ assert_eq!(hz_to_filter_index(false, 250), Some(4)); // 250 Hz
+ assert_eq!(hz_to_filter_index(true, 1000), Some(4)); // 1000 Hz
+ // Slightly off-step snaps to the nearest step (600 vs 700 -> 700).
+ assert_eq!(hz_to_filter_index(false, 670), Some(11));
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
@@ -16,6 +16,10 @@ extern "C" {
fn civ_bridge_tune(freq: f64);
fn civ_bridge_get_vfo_freq() -> f64;
fn civ_bridge_log(level: i32, msg: *const c_char);
+ fn civ_bridge_get_mode() -> i32;
+ fn civ_bridge_set_mode(mode: i32);
+ fn civ_bridge_get_bandwidth() -> f64;
+ fn civ_bridge_set_bandwidth(hz: f64);
}
struct FlogLogger;
@@ -54,6 +58,7 @@ pub struct CivCtx {
name: String,
port: String,
baud: u32,
+ sync_mode: bool,
enabled: bool,
status: Arc<Mutex<String>>,
stop: Arc<AtomicBool>,
@@ -74,6 +79,7 @@ pub unsafe extern "C" fn civ_create(name: *const c_char) -> *mut CivCtx {
name: name.clone(),
port: DEFAULT_PORT.to_string(),
baud: 0,
+ sync_mode: true,
enabled: false,
status: Arc::new(Mutex::new("Disconnected".to_string())),
stop: Arc::new(AtomicBool::new(false)),
@@ -116,11 +122,12 @@ pub unsafe extern "C" fn civ_enable(ctx: *mut CivCtx) {
ctx.stop.store(false, Ordering::Relaxed);
let port = ctx.port.clone();
let baud = ctx.baud;
+ let sync_mode = ctx.sync_mode;
let stop = Arc::clone(&ctx.stop);
let status = Arc::clone(&ctx.status);
debug!("starting sync thread (instance: {})", ctx.name);
ctx.handle = Some(std::thread::spawn(move || {
- sync::run(port, baud, stop, status);
+ sync::run(port, baud, sync_mode, stop, status);
}));
}
@@ -200,3 +207,12 @@ pub unsafe extern "C" fn civ_set_serial_baud(ctx: *mut CivCtx, baud: u32) {
);
(*ctx).baud = baud;
}
+
+#[no_mangle]
+pub unsafe extern "C" fn civ_set_sync_mode(ctx: *mut CivCtx, sync_mode: bool) {
+ if ctx.is_null() {
+ return;
+ }
+ debug!("mode/filter sync set to {}", sync_mode);
+ (*ctx).sync_mode = sync_mode;
+}
diff --git a/src/sync.rs b/src/sync.rs
@@ -8,18 +8,26 @@ use std::time::{Duration, Instant};
use log::{debug, error, trace};
use serialport::{DataBits, Parity, StopBits};
-use crate::civ::{self, CivMsg, Framer, Freq, TOL_HZ};
+use crate::civ::{self, CivMsg, CivMsgError, Framer, Freq, TOL_HZ};
const DEFAULT_BAUD: u32 = 115_200;
const DETECT_TIMEOUT: Duration = Duration::from_millis(300);
const READ_TIMEOUT: Duration = Duration::from_millis(100);
const POLL_INTERVAL: Duration = Duration::from_millis(100);
+/// How often to poll the radio's filter width (radio -> SDR bandwidth).
+const FILTER_POLL_INTERVAL: Duration = Duration::from_millis(500);
/// Candidate rates probed during auto-detection, most common first. Standard
/// Icom CI-V rates (19200/9600/4800) precede the USB-only high rates.
const BAUD_CANDIDATES: [u32; 6] = [19_200, 9_600, 4_800, 115_200, 38_400, 57_600];
-pub fn run(port: String, baud: u32, stop: Arc<AtomicBool>, status: Arc<Mutex<String>>) {
+pub fn run(
+ port: String,
+ baud: u32,
+ sync_mode: bool,
+ stop: Arc<AtomicBool>,
+ status: Arc<Mutex<String>>,
+) {
set_status(&status, "Connecting...");
// 0 means "auto": probe the radio with a read-frequency command at each
@@ -50,12 +58,18 @@ pub fn run(port: String, baud: u32, stop: Arc<AtomicBool>, status: Arc<Mutex<Str
// Adopt the SDR's current frequency as the baseline so we don't force a
// retune in either direction on connect.
- let mut current = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq;
- debug!("initial frequency baseline: {} Hz", current);
+ let mut freq = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq;
+ debug!("initial frequency baseline: {} Hz", freq);
+
+ // Baseline mode (-1 means the SDR VFO is not a "radio" demodulator) and
+ // bandwidth for the mode/filter sync.
+ let mut mode = unsafe { crate::civ_bridge_get_mode() };
+ let mut bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32;
let mut framer = Framer::new();
let mut read_buf = [0u8; 1024];
let mut last_poll = Instant::now();
+ let mut last_filter_poll = Instant::now();
while !stop.load(Ordering::Relaxed) {
// Radio -> SDR: parse incoming CI-V frames.
@@ -65,14 +79,40 @@ pub fn run(port: String, baud: u32, stop: Arc<AtomicBool>, status: Arc<Mutex<Str
trace!("read {} bytes: {:02x?}", n, &read_buf[..n]);
for frame in framer.push(&read_buf[..n]) {
match CivMsg::try_from(frame.as_slice()) {
- Ok(CivMsg::Freq(freq)) => {
- trace!("radio -> SDR: {} Hz", freq);
- if freq_diff(freq, current) > TOL_HZ {
- debug!("tuning SDR to {} Hz", freq);
- unsafe { crate::civ_bridge_tune(freq as f64) };
- current = freq;
+ Ok(CivMsg::Freq(f)) => {
+ trace!("radio -> SDR: {} Hz", f);
+ if freq_diff(f, freq) > TOL_HZ {
+ debug!("tuning SDR to {} Hz", f);
+ unsafe { crate::civ_bridge_tune(f as f64) };
+ freq = f;
+ }
+ }
+ Ok(CivMsg::Mode(m)) if sync_mode => {
+ if let Some(s) = civ::icmode_to_sdrmode(m) {
+ if s != mode {
+ debug!("radio -> SDR mode: {}", s);
+ unsafe { crate::civ_bridge_set_mode(s) };
+ mode = s;
+ bw =
+ unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32;
+ }
+ }
+ }
+ Ok(CivMsg::Filter(i)) if sync_mode => {
+ let is_am = mode == civ::sdrmode::AM;
+ if let Some(hz) = civ::filter_index_to_hz(is_am, i) {
+ if freq_diff(hz, bw) > bw_tolerance(is_am) {
+ debug!("radio -> SDR bandwidth: {} Hz", hz);
+ unsafe { crate::civ_bridge_set_bandwidth(hz as f64) };
+ bw = hz;
+ }
}
}
+ Ok(_) => {}
+ Err(CivMsgError::WrongAddress) => {
+ // Echo of our own command (`to`/`from` = `94 E1`),
+ // e.g. the `1A 03` read poll; expected and noisy.
+ }
Err(e) => debug!("ignored CI-V frame ({:?}): {:02x?}", e, frame),
}
}
@@ -85,10 +125,10 @@ pub fn run(port: String, baud: u32, stop: Arc<AtomicBool>, status: Arc<Mutex<Str
}
}
- // SDR -> Radio: poll the VFO frequency every 100 ms.
+ // SDR -> Radio: poll frequency/mode/bandwidth every 100 ms.
if last_poll.elapsed() >= POLL_INTERVAL {
let sdr = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq;
- if freq_diff(sdr, current) > TOL_HZ {
+ if freq_diff(sdr, freq) > TOL_HZ {
debug!("setting radio to {} Hz", sdr);
let frame = civ::encode_set_freq(sdr);
if let Err(e) = serial.write_all(&frame) {
@@ -96,10 +136,59 @@ pub fn run(port: String, baud: u32, stop: Arc<AtomicBool>, status: Arc<Mutex<Str
error!("serial write failed: {}", e);
break;
}
- current = sdr;
+ freq = sdr;
}
+
+ if sync_mode {
+ let s = unsafe { crate::civ_bridge_get_mode() };
+ if s >= 0 && s != mode {
+ if let Some(m) = civ::sdrmode_to_icmode(s) {
+ debug!("setting radio mode to {:02x}", m);
+ let frame = civ::encode_set_mode(m);
+ if let Err(e) = serial.write_all(&frame) {
+ set_status(&status, &format!("Serial error: {}", e));
+ error!("serial write failed: {}", e);
+ break;
+ }
+ }
+ mode = s;
+ bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32;
+ }
+
+ if civ::bandwidth_applicable(mode) {
+ let sdr_bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32;
+ let is_am = mode == civ::sdrmode::AM;
+ if freq_diff(sdr_bw, bw) > bw_tolerance(is_am) {
+ if let Some(i) = civ::hz_to_filter_index(is_am, sdr_bw) {
+ debug!("setting radio filter to index {}", i);
+ let frame = civ::encode_set_filter(i);
+ if let Err(e) = serial.write_all(&frame) {
+ set_status(&status, &format!("Serial error: {}", e));
+ error!("serial write failed: {}", e);
+ break;
+ }
+ bw = civ::filter_index_to_hz(is_am, i).unwrap_or(sdr_bw);
+ }
+ }
+ }
+ }
+
last_poll = Instant::now();
}
+
+ // Radio -> SDR bandwidth: poll the radio's filter width (no transceive
+ // broadcast for width, so read it explicitly).
+ if sync_mode && last_filter_poll.elapsed() >= FILTER_POLL_INTERVAL {
+ if civ::bandwidth_applicable(mode) {
+ let frame = civ::encode_read_filter();
+ if let Err(e) = serial.write_all(&frame) {
+ set_status(&status, &format!("Serial error: {}", e));
+ error!("serial write failed: {}", e);
+ break;
+ }
+ }
+ last_filter_poll = Instant::now();
+ }
}
set_status(&status, "Disconnected");
@@ -157,6 +246,17 @@ fn freq_diff(a: Freq, b: Freq) -> i64 {
(a as i64 - b as i64).abs()
}
+/// Half-step bandwidth tolerance in Hz. AM steps are 200 Hz, SSB/CW steps are
+/// 50/100 Hz, so this is enough to avoid ping-pong while still tracking real
+/// changes.
+fn bw_tolerance(is_am: bool) -> i64 {
+ if is_am {
+ 100
+ } else {
+ 50
+ }
+}
+
fn set_status(status: &Arc<Mutex<String>>, text: &str) {
if let Ok(mut s) = status.lock() {
*s = text.to_string();