vfo-sync

Bidirectional VFO synchronization for IC-7300 and SDR++
git clone https://git.bradleygannon.com/vfo-sync.git
Log | Files | Refs

commit 9f1349db9ad1667979f169b85f98f9c537e10215
parent c691339ca12a19dc604f0322bea9a0704bdc34cc
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Tue,  8 Sep 2026 14:59:01 -0400

Improve device selection

Diffstat:
Mshim/src/main.cpp | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/lib.rs | 31+++++++++++++++++++++++++++++++
Msrc/sync.rs | 36++++++++++++++++++++++++++++++++++--
3 files changed, 119 insertions(+), 5 deletions(-)

diff --git a/shim/src/main.cpp b/shim/src/main.cpp @@ -9,8 +9,10 @@ #include <cstring> #include <cstdint> +#include <algorithm> #include <mutex> #include <string> +#include <vector> extern "C" { void* civ_create(const char* name); @@ -23,6 +25,7 @@ extern "C" { 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); + size_t civ_list_serial_ports(char* out, size_t out_len); } SDRPP_MOD_INFO{ @@ -108,7 +111,18 @@ class CivInstance : public ModuleManager::Instance { public: CivInstance(std::string name) { this->name = name; - strncpy(portBuf, "/dev/ttyUSB1", sizeof(portBuf) - 1); + // Default to a detected port (preferring ttyUSB*) rather than a + // hardcoded device that may not exist on this system. + strncpy(portBuf, "/dev/ttyUSB0", sizeof(portBuf) - 1); + refreshPorts(); + if (!ports.empty()) { + auto it = std::find_if(ports.begin(), ports.end(), [](const std::string& p) { + return p.find("ttyUSB") != std::string::npos; + }); + if (it == ports.end()) { it = ports.begin(); } + strncpy(portBuf, it->c_str(), sizeof(portBuf) - 1); + portBuf[sizeof(portBuf) - 1] = 0; + } rustCtx = civ_create(name.c_str()); civ_set_serial_port(rustCtx, portBuf); civ_set_serial_baud(rustCtx, baud); @@ -126,6 +140,18 @@ public: bool isEnabled() override { return civ_is_enabled(rustCtx); } private: + void refreshPorts() { + ports.clear(); + char buf[4096] = {0}; + size_t n = civ_list_serial_ports(buf, sizeof(buf)); + const char* p = buf; + for (size_t i = 0; i < n; i++) { + std::string name(p); + if (!name.empty()) { ports.push_back(name); } + p += name.size() + 1; + } + } + static void menuHandler(void* ctx) { CivInstance* _this = (CivInstance*)ctx; @@ -133,8 +159,31 @@ private: civ_status(_this->rustCtx, statusBuf, sizeof(statusBuf)); ImGui::TextUnformatted(statusBuf); - if (ImGui::InputText("Serial device", _this->portBuf, sizeof(_this->portBuf))) { - civ_set_serial_port(_this->rustCtx, _this->portBuf); + std::string portPreview = _this->customPortMode + ? "Custom (" + std::string(_this->portBuf) + ")" + : std::string(_this->portBuf); + + if (ImGui::BeginCombo("Serial device", portPreview.c_str())) { + _this->refreshPorts(); + for (const auto& p : _this->ports) { + bool selected = !_this->customPortMode && p == _this->portBuf; + if (ImGui::Selectable(p.c_str(), selected)) { + strncpy(_this->portBuf, p.c_str(), sizeof(_this->portBuf) - 1); + _this->portBuf[sizeof(_this->portBuf) - 1] = 0; + _this->customPortMode = false; + civ_set_serial_port(_this->rustCtx, _this->portBuf); + } + } + if (ImGui::Selectable("Custom...", _this->customPortMode)) { + _this->customPortMode = true; + } + ImGui::EndCombo(); + } + + if (_this->customPortMode) { + if (ImGui::InputText("Custom device", _this->portBuf, sizeof(_this->portBuf))) { + civ_set_serial_port(_this->rustCtx, _this->portBuf); + } } static const uint32_t baudRates[] = { 0, 4800, 9600, 19200, 38400, 57600, 115200 }; @@ -200,6 +249,8 @@ private: std::string name; void* rustCtx = nullptr; char portBuf[256] = {0}; + std::vector<std::string> ports; + bool customPortMode = false; uint32_t baud = 0; bool customBaudMode = false; int customBaud = 115200; diff --git a/src/lib.rs b/src/lib.rs @@ -216,3 +216,34 @@ pub unsafe extern "C" fn civ_set_sync_mode(ctx: *mut CivCtx, sync_mode: bool) { debug!("mode/filter sync set to {}", sync_mode); (*ctx).sync_mode = sync_mode; } + +/// Fill `out` (up to `out_len` bytes) with NUL-separated serial port names +/// (double-NUL terminated) from `serialport::available_ports()`, returning the +/// number of names written. Used by the shim to build the device dropdown. +#[no_mangle] +pub unsafe extern "C" fn civ_list_serial_ports(out: *mut c_char, out_len: usize) -> usize { + if out.is_null() || out_len < 2 { + return 0; + } + let ports = serialport::available_ports().unwrap_or_default(); + let buf = std::slice::from_raw_parts_mut(out as *mut u8, out_len); + let mut pos = 0usize; + let mut count = 0usize; + for p in &ports { + if p.port_name.is_empty() { + continue; + } + let name = p.port_name.as_bytes(); + // Need room for the name, its NUL, and the final double-NUL byte. + if pos + name.len() + 2 > out_len { + break; + } + buf[pos..pos + name.len()].copy_from_slice(name); + pos += name.len(); + buf[pos] = 0; + pos += 1; + count += 1; + } + buf[pos] = 0; // double-NUL terminator + count +} diff --git a/src/sync.rs b/src/sync.rs @@ -56,8 +56,7 @@ pub fn run( return; } - // Adopt the SDR's current frequency as the baseline so we don't force a - // retune in either direction on connect. + // Adopt the SDR's current state as the baseline. let mut freq = unsafe { crate::civ_bridge_get_vfo_freq() }.round() as Freq; debug!("initial frequency baseline: {} Hz", freq); @@ -66,6 +65,39 @@ pub fn run( let mut mode = unsafe { crate::civ_bridge_get_mode() }; let mut bw = unsafe { crate::civ_bridge_get_bandwidth() }.round() as u32; + // On connect, the SDR++ VFO is authoritative: push its current frequency, + // mode, and filter width to the radio once so the two start in agreement. + let frame = civ::encode_set_freq(freq); + if let Err(e) = serial.write_all(&frame) { + set_status(&status, &format!("Serial error: {}", e)); + error!("initial frequency sync failed: {}", e); + return; + } + if sync_mode && mode >= 0 { + if let Some(m) = civ::sdrmode_to_icmode(mode) { + if let Err(e) = serial.write_all(&civ::encode_set_mode(m)) { + set_status(&status, &format!("Serial error: {}", e)); + error!("initial mode sync failed: {}", e); + return; + } + } + if civ::bandwidth_applicable(mode) { + let is_am = mode == civ::sdrmode::AM; + if let Some(i) = civ::hz_to_filter_index(is_am, bw) { + if let Err(e) = serial.write_all(&civ::encode_set_filter(i)) { + set_status(&status, &format!("Serial error: {}", e)); + error!("initial filter sync failed: {}", e); + return; + } + bw = civ::filter_index_to_hz(is_am, i).unwrap_or(bw); + } + } + } + debug!( + "pushed initial state to radio: {} Hz, mode {}, bw {} Hz", + freq, mode, bw + ); + let mut framer = Framer::new(); let mut read_buf = [0u8; 1024]; let mut last_poll = Instant::now();