use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::port::{PortDriver, PortDriverBase, PortFlags};
use crate::user::AsynUser;
pub const USBTMC_INTERFACE_CLASS: u8 = 0xFE;
pub const USBTMC_INTERFACE_SUBCLASS: u8 = 0x03;
pub const MESSAGE_ID_DEV_DEP_MSG_OUT: u8 = 1;
pub const MESSAGE_ID_REQUEST_DEV_DEP_MSG_IN: u8 = 2;
pub const MESSAGE_ID_DEV_DEP_MSG_IN: u8 = 2;
pub const BULK_IO_HEADER_SIZE: usize = 12;
pub const BULK_IO_PAYLOAD_CAPACITY: usize = 1024 * 1024;
pub const USBTMC_FLAG_NO_AUTO_CONNECT: i32 = 0x1;
pub fn build_bulk_out_header(
b_tag: u8,
transfer_size: u32,
end_of_message: bool,
) -> [u8; BULK_IO_HEADER_SIZE] {
let mut h = [0u8; BULK_IO_HEADER_SIZE];
h[0] = MESSAGE_ID_DEV_DEP_MSG_OUT;
h[1] = b_tag;
h[2] = !b_tag;
h[3] = 0;
h[4..8].copy_from_slice(&transfer_size.to_le_bytes());
h[8] = if end_of_message { 0x01 } else { 0x00 };
h
}
pub fn build_request_bulk_in_header(
b_tag: u8,
max_transfer_size: u32,
) -> [u8; BULK_IO_HEADER_SIZE] {
let mut h = [0u8; BULK_IO_HEADER_SIZE];
h[0] = MESSAGE_ID_REQUEST_DEV_DEP_MSG_IN;
h[1] = b_tag;
h[2] = !b_tag;
h[3] = 0;
h[4..8].copy_from_slice(&max_transfer_size.to_le_bytes());
h
}
pub fn next_b_tag(prev: u8) -> u8 {
if prev == 0xFF { 1 } else { prev + 1 }
}
pub fn pad4(n: usize) -> usize {
(n + 3) & !3
}
#[derive(Debug, Clone)]
pub struct UsbtmcConfig {
pub vendor_id: u16,
pub product_id: u16,
pub serial_number: String,
pub priority: u32,
pub flags: i32,
}
impl UsbtmcConfig {
pub fn from_positional(
vendor_id: i32,
product_id: i32,
serial_number: &str,
priority: i32,
flags: i32,
) -> Self {
Self {
vendor_id: vendor_id as u16,
product_id: product_id as u16,
serial_number: serial_number.to_string(),
priority: priority.max(0) as u32,
flags,
}
}
pub fn no_auto_connect(&self) -> bool {
(self.flags & USBTMC_FLAG_NO_AUTO_CONNECT) != 0
}
}
pub struct DrvAsynUsbtmcPort {
base: PortDriverBase,
config: UsbtmcConfig,
b_tag: u8,
}
impl DrvAsynUsbtmcPort {
#[allow(clippy::too_many_arguments)] pub fn configure(
port_name: &str,
vendor_id: i32,
product_id: i32,
serial_number: &str,
priority: i32,
flags: i32,
) -> AsynResult<Self> {
let config =
UsbtmcConfig::from_positional(vendor_id, product_id, serial_number, priority, flags);
let mut base = PortDriverBase::new(
port_name,
1,
PortFlags {
multi_device: false,
can_block: true,
destructible: true,
},
);
base.connected = false;
base.auto_connect = !config.no_auto_connect();
Ok(Self {
base,
config,
b_tag: 1, })
}
pub fn config(&self) -> &UsbtmcConfig {
&self.config
}
pub fn current_b_tag(&self) -> u8 {
self.b_tag
}
pub fn has_hw_support() -> bool {
cfg!(feature = "usbtmc")
}
}
impl PortDriver for DrvAsynUsbtmcPort {
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!(
"USBTMC driver scaffold: hardware feature 'usbtmc' not enabled \
in this build. Config parsed (vid=0x{:04X}, pid=0x{:04X}, \
serial={:?}, flags=0x{:X}) — rebuild with \
`--features asyn-rs/usbtmc` to enable.",
self.config.vendor_id,
self.config.product_id,
self.config.serial_number,
self.config.flags,
),
});
}
Err(AsynError::Status {
status: AsynStatus::Error,
message: "USBTMC hardware path not yet implemented".into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bulk_out_header_matches_c_layout() {
let h = build_bulk_out_header(0x42, 0x1234_5678, true);
assert_eq!(h[0], MESSAGE_ID_DEV_DEP_MSG_OUT);
assert_eq!(h[1], 0x42);
assert_eq!(h[2], !0x42);
assert_eq!(h[3], 0);
assert_eq!(&h[4..8], &[0x78, 0x56, 0x34, 0x12]);
assert_eq!(h[8], 0x01);
assert_eq!(&h[9..12], &[0, 0, 0]);
}
#[test]
fn bulk_out_header_eom_bit_clear_when_not_end() {
let h = build_bulk_out_header(0x10, 256, false);
assert_eq!(h[8], 0x00);
}
#[test]
fn request_bulk_in_header_matches_c_layout() {
let h = build_request_bulk_in_header(0x10, BULK_IO_PAYLOAD_CAPACITY as u32);
assert_eq!(h[0], MESSAGE_ID_REQUEST_DEV_DEP_MSG_IN);
assert_eq!(h[1], 0x10);
assert_eq!(h[2], !0x10);
let cap = BULK_IO_PAYLOAD_CAPACITY as u32;
assert_eq!(&h[4..8], &cap.to_le_bytes());
assert!(h[8..12].iter().all(|&b| b == 0));
}
#[test]
fn b_tag_wraps_at_ff_to_1_not_0() {
assert_eq!(next_b_tag(0x01), 0x02);
assert_eq!(next_b_tag(0xFE), 0xFF);
assert_eq!(next_b_tag(0xFF), 0x01);
}
#[test]
fn pad4_rounds_up_to_4_byte_boundary() {
assert_eq!(pad4(0), 0);
assert_eq!(pad4(1), 4);
assert_eq!(pad4(2), 4);
assert_eq!(pad4(3), 4);
assert_eq!(pad4(4), 4);
assert_eq!(pad4(5), 8);
assert_eq!(pad4(BULK_IO_HEADER_SIZE), BULK_IO_HEADER_SIZE);
assert_eq!(pad4(112), 112);
assert_eq!(pad4(113), 116);
}
#[test]
fn config_no_auto_connect_flag_bit() {
let cfg = UsbtmcConfig::from_positional(0x0957, 0x0407, "MY01234", 0, 0);
assert!(!cfg.no_auto_connect());
let cfg = UsbtmcConfig::from_positional(0x0957, 0x0407, "MY01234", 0, 0x1);
assert!(cfg.no_auto_connect());
}
#[test]
fn configure_records_all_positional_fields() {
let drv =
DrvAsynUsbtmcPort::configure("usbtmc0", 0x0957, 0x0407, "MY12345", 50, 0).unwrap();
assert_eq!(drv.config().vendor_id, 0x0957);
assert_eq!(drv.config().product_id, 0x0407);
assert_eq!(drv.config().serial_number, "MY12345");
assert_eq!(drv.config().priority, 50);
assert_eq!(drv.config().flags, 0);
assert_eq!(drv.current_b_tag(), 1);
assert!(drv.base().auto_connect);
}
#[test]
fn configure_flags_bit0_disables_auto_connect() {
let drv = DrvAsynUsbtmcPort::configure(
"usbtmc0",
0x0957,
0x0407,
"",
0,
USBTMC_FLAG_NO_AUTO_CONNECT,
)
.unwrap();
assert!(!drv.base().auto_connect);
}
#[cfg(not(feature = "usbtmc"))]
#[test]
fn connect_without_hw_feature_reports_error() {
let mut drv = DrvAsynUsbtmcPort::configure("usbtmc0", 0x0957, 0x0407, "", 0, 0).unwrap();
let err = drv.connect(&AsynUser::default()).unwrap_err();
match err {
AsynError::Status { message, .. } => {
assert!(
message.contains("usbtmc"),
"must mention feature: {message}"
);
}
_ => panic!("expected Status error"),
}
}
#[test]
fn has_hw_support_matches_feature_flag() {
assert_eq!(
DrvAsynUsbtmcPort::has_hw_support(),
cfg!(feature = "usbtmc")
);
}
}