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}