//! `hpcs6500`: command line tool for the HPCS 6500. //! //! The flags are those of the former Python tool (`hpcs6500.py`), so //! `uv run hpcs6500.py --continuous --csv out.csv` becomes //! `hpcs6500 --continuous --csv out.csv`. mod capture; use clap::{Parser, Subcommand}; use hpcs6500::format::{csv_header, csv_row, format_quick, format_reading, pcap_report}; use hpcs6500::sim::Simulator; use hpcs6500::{Error, Hpcs6500, Mode, Reading, Transport, pcap}; use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; #[derive(Parser)] #[command( name = "hpcs6500", version, about = "HPCS 6500 Spectrophotometer Reader" )] struct Cli { #[command(subcommand)] command: Option, /// COM port (auto-detect if not specified) #[arg(long)] port: Option, /// Continuous reading mode #[arg(long)] continuous: bool, /// With --continuous: stop after this many readings #[arg(long, value_name = "N")] count: Option, /// Save readings to CSV file #[arg(long, value_name = "FILE")] csv: Option, /// Show full spectrum data #[arg(long)] spectrum: bool, /// Passive mode: read data without controlling instrument #[arg(long)] passive: bool, /// Quick test mode: only report lumen and CCT #[arg(long)] quick: bool, /// Show harmonics data (voltage/current THD and per-harmonic) #[arg(long)] harmonics: bool, /// Parse a pcap capture file instead #[arg(long, value_name = "PCAP")] parse: Option, /// Print each reading as one JSON line on stdout (messages go to stderr) #[arg(long)] json: bool, /// Talk to a simulated instrument instead of the serial port; replays /// the readings of a USB capture, or a built-in sample without one #[arg(long, value_name = "PCAP", num_args = 0..=1, default_missing_value = "")] simulate: Option, /// Set output mode #[arg(long, help_heading = "Power supply")] mode: Option, /// Set output voltage (V) #[arg(long, help_heading = "Power supply")] voltage: Option, /// Set AC frequency (Hz) #[arg(long, help_heading = "Power supply")] frequency: Option, /// Set DC current limit (A) #[arg(long, help_heading = "Power supply")] current: Option, /// Set integration time (ms), 0=auto #[arg(long, help_heading = "Power supply")] integration: Option, /// Turn PSU output on #[arg(long, help_heading = "Power supply")] psu_on: bool, /// Turn PSU output off #[arg(long, help_heading = "Power supply")] psu_off: bool, /// Don't auto-control PSU during measurements #[arg(long, help_heading = "Power supply")] no_psu: bool, /// Read power supply settings #[arg(long, help_heading = "Power supply")] psu_status: bool, } #[derive(Subcommand)] enum Command { /// List serial ports (the HPCS 6500 is marked) Ports, /// Show the USB traffic in a capture and save the TX/RX byte streams Dump { pcap: PathBuf, /// Packets to show in the timeline #[arg(long, default_value_t = 100)] limit: usize, /// Do not write _TX.bin / _RX.bin #[arg(long)] no_bin: bool, }, /// Capture the USB traffic of the vendor software with USBPcap (Windows, run as administrator) Capture { /// USB device address of the instrument on its hub #[arg(long, default_value_t = 4)] address: u32, #[arg(long, default_value = "captures")] out_dir: PathBuf, #[arg(long, default_value = r"C:\Program Files\USBPcap\USBPcapCMD.exe")] usbpcap: PathBuf, }, } /// Human messages: stdout normally, stderr with --json. macro_rules! say { ($json:expr, $($t:tt)*) => { if $json { eprintln!($($t)*) } else { println!($($t)*) } }; } fn ok(r: &hpcs6500::Result<()>) -> &'static str { if r.is_ok() { "OK" } else { "FAILED" } } fn main() { let cli = Cli::parse(); let code = match &cli.command { Some(Command::Ports) => ports(), Some(Command::Dump { pcap, limit, no_bin, }) => capture::dump(pcap, *limit, !*no_bin), Some(Command::Capture { address, out_dir, usbpcap, }) => capture::capture(*address, out_dir, usbpcap), None => run(cli), }; std::process::exit(code); } fn ports() -> i32 { for p in hpcs6500::list_ports() { println!( " {}: {} [{}]{}", p.name, p.description, p.hwid, if p.is_hpcs { " <- HPCS 6500" } else { "" } ); } 0 } fn run(mut cli: Cli) -> i32 { // --harmonics implies --spectrum for the detailed display if cli.harmonics { cli.spectrum = true; } if let Some(path) = &cli.parse { return match pcap::read_messages(path) { Ok(msgs) if cli.json => { for r in pcap::readings(&msgs) { println!("{}", serde_json::to_string(&r).expect("json")); } 0 } Ok(msgs) => { print!("{}", pcap_report(&msgs, cli.quick, cli.spectrum)); 0 } Err(e) => { eprintln!("ERROR: {e}"); 1 } }; } match &cli.simulate { Some(src) => { let sim = if src.is_empty() { Ok(Simulator::sample()) } else { Simulator::from_pcap(src) }; match sim { Ok(s) => { let label = if src.is_empty() { "simulator (built-in sample)".to_string() } else { format!("simulator ({src})") }; session(&cli, Hpcs6500::with_transport(s, label)) } Err(e) => { eprintln!("ERROR: {e}"); 1 } } } None => { let Some(port) = cli.port.clone().or_else(hpcs6500::find_port) else { println!("ERROR: HPCS 6500 not found. Connect the device or specify --port."); println!("\nAvailable ports:"); ports(); return 1; }; match Hpcs6500::open(&port) { Ok(dev) => session(&cli, dev), Err(e) => { eprintln!("ERROR: cannot open {port}: {e}"); 1 } } } } } fn session(cli: &Cli, mut dev: Hpcs6500) -> i32 { let j = cli.json; let stop = Arc::new(AtomicBool::new(false)); { let stop = stop.clone(); let _ = ctrlc::set_handler(move || stop.store(true, Ordering::SeqCst)); } dev.set_cancel_flag(stop.clone()); say!(j, "HPCS 6500 Spectrophotometer Reader"); say!(j, "Port: {}", dev.port()); say!(j, "{}", "=".repeat(60)); match dev.identify() { Ok(name) => say!(j, "Device: {name}"), Err(_) => say!(j, "Warning: no identify response (device may be busy)"), } let has_psu_cmd = cli.mode.is_some() || cli.voltage.is_some() || cli.frequency.is_some() || cli.current.is_some() || cli.integration.is_some() || cli.psu_status || cli.psu_on || cli.psu_off; if cli.psu_status { match dev.read_psu_settings() { Ok(s) => { say!(j, "\nPower Supply Settings:"); say!(j, " Mode: {}", s.mode); say!(j, " AC Voltage: {:.0} V", s.ac_voltage); say!(j, " AC Frequency: {:.0} Hz", s.ac_frequency); say!(j, " DC Voltage: {:.1} V", s.dc_voltage); say!(j, " DC Current: {:.1} A", s.dc_current); } Err(_) => say!(j, "Failed to read PSU settings"), } if !cli.continuous && cli.voltage.is_none() && cli.mode.is_none() { return 0; } } if let Some(m) = cli.mode { let r = dev.set_mode(m); say!(j, "Set mode to {m}: {}", ok(&r)); } if let Some(v) = cli.voltage { let mode = cli.mode.unwrap_or(Mode::Ac); let r = if mode == Mode::Ac { dev.set_ac_voltage(v as f32) } else { dev.set_dc_voltage(v as f32) }; say!( j, "Set {mode} voltage to {} V: {}", hpcs6500::pyfmt::repr(v), ok(&r) ); } if let Some(f) = cli.frequency { let r = dev.set_ac_frequency(f as f32); say!( j, "Set AC frequency to {} Hz: {}", hpcs6500::pyfmt::repr(f), ok(&r) ); } if let Some(a) = cli.current { let r = dev.set_dc_current(a as f32); say!( j, "Set DC current limit to {} A: {}", hpcs6500::pyfmt::repr(a), ok(&r) ); } if let Some(ms) = cli.integration { let r = dev.set_integration_time(ms); say!( j, "Set integration time to {} ms: {}", hpcs6500::pyfmt::repr(ms), ok(&r) ); } if cli.psu_on { let r = dev.psu_on(); say!(j, "PSU output ON: {}", ok(&r)); } if cli.psu_off { let r = dev.psu_off(); say!(j, "PSU output OFF: {}", ok(&r)); } // only supply commands, no measurement requested if has_psu_cmd && !cli.continuous && !cli.passive { return 0; } let mut csv = None; if let Some(path) = &cli.csv { match File::create(path) { Ok(mut f) => { let _ = f.write_all(csv_header().as_bytes()); say!(j, "Saving to: {}", path.display()); csv = Some(f); } Err(e) => { eprintln!("ERROR: {}: {e}", path.display()); return 1; } } } // the supply is only controlled in continuous mode let use_psu = cli.continuous && !cli.no_psu && !cli.passive; let mut code = 0; if use_psu { let _ = dev.psu_on(); std::thread::sleep(Duration::from_millis(200)); } let mut n = 0; loop { if stop.load(Ordering::SeqCst) { say!(j, "\nStopped."); break; } n += 1; if !cli.quick { say!(j, "\n{}", "=".repeat(60)); say!( j, "Reading {n} ({})", chrono::Local::now().format("%H:%M:%S") ); say!(j, "{}", "=".repeat(60)); } let result: hpcs6500::Result = if cli.passive { dev.read_current() } else if cli.continuous { dev.take_reading(false) } else { dev.take_single_reading() }; let r = match result { Ok(r) => r, Err(Error::Cancelled) => { say!(j, "\nStopped."); break; } Err(e) => { say!(j, "Failed to get reading ({e})"); if !cli.continuous { code = 1; break; } let _ = dev.pause(Duration::from_secs(1)); continue; } }; if j { println!("{}", serde_json::to_string(&r).expect("json")); } else if cli.quick { println!("{}", format_quick(&r, Some(n))); } else { println!("{}", format_reading(&r, cli.spectrum)); } if let Some(f) = csv.as_mut() { let now = chrono::Local::now().naive_local(); let ts = if now.and_utc().timestamp_subsec_micros() == 0 { now.format("%Y-%m-%dT%H:%M:%S").to_string() } else { now.format("%Y-%m-%dT%H:%M:%S%.6f").to_string() }; let _ = f.write_all(csv_row(&ts, &r).as_bytes()); let _ = f.flush(); } if !cli.continuous || cli.count.is_some_and(|c| n >= c) { break; } if dev.pause(Duration::from_millis(500)).is_err() { say!(j, "\nStopped."); break; } } if use_psu { let _ = dev.psu_off(); } let _ = dev.reset(); if let Some(path) = &cli.csv { say!(j, "Data saved to: {}", path.display()); } code }