From b8c25b7380d7e177d25442028b0326b6d49f9b93 Mon Sep 17 00:00:00 2001 From: "Sarver, Edwin" Date: Thu, 22 Aug 2024 14:11:52 -0400 Subject: [PATCH] Add 'kic-visa' Executable and call into it if visa is installed --- Cargo.lock | 153 ++++-- Cargo.toml | 7 + instrument-repl/src/repl.rs | 11 +- kic-discover/Cargo.toml | 2 + kic-visa/Cargo.toml | 26 + kic-visa/src/error.rs | 18 + kic-visa/src/main.rs | 987 ++++++++++++++++++++++++++++++++++++ kic-visa/src/process.rs | 87 ++++ kic/src/main.rs | 47 +- 9 files changed, 1248 insertions(+), 90 deletions(-) create mode 100644 kic-visa/Cargo.toml create mode 100644 kic-visa/src/error.rs create mode 100644 kic-visa/src/main.rs create mode 100644 kic-visa/src/process.rs diff --git a/Cargo.lock b/Cargo.lock index 944f1dc..f6d2ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -419,9 +419,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "shlex", ] @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.15" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -483,7 +483,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -779,7 +779,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -872,9 +872,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -1013,7 +1013,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -1303,6 +1303,25 @@ dependencies = [ "tsp-toolkit-kic-lib", ] +[[package]] +name = "kic-visa" +version = "0.17.1" +dependencies = [ + "anyhow", + "clap", + "colored", + "instrument-repl", + "regex", + "rpassword", + "serde", + "serde_json", + "thiserror", + "tracing", + "tracing-subscriber", + "tsp-toolkit-kic-lib", + "windows-sys 0.52.0", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1320,9 +1339,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libusb1-sys" @@ -1526,7 +1545,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -1573,7 +1592,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -1665,7 +1684,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -1694,7 +1713,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -1893,9 +1912,9 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -1903,7 +1922,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "http-body-util", @@ -1932,7 +1951,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -2135,29 +2154,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.207" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.207" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] name = "serde_json" -version = "1.0.124" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", @@ -2319,9 +2338,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.74" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", @@ -2333,6 +2352,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2348,20 +2370,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -2397,7 +2419,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -2437,9 +2459,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.39.2" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", @@ -2461,7 +2483,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -2576,7 +2598,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] @@ -2635,8 +2657,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tsp-toolkit-kic-lib" -version = "0.17.0" -source = "git+https://github.com/tektronix/tsp-toolkit-kic-lib.git?branch=task/implement-visa#b5667c2753039c291d576ba45d02e7b3588c1388" +version = "0.17.1" dependencies = [ "bytes", "chrono", @@ -2682,9 +2703,9 @@ dependencies = [ [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "untrusted" @@ -2807,7 +2828,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", "wasm-bindgen-shared", ] @@ -2841,7 +2862,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2893,6 +2914,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3050,16 +3101,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if 1.0.0", - "windows-sys 0.48.0", -] - [[package]] name = "zerocopy" version = "0.7.35" @@ -3078,7 +3119,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.75", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index befe139..09429ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "kic", + "kic-visa", "kic-discover", "instrument-repl", ] @@ -44,6 +45,9 @@ tracing = { version = "0.1.40", features = ["async-await"] } tracing-subscriber = { version = "0.3.18", features = ["json"] } tsp-toolkit-kic-lib = { git = "https://github.com/tektronix/tsp-toolkit-kic-lib.git", branch = "task/implement-visa" } +[workspace.features] +visa = ["tsp-toolkit-kic-lib/visa"] + [workspace.lints.rust] warnings = "deny" @@ -56,3 +60,6 @@ arithmetic_side_effects = "deny" [workspace.lints.rustdoc] all = "warn" missing_doc_code_examples = "warn" + +[patch."https://github.com/tektronix/tsp-toolkit-kic-lib.git"] +tsp-toolkit-kic-lib = { path="../tsp-toolkit-kic-lib" } diff --git a/instrument-repl/src/repl.rs b/instrument-repl/src/repl.rs index 9cdafc4..cca1864 100644 --- a/instrument-repl/src/repl.rs +++ b/instrument-repl/src/repl.rs @@ -633,16 +633,7 @@ impl Repl { }); }; let file = file.clone(); - let Ok(file) = file.parse::() else { - return Ok(Request::Usage( - InstrumentReplError::CommandError { - details: format!( - "expected file path, but unable to parse from \"{file}\"" - ), - } - .to_string(), - )); - }; + let file = file.parse::().unwrap(); //PathBuf::parse is infallible so unwrapping is OK here. if !file.is_file() { return Ok(Request::Usage( diff --git a/kic-discover/Cargo.toml b/kic-discover/Cargo.toml index 7d803a1..efe8efc 100644 --- a/kic-discover/Cargo.toml +++ b/kic-discover/Cargo.toml @@ -33,3 +33,5 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tsp-toolkit-kic-lib = { workspace = true } +[features] +visa=["tsp-toolkit-kic-lib/visa"] diff --git a/kic-visa/Cargo.toml b/kic-visa/Cargo.toml new file mode 100644 index 0000000..a43eb13 --- /dev/null +++ b/kic-visa/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "kic-visa" +description = "Keithley Instruments TSPĀ® communications commandline application with VISA support." +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +colored = { workspace = true } +instrument-repl = { workspace = true } +rpassword = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tsp-toolkit-kic-lib = { workspace = true, features=["visa"] } +regex = "1.10.3" +windows-sys = { version = "0.52.0", features = [ + "Win32_System_Console", + "Win32_Foundation", +] } + diff --git a/kic-visa/src/error.rs b/kic-visa/src/error.rs new file mode 100644 index 0000000..c0e7a23 --- /dev/null +++ b/kic-visa/src/error.rs @@ -0,0 +1,18 @@ +use thiserror::Error; + +/// Define errors that originate from this crate +#[derive(Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum KicError { + /// The user didn't provide required information or the information provided was + /// invalid + #[error("Error parsing arguments: {details}")] + ArgParseError { + /// The reason why the arguments failed to parse. + details: String, + }, + + /// Another user must relinquish the instrument before it can be logged into. + #[error("there is another session connected to the instrument that must logout")] + InstrumentLogoutRequired, +} diff --git a/kic-visa/src/main.rs b/kic-visa/src/main.rs new file mode 100644 index 0000000..6ec1df5 --- /dev/null +++ b/kic-visa/src/main.rs @@ -0,0 +1,987 @@ +#![feature(rustdoc_missing_doc_code_examples, stmt_expr_attributes)] +#![doc(html_logo_url = "../../../ki-comms_doc_icon.png")] + +//! The `kic` executable is a command-line tool that will allow a user to interact with +//! an instrument over all the media provided by the [`tsp-instrument`] crate. +//! This is done via an easy to understand command-line interface and, when +//! interactively connected to an instrument, with a REPL + +mod error; +use crate::error::KicError; + +mod process; +use crate::process::Process; + +use anyhow::Context; +use clap::{ + arg, builder::PathBufValueParser, command, value_parser, Arg, ArgAction, ArgMatches, Args, + Command, Subcommand, +}; +use colored::Colorize; +use instrument_repl::repl::{self}; +use regex::Regex; +use std::{ + collections::HashMap, + env::set_var, + fs::OpenOptions, + io::{stdin, Read, Write}, + net::{IpAddr, SocketAddr, TcpStream}, + path::PathBuf, + process::exit, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; +use tracing::{debug, error, info, instrument, level_filters::LevelFilter, trace, warn}; +use tracing_subscriber::{layer::SubscriberExt, Layer, Registry}; + +use tsp_toolkit_kic_lib::{ + instrument::Instrument, + interface::async_stream::AsyncStream, + protocol::Protocol, + usbtmc::{self, UsbtmcAddr}, + Interface, +}; + +#[derive(Debug, Subcommand)] +enum TerminateType { + /// Perform the given action over a LAN connection. + Lan(LanTerminateArgs), +} + +#[derive(Debug, Args)] +struct LanTerminateArgs { + /// The port to which to connect in order to terminate all other connections to the + /// instrument + #[arg(long, short = 'p', default_value = "5030")] + port: Option, + + /// The IP address of the instrument to connect to. + ip_addr: IpAddr, +} + +// hack to make sure we rebuild if either Cargo.toml changes, since `clap` gets +// information from there. +#[cfg(not(debug_assertions))] +const _: &str = include_str!("../Cargo.toml"); +#[cfg(not(debug_assertions))] +const _: &str = include_str!("../../Cargo.toml"); + +fn add_connection_subcommands( + command: impl Into, + additional_args: impl IntoIterator, +) -> Command { + let command: Command = command.into(); + + let mut lan = Command::new("lan") + .about("Perform the given action over a LAN connection") + .arg( + Arg::new("port") + .help("The port on which to connect to the instrument") + .short('p') + .long("port") + .value_parser(value_parser!(u16)) + .default_value("5025"), + ) + .arg( + Arg::new("ip_addr") + .help("The IP address of the instrument to connect to") + .required(true) + .value_parser(value_parser!(IpAddr)), + ); + + let mut visa = Command::new("visa") + .about("Perform the given action over the installed VISA driver") + .arg( + Arg::new("visa_resource_string") + .help("The VISA Resource String used to find the desired resource") + .required(true), + ); + + //TODO(Fix async USB): let mut usb = Command::new("usb") + // .about("Perform the given action over a USBTMC connection") + // .arg( + // Arg::new("addr") + // .help("The instrument address in the form of, for example, `USB:2461:012345` where the second part is the product id, and the third part is the serial number.") + // .required(true) + // .value_parser(value_parser!(UsbtmcAddr)), + // ); + + for arg in additional_args { + lan = lan.arg(arg.clone()); + + visa = visa.arg(arg.clone()); + //TODO(Fix async USB): usb = usb.arg(arg.clone()); + } + + command.subcommand(lan).subcommand(visa) //TODO(Fix async USB): .subcommand(usb) +} + +#[must_use] +fn cmds() -> Command { + command!() + .propagate_version(true) + .subcommand_required(true) + .allow_external_subcommands(true) + .arg( + Arg::new("log-file") + .short('l') + .long("log-file") + .required(false) + .help("Log to the given log file path. Can be used in conjunction with `--log-socket` and `--verbose`.") + .global(true) + .value_parser(PathBufValueParser::new()), + ) + .arg( + Arg::new("log-socket") + .short('s') + .long("log-socket") + .required(false) + .help("Log to the given socket (in IPv4 or IPv6 format with port number). Can be used in conjunction with `--log-file` and `--verbose`.") + .global(true) + .value_parser(clap::value_parser!(SocketAddr)), + ) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .required(false) + .help("Enable logging to stderr. Can be used in conjunction with `--log-file` and `--verbose`.") + .global(true) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-color") + .short('c') + .long("no-color") + .help("Turn off ANSI color and formatting codes") + .global(true) + .action(ArgAction::SetTrue), + ) + // This is mostly for subcommands, but is left here as an example. + // We want to find all `kic-*` applications and run it with this option in order to add the sub command here. + .subcommand(Command::new("print-description").hide(true)) + .subcommand({ + let cmd = Command::new("connect") + .about("Connect to an instrument over one of the provided interfaces"); + add_connection_subcommands(cmd, []) + }) + .subcommand({ + let cmd = Command::new("reset") + .about("Connect to an instrument, cancel any ongoing jobs, send *RST then exit."); + add_connection_subcommands(cmd, []) + }) + .subcommand({ + let cmd = Command::new("info") + .about("Get the IDN information about an instrument."); + add_connection_subcommands(cmd, [ + Arg::new("json") + .help("Print the instrument information in JSON format.") + .long("json") + .short('j') + .action(ArgAction::SetTrue) + ]) + }) + .subcommand({ + let cmd = Command::new("upgrade") + .about("Upgrade the firmware of an instrument or module."); + + add_connection_subcommands(cmd, [ + Arg::new("file") + .help("The file path of the firmware image.") + .required(true) + .value_parser(PathBufValueParser::new()), + + Arg::new("slot") + .short('m') + .long("slot") + .help("[VersaTest only] Update a module in given slot number instead of the VersaTest mainframe") + .required(false) + .value_parser(value_parser!(u16).range(1..=3)), + ]) + }) + .subcommand({ + let cmd = Command::new("script") + .about("Load the script onto the selected instrument"); + add_connection_subcommands(cmd, [ + Arg::new("file") + .required(true) + .help("The file path of the firmware image") + .value_parser(PathBufValueParser::new()), + + Arg::new("run") + .short('r') + .long("run") + .value_parser(value_parser!(bool)) + .default_value("true") + .default_missing_value("true") + .action(ArgAction::Set) + .help("Run the script immediately after loading. "), + + Arg::new("save") + .short('s') + .long("save") + .action(ArgAction::SetTrue) + .help("Save the script to the non-volatile memory of the instrument"), + ]) + }) + .subcommand({ + let cmd = Command::new("terminate") + .about("Terminate all the connections on the given instrument. Only supports LAN"); + TerminateType::augment_subcommands(cmd) + }) +} + +fn main() -> anyhow::Result<()> { + let parent_dir: Option = std::env::current_exe().map_or(None, |path| { + path.canonicalize() + .expect("should have canonicalized path") + .parent() + .map(std::convert::Into::into) + }); + let cmd = cmds(); + + let Ok((external_cmd_lut, mut cmd)) = find_subcommands_from_path(&parent_dir, cmd) else { + return Err(anyhow::Error::msg( + "Unable to search directory for possible subcommands.", + )); + }; + + let matches = cmd.clone().get_matches(); + + if matches.get_flag("no-color") { + set_var("NO_COLOR", "1"); + } + + let verbose: bool = matches.get_flag("verbose"); + let log_file: Option<&PathBuf> = matches.get_one("log-file"); + let log_socket: Option<&SocketAddr> = matches.get_one("log-socket"); + + match (verbose, log_file, log_socket) { + (true, Some(l), Some(s)) => { + let err = tracing_subscriber::fmt::layer() + .with_ansi(true) + .with_writer(std::io::stderr) + .with_filter(LevelFilter::INFO); + + let log = OpenOptions::new().append(true).create(true).open(l)?; + + let log = tracing_subscriber::fmt::layer() + .with_writer(log) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .with_ansi(false); + + let sock = TcpStream::connect(s)?; + let sock = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(sock)) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .json(); + + let logger = Registry::default() + .with(LevelFilter::TRACE) + .with(err) + .with(log) + .with(sock); + + tracing::subscriber::set_global_default(logger)?; + } + (true, Some(l), None) => { + let err = tracing_subscriber::fmt::layer() + .with_ansi(true) + .with_writer(std::io::stderr) + .with_filter(LevelFilter::INFO); + + let log = OpenOptions::new().append(true).create(true).open(l)?; + + let log = tracing_subscriber::fmt::layer() + .with_writer(log) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .with_ansi(false); + + let logger = Registry::default() + .with(LevelFilter::TRACE) + .with(err) + .with(log); + + tracing::subscriber::set_global_default(logger)?; + } + (false, Some(l), Some(s)) => { + let log = OpenOptions::new().append(true).create(true).open(l)?; + + let log = tracing_subscriber::fmt::layer() + .with_writer(log) + .with_ansi(false); + + let sock = TcpStream::connect(s)?; + let sock = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(sock)) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .json(); + + let logger = Registry::default() + .with(LevelFilter::TRACE) + .with(log) + .with(sock); + + tracing::subscriber::set_global_default(logger)?; + } + (false, Some(l), None) => { + let log = OpenOptions::new().append(true).create(true).open(l)?; + + let log = tracing_subscriber::fmt::layer() + .with_writer(log) + .with_ansi(false); + + let logger = Registry::default().with(LevelFilter::TRACE).with(log); + + tracing::subscriber::set_global_default(logger)?; + } + (true, None, Some(s)) => { + let err = tracing_subscriber::fmt::layer() + .with_ansi(true) + .with_writer(std::io::stderr); + + let sock = TcpStream::connect(s)?; + let sock = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(sock)) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .json(); + + let logger = Registry::default() + .with(LevelFilter::TRACE) + .with(err) + .with(sock); + + tracing::subscriber::set_global_default(logger)?; + } + (true, None, None) => { + let err = tracing_subscriber::fmt::layer() + .with_ansi(true) + .with_writer(std::io::stderr); + + let logger = Registry::default().with(LevelFilter::TRACE).with(err); + + tracing::subscriber::set_global_default(logger)?; + } + (false, None, Some(s)) => { + let sock = TcpStream::connect(s)?; + let sock = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(sock)) + .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new()) + .json(); + + let logger = Registry::default().with(LevelFilter::TRACE).with(sock); + + tracing::subscriber::set_global_default(logger)?; + } + (false, None, None) => {} + } + + info!("Application started"); + trace!( + "Application starting with the following args: {:?}", + std::env::args() + ); + + match matches.subcommand() { + Some(("print-description", _)) => { + println!("{}", clap::crate_description!()); + return Ok(()); + } + Some(("connect", sub_matches)) => { + return connect(sub_matches); + } + Some(("reset", sub_matches)) => { + return reset(sub_matches); + } + Some(("upgrade", sub_matches)) => { + return upgrade(sub_matches); + } + Some(("terminate", sub_matches)) => { + return terminate(sub_matches); + } + Some(("script", sub_matches)) => { + return script(sub_matches); + } + Some(("info", sub_matches)) => { + return info(sub_matches); + } + Some((ext, sub_matches)) => { + debug!("Subcommand '{ext}' not defined internally, checking external commands"); + if let Some((path, ..)) = external_cmd_lut.get(ext) { + trace!("Subcommand exists at '{path:?}'"); + + let mut args: Vec<_> = sub_matches + .get_many::("options") + .into_iter() + .flatten() + .cloned() + .collect(); + + if verbose { + args.push("--verbose".to_string()) + } + + if let Some(log_file) = log_file { + args.push("--log-file".to_string()); + args.push(log_file.to_str().unwrap().to_string()) + } + + if let Some(log_socket) = log_socket { + args.push("--log-socket".to_string()); + args.push(log_socket.to_string()); + } + + debug!("Replacing this executable with '{path:?}' args: {args:?}"); + + if let Err(e) = Process::new(path.clone(), args) + .exec_replace() + .context(format!("{ext} subcommand should launch in a child process")) + { + error!("{e}"); + return Err(e); + } + //Process::exec_replace() only returns to this function if there was a error. + } else { + let err = clap::Error::new(clap::error::ErrorKind::UnknownArgument); + error!("{err}"); + println!("{err}"); + cmd.print_help()?; + return Err(err.into()); + } + } + _ => unreachable!(), + } + + info!("Application closing"); + + Ok(()) +} + +#[derive(Debug)] +enum ConnectionType { + Lan(SocketAddr), + Usb(UsbtmcAddr), + Visa(String), +} + +impl ConnectionType { + fn try_from_arg_matches(args: &ArgMatches) -> anyhow::Result { + match args.subcommand() { + Some(("lan", sub_matches)) => { + let ip_addr: IpAddr = + *sub_matches.get_one::("ip_addr").ok_or_else(|| { + KicError::ArgParseError { + details: "no IP address provided".to_string(), + } + })?; + + let port: u16 = *sub_matches.get_one::("port").unwrap_or(&5025); + + let socket_addr = SocketAddr::new(ip_addr, port); + Ok(Self::Lan(socket_addr)) + } + Some(("visa", sub_matches)) => { + let visa_string: String = sub_matches + .get_one::("visa_resource_string") + .ok_or_else(|| KicError::ArgParseError { + details: "no VISA resource string provided".to_string(), + })? + .clone(); + + Ok(Self::Visa(visa_string)) + } + Some(("usb", sub_matches)) => { + let usb_addr: UsbtmcAddr = sub_matches + .get_one::("addr") + .ok_or_else(|| KicError::ArgParseError { + details: "no USB address provided".to_string(), + })? + .clone(); + + Ok(Self::Usb(usb_addr)) + } + Some((ct, _sub_matches)) => { + println!(); + Err(KicError::ArgParseError { + details: format!("unknown connection type: \"{ct}\""), + } + .into()) + } + None => unreachable!("connection type not specified"), + } + } +} + +#[instrument] +fn connect_sync_instrument(t: ConnectionType) -> anyhow::Result> { + info!("Synchronously connecting to instrument"); + let interface: Protocol = match t { + ConnectionType::Lan(addr) => { + (Box::new(TcpStream::connect(addr)?) as Box).into() + } + ConnectionType::Usb(addr) => { + (Box::new(usbtmc::Stream::try_from(addr)?) as Box).into() + } + ConnectionType::Visa(r) => Protocol::try_from_visa(r)?, + }; + trace!("Synchronously connected to interface"); + + trace!("Converting interface to instrument"); + let instrument: Box = interface.try_into()?; + trace!("Converted interface to instrument"); + info!("Successfully connected to instrument"); + Ok(instrument) +} + +#[instrument] +fn connect_async_instrument(t: ConnectionType) -> anyhow::Result> { + info!("Asynchronously connecting to instrument"); + let interface: Protocol = match t { + ConnectionType::Lan(addr) => Protocol::Raw(Box::new(AsyncStream::try_from(Arc::new( + TcpStream::connect(addr)?, + ) + as Arc)?)), + ConnectionType::Usb(addr) => { + tsp_toolkit_kic_lib::protocol::Protocol::Raw(Box::new(AsyncStream::try_from( + Arc::new(usbtmc::Stream::try_from(addr)?) as Arc, + )?)) + } + ConnectionType::Visa(r) => Protocol::try_from_visa(r)?, + }; + + trace!("Asynchronously connected to interface"); + + trace!("Converting interface to instrument"); + let instrument: Box = interface.try_into()?; + trace!("Converted interface to instrument"); + info!("Successfully connected to instrument"); + Ok(instrument) +} + +#[instrument(skip(inst))] +fn get_instrument_access(inst: &mut Box) -> anyhow::Result<()> { + info!("Configuring instrument for usage."); + debug!("Checking login"); + match inst.as_mut().check_login()? { + tsp_toolkit_kic_lib::instrument::State::Needed => { + trace!("Login required"); + inst.as_mut().login()?; + debug!("Login complete"); + } + tsp_toolkit_kic_lib::instrument::State::LogoutNeeded => { + return Err(KicError::InstrumentLogoutRequired.into()); + } + tsp_toolkit_kic_lib::instrument::State::NotNeeded => { + debug!("Login not required"); + } + }; + debug!("Checking instrument language"); + match inst.as_mut().get_language()? { + tsp_toolkit_kic_lib::instrument::CmdLanguage::Scpi => { + warn!("Instrument language set to SCPI, only TSP is supported. Prompting user..."); + eprintln!("Instrument command-set is not set to TSP. Would you like to change the command-set to TSP and reboot? (Y/n)"); + + let mut buf = String::new(); + stdin().read_line(&mut buf)?; + let buf = buf.trim(); + if buf.is_empty() || buf.contains(['Y', 'y']) { + debug!("User accepted language change on the instrument."); + info!("Changing instrument language to TSP."); + inst.as_mut() + .change_language(tsp_toolkit_kic_lib::instrument::CmdLanguage::Tsp)?; + info!("Instrument language changed to TSP."); + warn!("Instrument rebooting."); + inst.write_all(b"ki.reboot()\n")?; + eprintln!("Instrument rebooting, please reconnect after reboot completes."); + thread::sleep(Duration::from_millis(1500)); + info!("Exiting after instrument reboot"); + exit(0); + } + } + tsp_toolkit_kic_lib::instrument::CmdLanguage::Tsp => { + debug!("Instrument language already set to TSP, no change necessary."); + } + } + + info!("Instrument configured for usage"); + + Ok(()) +} + +#[instrument(skip(args))] +fn connect(args: &ArgMatches) -> anyhow::Result<()> { + info!("Connecting to instrument"); + trace!("args: {args:?}"); + eprintln!( + "\nKeithley TSP Shell\nType {} for more commands.\n", + ".help".bold() + ); + let conn = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + let mut instrument: Box = match connect_async_instrument(conn) { + Ok(i) => i, + Err(e) => { + error!("Error connecting to async instrument: {e}"); + return Err(e); + } + }; + + if let Err(e) = get_instrument_access(&mut instrument) { + error!("Error setting up instrument: {e}"); + return Err(e); + } + + let info = match instrument.info() { + Ok(i) => i, + Err(e) => { + error!("Error getting instrument info: {e}"); + return Err(e.into()); + } + }; + info!("IDN: {info}"); + eprintln!("{info}"); + + let mut repl = repl::Repl::new(instrument); + + info!("Starting instrument REPL"); + if let Err(e) = repl.start() { + error!("Error in REPL: {e}"); + } + + Ok(()) +} + +#[instrument(skip(args))] +fn upgrade(args: &ArgMatches) -> anyhow::Result<()> { + info!("Upgrading instrument"); + trace!("args: {args:?}"); + eprintln!("\nKeithley TSP Shell\n"); + + let lan = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + + let Some((_, args)) = args.subcommand() else { + unreachable!("arguments didn't exist") + }; + + let mut instrument: Box = match connect_sync_instrument(lan) { + Ok(i) => i, + Err(e) => { + error!("Error connecting to sync instrument: {e}"); + return Err(e); + } + }; + + if let Err(e) = get_instrument_access(&mut instrument) { + error!("Error setting up instrument: {e}"); + return Err(e); + } + + let info = match instrument.info() { + Ok(i) => i, + Err(e) => { + error!("Error getting instrument info: {e}"); + return Err(e.into()); + } + }; + info!("IDN: {info}"); + eprintln!("{info}"); + + let slot: Option = args.get_one::("slot").copied(); + let Some(file) = args.get_one::("file").cloned() else { + let e = KicError::ArgParseError { + details: "firmware file path was not provided".to_string(), + }; + error!("{e}"); + return Err(e.into()); + }; + + let mut image: Vec = Vec::new(); + + let mut file = match std::fs::File::open(file) { + Ok(file) => file, + Err(e) => { + error!("Error opening firmware file: {e}"); + return Err(e.into()); + } + }; + + if let Err(e) = file.read_to_end(&mut image) { + error!("Error reading firmware file: {e}"); + return Err(e.into()); + } + + eprintln!("Flashing instrument firmware. Please do NOT power off or disconnect."); + if let Err(e) = instrument.flash_firmware(&image, slot) { + error!("Error upgrading instrument: {e}"); + return Err(e.into()); + } + eprintln!("Flashing instrument firmware completed. Instrument will restart."); + info!("Instrument upgrade complete"); + Ok(()) +} + +fn script(args: &ArgMatches) -> anyhow::Result<()> { + info!("Loading script to instrument"); + trace!("args: {args:?}"); + + eprintln!("\nKeithley TSP Shell\n"); + + let conn = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + + let mut instrument: Box = match connect_sync_instrument(conn) { + Ok(i) => i, + Err(e) => { + error!("Error connecting to sync instrument: {e}"); + return Err(e); + } + }; + + if let Err(e) = get_instrument_access(&mut instrument) { + error!("Error setting up instrument: {e}"); + return Err(e); + } + + let info = match instrument.info() { + Ok(i) => i, + Err(e) => { + error!("Error getting instrument info: {e}"); + return Err(e.into()); + } + }; + info!("IDN: {info}"); + eprintln!("{info}"); + + let Some((_, args)) = args.subcommand() else { + unreachable!("arguments didn't exist") + }; + + let run: bool = *args.get_one::("run").unwrap_or(&true); + let save: bool = *args.get_one::("save").unwrap_or(&false); + + let Some(path) = args.get_one::("file").cloned() else { + let e = KicError::ArgParseError { + details: "script file path was not provided".to_string(), + }; + error!("{e}"); + return Err(e.into()); + }; + + let Some(stem) = path.file_stem() else { + let e = KicError::ArgParseError { + details: "unable to get file stem".to_string(), + }; + + error!("{e}"); + return Err(e.into()); + }; + + let stem = stem.to_string_lossy(); + + let re = Regex::new(r"[^A-Za-z\d_]"); + + match re { + Ok(re_res) => { + let result = re_res.replace_all(&stem, "_"); + + let script_name = format!("kic_{result}"); + + let mut script_content: Vec = Vec::new(); + + let mut file = match std::fs::File::open(path) { + Ok(f) => f, + Err(e) => { + error!("Error opening script file: {e}"); + return Err(e.into()); + } + }; + if let Err(e) = file.read_to_end(&mut script_content) { + error!("Error reading script file: {e}"); + return Err(e.into()); + } + + eprintln!("Loading script to instrument."); + instrument.write_script(script_name.as_bytes(), &script_content, save, run)?; + eprintln!("Script loading completed."); + info!("Script loading completed."); + } + Err(err_msg) => { + unreachable!("Issue with regex creation: {}", err_msg.to_string()); + } + } + + Ok(()) +} + +#[instrument(skip(args))] +fn reset(args: &ArgMatches) -> anyhow::Result<()> { + info!("Resetting instrument"); + let conn = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + let instrument: Box = match connect_sync_instrument(conn) { + Ok(i) => i, + Err(e) => { + error!("Error connecting to sync instrument: {e}"); + return Err(e); + } + }; + + // dropping the instrument will reset it appropriately. + drop(instrument); + + info!("Instrument reset"); + + Ok(()) +} + +#[instrument(skip(args))] +fn info(args: &ArgMatches) -> anyhow::Result<()> { + info!("Getting instrument info"); + let conn = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + let mut instrument: Box = match connect_sync_instrument(conn) { + Ok(i) => i, + Err(e) => { + error!("Error connecting to sync instrument: {e}"); + return Err(e); + } + }; + + let Some((_, args)) = args.subcommand() else { + unreachable!("arguments didn't exist") + }; + + let json: bool = *args.get_one::("json").unwrap_or(&true); + + let info = match instrument.info() { + Ok(i) => i, + Err(e) => { + error!("Error getting instrument info: {e}"); + return Err(e.into()); + } + }; + + trace!("print as json?: {json:?}"); + + let info: String = if json { + serde_json::to_string(&info)? + } else { + info.to_string() + }; + + info!("Information to print: {info}"); + println!("{info}"); + + Ok(()) +} + +#[instrument(skip(args))] +fn terminate(args: &ArgMatches) -> anyhow::Result<()> { + info!("Terminating existing operations"); + trace!("args: {args:?}"); + eprintln!("\nKeithley TSP Shell\n"); + + let connection = match ConnectionType::try_from_arg_matches(args) { + Ok(c) => c, + Err(e) => { + error!("Unable to parse connection information: {e}"); + return Err(e); + } + }; + match connection { + ConnectionType::Lan(socket) => { + let mut connection = match TcpStream::connect(socket) { + Ok(c) => c, + Err(e) => { + error!("{e}"); + return Err(e.into()); + } + }; + + if let Err(e) = connection.write_all(b"ABORT\n") { + error!("Unable to write 'ABORT': {e}"); + return Err(e.into()); + } + } + ConnectionType::Usb(_) => {} + ConnectionType::Visa(_) => {} + } + + info!("Operations terminated"); + + Ok(()) +} + +type FindSubcommands = (HashMap)>, Command); + +fn find_subcommands_from_path( + path: &Option, + mut cmd: Command, +) -> anyhow::Result { + let mut lut = HashMap::new(); + if let Some(ref dir) = path { + let contents: Vec = dir.read_dir()?.map(|de| de.unwrap().path()).collect(); + + for path in contents { + let filename = path + .file_stem() + .unwrap_or_default() + .to_str() + .unwrap_or_default(); + if path.is_file() && filename.contains("kic-") && !filename.contains("visa") { + let cmd_name = filename + .split("kic-") + .last() + .expect("should have been able to split filename") + .to_string(); + + let Ok(result) = std::process::Command::new(path.clone()) + .args(vec!["print-description"]) + .output() + else { + //ignore any issues. + continue; + }; + let result = String::from_utf8_lossy(&result.stdout).trim().to_string(); + lut.insert(cmd_name.clone(), (path.clone(), Some(result.clone()))); + + cmd = cmd.subcommand( + Command::new(cmd_name.clone()) + .about(result) + .allow_external_subcommands(true) + .arg(arg!( ...).trailing_var_arg(true)) + .override_help(format!("For help on this command, run `{0} {1} help` or `{0} {1} --help` instead.", "kic", cmd_name)) + ); + } + } + } + + Ok((lut, cmd)) +} diff --git a/kic-visa/src/process.rs b/kic-visa/src/process.rs new file mode 100644 index 0000000..c3dbdce --- /dev/null +++ b/kic-visa/src/process.rs @@ -0,0 +1,87 @@ +use std::{io::ErrorKind, path::PathBuf}; + +#[derive(Debug)] +pub struct Process { + path: PathBuf, + args: Vec, +} +impl Process { + pub fn new(path: PathBuf, args: I) -> Self + where + I: IntoIterator, + I::Item: AsRef, + { + Self { + path, + args: args.into_iter().map(|s| s.as_ref().to_string()).collect(), + } + } + + pub fn exec_replace(self) -> anyhow::Result<()> { + imp::exec_replace(&self) + } + + #[cfg_attr(unix, allow(dead_code))] + pub fn exec(&self) -> anyhow::Result<()> { + let exit = std::process::Command::new(&self.path) + .args(&self.args) + .spawn()? + .wait()?; + if exit.success() { + Ok(()) + } else { + Err(std::io::Error::new( + ErrorKind::Other, + format!( + "child process did not exit successfully: {}", + self.path.display() + ), + ) + .into()) + } + } +} + +#[cfg(windows)] +mod imp { + use std::io::ErrorKind; + + use super::Process; + use windows_sys::Win32::{ + Foundation::{BOOL, FALSE, TRUE}, + System::Console::SetConsoleCtrlHandler, + }; + + #[allow(clippy::missing_const_for_fn)] // This lint seems to be broken for this specific case + unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL { + TRUE + } + + pub(super) fn exec_replace(process: &Process) -> anyhow::Result<()> { + //Safety: This is an external handler that calls into the windows API. It is + // expected to be safe. + unsafe { + if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE { + return Err(std::io::Error::new( + ErrorKind::Other, + "Unable to set Ctrl+C Handler".to_string(), + ) + .into()); + } + } + + process.exec() + } +} + +#[cfg(unix)] +mod imp { + use crate::Process; + use std::os::unix::process::CommandExt; + + pub(super) fn exec_replace(process: &Process) -> anyhow::Result<()> { + let mut command = std::process::Command::new(&process.path); + command.args(&process.args); + Err(command.exec().into()) // Exec replaces the current application's program memory, therefore execution will + } +} diff --git a/kic/src/main.rs b/kic/src/main.rs index 1f48854..8331ef4 100644 --- a/kic/src/main.rs +++ b/kic/src/main.rs @@ -1,4 +1,4 @@ -#![feature(rustdoc_missing_doc_code_examples)] +#![feature(rustdoc_missing_doc_code_examples, stmt_expr_attributes)] #![doc(html_logo_url = "../../../ki-comms_doc_icon.png")] //! The `kic` executable is a command-line tool that will allow a user to interact with @@ -88,14 +88,6 @@ fn add_connection_subcommands( .value_parser(value_parser!(IpAddr)), ); - let mut visa = Command::new("visa") - .about("Perform the given action over the installed VISA driver") - .arg( - Arg::new("visa_resource_string") - .help("The VISA Resource String used to find the desired resource") - .required(true), - ); - //TODO(Fix async USB): let mut usb = Command::new("usb") // .about("Perform the given action over a USBTMC connection") // .arg( @@ -107,11 +99,10 @@ fn add_connection_subcommands( for arg in additional_args { lan = lan.arg(arg.clone()); - visa = visa.arg(arg.clone()); //TODO(Fix async USB): usb = usb.arg(arg.clone()); } - command.subcommand(lan).subcommand(visa) //TODO(Fix async USB): .subcommand(usb) + command.subcommand(lan) //TODO(Fix async USB): .subcommand(usb) } #[must_use] @@ -230,12 +221,33 @@ fn cmds() -> Command { } fn main() -> anyhow::Result<()> { + eprintln!("args: {:?}", std::env::args().skip(1)); let parent_dir: Option = std::env::current_exe().map_or(None, |path| { path.canonicalize() .expect("should have canonicalized path") .parent() .map(std::convert::Into::into) }); + + if tsp_toolkit_kic_lib::is_visa_installed() { + #[cfg(target_os = "windows")] + let kic_visa_exe: Option = parent_dir.clone().map(|d| d.join("kic-visa.exe")); + + #[cfg(target_family = "unix")] + let kic_visa_exe: Option = parent_dir.clone().map(|d| d.join("kic-visa")); + + if let Some(kv) = kic_visa_exe { + if kv.exists() { + Process::new(kv.clone(), std::env::args().skip(1)) + .exec_replace() + .context(format!( + "{} should have been launched because VISA was detected", + kv.display(), + ))?; + return Ok(()); + } + } + } let cmd = cmds(); let Ok((external_cmd_lut, mut cmd)) = find_subcommands_from_path(&parent_dir, cmd) else { @@ -459,7 +471,6 @@ fn main() -> anyhow::Result<()> { enum ConnectionType { Lan(SocketAddr), Usb(UsbtmcAddr), - Visa(String), } impl ConnectionType { @@ -478,16 +489,7 @@ impl ConnectionType { let socket_addr = SocketAddr::new(ip_addr, port); Ok(Self::Lan(socket_addr)) } - Some(("visa", sub_matches)) => { - let visa_string: String = sub_matches - .get_one::("visa_resource_string") - .ok_or_else(|| KicError::ArgParseError { - details: "no VISA resource string provided".to_string(), - })? - .clone(); - Ok(Self::Visa(visa_string)) - } Some(("usb", sub_matches)) => { let usb_addr: UsbtmcAddr = sub_matches .get_one::("addr") @@ -520,7 +522,6 @@ fn connect_sync_instrument(t: ConnectionType) -> anyhow::Result { (Box::new(usbtmc::Stream::try_from(addr)?) as Box).into() } - ConnectionType::Visa(r) => Protocol::try_from_visa(r)?, }; trace!("Synchronously connected to interface"); @@ -544,7 +545,6 @@ fn connect_async_instrument(t: ConnectionType) -> anyhow::Result, )?)) } - ConnectionType::Visa(r) => Protocol::try_from_visa(r)?, }; trace!("Asynchronously connected to interface"); @@ -928,7 +928,6 @@ fn terminate(args: &ArgMatches) -> anyhow::Result<()> { } } ConnectionType::Usb(_) => {} - ConnectionType::Visa(_) => {} } info!("Operations terminated");