Rewrite the driver in Rust; move the testbench tools out
The repository is now only the HPCS 6500 driver: a Rust library (serial transport, every protocol command, block parsing, USBPcap captures, a simulated instrument) and the hpcs6500 CLI with the flags of the former hpcs6500.py, plus dump/capture for reverse engineering. The lamp station, lamp export, TM-30, label printer and LED calibration move to B4L/lamp-testbench. Golden tests pin the Rust output to the Python driver's: parsed values bit-exact, report text, CSV and number formatting. The Python driver is at tag python-final.
This commit is contained in:
1 parent
3ac7cd243b
commit
54242da04b
79 files changed
+23340
-3246
No files matched your search
+143
@@ -0,0 +1,143 @@
|
||||
//! The driver against the simulated instrument: command bytes, measurement
|
||||
//! sequences, supply handling, timeouts and cancellation.
|
||||
|
||||
use hpcs6500::protocol::{self, cmd};
|
||||
use hpcs6500::sim::Simulator;
|
||||
use hpcs6500::{Error, Hpcs6500, Mode};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn dev() -> Hpcs6500<Simulator> {
|
||||
Hpcs6500::with_transport(Simulator::sample(), "sim")
|
||||
}
|
||||
|
||||
fn cmds(d: &Hpcs6500<Simulator>) -> Vec<u8> {
|
||||
d.transport().log.iter().map(|f| f[1]).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identify_and_supply_commands() {
|
||||
let mut d = dev();
|
||||
assert_eq!(d.identify().unwrap(), "HPCS6500");
|
||||
d.set_mode(Mode::Dc).unwrap();
|
||||
d.set_dc_voltage(12.0).unwrap();
|
||||
d.set_dc_current(1.5).unwrap();
|
||||
d.set_ac_voltage(230.0).unwrap();
|
||||
d.set_ac_frequency(60.0).unwrap();
|
||||
d.set_integration_time(500.0).unwrap();
|
||||
let s = d.read_psu_settings().unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
s.mode,
|
||||
s.dc_voltage,
|
||||
s.dc_current,
|
||||
s.ac_voltage,
|
||||
s.ac_frequency
|
||||
),
|
||||
(Mode::Dc, 12.0, 1.5, 230.0, 60.0)
|
||||
);
|
||||
let log = &d.transport().log;
|
||||
assert_eq!(log[1], vec![0x8C, 0x7A, 0x01]);
|
||||
assert_eq!(log[2], protocol::frame_f32(cmd::SET_DC, 1, 12.0));
|
||||
assert_eq!(log[3], protocol::frame_f32(cmd::SET_DC, 0, 1.5));
|
||||
assert_eq!(log[6], vec![0x8C, 0x01, 0x20, 0xA1, 0x07, 0x00]); // 500 000 us
|
||||
d.psu_on().unwrap();
|
||||
assert!(d.transport().output_on);
|
||||
d.psu_off().unwrap();
|
||||
assert!(!d.transport().output_on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_reading_follows_the_vendor_sequence() {
|
||||
let mut d = dev();
|
||||
d.send_test_config(false).unwrap();
|
||||
let r = d.take_single_reading().unwrap();
|
||||
assert_eq!(r.device, "HPCS6500");
|
||||
assert!((r.flux_lm() - 483.7375).abs() < 1e-3);
|
||||
assert!(r.electrical.is_some());
|
||||
// 2B 2B, trigger (0E 02), polls, 13, 77, reset
|
||||
let c = cmds(&d);
|
||||
assert_eq!(&c[..3], &[cmd::TEST_CONFIG, cmd::TEST_CONFIG, cmd::START]);
|
||||
assert_eq!(d.transport().log[2], vec![0x8C, 0x0E, 0x02]);
|
||||
assert!(c[3..c.len() - 3].iter().all(|&x| x == cmd::POLL_STATE));
|
||||
assert_eq!(
|
||||
&c[c.len() - 3..],
|
||||
&[cmd::READ_MEASUREMENT, cmd::READ_ELECTRICAL, cmd::RESET]
|
||||
);
|
||||
assert!(!d.transport().output_on, "the supply is not touched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuous_reading_switches_the_supply_around_it() {
|
||||
let mut d = dev();
|
||||
let r = d.take_reading(true).unwrap();
|
||||
assert_eq!(r.spectrum.len(), 350);
|
||||
let c = cmds(&d);
|
||||
assert_eq!(c[0], cmd::PSU_OUTPUT);
|
||||
assert_eq!(d.transport().log[0], vec![0x8C, 0x72, 0x00]);
|
||||
assert_eq!(d.transport().log[1], vec![0x8C, 0x0E, 0x01]);
|
||||
assert_eq!(
|
||||
&c[c.len() - 4..],
|
||||
&[
|
||||
cmd::READ_MEASUREMENT,
|
||||
cmd::READ_ELECTRICAL,
|
||||
cmd::START,
|
||||
cmd::PSU_OUTPUT
|
||||
]
|
||||
);
|
||||
assert_eq!(d.transport().log.last().unwrap(), &vec![0x8C, 0x72, 0x01]);
|
||||
assert!(!d.transport().output_on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silent_instrument_times_out_and_the_supply_still_goes_off() {
|
||||
let mut d = dev();
|
||||
d.transport_mut().mute = vec![cmd::POLL_STATE];
|
||||
let mut sim = Simulator::sample();
|
||||
sim.mute = vec![cmd::IDENTIFY];
|
||||
let mut quiet = Hpcs6500::with_transport(sim, "sim");
|
||||
let t = Instant::now();
|
||||
assert!(matches!(
|
||||
quiet.identify(),
|
||||
Err(Error::BadResponse { cmd: 0, .. })
|
||||
));
|
||||
assert!(t.elapsed() >= Duration::from_secs(2) && t.elapsed() < Duration::from_secs(4));
|
||||
|
||||
// a reading whose data never arrives: cancel it instead of waiting 30 s
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
d.set_cancel_flag(stop.clone());
|
||||
let s2 = stop.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
s2.store(true, Ordering::SeqCst);
|
||||
});
|
||||
let t = Instant::now();
|
||||
let r = d.take_reading(true);
|
||||
assert!(matches!(r, Err(Error::Cancelled)), "{r:?}");
|
||||
assert!(t.elapsed() < Duration::from_secs(8));
|
||||
assert!(
|
||||
!d.transport().output_on,
|
||||
"supply off after the failed reading"
|
||||
);
|
||||
assert_eq!(d.transport().log.last().unwrap(), &vec![0x8C, 0x72, 0x01]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_ack_is_an_error() {
|
||||
let mut sim = Simulator::sample();
|
||||
sim.mute = vec![cmd::PSU_OUTPUT];
|
||||
let mut d = Hpcs6500::with_transport(sim, "sim");
|
||||
assert!(matches!(d.psu_on(), Err(Error::NoAck { cmd: 0x72, .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passive_read_and_replay_cycle_through_readings() {
|
||||
let mut sim = Simulator::sample();
|
||||
sim.polls_to_ready = 0;
|
||||
let mut d = Hpcs6500::with_transport(sim, "sim");
|
||||
let a = d.read_current().unwrap();
|
||||
let b = d.take_single_reading().unwrap();
|
||||
assert_eq!(a, b);
|
||||
assert_eq!(d.transport().served, 1);
|
||||
}
|
||||
Reference in new issue
Block a user