Skip to content

Commit

Permalink
copying tsp-instrument from tsp-toolkit-kic-cli
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaffery committed Nov 23, 2023
1 parent c4f6e8e commit 1acf97b
Show file tree
Hide file tree
Showing 26 changed files with 5,759 additions and 0 deletions.
1,489 changes: 1,489 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "tsp-tookit-kic-lib"
description = "A library specifically enabling communication to the Keithley product-line of instruments"
version = "0.10.2"
authors = ["Keithley Instruments, LLC"]
edition = "2021"
repository = "https://git.keithley.com/trebuchet/teaspoon/ki-comms"

[dependencies]
bytes = "1.4.0"
phf = { version = "0.11.1", features = ["macros"] }
reqwest = "0.11"
rpassword = "7.2.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.38"
tmc = { git = "https://github.com/esarver/rusb-usbtmc" }
tracing = { version = "0.1", features = ["async-await"] }
rusb = "0.9.1"
chrono = "0.4.30"
#oklib-rs = { git = "https://git.keithley.com/trebuchet/teaspoon/oklib-rs.git", branch = "main", optional = true }

[dev-dependencies]
anyhow = "1.0.69"
bytes = "1"
colored = "2.0.0"
mockall = { version = "0.11.4", features = ["nightly"] }
149 changes: 149 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! All the errors that this crate can emit are defined in the
//! [`error::InstrumentError`] enum.
use std::{num::ParseIntError, string::FromUtf8Error};

use thiserror::Error;

use crate::instrument::info::ConnectionAddr;

/// Define errors that originate from this crate
#[derive(Error, Debug)]
#[allow(clippy::module_name_repetitions)]
pub enum InstrumentError {
/// The `unparsable_string` was passed where an address was expected, but
/// it couldn't be parsed to a valid address.
#[error("unable to parse `{unparsable_string}`, expected an address")]
AddressParsingError {
///The string that couldn't be parsed
unparsable_string: String,
},

/// The [`ConnectionAddr`] was not able to be converted to the desired device
/// interface type
#[error("unable to convert from `{from:?}` to {to}")]
ConnectionAddressConversionError {
/// The address information trying to be converted from
from: ConnectionAddr,
/// A string of name of the type trying to be converted to
to: String,
},

/// There was an error while trying to connect to the interface or instrument.
#[error("connection error occurred: {details}")]
ConnectionError {
// The details of the [`ConnectionError`]
details: String,
},

#[cfg(feature = "debugging")]
/// There was an error when performing a debugging action.
#[error("debug error occurred: {details}")]
DebugError {
/// The details of the [`DebugError`]
details: String,
},

#[cfg(feature = "debugging")]
/// The Debugger license was not accepted.
#[error("debug license was not valid: {reason}")]
DebugLicenseRejection {
/// The reason the license was rejected
reason: String,
},

/// There was an issue while disconnecting from an instrument.
#[error("unable to gracefully disconnect from instrument: {details}")]
DisconnectError {
/// More information about the disconnection error.
details: String,
},

/// A resource file was unable to be decrypted.
#[error("unable to decrypt resource: {source}")]
ResourceDecryptError {
///**TODO:** Change this to the error that is produced when decryption fails
#[from]
source: FromUtf8Error,
},

/// An error that occurs while trying to retrieve information about an instrument
/// such as the serial number, model, manufacturer, etc.
#[error("instrument information retrieval error: {details}")]
InformationRetrievalError {
/// Any extra information about why the instrument information could not be
/// retrieved.
details: String,
},

/// An Error that originates from an instrument. This is generic for all instruments
/// and is therefore just a [`String`].
#[error("{error}")]
InstrumentError {
/// The error string provided by the instrument.
error: String,
},

/// Converts a [`std::io::Error`] to a [`TeaspoonInterfaceError`]
#[error("IO error: {source}")]
IoError {
/// The [`std::io::Error`] from which this [`TeaspoonInterfaceError::IoError`]
/// was derived.
#[from]
source: std::io::Error,
},

/// The provided login details were either incorrect or the instrument is already
/// claimed and cannot be claimed again.
#[error("provided login details rejected or instrument already claimed")]
LoginRejected,

#[error("{source}")]
ParseIntError {
#[from]
source: ParseIntError,
},

/// An error with communicating through rusb to the instrument
#[error("rusb error: {source}")]
RusbError {
#[from]
source: rusb::Error,
},

/// The TSP error that was received from the instrument was malformed.
#[error("unable to parse TSP error from instrument {error}")]
TspErrorParseError {
/// The text of the malformed error that was provided by the instrument
error: String,
},

/// Converts a [`tmc::TMCError`] to a [`TeaspoonInterfaceError`]
#[error("USBTMC error: {source}")]
TmcError {
/// The [`tmc::TMCError`] from which this [`TeaspoonInterfaceError::TmcError`]
/// was derived.
#[from]
source: tmc::TMCError,
},

/// The queried instrument returned an unknown model number
#[error("\"{model}\" is not a recognized model number")]
UnknownInstrumentModel {
/// The unknown model number
model: String,
},

/// The queried instrument returned an unknown language type
#[error("\"{lang} is not a recognized embedded instrument language\"")]
UnknownLanguage {
/// The unknown language type
lang: String,
},

/// An uncategorized error.
#[error("{0}")]
Other(String),
}

pub(crate) type Result<T> = std::result::Result<T, InstrumentError>;
12 changes: 12 additions & 0 deletions src/instrument/firmware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::error::Result;

/// The trait an instrument must implement in order to flash the firmware onto an
/// instrument.
pub trait Flash {
/// The method to flash a firmware image to an instrument.
///
/// # Errors
/// An error can occur in the write to or reading from the instrument as well as in
/// reading the firmware image.
fn flash_firmware(&mut self, image: &[u8], firmware_info: Option<u16>) -> Result<()>;
}
178 changes: 178 additions & 0 deletions src/instrument/info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Define the trait and datatypes necessary to describe an instrument.
use crate::{error::Result, usbtmc::UsbtmcAddr, InstrumentError};
use std::{
fmt::Display,
io::{Read, Write},
net::SocketAddr,
time::Duration,
};

/// A generic connection address that covers all the different connection types.
/// Each device interface type will also have a [`TryFrom`] impl that converts from
/// this enum to itself. [`From`] is **not** implemented because the conversion could
/// fail.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConnectionAddr {
/// A LAN connection is created with a [`SocketAddr`], which includes an IpAddr and
/// a port for the connection.
Lan(SocketAddr),

/// A USBTMC connection is created with a [`UsbtmcAddr`].
Usbtmc(UsbtmcAddr),
//Add other device interface types here
Unknown,
}

impl Display for ConnectionAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Lan(lan_info) => lan_info.to_string(),
Self::Usbtmc(usb_info) => usb_info.to_string(),
Self::Unknown => "<UNKNOWN>".to_string(),
};
write!(f, "{s}")
}
}

/// The information about an instrument.
#[allow(clippy::module_name_repetitions)]
#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct InstrumentInfo {
/// The human-readable name of the vendor that makes the instrument
pub vendor: Option<String>,
/// The model of the instrument
pub model: Option<String>,
/// The serial number of the instrument
pub serial_number: Option<String>,
/// The firmware revision of the instrument.
pub firmware_rev: Option<String>,
/// The [`ConnectionAddr`] of the instrument.
#[serde(skip)]
pub address: Option<ConnectionAddr>,
}

/// Get the [`InstrumentInfo`] from the given object that implements [`Read`] and
/// [`Write`].
///
/// # Errors
/// The following error classes can occur:
/// - Any [`std::io::Error`] that can occur with a [`Read`] or [`Write`] call
/// - Any error in converting the retrieved IDN string into [`InstrumentInfo`]
#[allow(clippy::module_name_repetitions)]
pub fn get_info<T: Read + Write + ?Sized>(rw: &mut T) -> Result<InstrumentInfo> {
rw.write_all(b"*IDN?\n")?;
let mut info: Option<InstrumentInfo> = None;
for _ in 0..100 {
std::thread::sleep(Duration::from_millis(100));

let mut buf = vec![0u8; 100];
let _ = rw.read(&mut buf)?;
let first_null = buf.iter().position(|&x| x == b'\0').unwrap_or(buf.len());
let buf = &buf[..first_null];
if let Ok(i) = buf.try_into() {
info = Some(i);
break;
}
}
info.ok_or(InstrumentError::InformationRetrievalError {
details: "unable to read instrument info".to_string(),
})
}

/// A trait to get the information from an instrument.
pub trait Info: Read + Write {
/// Get the information for the instrument.
///
/// # Errors
/// [`TeaspoonInstrumentError::InformationRetrievalError`] if an instrument did not
/// return the requested information.
fn info(&mut self) -> Result<InstrumentInfo> {
get_info(self)
}
}

impl TryFrom<&[u8]> for InstrumentInfo {
type Error = InstrumentError;

fn try_from(idn: &[u8]) -> std::result::Result<Self, Self::Error> {
let parts: Vec<&[u8]> = idn
.split(|c| *c == b',' || *c == b'\n' || *c == b'\0')
.collect();

let (vendor, model, serial_number, firmware_rev) = match &parts[..] {
&[v, m, s, f, ..] => {
let fw_rev = String::from_utf8_lossy(f)
.to_string()
.trim_end_matches(|c| c == char::from(0))
.trim()
.to_string();
(
Some(String::from_utf8_lossy(v).to_string()),
String::from_utf8_lossy(m)
.to_string()
.split("MODEL ")
.last()
.map(std::string::ToString::to_string),
Some(String::from_utf8_lossy(s).to_string()),
Some(fw_rev),
)
}
_ => {
return Err(InstrumentError::InformationRetrievalError {
details: "unable to parse instrument information".to_string(),
});
}
};

if model.is_none() {
return Err(InstrumentError::InformationRetrievalError {
details: "unable to parse model".to_string(),
});
}

Ok(Self {
vendor,
model,
serial_number,
firmware_rev,
address: None,
})
}
}

impl Display for InstrumentInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let vendor = self.vendor.as_ref().map_or_else(
|| String::from("<UNKNOWN VENDOR>"),
std::clone::Clone::clone,
);

let model: String = self
.model
.as_ref()
.map_or_else(|| String::from("<UNKNOWN MODEL>"), std::clone::Clone::clone);

let sn: String = self.serial_number.as_ref().map_or_else(
|| String::from("<UNKNOWN SERIAL NUMBER>"),
std::clone::Clone::clone,
);

let fw_rev = self.firmware_rev.as_ref().map_or_else(
|| String::from("<UNKNOWN FIRMWARE REVISION>"),
std::clone::Clone::clone,
);

let addr = self
.address
.as_ref()
.map_or_else(|| ConnectionAddr::Unknown, std::clone::Clone::clone);

if addr == ConnectionAddr::Unknown {
write!(f, "{vendor},MODEL {model},{sn},{fw_rev}")
} else {
write!(f, "{vendor},MODEL {model},{sn},{fw_rev},{addr}")
}
}
}
Loading

0 comments on commit 1acf97b

Please sign in to comment.