use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::port::{PortDriver, PortDriverBase, PortFlags};
use crate::user::AsynUser;
use std::time::Duration;
pub const FLAG_RECOVER_WITH_IFC: i32 = 0x1;
pub const FLAG_LOCK_DEVICES: i32 = 0x2;
pub const FLAG_NO_SRQ: i32 = 0x4;
pub const DEFAULT_RPC_TIMEOUT_SECS: u64 = 4;
pub const DEVICE_CORE_PROG: u32 = 0x0006_07AF;
pub const DEVICE_CORE_VERS: u32 = 1;
pub const DEVICE_ASYNC_PROG: u32 = 0x0006_07B0;
pub const DEVICE_ASYNC_VERS: u32 = 1;
pub const DEVICE_INTR_PROG: u32 = 0x0006_07B1;
pub const DEVICE_INTR_VERS: u32 = 1;
pub const PROC_CREATE_LINK: u32 = 10;
pub const PROC_DEVICE_WRITE: u32 = 11;
pub const PROC_DEVICE_READ: u32 = 12;
pub const PROC_DEVICE_READSTB: u32 = 13;
pub const PROC_DEVICE_TRIGGER: u32 = 14;
pub const PROC_DEVICE_CLEAR: u32 = 15;
pub const PROC_DEVICE_REMOTE: u32 = 16;
pub const PROC_DEVICE_LOCAL: u32 = 17;
pub const PROC_DEVICE_LOCK: u32 = 18;
pub const PROC_DEVICE_UNLOCK: u32 = 19;
pub const PROC_DEVICE_ENABLE_SRQ: u32 = 20;
pub const PROC_DEVICE_DOCMD: u32 = 22;
pub const PROC_DESTROY_LINK: u32 = 23;
pub const PROC_CREATE_INTR_CHAN: u32 = 25;
pub const PROC_DESTROY_INTR_CHAN: u32 = 26;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VxiLinkKind {
Single,
Gpib,
Other,
}
impl VxiLinkKind {
pub fn from_vxi_name(name: &str) -> Self {
let lc = name.to_ascii_lowercase();
if lc.starts_with("gpib") || lc.starts_with("hpib") {
VxiLinkKind::Gpib
} else if lc.starts_with("inst") || lc.starts_with("com") {
VxiLinkKind::Single
} else {
VxiLinkKind::Other
}
}
}
#[derive(Debug, Clone)]
pub struct Vxi11Config {
pub host_name: String,
pub flags: i32,
pub default_timeout: Duration,
pub vxi_name: String,
pub priority: u32,
pub link_kind: VxiLinkKind,
}
impl Vxi11Config {
pub fn from_positional(
host_name: &str,
flags: i32,
def_timeout_string: &str,
vxi_name: &str,
priority: i32,
) -> Self {
let parsed: f64 = def_timeout_string.trim().parse().unwrap_or(0.0);
let default_timeout = if parsed > 0.0001 {
Duration::from_secs_f64(parsed)
} else {
Duration::from_secs(DEFAULT_RPC_TIMEOUT_SECS)
};
Self {
host_name: host_name.to_string(),
flags,
default_timeout,
vxi_name: vxi_name.to_string(),
priority: priority.max(0) as u32,
link_kind: VxiLinkKind::from_vxi_name(vxi_name),
}
}
pub fn recover_with_ifc(&self) -> bool {
(self.flags & FLAG_RECOVER_WITH_IFC) != 0
}
pub fn lock_devices(&self) -> bool {
(self.flags & FLAG_LOCK_DEVICES) != 0
}
pub fn has_srq(&self) -> bool {
(self.flags & FLAG_NO_SRQ) == 0
}
}
pub struct DrvVxi11Port {
base: PortDriverBase,
config: Vxi11Config,
}
impl DrvVxi11Port {
#[allow(clippy::too_many_arguments)] pub fn configure(
port_name: &str,
host_name: &str,
flags: i32,
def_timeout_string: &str,
vxi_name: &str,
priority: i32,
no_auto_connect: bool,
) -> AsynResult<Self> {
let config =
Vxi11Config::from_positional(host_name, flags, def_timeout_string, vxi_name, priority);
let multi_device = matches!(config.link_kind, VxiLinkKind::Gpib);
let max_addr = if multi_device { 31 } else { 1 };
let mut base = PortDriverBase::new(
port_name,
max_addr,
PortFlags {
multi_device,
can_block: true,
destructible: true,
},
);
base.connected = false;
base.auto_connect = !no_auto_connect;
Ok(Self { base, config })
}
pub fn config(&self) -> &Vxi11Config {
&self.config
}
pub fn has_hw_support() -> bool {
cfg!(feature = "vxi11")
}
}
impl PortDriver for DrvVxi11Port {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
if !Self::has_hw_support() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"VXI-11 driver scaffold: hardware feature 'vxi11' not enabled \
in this build. Config parsed (host={:?}, vxiName={:?}, \
link_kind={:?}, flags=0x{:X}, defTimeout={:?}) — rebuild \
with `--features asyn-rs/vxi11` to enable.",
self.config.host_name,
self.config.vxi_name,
self.config.link_kind,
self.config.flags,
self.config.default_timeout,
),
});
}
Err(AsynError::Status {
status: AsynStatus::Error,
message: "VXI-11 hardware path not yet implemented".into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn link_kind_recognises_gpib_hpib_inst_com() {
assert_eq!(VxiLinkKind::from_vxi_name("gpib0"), VxiLinkKind::Gpib);
assert_eq!(VxiLinkKind::from_vxi_name("GPIB1"), VxiLinkKind::Gpib);
assert_eq!(VxiLinkKind::from_vxi_name("hpib0"), VxiLinkKind::Gpib);
assert_eq!(VxiLinkKind::from_vxi_name("HPIB7"), VxiLinkKind::Gpib);
assert_eq!(VxiLinkKind::from_vxi_name("inst0"), VxiLinkKind::Single);
assert_eq!(VxiLinkKind::from_vxi_name("INST7"), VxiLinkKind::Single);
assert_eq!(VxiLinkKind::from_vxi_name("com1"), VxiLinkKind::Single);
assert_eq!(VxiLinkKind::from_vxi_name("COM2"), VxiLinkKind::Single);
assert_eq!(VxiLinkKind::from_vxi_name("foo"), VxiLinkKind::Other);
}
#[test]
fn flag_bits_decode() {
let cfg = Vxi11Config::from_positional("h", 0, "", "inst0", 0);
assert!(!cfg.recover_with_ifc());
assert!(!cfg.lock_devices());
assert!(cfg.has_srq());
let cfg = Vxi11Config::from_positional("h", 0x7, "", "inst0", 0);
assert!(cfg.recover_with_ifc());
assert!(cfg.lock_devices());
assert!(!cfg.has_srq());
}
#[test]
fn default_timeout_falls_back_when_unparseable() {
let cfg = Vxi11Config::from_positional("h", 0, "", "inst0", 0);
assert_eq!(cfg.default_timeout, Duration::from_secs(4));
let cfg = Vxi11Config::from_positional("h", 0, "garbage", "inst0", 0);
assert_eq!(cfg.default_timeout, Duration::from_secs(4));
let cfg = Vxi11Config::from_positional("h", 0, "0.00005", "inst0", 0);
assert_eq!(cfg.default_timeout, Duration::from_secs(4));
}
#[test]
fn default_timeout_honours_user_value() {
let cfg = Vxi11Config::from_positional("h", 0, "1.5", "inst0", 0);
assert!((cfg.default_timeout.as_secs_f64() - 1.5).abs() < 1e-9);
let cfg = Vxi11Config::from_positional("h", 0, "10", "inst0", 0);
assert_eq!(cfg.default_timeout, Duration::from_secs(10));
}
#[test]
fn gateway_link_is_multi_device() {
let drv = DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "gpib0", 0, false).unwrap();
assert!(drv.base().flags.multi_device);
assert_eq!(drv.base().max_addr, 31);
}
#[test]
fn single_link_is_not_multi_device() {
let drv = DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "inst0", 0, false).unwrap();
assert!(!drv.base().flags.multi_device);
assert_eq!(drv.base().max_addr, 1);
}
#[test]
fn unrecognized_link_is_not_multi_device() {
let drv = DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "foo0", 0, false).unwrap();
assert_eq!(VxiLinkKind::from_vxi_name("foo0"), VxiLinkKind::Other);
assert!(!drv.base().flags.multi_device);
assert_eq!(drv.base().max_addr, 1);
}
#[test]
fn no_auto_connect_disables_framework_auto() {
let drv = DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "inst0", 0, true).unwrap();
assert!(!drv.base().auto_connect);
let drv = DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "inst0", 0, false).unwrap();
assert!(drv.base().auto_connect);
}
#[test]
fn rpc_program_numbers_match_vxi11_spec() {
assert_eq!(DEVICE_CORE_PROG, 0x0006_07AF);
assert_eq!(DEVICE_CORE_VERS, 1);
assert_eq!(DEVICE_ASYNC_PROG, 0x0006_07B0);
assert_eq!(DEVICE_INTR_PROG, 0x0006_07B1);
}
#[test]
fn rpc_procedure_numbers_match_vxi11_rpcl() {
assert_eq!(PROC_CREATE_LINK, 10);
assert_eq!(PROC_DEVICE_WRITE, 11);
assert_eq!(PROC_DEVICE_READ, 12);
assert_eq!(PROC_DEVICE_CLEAR, 15);
assert_eq!(PROC_DESTROY_LINK, 23);
}
#[cfg(not(feature = "vxi11"))]
#[test]
fn connect_without_hw_feature_reports_error() {
let mut drv =
DrvVxi11Port::configure("vxi0", "10.0.0.1", 0, "", "inst0", 0, false).unwrap();
let err = drv.connect(&AsynUser::default()).unwrap_err();
match err {
AsynError::Status { message, .. } => {
assert!(message.contains("vxi11"), "must mention feature: {message}");
}
_ => panic!("expected Status error"),
}
}
#[test]
fn has_hw_support_matches_feature_flag() {
assert_eq!(DrvVxi11Port::has_hw_support(), cfg!(feature = "vxi11"));
}
}