-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
copying tsp-instrument from tsp-toolkit-kic-cli
- Loading branch information
sjaffery
committed
Nov 23, 2023
1 parent
c4f6e8e
commit 1acf97b
Showing
26 changed files
with
5,759 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<()>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") | ||
} | ||
} | ||
} |
Oops, something went wrong.