Skip to main content

visa_rs/
lib.rs

1//!
2//! Safe rust bindings for VISA(Virtual Instrument Software Architecture) library
3//!
4//! Most documentation comes from [NI-VISA Product Documentation](https://www.ni.com/docs/en-US/bundle/ni-visa-20.0/page/ni-visa/help_file_title.html)
5//!
6//! # Requirements
7//! This crate needs to link to an installed visa library, for example, [NI-VISA](https://www.ni.com/en-us/support/downloads/drivers/download.ni-visa.html).
8//!
9//! A default link configuration is used for the default installation setup on Windows, Linux and MacOs.
10//!
11//! You can overwrite the configuration by specifying the name of the visa library file
12//! (default to `visa` for linux, `visa64` or `visa32` for windows) by environment variable
13//! `LIB_VISA_NAME`, and the path of the file by environment variable `LIB_VISA_PATH`.
14//!
15//! # Features
16//!
17//! | Feature | Default | What it does |
18//! | --------------- | ------- | ------------ |
19//! | `tokio` | yes | Enables `InstrumentTokioAdapter`, which implements `tokio::io::AsyncRead` and `tokio::io::AsyncWrite` for a VISA session. |
20//!
21//! The async API itself ([`Instrument::into_async`], [`AsyncInstrument`]) is always available;
22//! `tokio` only adds the adapter that plugs it into Tokio's IO traits.
23//!
24//! Fixed-width VISA enum representations support cross-compilation by default.
25//!
26//! # Example
27//!
28//! Codes below will find the first Keysight instrument in your environment and print out its `*IDN?` response.
29//!
30//! ```no_run
31//! fn main() -> visa_rs::Result<()>{
32//!     use std::ffi::CString;
33//!     use std::io::{BufRead, BufReader, Read, Write};
34//!     use visa_rs::prelude::*;
35//!
36//!     // open default resource manager
37//!     let rm: DefaultRM = DefaultRM::new()?;
38//!
39//!     // expression to match resource name
40//!     let expr = CString::new("?*KEYSIGH?*INSTR").unwrap().into();
41//!
42//!     // find the first resource matched
43//!     let rsc = rm.find_res(&expr)?;
44//!
45//!     // open a session to the resource, the session will be closed when rm is dropped
46//!     let instr: Instrument = rm.open(&rsc, AccessMode::NO_LOCK, TIMEOUT_IMMEDIATE)?;
47//!
48//!     // write message
49//!     (&instr).write_all(b"*IDN?\n").map_err(io_to_vs_err)?;
50//!
51//!     // read response
52//!     let mut buf_reader = BufReader::new(&instr);
53//!     let mut buf = String::new();
54//!     buf_reader.read_line(&mut buf).map_err(io_to_vs_err)?;
55//!
56//!     eprintln!("{}", buf);
57//!     Ok(())
58//! }
59//! ```
60
61use enums::{attribute, event};
62use std::ffi::CStr;
63use std::{borrow::Cow, ffi::CString, fmt::Display, time::Duration};
64pub use visa_sys as vs;
65
66mod abi;
67pub mod async_io;
68#[cfg(feature = "tokio")]
69mod async_tokio;
70pub mod enums;
71pub mod flags;
72pub mod handler;
73mod instrument;
74pub mod prelude;
75pub mod session;
76
77pub use async_io::AsyncInstrument;
78#[cfg(feature = "tokio")]
79pub use async_tokio::InstrumentTokioAdapter;
80pub use instrument::Instrument;
81
82use session::{AsRawSs, AsSs, FromRawSs, IntoRawSs, OwnedSs};
83
84pub const TIMEOUT_IMMEDIATE: Duration = Duration::from_millis(vs::VI_TMO_IMMEDIATE as _);
85pub const TIMEOUT_INFINITE: Duration = Duration::from_millis(vs::VI_TMO_INFINITE as _);
86macro_rules! impl_session_traits {
87    ($($id:ident),* $(,)?) => {
88        $(
89            impl IntoRawSs for $id {
90                fn into_raw_ss(self) -> session::RawSs {
91                    self.0.into_raw_ss()
92                }
93            }
94
95            impl AsRawSs for $id {
96                fn as_raw_ss(&self) -> session::RawSs {
97                    self.0.as_raw_ss()
98                }
99            }
100
101            impl AsSs for $id {
102                fn as_ss(&self) -> session::BorrowedSs<'_> {
103                    self.0.as_ss()
104                }
105            }
106
107            impl FromRawSs for $id {
108                unsafe fn from_raw_ss(s: session::RawSs) -> Self {
109                    Self(FromRawSs::from_raw_ss(s))
110                }
111            }
112        )*
113    };
114}
115
116macro_rules! impl_session_traits_for_borrowed {
117    ($($id:ident),* $(,)?) => {
118        $(
119            impl AsRawSs for $id<'_> {
120                fn as_raw_ss(&self) -> session::RawSs {
121                    self.0.as_raw_ss()
122                }
123            }
124
125            impl AsSs for $id<'_> {
126                fn as_ss(&self) -> session::BorrowedSs<'_> {
127                    self.0.as_ss()
128                }
129            }
130        )*
131    };
132}
133
134impl_session_traits! { DefaultRM, Instrument }
135impl_session_traits_for_borrowed! { WeakRM }
136
137#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
138pub struct Error(pub enums::status::ErrorCode);
139
140impl std::error::Error for Error {}
141
142impl std::fmt::Display for Error {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        self.0.fmt(f)
145    }
146}
147
148impl From<enums::status::ErrorCode> for Error {
149    fn from(s: enums::status::ErrorCode) -> Self {
150        Self(s)
151    }
152}
153
154impl From<Error> for enums::status::ErrorCode {
155    fn from(s: Error) -> Self {
156        s.0
157    }
158}
159
160impl From<Error> for vs::ViStatus {
161    fn from(s: Error) -> Self {
162        s.0.into()
163    }
164}
165
166impl TryFrom<vs::ViStatus> for Error {
167    type Error = <enums::status::ErrorCode as TryFrom<vs::ViStatus>>::Error;
168    fn try_from(value: vs::ViStatus) -> std::result::Result<Self, Self::Error> {
169        Ok(Self(value.try_into()?))
170    }
171}
172
173impl TryFrom<std::io::Error> for Error {
174    type Error = std::io::Error;
175    fn try_from(value: std::io::Error) -> std::result::Result<Self, Self::Error> {
176        if let Some(e) = value.get_ref() {
177            if let Some(e) = e.downcast_ref::<Error>() {
178                return Ok(*e);
179            }
180        }
181        Err(value)
182    }
183}
184
185/// Quickly convert [std::io::Error].
186///
187///  # Panics
188///
189/// Panic if the input Error is not converted from [visa_rs::Error](Error), use [TryInto] to perform conversion,
190/// the io error must be generated from [Instrument] IO ops
191pub fn io_to_vs_err(e: std::io::Error) -> Error {
192    e.try_into().unwrap()
193}
194
195fn vs_to_io_err(err: Error) -> std::io::Error {
196    use enums::status::ErrorCode::*;
197    use std::io::ErrorKind::*;
198    std::io::Error::new(
199        match err.0 {
200            ErrorInvObject => AddrNotAvailable,
201            ErrorNsupOper => Unsupported,
202            ErrorRsrcLocked => ConnectionRefused,
203            ErrorTmo => TimedOut,
204            ErrorRawWrProtViol | ErrorRawRdProtViol => InvalidData,
205            ErrorInpProtViol | ErrorOutpProtViol => BrokenPipe,
206            ErrorBerr => BrokenPipe,
207            ErrorInvSetup => InvalidInput,
208            ErrorNcic => PermissionDenied,
209            ErrorNlisteners => Other,
210            ErrorAsrlParity | ErrorAsrlFraming => Other,
211            ErrorAsrlOverrun => Other,
212            ErrorConnLost => BrokenPipe,
213            ErrorInvMask => InvalidInput,
214            ErrorAlloc => OutOfMemory,
215            ErrorInvFmt | ErrorNsupFmt => InvalidInput,
216            ErrorAbort => Interrupted,
217            ErrorRsrcNfound => NotFound,
218            ErrorNsupMode | ErrorNsupAttr | ErrorNsupAttrState => Unsupported,
219            ErrorAttrReadonly => PermissionDenied,
220            // `last_os_error` here would report an unrelated errno, so stay generic.
221            ErrorIo => Other,
222            // Anything not mapped above is still a real error, not an impossible state.
223            _ => Other,
224        },
225        err,
226    )
227}
228
229pub type Result<T> = std::result::Result<T, Error>;
230
231/// Splits a raw `VI_ATTR_STATUS` into the completion/error halves of [`Result`].
232///
233/// Total: a status matching no known code becomes
234/// [`ErrorSystemError`](enums::status::ErrorCode::ErrorSystemError) -- "unknown system
235/// error" -- rather than panicking, since visa implementations may return codes this
236/// crate does not enumerate.
237impl From<enums::attribute::AttrStatus> for Result<enums::status::CompletionCode> {
238    fn from(a: enums::attribute::AttrStatus) -> Self {
239        use enums::status::{CompletionCode, ErrorCode};
240        // `ViStatus` is 32 bits on every platform, so error codes are genuinely negative
241        // and this sign test is sufficient on its own.
242        let state = a.into_inner();
243        if state >= SUCCESS {
244            CompletionCode::try_from(state).map_err(|_| Error(ErrorCode::ErrorSystemError))
245        } else {
246            Err(Error(
247                ErrorCode::try_from(state).unwrap_or(ErrorCode::ErrorSystemError),
248            ))
249        }
250    }
251}
252
253const SUCCESS: vs::ViStatus = vs::VI_SUCCESS as _;
254
255#[doc(hidden)]
256#[macro_export]
257macro_rules! wrap_raw_error_in_unsafe {
258    ($s:expr) => {
259        match unsafe { $s } {
260            state if state >= $crate::SUCCESS && state <= i32::MAX as _ => {
261                $crate::Result::<$crate::enums::status::CompletionCode>::Ok(
262                    state.try_into().expect(&format!(
263                        "Converting `{state}({state:#0X})` to CompletionCode failed"
264                    )),
265                )
266            }
267            e => $crate::Result::<$crate::enums::status::CompletionCode>::Err(
268                e.try_into()
269                    .expect(&format!("Converting `{e}({e:#0X})` to ErrorCode failed")),
270            ),
271        }
272    };
273}
274
275/// Ability as the Default Resource Manager for VISA
276pub trait AsResourceManager: AsRawSs {
277    ///
278    /// Queries a VISA system to locate the resources associated with a specified interface.
279    ///
280    /// The viFindRsrc() operation matches the value specified in the expr parameter with the resources available for a particular interface. A regular expression is a string consisting of ordinary characters as well as special characters. You use a regular expression to specify patterns to match in a given string; in other words, it is a search criterion. The viFindRsrc() operation uses a case-insensitive compare feature when matching resource names against the regular expression specified in expr. For example, calling viFindRsrc() with "VXI?*INSTR" would return the same resources as invoking it with "vxi?*instr".
281    ///
282    /// All resource strings returned by viFindRsrc() will always be recognized by viOpen(). However, viFindRsrc() will not necessarily return all strings that you can pass to viParseRsrc() or viOpen(). This is especially true for network and TCPIP resources.
283    ///
284    /// The search criteria specified in the expr parameter has two parts: a regular expression over a resource string, and an optional logical expression over attribute values. The regular expression is matched against the resource strings of resources known to the VISA Resource Manager. If the resource string matches the regular expression, the attribute values of the resource are then matched against the expression over attribute values. If the match is successful, the resource has met the search criteria and gets added to the list of resources found.
285    ///
286    /// Special Characters and Operators|       Meaning
287    /// :-----------------------------: |       :----------------------------------
288    /// \?                              |       Matches any one character.
289    /// \\                              |       Makes the character that follows it an ordinary character instead of special character. For example, when a question mark follows a backslash (\?), it matches the ? character instead of  any one character.
290    /// \[list\]                        |       Matches any one character from the enclosed list. You can use a hyphen to match a range of characters.
291    /// \[^list\]                       |       Matches any character not in the enclosed list. You can use a hyphen to match a range of characters.
292    /// \*                              |       Matches 0 or more occurrences of the preceding character or expression.
293    /// \+                              |       Matches 1 or more occurrences of the preceding character or expression.
294    /// Exp\|exp                        |       Matches either the preceding or following expression. The or operator | matches the entire expression that precedes or follows it and not just the character that precedes or    follows it. For example, VXI|GPIB means (VXI)|(GPIB), not VX(I|G)PIB.
295    /// (exp)                           |       Grouping characters or expressions.
296    ///
297    ///  Regular Expression             |   Sample Matches
298    /// :-----------------------------: |   :----------------------------------
299    ///  GPIB?*INSTR                    |    GPIB0::2::INSTR, and GPIB1::1::1::INSTR.
300    ///  GPIB\[0-9\]\*::?*INSTR         |    GPIB0::2::INSTR and GPIB1::1::1::INSTR.
301    ///  GPIB\[^0\]::?*INSTR            |    GPIB1::1::1::INSTR but not GPIB0::2::INSTR or GPIB12::8::INSTR.
302    ///  VXI?*INSTR                     |    VXI0::1::INSTR.
303    ///  ?*VXI\[0-9\]\*::?*INSTR        |    VXI0::1::INSTR.
304    ///  ASRL\[0-9\]\*::?*INSTR         |    ASRL1::INSTR but not VXI0::5::INSTR.
305    ///  ASRL1\+::INSTR                 |    ASRL1::INSTR and ASRL11::INSTR but not ASRL2::INSTR.
306    ///  (GPIB\|VXI)?*INSTR             |    GPIB1::5::INSTR and VXI0::3::INSTR but not ASRL2::INSTR.
307    ///  (GPIB0\|VXI0)::1::INSTR        |    GPIB0::1::INSTR and VXI0::1::INSTR.
308    ///  ?*INSTR                        |    all INSTR (device) resources.
309    ///  ?*VXI\[0-9\]\*::?*MEMACC       |    VXI0::MEMACC.
310    ///  VXI0::?*                       |    VXI0::1::INSTR, VXI0::2::INSTR, and VXI0::MEMACC.
311    ///  ?*                             |    all resources.
312    ///  visa://hostname/?*             |    all resources on the specified remote system. The hostname can be represented as either an IP address (dot-notation) or network machine name. This remote system need not be a configured remote system.
313    ///  /?*                            |    all resources on the local machine. Configured remote systems are not queried.
314    ///  visa:/ASRL?*INSTR              |    all ASRL resources on the local machine and returns them in URL format (for example, visa:/ASRL1::INSTR).
315    ///
316    /// see also [official doc](https://www.ni.com/docs/en-US/bundle/ni-visa-20.0/page/ni-visa/vifindrsrc.html)
317    ///
318    fn find_res_list(&self, expr: &ResID) -> Result<ResList> {
319        let mut list: vs::ViFindList = 0;
320        let mut cnt: vs::ViUInt32 = 0;
321        let mut instr_desc = new_visa_buf();
322        wrap_raw_error_in_unsafe!(vs::viFindRsrc(
323            self.as_raw_ss(),
324            expr.as_vi_const_string(),
325            &mut list,
326            &mut cnt,
327            instr_desc.as_mut_ptr() as _,
328        ))?;
329        Ok(ResList {
330            list,
331            cnt: cnt as _,
332            instr_desc,
333        })
334    }
335
336    ///
337    /// Queries a VISA system to locate the resources associated with a specified interface, return the first resource matched
338    ///
339    fn find_res(&self, expr: &ResID) -> Result<ResID> {
340        /*
341        !keysight impl visa will try to write at address vs::VI_NULL, cause exit code: 0xc0000005, STATUS_ACCESS_VIOLATION
342        let mut instr_desc = new_visa_buf();
343        let mut cnt: vs::ViUInt32 = 0;
344        wrap_raw_error_in_unsafe!(vs::viFindRsrc(
345            self.as_raw_ss(),
346            expr.as_vi_const_string(),
347            vs::VI_NULL as _,
348            &mut cnt as *mut _,
349            instr_desc.as_mut_ptr() as _,
350        ))?;
351        Ok(instr_desc.try_into().unwrap())
352        */
353
354        Ok(self.find_res_list(expr)?.instr_desc.try_into().unwrap())
355    }
356
357    /// Parse a resource string to get the interface information.
358    fn parse_res(&self, res: &ResID) -> Result<(attribute::AttrIntfType, attribute::AttrIntfNum)> {
359        let mut ty = 0;
360        let mut num = 0;
361        wrap_raw_error_in_unsafe!(vs::viParseRsrc(
362            self.as_raw_ss(),
363            res.as_vi_const_string(),
364            &mut ty as *mut _,
365            &mut num as *mut _
366        ))?;
367        unsafe {
368            Ok((
369                attribute::AttrIntfType::new_unchecked(ty),
370                attribute::AttrIntfNum::new_unchecked(num),
371            ))
372        }
373    }
374
375    /// Parse a resource string to get extended interface information.
376    ///
377    /// the returned three VisaStrings are:
378    ///
379    /// + Specifies the resource class (for example, "INSTR") of the given resource string.
380    ///
381    /// + This is the expanded version of the given resource string. The format should be similar to the VISA-defined canonical resource name.
382    ///
383    /// + Specifies the user-defined alias for the given resource string.
384    fn parse_res_ex(
385        &self,
386        res: &ResID,
387    ) -> Result<(
388        attribute::AttrIntfType,
389        attribute::AttrIntfNum,
390        VisaString,
391        VisaString,
392        VisaString,
393    )> {
394        let mut ty = 0;
395        let mut num = 0;
396        let mut str1 = new_visa_buf();
397        let mut str2 = new_visa_buf();
398        let mut str3 = new_visa_buf();
399        wrap_raw_error_in_unsafe!(vs::viParseRsrcEx(
400            self.as_raw_ss(),
401            res.as_vi_const_string(),
402            &mut ty as *mut _,
403            &mut num as *mut _,
404            str1.as_mut_ptr() as _,
405            str2.as_mut_ptr() as _,
406            str3.as_mut_ptr() as _,
407        ))?;
408        unsafe {
409            Ok((
410                attribute::AttrIntfType::new_unchecked(ty),
411                attribute::AttrIntfNum::new_unchecked(num),
412                str1.try_into().unwrap(),
413                str2.try_into().unwrap(),
414                str3.try_into().unwrap(),
415            ))
416        }
417    }
418
419    ///
420    /// Opens a session to the specified resource.
421    ///
422    /// For the parameter accessMode, either VI_EXCLUSIVE_LOCK (1) or VI_SHARED_LOCK (2).
423    ///
424    /// VI_EXCLUSIVE_LOCK (1) is used to acquire an exclusive lock immediately upon opening a session; if a lock cannot be acquired, the session is closed and an error is returned.
425    ///
426    /// VI_LOAD_CONFIG (4) is used to configure attributes to values specified by some external configuration utility. Multiple access modes can be used simultaneously by specifying a bit-wise OR of the values other than VI_NULL.
427    ///
428    ///  NI-VISA currently supports VI_LOAD_CONFIG only on Serial INSTR sessions.
429    ///
430    fn open(
431        &self,
432        res_name: &ResID,
433        access_mode: flags::AccessMode,
434        open_timeout: Duration,
435    ) -> Result<Instrument> {
436        let mut instr: vs::ViSession = 0;
437        wrap_raw_error_in_unsafe!(vs::viOpen(
438            self.as_raw_ss(),
439            res_name.as_vi_const_string(),
440            access_mode.bits(),
441            open_timeout.as_millis() as _,
442            &mut instr as _,
443        ))?;
444        Ok(unsafe { Instrument::from_raw_ss(instr) })
445    }
446}
447
448impl<'a> AsResourceManager for WeakRM<'a> {}
449impl AsResourceManager for DefaultRM {}
450
451/// A [`ResourceManager`](AsResourceManager) which is [`Clone`] and doesn't close everything on drop
452#[derive(Debug, PartialEq, Eq, Hash, Clone)]
453pub struct WeakRM<'a>(session::BorrowedSs<'a>);
454
455impl<'a> From<&'a attribute::AttrRmSession> for WeakRM<'a> {
456    fn from(value: &'a attribute::AttrRmSession) -> Self {
457        Self(unsafe { session::BorrowedSs::borrow_raw(value.clone().into_inner()) })
458    }
459}
460
461impl From<attribute::AttrRmSession> for WeakRM<'static> {
462    fn from(value: attribute::AttrRmSession) -> Self {
463        Self(unsafe { session::BorrowedSs::borrow_raw(value.into_inner()) })
464    }
465}
466
467/// A [`ResourceManager`](AsResourceManager) which close everything on drop
468#[derive(Debug, PartialEq, Eq, Hash)]
469pub struct DefaultRM(session::OwnedSs);
470
471impl DefaultRM {
472    /// [`DefaultRM`] will close everything opened by it on drop.
473    /// By converting to a [`WeakRM`], such behavior can be avoided.
474    ///
475    /// *Note*: Sessions opened by another resource manager (get from another call to [`Self::new`]) won't be influenced.
476    pub fn leak(self) -> WeakRM<'static> {
477        unsafe { WeakRM(session::BorrowedSs::borrow_raw(self.into_raw_ss())) }
478    }
479
480    /// [`DefaultRM`] will close everything opened by it on drop.
481    /// By converting to a [`WeakRM`], such behavior can be avoided.
482    ///
483    /// *Note*: Sessions opened by another resource manager (get from another call to [`Self::new`]) won't be influenced.
484    pub fn borrow(&'_ self) -> WeakRM<'_> {
485        WeakRM(self.as_ss())
486    }
487
488    /// Returns a session to the Default Resource Manager resource.
489    ///
490    /// The first call to this function initializes the VISA system, including the Default Resource Manager resource, and also returns a session to that resource. Subsequent calls to this function return unique sessions to the same Default Resource Manager resource.
491    ///
492    /// When a Resource Manager session is dropped, not only is that session closed, but also all find lists and device sessions (which that Resource Manager session was used to create) are closed.
493    ///
494    /// Close this session and, with it, all find lists and device sessions it opened.
495    ///
496    /// Exactly equivalent to dropping the [`DefaultRM`]; it consumes `self` so the
497    /// session cannot be used afterwards. Deliberately not available on [`WeakRM`],
498    /// which does not own its session -- closing through a borrow would leave the
499    /// owning [`DefaultRM`] to close the same raw session a second time.
500    pub fn close_all(self) {
501        std::mem::drop(self)
502    }
503
504    pub fn new() -> Result<Self> {
505        let mut new: vs::ViSession = 0;
506        wrap_raw_error_in_unsafe!(vs::viOpenDefaultRM(&mut new as _))?;
507        Ok(Self(unsafe { OwnedSs::from_raw_ss(new) }))
508    }
509}
510
511/// Returned by [`DefaultRM::find_res_list`], handler to iterator over matched resources
512#[derive(Debug)]
513pub struct ResList {
514    list: vs::ViFindList,
515    cnt: i32,
516    instr_desc: VisaBuf,
517}
518
519impl Drop for ResList {
520    fn drop(&mut self) {
521        // visa allocates a find list for `viFindRsrc`; without this it leaks until the
522        // resource manager itself is closed.
523        unsafe {
524            vs::viClose(self.list as _);
525        }
526    }
527}
528
529impl Iterator for ResList {
530    type Item = Result<ResID>;
531
532    fn next(&mut self) -> Option<Self::Item> {
533        match self.find_next() {
534            Ok(o) => o.map(Ok),
535            Err(e) => Some(Err(e)),
536        }
537    }
538}
539
540impl ResList {
541    /// Returns the next resource from the list of resources found
542    pub fn find_next(&mut self) -> Result<Option<ResID>> {
543        if self.cnt < 1 {
544            return Ok(None);
545        }
546        let next: ResID = self.instr_desc.try_into().unwrap();
547        if self.cnt > 1 {
548            wrap_raw_error_in_unsafe!(vs::viFindNext(
549                self.list,
550                self.instr_desc.as_mut_ptr() as _
551            ))?;
552        }
553        self.cnt -= 1;
554        Ok(Some(next))
555    }
556}
557
558#[repr(transparent)]
559/// Simple wrapper of [std::ffi::CString]
560#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Clone)]
561pub struct VisaString(CString);
562
563impl std::ops::Deref for VisaString {
564    type Target = CString;
565
566    fn deref(&self) -> &Self::Target {
567        &self.0
568    }
569}
570
571/// resource ID
572pub type ResID = VisaString;
573
574/// Access key used in [`Instrument::lock`]
575pub type AccessKey = VisaString;
576
577impl From<CString> for VisaString {
578    fn from(c: CString) -> Self {
579        Self(c)
580    }
581}
582
583impl From<VisaString> for CString {
584    fn from(value: VisaString) -> Self {
585        value.0
586    }
587}
588
589type VisaBuf = [u8; vs::VI_FIND_BUFLEN as _];
590
591const fn new_visa_buf() -> VisaBuf {
592    [0; vs::VI_FIND_BUFLEN as _]
593}
594
595#[derive(Debug, Clone, Copy)]
596pub struct FromBytesWithNulError;
597
598impl Display for FromBytesWithNulError {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        write!(f, "Bytes include none null(\\0) character")
601    }
602}
603
604impl std::error::Error for FromBytesWithNulError {}
605
606impl TryFrom<[u8; vs::VI_FIND_BUFLEN as _]> for VisaString {
607    type Error = FromBytesWithNulError;
608    fn try_from(f: [u8; vs::VI_FIND_BUFLEN as _]) -> std::result::Result<Self, Self::Error> {
609        let mut index = f.split_inclusive(|t| *t == b'\0');
610        let cstr = index.next().ok_or(FromBytesWithNulError)?;
611        Ok(Self(
612            CStr::from_bytes_with_nul(cstr)
613                .map_err(|_| FromBytesWithNulError)?
614                .to_owned(),
615        ))
616    }
617}
618
619impl VisaString {
620    fn as_vi_const_string(&self) -> vs::ViConstString {
621        self.0.as_ptr()
622    }
623    pub fn to_string_lossy(&self) -> Cow<'_, str> {
624        self.0.to_string_lossy()
625    }
626    pub fn from_string(s: String) -> Option<Self> {
627        CString::new(s).ok().map(|x| x.into())
628    }
629}
630
631impl Display for VisaString {
632    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
633        self.to_string_lossy().fmt(f)
634    }
635}
636
637/// Job ID of an asynchronous operation.
638///
639/// Returned by [`Instrument::visa_read_async`] or [`Instrument::visa_write_async`], used to be compared with the attribute [AttrJobId](enums::attribute::AttrJobId) got from [Event](enums::event::Event) to distinguish operations.
640#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Copy)]
641pub struct JobID(pub(crate) vs::ViJobId);
642
643impl JobID {
644    ///create JobId with value null, used in [`Instrument::terminate`] to abort all calls
645    pub fn null() -> Self {
646        Self(vs::VI_NULL as _)
647    }
648}
649
650impl From<enums::attribute::AttrJobId> for JobID {
651    fn from(s: enums::attribute::AttrJobId) -> Self {
652        Self(s.into_inner())
653    }
654}
655
656impl PartialEq<enums::attribute::AttrJobId> for JobID {
657    fn eq(&self, other: &enums::attribute::AttrJobId) -> bool {
658        self.eq(&JobID::from(other.clone()))
659    }
660}
661
662#[cfg(test)]
663mod test {
664    use crate::enums::status::{CompletionCode, ErrorCode};
665    use crate::*;
666
667    #[test]
668    fn convert_from_complete_code() {
669        assert_eq!(
670            CompletionCode::try_from(0 as vs::ViStatus).unwrap(),
671            CompletionCode::Success
672        );
673    }
674    #[test]
675    fn convert_from_error_code() {
676        assert_eq!(
677            ErrorCode::try_from(0xBFFF0011u32 as vs::ViStatus).unwrap(),
678            ErrorCode::ErrorRsrcNfound
679        );
680    }
681    #[test]
682    fn convert_to_complete_code() {
683        assert_eq!(0 as vs::ViStatus, CompletionCode::Success.into());
684    }
685    #[test]
686    fn convert_to_error_code() {
687        assert_eq!(
688            0xBFFF0011u32 as vs::ViStatus,
689            ErrorCode::ErrorRsrcNfound.into()
690        );
691    }
692    #[test]
693    #[should_panic]
694    fn convert_wrong_status1() {
695        CompletionCode::try_from(ErrorCode::ErrorRsrcNfound as vs::ViStatus).unwrap();
696    }
697    #[test]
698    #[should_panic]
699    fn convert_wrong_status2() {
700        ErrorCode::try_from(CompletionCode::Success as vs::ViStatus).unwrap();
701    }
702
703    use anyhow::{bail, Result};
704    #[test]
705    fn rm_behavior() -> Result<()> {
706        let rm1 = match DefaultRM::new() {
707            Ok(rm) => rm,
708            Err(crate::Error(crate::enums::status::ErrorCode::ErrorSystemError))
709            | Err(crate::Error(crate::enums::status::ErrorCode::ErrorLibraryNfound)) => {
710                return Ok(());
711            }
712            Err(e) => return Err(e.into()),
713        };
714        let rm2 = match DefaultRM::new() {
715            Ok(rm) => rm,
716            Err(crate::Error(crate::enums::status::ErrorCode::ErrorSystemError))
717            | Err(crate::Error(crate::enums::status::ErrorCode::ErrorLibraryNfound)) => {
718                return Ok(());
719            }
720            Err(e) => return Err(e.into()),
721        };
722        let r1 = rm1.as_raw_ss();
723        assert_ne!(rm1, rm2);
724        std::mem::drop(rm1);
725        // Not `"?*"`: some visa builds reject the bare-wildcard search with
726        // ErrorInvSetup even on a freshly opened resource manager, which has nothing to
727        // do with what this test is checking.
728        let expr = CString::new("?*INSTR").unwrap().into();
729        match unsafe { DefaultRM::from_raw_ss(r1).leak() }.find_res(&expr) {
730            Err(crate::Error(crate::enums::status::ErrorCode::ErrorInvObject)) => {
731                Ok::<_, crate::Error>(())
732            }
733            Err(e) => {
734                bail!("unexpected behavior using a resource manager after it is dropped: {e:?}",)
735            }
736            Ok(_) => bail!(
737                "unexpected behavior using a resource manager after it is dropped: found resource",
738            ),
739        }?;
740        match rm2.find_res(&expr) {
741            Ok(_)
742            | Err(crate::Error(crate::enums::status::ErrorCode::ErrorRsrcNfound))
743             => Ok(()),
744            Err(e) => bail!(
745                "unexpected behavior using a resource manager after dropping another resource manager: {e:?}",
746            ),
747        }
748    }
749
750    #[test]
751    fn attr_status_splits_without_panicking() {
752        use enums::attribute::AttrStatus;
753        use enums::status::{CompletionCode, ErrorCode};
754        // `Result` in this module is anyhow's, so name the crate's explicitly
755        let of = |v: vs::ViStatus| -> crate::Result<CompletionCode> {
756            unsafe { AttrStatus::new_unchecked(v) }.into()
757        };
758        assert_eq!(of(0).unwrap(), CompletionCode::Success);
759        assert_eq!(
760            of(ErrorCode::ErrorTmo.into()).unwrap_err(),
761            Error(ErrorCode::ErrorTmo)
762        );
763        // codes this crate does not enumerate must not panic, either sign
764        assert_eq!(
765            of(0x3FFF_7FFE).unwrap_err(),
766            Error(ErrorCode::ErrorSystemError)
767        );
768        assert_eq!(of(-12345).unwrap_err(), Error(ErrorCode::ErrorSystemError));
769    }
770
771    #[test]
772    fn convert_io_error() {
773        use enums::status::ErrorCode::ErrorTmo;
774        let vs_error = Error(ErrorTmo);
775        let io_error = vs_to_io_err(vs_error);
776        assert_eq!(Error::try_from(io_error).unwrap(), vs_error);
777        let no_vs_io_error = std::io::Error::other(FromBytesWithNulError);
778        assert!(Error::try_from(no_vs_io_error).is_err());
779
780        // The code rides along as the payload whichever `ErrorKind` it maps to, so this
781        // holds regardless of how the mapping table changes.
782        use enums::status::ErrorCode::{ErrorAlloc, ErrorInvJobId, ErrorQueueError};
783        for code in [ErrorTmo, ErrorAlloc, ErrorInvJobId, ErrorQueueError] {
784            assert_eq!(
785                Error::try_from(vs_to_io_err(Error(code))).unwrap(),
786                Error(code)
787            );
788        }
789        // Codes with no specific kind share the `Other` catch-all, but stay fully
790        // distinguishable through the payload and its description.
791        assert_eq!(
792            vs_to_io_err(Error(ErrorInvJobId)).kind(),
793            std::io::ErrorKind::Other
794        );
795        assert_ne!(
796            vs_to_io_err(Error(ErrorInvJobId)).to_string(),
797            vs_to_io_err(Error(ErrorQueueError)).to_string(),
798            "the visa description must survive into the io error"
799        );
800    }
801}