vfo-sync

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

lib.rs (6565B)


      1 #![allow(clippy::missing_safety_doc)]
      2 
      3 mod civ;
      4 mod sync;
      5 
      6 use std::ffi::{c_char, CStr};
      7 use std::sync::atomic::{AtomicBool, Ordering};
      8 use std::sync::{Arc, Mutex, Once};
      9 use std::thread::JoinHandle;
     10 
     11 use log::{debug, trace, Level, Log, Metadata, Record};
     12 
     13 const DEFAULT_PORT: &str = "/dev/ttyUSB1";
     14 
     15 extern "C" {
     16     fn civ_bridge_tune(freq: f64);
     17     fn civ_bridge_get_vfo_freq() -> f64;
     18     fn civ_bridge_log(level: i32, msg: *const c_char);
     19     fn civ_bridge_get_mode() -> i32;
     20     fn civ_bridge_set_mode(mode: i32);
     21     fn civ_bridge_get_bandwidth() -> f64;
     22     fn civ_bridge_set_bandwidth(hz: f64);
     23 }
     24 
     25 struct FlogLogger;
     26 
     27 impl Log for FlogLogger {
     28     fn enabled(&self, _metadata: &Metadata) -> bool {
     29         true
     30     }
     31 
     32     fn log(&self, record: &Record) {
     33         let level = match record.level() {
     34             Level::Error => 3,
     35             Level::Warn => 2,
     36             Level::Info => 1,
     37             Level::Debug | Level::Trace => 0,
     38         };
     39         let msg = std::ffi::CString::new(format!("[civ_sync] {}", record.args()))
     40             .unwrap_or_else(|_| std::ffi::CString::new("[civ_sync] <invalid utf8>").unwrap());
     41         unsafe { civ_bridge_log(level, msg.as_ptr()) };
     42     }
     43 
     44     fn flush(&self) {}
     45 }
     46 
     47 static LOGGER: FlogLogger = FlogLogger;
     48 static INIT: Once = Once::new();
     49 
     50 fn init_logger() {
     51     INIT.call_once(|| {
     52         log::set_logger(&LOGGER).ok();
     53         log::set_max_level(log::LevelFilter::Info);
     54     });
     55 }
     56 
     57 pub struct CivCtx {
     58     name: String,
     59     port: String,
     60     baud: u32,
     61     sync_mode: Arc<AtomicBool>,
     62     enabled: bool,
     63     status: Arc<Mutex<String>>,
     64     stop: Arc<AtomicBool>,
     65     handle: Option<JoinHandle<()>>,
     66 }
     67 
     68 #[no_mangle]
     69 pub unsafe extern "C" fn civ_create(name: *const c_char) -> *mut CivCtx {
     70     init_logger();
     71     let name = if name.is_null() {
     72         "<unnamed>".to_string()
     73     } else {
     74         unsafe { CStr::from_ptr(name) }
     75             .to_string_lossy()
     76             .into_owned()
     77     };
     78     let ctx = CivCtx {
     79         name: name.clone(),
     80         port: DEFAULT_PORT.to_string(),
     81         baud: 0,
     82         sync_mode: Arc::new(AtomicBool::new(true)),
     83         enabled: false,
     84         status: Arc::new(Mutex::new("Disconnected".to_string())),
     85         stop: Arc::new(AtomicBool::new(false)),
     86         handle: None,
     87     };
     88     debug!("Rust core created (instance: {})", name);
     89     Box::into_raw(Box::new(ctx))
     90 }
     91 
     92 #[no_mangle]
     93 pub unsafe extern "C" fn civ_destroy(ctx: *mut CivCtx) {
     94     if ctx.is_null() {
     95         return;
     96     }
     97     let mut ctx = Box::from_raw(ctx);
     98     if ctx.enabled {
     99         stop_thread(&mut ctx);
    100     }
    101     debug!("Rust core destroyed (instance: {})", ctx.name);
    102 }
    103 
    104 #[no_mangle]
    105 pub unsafe extern "C" fn civ_post_init(ctx: *mut CivCtx) {
    106     if ctx.is_null() {
    107         return;
    108     }
    109     trace!("Rust core post-init (instance: {})", (*ctx).name);
    110 }
    111 
    112 #[no_mangle]
    113 pub unsafe extern "C" fn civ_enable(ctx: *mut CivCtx) {
    114     if ctx.is_null() {
    115         return;
    116     }
    117     let ctx = &mut *ctx;
    118     if ctx.enabled {
    119         return;
    120     }
    121     ctx.enabled = true;
    122     ctx.stop.store(false, Ordering::Relaxed);
    123     let port = ctx.port.clone();
    124     let baud = ctx.baud;
    125     let sync_mode = Arc::clone(&ctx.sync_mode);
    126     let stop = Arc::clone(&ctx.stop);
    127     let status = Arc::clone(&ctx.status);
    128     debug!("starting sync thread (instance: {})", ctx.name);
    129     ctx.handle = Some(std::thread::spawn(move || {
    130         sync::run(port, baud, sync_mode, stop, status);
    131     }));
    132 }
    133 
    134 #[no_mangle]
    135 pub unsafe extern "C" fn civ_disable(ctx: *mut CivCtx) {
    136     if ctx.is_null() {
    137         return;
    138     }
    139     let ctx = &mut *ctx;
    140     if ctx.enabled {
    141         stop_thread(ctx);
    142     }
    143 }
    144 
    145 fn stop_thread(ctx: &mut CivCtx) {
    146     ctx.enabled = false;
    147     ctx.stop.store(true, Ordering::Relaxed);
    148     if let Some(handle) = ctx.handle.take() {
    149         let _ = handle.join();
    150     }
    151     if let Ok(mut s) = ctx.status.lock() {
    152         *s = "Disconnected".to_string();
    153     }
    154 }
    155 
    156 #[no_mangle]
    157 pub unsafe extern "C" fn civ_is_enabled(ctx: *mut CivCtx) -> bool {
    158     if ctx.is_null() {
    159         return false;
    160     }
    161     (*ctx).enabled
    162 }
    163 
    164 #[no_mangle]
    165 pub unsafe extern "C" fn civ_status(ctx: *mut CivCtx, out: *mut c_char, out_len: usize) {
    166     if ctx.is_null() || out.is_null() || out_len == 0 {
    167         return;
    168     }
    169     let text = {
    170         let s = (*ctx).status.lock().unwrap();
    171         s.clone()
    172     };
    173     let bytes = text.as_bytes();
    174     let n = bytes.len().min(out_len - 1);
    175     std::ptr::copy_nonoverlapping(bytes.as_ptr(), out as *mut u8, n);
    176     *out.add(n) = 0;
    177 }
    178 
    179 #[no_mangle]
    180 pub unsafe extern "C" fn civ_set_serial_port(ctx: *mut CivCtx, port: *const c_char) {
    181     if ctx.is_null() {
    182         return;
    183     }
    184     let port = if port.is_null() {
    185         DEFAULT_PORT.to_string()
    186     } else {
    187         unsafe { CStr::from_ptr(port) }
    188             .to_string_lossy()
    189             .into_owned()
    190     };
    191     debug!("serial port set to {}", port);
    192     (*ctx).port = port;
    193 }
    194 
    195 #[no_mangle]
    196 pub unsafe extern "C" fn civ_set_serial_baud(ctx: *mut CivCtx, baud: u32) {
    197     if ctx.is_null() {
    198         return;
    199     }
    200     debug!(
    201         "serial baud set to {}",
    202         if baud == 0 {
    203             "auto".to_string()
    204         } else {
    205             baud.to_string()
    206         }
    207     );
    208     (*ctx).baud = baud;
    209 }
    210 
    211 #[no_mangle]
    212 pub unsafe extern "C" fn civ_set_sync_mode(ctx: *mut CivCtx, sync_mode: bool) {
    213     if ctx.is_null() {
    214         return;
    215     }
    216     debug!("mode/filter sync set to {}", sync_mode);
    217     (*ctx).sync_mode.store(sync_mode, Ordering::Relaxed);
    218 }
    219 
    220 /// Fill `out` (up to `out_len` bytes) with NUL-separated serial port names
    221 /// (double-NUL terminated) from `serialport::available_ports()`, returning the
    222 /// number of names written. Used by the shim to build the device dropdown.
    223 #[no_mangle]
    224 pub unsafe extern "C" fn civ_list_serial_ports(out: *mut c_char, out_len: usize) -> usize {
    225     if out.is_null() || out_len < 2 {
    226         return 0;
    227     }
    228     let ports = serialport::available_ports().unwrap_or_default();
    229     let buf = std::slice::from_raw_parts_mut(out as *mut u8, out_len);
    230     let mut pos = 0usize;
    231     let mut count = 0usize;
    232     for p in &ports {
    233         if p.port_name.is_empty() {
    234             continue;
    235         }
    236         let name = p.port_name.as_bytes();
    237         // Need room for the name, its NUL, and the final double-NUL byte.
    238         if pos + name.len() + 2 > out_len {
    239             break;
    240         }
    241         buf[pos..pos + name.len()].copy_from_slice(name);
    242         pos += name.len();
    243         buf[pos] = 0;
    244         pos += 1;
    245         count += 1;
    246     }
    247     buf[pos] = 0; // double-NUL terminator
    248     count
    249 }