commit 9f1349db9ad1667979f169b85f98f9c537e10215
parent c691339ca12a19dc604f0322bea9a0704bdc34cc
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Tue, 8 Sep 2026 14:59:01 -0400
Improve device selection
Diffstat:
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();