Skip to main content

native/hamlib_nif/src/lib.rs

//! Rustler NIF wrapping the Hamlib C API via the `hlx_*` C shim.
//!
//! The BEAM owns the rig handle through a `ResourceArc<RigHandle>`; when the
//! Elixir term is garbage-collected, `Drop` calls `hlx_cleanup`, so a rig is
//! always freed even if the owning process crashes without closing it.
//!
//! All Hamlib operations that touch the radio (`open`, `set_freq`, `set_mode`,
//! `set_ptt`, …) can block on serial I/O, so their NIFs run on a **dirty IO**
//! scheduler — never stall a normal BEAM scheduler waiting on a UART.
//!
//! Errors come back as `{:error, {code, "reason"}}` where `code` is the Hamlib
//! integer error and `"reason"` is `hlx_strerror`'s text.
//!
//! ## Rustler 0.37 conventions (mirrors phy_modem / milwave)
//!
//! - `#[rustler::nif]` auto-registers each NIF via the `inventory` crate.
//! - `#[rustler::resource_impl]` on `impl Resource for RigHandle {}`
//!   auto-registers the resource type at load — no resource list.
//! - `rustler::init!("Elixir.Hamlib.Nif", load = load)` — the only reason we
//!   keep a `load` callback is to silence Hamlib's stdout debug firehose;
//!   resource registration does not need it.

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]

mod ffi {
    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}

use std::ffi::{CStr, CString};
use std::sync::Mutex;

use rustler::{Atom, Encoder, Env, Resource, ResourceArc, Term};

mod atoms {
    rustler::atoms! {
        ok,
        error,
        nil,
        invalid_handle,
        bad_string,
    }
}

/// BEAM-owned rig handle. The inner `hlx_handle` is an opaque pointer-sized
/// integer from the shim. A `Mutex` serializes access: Hamlib's per-rig state
/// is not safe to drive from two schedulers at once, and a single rig is a
/// single serial line anyway. (The mutex also satisfies `ResourceArc`'s
/// `Sync` requirement.)
struct RigHandle {
    inner: Mutex<ffi::hlx_handle>,
}

#[rustler::resource_impl]
impl Resource for RigHandle {}

unsafe impl Send for RigHandle {}
unsafe impl Sync for RigHandle {}

impl Drop for RigHandle {
    fn drop(&mut self) {
        if let Ok(h) = self.inner.lock() {
            if *h != 0 {
                unsafe {
                    // Best-effort: close then cleanup. cleanup frees the RIG*.
                    ffi::hlx_close(*h);
                    ffi::hlx_cleanup(*h);
                }
            }
        }
    }
}

/// Translate a Hamlib return code into either `:ok` (or another ok term) or
/// `{:error, {code, msg}}`.
fn check<'a>(env: Env<'a>, code: i32, ok_term: Term<'a>) -> Term<'a> {
    if code == 0 {
        ok_term
    } else {
        let msg = unsafe {
            let p = ffi::hlx_strerror(code);
            if p.is_null() {
                "unknown".to_string()
            } else {
                CStr::from_ptr(p).to_string_lossy().into_owned()
            }
        };
        (atoms::error(), (code, msg)).encode(env)
    }
}

fn cstring(s: &str) -> Result<CString, Atom> {
    CString::new(s).map_err(|_| atoms::bad_string())
}

/// Initialize a rig for the given Hamlib model number. Returns
/// `{:ok, handle}` or `{:error, :invalid_handle}`. Does not open the port.
#[rustler::nif]
fn init(env: Env, model: i32) -> Term {
    let h = unsafe { ffi::hlx_init(model) };
    if h == 0 {
        (atoms::error(), atoms::invalid_handle()).encode(env)
    } else {
        let res = ResourceArc::new(RigHandle {
            inner: Mutex::new(h),
        });
        (atoms::ok(), res).encode(env)
    }
}

/// Set a Hamlib config token (e.g. "rig_pathname", "serial_speed", "ptt_type").
#[rustler::nif(schedule = "DirtyIo")]
fn set_conf(env: Env, handle: ResourceArc<RigHandle>, token: String, value: String) -> Term {
    let tok = match cstring(&token) {
        Ok(c) => c,
        Err(a) => return (atoms::error(), a).encode(env),
    };
    let val = match cstring(&value) {
        Ok(c) => c,
        Err(a) => return (atoms::error(), a).encode(env),
    };

    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_set_conf(h, tok.as_ptr(), val.as_ptr()) };
    check(env, rc, atoms::ok().encode(env))
}

/// Open the configured rig port.
#[rustler::nif(schedule = "DirtyIo")]
fn open(env: Env, handle: ResourceArc<RigHandle>) -> Term {
    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_open(h) };
    check(env, rc, atoms::ok().encode(env))
}

/// Close the rig port (rig remains initialized; can be reopened).
#[rustler::nif(schedule = "DirtyIo")]
fn close(env: Env, handle: ResourceArc<RigHandle>) -> Term {
    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_close(h) };
    check(env, rc, atoms::ok().encode(env))
}

/// Set frequency in Hz on the current VFO.
#[rustler::nif(schedule = "DirtyIo")]
fn set_freq(env: Env, handle: ResourceArc<RigHandle>, freq_hz: f64) -> Term {
    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_set_freq(h, freq_hz) };
    check(env, rc, atoms::ok().encode(env))
}

/// Get frequency in Hz from the current VFO. `{:ok, hz}` on success.
#[rustler::nif(schedule = "DirtyIo")]
fn get_freq(env: Env, handle: ResourceArc<RigHandle>) -> Term {
    let h = *handle.inner.lock().unwrap();
    let mut hz: f64 = 0.0;
    let rc = unsafe { ffi::hlx_get_freq(h, &mut hz as *mut f64) };
    check(env, rc, (atoms::ok(), hz).encode(env))
}

/// Set mode by string ("USB","PKTUSB",…) + passband Hz (0 = rig normal width).
#[rustler::nif(schedule = "DirtyIo")]
fn set_mode(env: Env, handle: ResourceArc<RigHandle>, mode: String, passband_hz: i64) -> Term {
    let m = match cstring(&mode) {
        Ok(c) => c,
        Err(a) => return (atoms::error(), a).encode(env),
    };
    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_set_mode(h, m.as_ptr(), passband_hz as std::os::raw::c_long) };
    check(env, rc, atoms::ok().encode(env))
}

/// Get current mode + passband. `{:ok, {mode_string, passband_hz}}`.
#[rustler::nif(schedule = "DirtyIo")]
fn get_mode(env: Env, handle: ResourceArc<RigHandle>) -> Term {
    let h = *handle.inner.lock().unwrap();
    let mut buf = [0u8; 32];
    let mut pb: std::os::raw::c_long = 0;
    let rc = unsafe {
        ffi::hlx_get_mode(
            h,
            buf.as_mut_ptr() as *mut std::os::raw::c_char,
            buf.len() as i32,
            &mut pb as *mut std::os::raw::c_long,
        )
    };
    if rc != 0 {
        return check(env, rc, atoms::ok().encode(env));
    }
    let mode = unsafe { CStr::from_ptr(buf.as_ptr() as *const std::os::raw::c_char) }
        .to_string_lossy()
        .into_owned();
    (atoms::ok(), (mode, pb as i64)).encode(env)
}

/// Set PTT: `true` = transmit, `false` = receive.
#[rustler::nif(schedule = "DirtyIo")]
fn set_ptt(env: Env, handle: ResourceArc<RigHandle>, on: bool) -> Term {
    let h = *handle.inner.lock().unwrap();
    let rc = unsafe { ffi::hlx_set_ptt(h, if on { 1 } else { 0 }) };
    check(env, rc, atoms::ok().encode(env))
}

/// Get PTT state. `{:ok, true|false}`.
#[rustler::nif(schedule = "DirtyIo")]
fn get_ptt(env: Env, handle: ResourceArc<RigHandle>) -> Term {
    let h = *handle.inner.lock().unwrap();
    let mut on: i32 = 0;
    let rc = unsafe { ffi::hlx_get_ptt(h, &mut on as *mut i32) };
    check(env, rc, (atoms::ok(), on != 0).encode(env))
}

/// Set Hamlib's global debug level (0 = silent … 5 = trace).
#[rustler::nif]
fn set_debug(level: i32) -> Atom {
    unsafe { ffi::hlx_set_debug(level) };
    atoms::ok()
}

/// Hamlib library version string.
#[rustler::nif]
fn version() -> String {
    unsafe {
        let p = ffi::hlx_version();
        if p.is_null() {
            String::new()
        } else {
            CStr::from_ptr(p).to_string_lossy().into_owned()
        }
    }
}

fn load(_env: Env, _info: Term) -> bool {
    // Silence Hamlib's stdout debug firehose by default; callers can raise it
    // via Hamlib.Nif.set_debug/1. Resource registration is handled at
    // compile time by #[rustler::resource_impl], so it needs nothing here.
    unsafe { ffi::hlx_set_debug(0) };
    true
}

rustler::init!("Elixir.Hamlib.Nif", load = load);