| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::protocol::{self, ParameterValue, ReportFormat, Value, MAX_PACKET_SIZE}, |
| anyhow::{bail, format_err, Error}, |
| serde::Serialize, |
| serde_json as json, |
| std::{ |
| cell::RefCell, |
| collections::HashMap, |
| io::{Read, Write}, |
| os::raw::{c_uchar, c_ushort}, |
| time::SystemTime, |
| }, |
| usb_bulk::{InterfaceInfo, Open}, |
| }; |
| |
| const GOOGLE_VENDOR_ID: c_ushort = 0x18d1; |
| const ZEDMON_PRODUCT_ID: c_ushort = 0xaf00; |
| const VENDOR_SPECIFIC_CLASS_ID: c_uchar = 0xff; |
| const ZEDMON_SUBCLASS_ID: c_uchar = 0xff; |
| const ZEDMON_PROTOCOL_ID: c_uchar = 0x00; |
| |
| /// Matches the USB interface info of a Zedmon device. |
| fn zedmon_match(ifc: &InterfaceInfo) -> bool { |
| (ifc.dev_vendor == GOOGLE_VENDOR_ID) |
| && (ifc.dev_product == ZEDMON_PRODUCT_ID) |
| && (ifc.ifc_class == VENDOR_SPECIFIC_CLASS_ID) |
| && (ifc.ifc_subclass == ZEDMON_SUBCLASS_ID) |
| && (ifc.ifc_protocol == ZEDMON_PROTOCOL_ID) |
| } |
| |
| /// Used by ZedmonClient to determine when data reporting should stop. |
| pub trait StopSignal { |
| fn should_stop(&mut self) -> Result<bool, Error>; |
| } |
| |
| /// Properties that can be queried using ZedmonClient::describe. |
| pub const DESCRIBABLE_PROPERTIES: [&'static str; 2] = ["shunt_resistance", "csv_header"]; |
| |
| /// A single record of output from ZedmonClient. |
| #[derive(Debug, Default, Serialize)] |
| struct ZedmonRecord { |
| timestamp_micros: u64, |
| shunt_voltage: f32, |
| bus_voltage: f32, |
| power: f32, |
| } |
| |
| /// Interface to a Zedmon device. |
| #[derive(Debug)] |
| pub struct ZedmonClient<InterfaceType> |
| where |
| InterfaceType: usb_bulk::Open<InterfaceType> + Read + Write, |
| { |
| /// USB interface to the Zedmon device, or equivalent. |
| interface: RefCell<InterfaceType>, |
| |
| /// Format of each field in a Report. |
| field_formats: Vec<ReportFormat>, |
| |
| /// Information necessary to obtain power data from direct Zedmon measurements. |
| shunt_resistance: f32, |
| v_shunt_index: usize, |
| v_bus_index: usize, |
| } |
| |
| impl<InterfaceType: usb_bulk::Open<InterfaceType> + Read + Write> ZedmonClient<InterfaceType> { |
| /// Enumerates all connected Zedmons. Returns a `Vec<String>` of their serial numbers. |
| fn enumerate() -> Vec<String> { |
| let mut serials = Vec::new(); |
| |
| // Instead of matching any devices, this callback extracts Zedmon serial numbers as |
| // InterfaceType::open iterates through them. InterfaceType::open is expected to return an |
| // error because no devices match. |
| let mut cb = |info: &InterfaceInfo| -> bool { |
| if zedmon_match(info) { |
| let null_pos = match info.serial_number.iter().position(|&c| c == 0) { |
| Some(p) => p, |
| None => { |
| eprintln!("Warning: Detected a USB device whose serial number was not null-terminated:"); |
| eprintln!( |
| "{}", |
| (*String::from_utf8_lossy(&info.serial_number)).to_string() |
| ); |
| return false; |
| } |
| }; |
| serials |
| .push((*String::from_utf8_lossy(&info.serial_number[..null_pos])).to_string()); |
| } |
| false |
| }; |
| |
| assert!( |
| InterfaceType::open(&mut cb).is_err(), |
| "open() should return an error, as the supplied callback cannot match any devices." |
| ); |
| serials |
| } |
| |
| // Number of USB packets that can be queued in Zedmon's USB interface, based on STM32F072 |
| // hardware limitations. The firmware currently only enqueues one packet, but the STM32F072 does |
| // support double-buffering. |
| const ZEDMON_USB_QUEUE_SIZE: u32 = 2; |
| |
| /// Disables reporting and drains all enqueued packets from `interface`. |
| /// |
| /// This should be done if a packet of incorrect type is received on the first read from |
| /// `interface`. Typically, that scenario occurs if a previous invocation of the client |
| /// terminated irregularly and left reporting enabled, but in principle a packet could still be |
| /// enqueued from another request as well. |
| /// |
| /// Empirically, this process takes 4-5x as long as the timeout configured on `interface`, and |
| /// it should not be performed unconditionally to avoid unnecessary delays. |
| /// |
| /// An error is returned if a packet is still received once the packet queue should be clear. |
| fn disable_reporting_and_drain_packets(interface: &mut InterfaceType) -> Result<(), Error> { |
| Self::disable_reporting(interface)?; |
| |
| // Read packets from the USB interface until an error (assumed to be due to lack of packets |
| // -- the reason for an error is not exposed) is encountered. If we do not encounter an |
| // error after reading more than the queue length, Zedmon is in an unexpected state. |
| let mut response = [0; MAX_PACKET_SIZE]; |
| for _ in 0..Self::ZEDMON_USB_QUEUE_SIZE + 1 { |
| if interface.read(&mut response).is_err() { |
| return Ok(()); |
| } |
| } |
| return Err(format_err!( |
| "The Zedmon device is in an unexpected state; received more than {} packets after \ |
| disabling reporting. Consider rebooting the device.", |
| Self::ZEDMON_USB_QUEUE_SIZE |
| )); |
| } |
| |
| /// Creates a new ZedmonClient instance. |
| // TODO(fxbug.dev/61148): Make the behavior predictable if multiple Zedmons are attached. |
| fn new(mut interface: InterfaceType) -> Result<Self, Error> { |
| // Query parameters, disabling reporting and draining packets if a packet of the wrong type |
| // is received on the first attempt. |
| let parameters = match Self::get_parameters(&mut interface) { |
| Err(e) => match e.downcast_ref::<protocol::Error>() { |
| Some(protocol::Error::WrongPacketType { .. }) => { |
| eprintln!( |
| "WARNING: Received unexpected packet type while initializing; a prior \ |
| client invocation may have not terminated cleanly. Disabling reporting and \ |
| draining buffered packets. Be sure to terminate recording with ENTER." |
| ); |
| Self::disable_reporting_and_drain_packets(&mut interface)?; |
| Self::get_parameters(&mut interface) |
| } |
| _ => Err(e), |
| }, |
| ok => ok, |
| }?; |
| |
| let field_formats = Self::get_field_formats(&mut interface)?; |
| |
| let shunt_resistance = { |
| let value = parameters["shunt_resistance"]; |
| if let Value::F32(v) = value { |
| v |
| } else { |
| bail!("Wrong value type for shunt_resistance: {:?}", value); |
| } |
| }; |
| |
| // Use a HashMap to assist in field lookup for simplicity. Note that the Vec<ReportFormat> |
| // representation needs to be retained for later Report-parsing. |
| let formats_by_name: HashMap<String, ReportFormat> = |
| field_formats.iter().map(|f| (f.name.clone(), f.clone())).collect(); |
| let v_shunt_index = formats_by_name["v_shunt"].index as usize; |
| let v_bus_index = formats_by_name["v_bus"].index as usize; |
| |
| Ok(Self { |
| interface: RefCell::new(interface), |
| field_formats, |
| shunt_resistance, |
| v_shunt_index, |
| v_bus_index, |
| }) |
| } |
| |
| /// Describes properties of the Zedmon device and/or ZedmonClient. |
| pub fn describe(&self, name: &str) -> Result<json::Value, Error> { |
| match name { |
| "shunt_resistance" => Ok(json::json!(self.shunt_resistance)), |
| "csv_header" => { |
| let mut writer = csv::Writer::from_writer(Vec::new()); |
| writer.serialize(ZedmonRecord::default())?; |
| let lines = String::from_utf8(writer.into_inner()?)?; |
| let header = lines.split('\n').nth(0).unwrap(); |
| Ok(json::json!(header)) |
| } |
| _ => panic!("'{}' is not a valid parameter name.", name), |
| } |
| } |
| |
| /// Retrieves a ParameterValue from the provided Zedmon interface. |
| fn get_parameter(interface: &mut InterfaceType, index: u8) -> Result<ParameterValue, Error> { |
| let request = protocol::encode_query_parameter(index); |
| interface.write(&request)?; |
| |
| let mut response = [0; MAX_PACKET_SIZE]; |
| let len = interface.read(&mut response)?; |
| |
| Ok(protocol::parse_parameter_value(&mut &response[0..len])?) |
| } |
| |
| /// Retrieves every ParameterValue from the provided Zedmon interface. |
| fn get_parameters(interface: &mut InterfaceType) -> Result<HashMap<String, Value>, Error> { |
| let mut parameters = HashMap::new(); |
| loop { |
| let parameter = Self::get_parameter(interface, parameters.len() as u8)?; |
| if parameter.name.is_empty() { |
| return Ok(parameters); |
| } |
| parameters.insert(parameter.name, parameter.value); |
| } |
| } |
| |
| /// Retrieves a ReportFormat from the provided Zedmon interface. |
| fn get_report_format(interface: &mut InterfaceType, index: u8) -> Result<ReportFormat, Error> { |
| let request = protocol::encode_query_report_format(index); |
| interface.write(&request)?; |
| |
| let mut response = [0; MAX_PACKET_SIZE]; |
| let len = interface.read(&mut response)?; |
| |
| Ok(protocol::parse_report_format(&response[..len])?) |
| } |
| |
| /// Retrieves the ReportFormat for each Report field from the provided Zedmon interface. |
| fn get_field_formats(interface: &mut InterfaceType) -> Result<Vec<ReportFormat>, Error> { |
| let mut all_fields = vec![]; |
| loop { |
| let format = Self::get_report_format(interface, all_fields.len() as u8)?; |
| if format.index == protocol::REPORT_FORMAT_INDEX_END { |
| return Ok(all_fields); |
| } |
| all_fields.push(format); |
| } |
| } |
| |
| /// Disables reporting on the Zedmon device. |
| fn disable_reporting(interface: &mut InterfaceType) -> Result<(), Error> { |
| let request = protocol::encode_disable_reporting(); |
| interface.write(&request)?; |
| Ok(()) |
| } |
| |
| /// Enables reporting on the Zedmon device. |
| fn enable_reporting(&self) -> Result<(), Error> { |
| let request = protocol::encode_enable_reporting(); |
| self.interface.borrow_mut().write(&request)?; |
| Ok(()) |
| } |
| |
| /// Reads reported data from the Zedmon device, taking care of enabling/disabling reporting. |
| /// |
| /// Measurement data is written to `writer`. Reporting will cease when `stopper` raises its stop |
| /// signal. |
| pub fn read_reports( |
| &self, |
| writer: Box<dyn Write + Send>, |
| mut stopper: impl StopSignal, |
| ) -> Result<(), Error> { |
| // This function's workload is shared between its main thread and `output_thread`. |
| // |
| // The main thread enables reporting, reads USB packets via blocking reads, and sends those |
| // packets to `output_thread` via `packet_sender`. When `stopper` indicates that reporting |
| // should stop, it drops `packet_sender` to close the channel. `output_thread` will still |
| // receive packets that have been sent before closure. |
| // |
| // Meanwhile, `output_thread` parses each packet it receives to a Vec<Report>, which it then |
| // formats and outputs via `writer`. |
| // |
| // The multithreading has not been confirmed as necessary for performance reasons, but it |
| // seems like a reasonable thing to do, as both reading from USB and outputting (typically |
| // to stdout or a file) involve blocking on I/O. |
| let (packet_sender, packet_receiver) = std::sync::mpsc::channel::<Vec<u8>>(); |
| |
| // Prepare data to move into `output_thread`. |
| let parser = protocol::ReportParser::new(&self.field_formats)?; |
| let shunt_resistance = self.shunt_resistance; |
| let v_shunt_index = self.v_shunt_index; |
| let v_bus_index = self.v_bus_index; |
| |
| let output_thread = std::thread::spawn(move || -> Result<(), Error> { |
| // The CSV header is suppressed. Clients may query it by using `describe`. |
| let mut writer = csv::WriterBuilder::new().has_headers(false).from_writer(writer); |
| |
| for buffer in packet_receiver.iter() { |
| let reports = parser.parse_reports(&buffer).unwrap(); |
| for report in reports.into_iter() { |
| let (shunt_voltage, bus_voltage) = |
| match (report.values[v_shunt_index], report.values[v_bus_index]) { |
| (Value::F32(x), Value::F32(y)) => (x, y), |
| t => panic!("Got wrong value types for (v_shunt, v_bus): {:?}", t), |
| }; |
| |
| let record = ZedmonRecord { |
| timestamp_micros: report.timestamp_micros, |
| shunt_voltage, |
| bus_voltage, |
| power: bus_voltage * shunt_voltage / shunt_resistance, |
| }; |
| writer.serialize(record)?; |
| } |
| } |
| writer.flush()?; |
| Ok(()) |
| }); |
| |
| // Enable reporting and run the main loop. |
| self.enable_reporting()?; |
| loop { |
| let mut buffer = vec![0; MAX_PACKET_SIZE]; |
| match self.interface.borrow_mut().read(&mut buffer) { |
| Err(e) => eprintln!("USB read error: {}", e), |
| Ok(bytes_read) => { |
| buffer.truncate(bytes_read); |
| packet_sender.send(buffer).unwrap(); |
| } |
| } |
| |
| if stopper.should_stop()? { |
| Self::disable_reporting(&mut self.interface.borrow_mut())?; |
| drop(packet_sender); |
| break; |
| } |
| } |
| |
| // Wait for the parsing thread to complete upon draining the channel buffer. |
| output_thread.join().unwrap().unwrap(); |
| Ok(()) |
| } |
| |
| /// Number of timestamp query round trips used to determine time offset. |
| const TIME_OFFSET_NUM_QUERIES: u32 = 10; |
| |
| /// Returns a tuple consisting of: |
| /// - An estimate of the offset between the Zedmon clock and the host clock, in nanoseconds. |
| /// The offset is defined such that, in the absence of drift, |
| /// `zedmon_clock + offset = host_time`. |
| /// It is typically, but not necessarily, positive. (Note that: (1) the signedness prevents |
| /// std::time::Duration from being a valid return type, and (2) i64 will suffice to |
| /// represent an offset of over 290 years in nanoseconds.) |
| /// - An estimate of the uncertainty in the offset, in nanoseconds. In the absence of clock |
| /// drift, the offset is accurate within ±uncertainty. |
| // TODO(fxbug.dev/61471): Consider using microseconds instead, in correspondence with Zedmon |
| // timestamp units. |
| pub fn get_time_offset_nanos(&self) -> Result<(i64, i64), Error> { |
| let mut interface = self.interface.borrow_mut(); |
| |
| // For each query, we estimate that the retrieved timestamp reflects Zedmon's clock halfway |
| // between the duration spanned by our QueryTime request and Zedmon's Timestamp response. |
| // That allows us to estimate the offset between the host clock and Zedmon's. |
| // |
| // We run `TIME_OFFSET_NUM_QUERIES` queries and keep the estimate corresponding to the |
| // shortest query duration. That provides some guarding against transient sources of |
| // latency. |
| // |
| // Note: Unlike other methods that interact with the USB interface, this method does not |
| // separate out a "get_timestamp" function to keep the parsing time from contributing to |
| // transit time. |
| let mut best_offset_nanos = 0; |
| let mut best_query_duration_nanos = i64::MAX; |
| |
| for _ in 0..Self::TIME_OFFSET_NUM_QUERIES { |
| let request = protocol::encode_query_time(); |
| let mut response = [0u8; MAX_PACKET_SIZE]; |
| |
| let host_clock_at_start = SystemTime::now(); |
| interface.write(&request)?; |
| let len = interface.read(&mut response)?; |
| |
| // `elapsed` could return an error if the host clock moved backwards. That should be |
| // rare, but if it does occur, throw out this query. (Note, however, that the offset |
| // would be invalidated by future jumps in the system clock.) |
| let query_duration_nanos = match host_clock_at_start.elapsed() { |
| Ok(duration) => duration.as_nanos() as i64, |
| Err(_) => continue, |
| }; |
| |
| if query_duration_nanos < best_query_duration_nanos { |
| let timestamp_micros = protocol::parse_timestamp_micros(&response[..len])? as i64; |
| let host_nanos_at_timestamp = |
| host_clock_at_start.duration_since(SystemTime::UNIX_EPOCH)?.as_nanos() as i64 |
| + query_duration_nanos / 2; |
| best_offset_nanos = host_nanos_at_timestamp - timestamp_micros * 1_000; |
| best_query_duration_nanos = query_duration_nanos; |
| } |
| } |
| |
| // Uncertainty comes from two sources: |
| // - Mapping a microsecond Zedmon timestamp to nanoseconds. In the worst case, the |
| // timestamp is 999ns stale. |
| // - Estimating the instant of the timestamp in the query interval. Since we used the |
| // midpoint, at worst we're off by one half the query duration. |
| let uncertainty_nanos = 999 + best_query_duration_nanos / 2 + best_query_duration_nanos % 2; |
| |
| Ok((best_offset_nanos, uncertainty_nanos)) |
| } |
| |
| /// Enables or disables the relay on the Zedmon device. |
| pub fn set_relay(&self, enable: bool) -> Result<(), Error> { |
| let request = protocol::encode_set_output(protocol::Output::Relay as u8, enable); |
| self.interface.borrow_mut().write(&request)?; |
| Ok(()) |
| } |
| } |
| |
| /// Lists the serial numbers of all connected Zedmons. |
| pub fn list() -> Vec<String> { |
| ZedmonClient::<usb_bulk::Interface>::enumerate() |
| } |
| |
| pub fn zedmon() -> ZedmonClient<usb_bulk::Interface> { |
| let interface = usb_bulk::Interface::open(&mut zedmon_match).unwrap(); |
| let result = ZedmonClient::new(interface); |
| if result.is_err() { |
| eprintln!("Error initializing ZedmonClient: {:?}", result); |
| } |
| result.unwrap() |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| anyhow::{format_err, Error}, |
| num::FromPrimitive, |
| protocol::{tests::serialize_reports, PacketType, Report, ScalarType}, |
| std::collections::VecDeque, |
| std::rc::Rc, |
| std::sync::{ |
| atomic::{AtomicBool, Ordering}, |
| mpsc, Arc, |
| }, |
| std::time::Duration, |
| test_util::assert_near, |
| }; |
| |
| // Used by `interface_info`, below, as a convenient means of constructing InterfaceInfo. |
| struct ShortInterface<'a> { |
| dev_vendor: ::std::os::raw::c_ushort, |
| dev_product: ::std::os::raw::c_ushort, |
| ifc_class: ::std::os::raw::c_uchar, |
| ifc_subclass: ::std::os::raw::c_uchar, |
| ifc_protocol: ::std::os::raw::c_uchar, |
| serial_number: &'a str, |
| } |
| |
| fn interface_info(short: ShortInterface<'_>) -> InterfaceInfo { |
| let mut serial = [0; 256]; |
| for (i, c) in short.serial_number.as_bytes().iter().enumerate() { |
| serial[i] = *c; |
| } |
| |
| InterfaceInfo { |
| dev_vendor: short.dev_vendor, |
| dev_product: short.dev_product, |
| dev_class: 0, |
| dev_subclass: 0, |
| dev_protocol: 0, |
| ifc_class: short.ifc_class, |
| ifc_subclass: short.ifc_subclass, |
| ifc_protocol: short.ifc_protocol, |
| has_bulk_in: 0, |
| has_bulk_out: 0, |
| writable: 0, |
| serial_number: serial, |
| device_path: [0; 256usize], |
| } |
| } |
| |
| #[test] |
| fn test_enumerate() { |
| // AVAILABLE_DEVICES is state for the static method FakeEnumerationInterface::open. This |
| // test is single-threaded, so a thread-local static provides the most appropriate safe |
| // interface. |
| thread_local! { |
| static AVAILABLE_DEVICES: RefCell<Vec<InterfaceInfo>> = RefCell::new(Vec::new()); |
| } |
| fn push_device(short: ShortInterface<'_>) { |
| AVAILABLE_DEVICES.with(|devices| { |
| devices.borrow_mut().push(interface_info(short)); |
| }); |
| } |
| |
| struct FakeEnumerationInterface {} |
| |
| impl usb_bulk::Open<FakeEnumerationInterface> for FakeEnumerationInterface { |
| fn open<F>(matcher: &mut F) -> Result<FakeEnumerationInterface, Error> |
| where |
| F: FnMut(&InterfaceInfo) -> bool, |
| { |
| AVAILABLE_DEVICES.with(|devices| { |
| let devices = devices.borrow(); |
| for device in devices.iter() { |
| if matcher(device) { |
| return Ok(FakeEnumerationInterface {}); |
| } |
| } |
| Err(format_err!("No matching devices found.")) |
| }) |
| } |
| } |
| |
| impl Read for FakeEnumerationInterface { |
| fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> { |
| Ok(0) |
| } |
| } |
| |
| impl Write for FakeEnumerationInterface { |
| fn write(&mut self, _: &[u8]) -> std::io::Result<usize> { |
| Ok(0) |
| } |
| fn flush(&mut self) -> std::io::Result<()> { |
| Ok(()) |
| } |
| } |
| |
| // No devices connected |
| let serials = ZedmonClient::<FakeEnumerationInterface>::enumerate(); |
| assert!(serials.is_empty()); |
| |
| // One device: not-a-zedmon-1 |
| push_device(ShortInterface { |
| dev_vendor: 0xdead, |
| dev_product: ZEDMON_PRODUCT_ID, |
| ifc_class: VENDOR_SPECIFIC_CLASS_ID, |
| ifc_subclass: ZEDMON_SUBCLASS_ID, |
| ifc_protocol: ZEDMON_PROTOCOL_ID, |
| serial_number: "not-a-zedmon-1", |
| }); |
| let serials = ZedmonClient::<FakeEnumerationInterface>::enumerate(); |
| assert!(serials.is_empty()); |
| |
| // Two devices: not-a-zedmon-1, zedmon-1 |
| push_device(ShortInterface { |
| dev_vendor: GOOGLE_VENDOR_ID, |
| dev_product: ZEDMON_PRODUCT_ID, |
| ifc_class: VENDOR_SPECIFIC_CLASS_ID, |
| ifc_subclass: ZEDMON_SUBCLASS_ID, |
| ifc_protocol: ZEDMON_PROTOCOL_ID, |
| serial_number: "zedmon-1", |
| }); |
| let serials = ZedmonClient::<FakeEnumerationInterface>::enumerate(); |
| assert_eq!(serials, ["zedmon-1"]); |
| |
| // Three devices: not-a-zedmon-1, zedmon-1, not-a-zedmon-2 |
| push_device(ShortInterface { |
| dev_vendor: GOOGLE_VENDOR_ID, |
| dev_product: 0xbeef, |
| ifc_class: VENDOR_SPECIFIC_CLASS_ID, |
| ifc_subclass: ZEDMON_SUBCLASS_ID, |
| ifc_protocol: ZEDMON_PROTOCOL_ID, |
| serial_number: "not-a-zedmon-2", |
| }); |
| let serials = ZedmonClient::<FakeEnumerationInterface>::enumerate(); |
| assert_eq!(serials, ["zedmon-1"]); |
| |
| // Four devices: not-a-zedmon-1, zedmon-1, not-a-zedmon-2, zedmon-2 |
| push_device(ShortInterface { |
| dev_vendor: GOOGLE_VENDOR_ID, |
| dev_product: ZEDMON_PRODUCT_ID, |
| ifc_class: VENDOR_SPECIFIC_CLASS_ID, |
| ifc_subclass: ZEDMON_SUBCLASS_ID, |
| ifc_protocol: ZEDMON_PROTOCOL_ID, |
| serial_number: "zedmon-2", |
| }); |
| let serials = ZedmonClient::<FakeEnumerationInterface>::enumerate(); |
| assert_eq!(serials, ["zedmon-1", "zedmon-2"]); |
| } |
| |
| // Provides test support for ZedmonClient functionality that interacts with a Zedmon device. It |
| // chiefly provides: |
| // - FakeZedmonInterface, for faking ZedmonClient's access to a Zedmon device. |
| // - Coordinator, for coordinating activity between the test and a FakeZedmonInterface. |
| // - CoordinatorBuilder, for producing a Coordinator instance with its various optional |
| // settings. |
| mod fake_device { |
| use { |
| super::*, |
| num::FromPrimitive, |
| protocol::{tests::*, PacketType, Unit}, |
| }; |
| |
| // StopSignal implementer for testing ZedmonClient::read_reports. The state is set by a |
| // Coordinator. |
| pub struct Stopper { |
| signal: Arc<AtomicBool>, |
| } |
| |
| impl StopSignal for Stopper { |
| fn should_stop(&mut self) -> Result<bool, Error> { |
| return Ok(self.signal.load(Ordering::SeqCst)); |
| } |
| } |
| |
| // Coordinates interactions between FakeZedmonInterface and a test. |
| // |
| // To test ZedmonClient::read_reports: |
| // - Use CoordinatorBuilder::with_report_queue to populate the `report_queue` field. Each |
| // entry in the queue is a Vec<Report> that will be serialized into a single packet. The |
| // caller will need to ensure that reports are written to an accessible location. |
| // - Use CoordinatorHandle::get_stopper to build a Stopper for `read_reports` that will |
| // end reporting when the report queue is exhausted. |
| // |
| // To test ZedmonClient::get_time_offset_nanos, use CoordinatorBuilder::with_offset_time to |
| // populate `offset_time`. Timestamps will be reported as `host_time - offset_time`; the |
| // fake Zedmon's clock will run perfectly in parallel to the host clock. Note that only |
| // timestamps reported in Timestamp packets are affected by `offset_time`; timestamps in |
| // Report packets are directly specified by the test, via `report_queue`. |
| // |
| // To test ZedmonClient::set_relay, use CoordinatorBuilder::with_relay_enabled to |
| // populate the relay state, and CoordinatorHandle::relay_enabled to check expectations. |
| pub struct Coordinator { |
| // Constants that define the fake device. |
| device_config: DeviceConfiguration, |
| |
| // See struct comment. |
| report_queue: Option<VecDeque<Vec<Report>>>, |
| |
| // Signal to trigger the end of reporting. Initialized to `false`, and set to `true` |
| // when `report_queue` is exhausted. |
| stop_signal: Arc<AtomicBool>, |
| |
| // Offset between the fake Zedmon's clock and host clock, such that `zedmon_time + |
| // offset_time = host_time`. Only used for Timestamp packets; the timestamps in |
| // Reports are set by the test when populating `report_queue`. |
| offset_time: Option<Duration>, |
| |
| // Whether Zedmon's relay is enabled. |
| relay_enabled: Option<bool>, |
| } |
| |
| impl Coordinator { |
| pub fn get_stopper(&self) -> Stopper { |
| Stopper { signal: self.stop_signal.clone() } |
| } |
| |
| pub fn relay_enabled(&self) -> bool { |
| self.relay_enabled.expect("relay_enabled not set") |
| } |
| |
| fn get_device_config(&self) -> DeviceConfiguration { |
| self.device_config.clone() |
| } |
| |
| // Gets the next packet's worth of Reports. |
| fn get_reports_for_packet(&mut self) -> Vec<Report> { |
| let report_queue = self.report_queue.as_mut().expect("report_queue not set"); |
| |
| assert!(report_queue.len() > 0, "No reports left in queue"); |
| if report_queue.len() == 1 { |
| self.stop_signal.store(true, Ordering::SeqCst); |
| } |
| report_queue.pop_front().unwrap() |
| } |
| |
| // Retrieves a timestamp in microseconds to fill a Timestamp packet. |
| fn get_timestamp_micros(&self) -> u64 { |
| let offset_time = self.offset_time.expect("offset_time not set"); |
| |
| let zedmon_now = SystemTime::now() - offset_time; |
| let timestamp = zedmon_now.duration_since(SystemTime::UNIX_EPOCH).unwrap(); |
| timestamp.as_micros() as u64 |
| } |
| |
| fn set_relay_enabled(&mut self, enabled: bool) { |
| let relay_enabled = self.relay_enabled.as_mut().expect("relay_enabled not set"); |
| *relay_enabled = enabled; |
| } |
| } |
| |
| // Constants that are inherent to a Zedmon device. Even if these values are not exercised by |
| // a test, dummy values will be required by ZedmonClient::new(). |
| #[derive(Clone, Debug)] |
| pub struct DeviceConfiguration { |
| pub shunt_resistance: f32, |
| pub v_shunt_scale: f32, |
| pub v_bus_scale: f32, |
| } |
| |
| // Provides the interface for building a Coordinator with its various optional settings. |
| pub struct CoordinatorBuilder { |
| device_config: DeviceConfiguration, |
| report_queue: Option<VecDeque<Vec<Report>>>, |
| offset_time: Option<Duration>, |
| relay_enabled: Option<bool>, |
| } |
| |
| impl CoordinatorBuilder { |
| pub fn new(device_config: DeviceConfiguration) -> Self { |
| CoordinatorBuilder { |
| device_config, |
| report_queue: None, |
| offset_time: None, |
| relay_enabled: None, |
| } |
| } |
| |
| pub fn with_report_queue(mut self, report_queue: VecDeque<Vec<Report>>) -> Self { |
| self.report_queue.replace(report_queue); |
| self |
| } |
| |
| pub fn with_offset_time(mut self, offset_time: Duration) -> Self { |
| self.offset_time.replace(offset_time); |
| self |
| } |
| |
| pub fn with_relay_enabled(mut self, enabled: bool) -> Self { |
| self.relay_enabled.replace(enabled); |
| self |
| } |
| |
| pub fn build(self) -> Rc<RefCell<Coordinator>> { |
| Rc::new(RefCell::new(Coordinator { |
| device_config: self.device_config, |
| report_queue: self.report_queue, |
| offset_time: self.offset_time, |
| relay_enabled: self.relay_enabled, |
| stop_signal: Arc::new(AtomicBool::new(false)), |
| })) |
| } |
| } |
| |
| // Indicates the contents of the next read from FakeZedmonInterface. |
| enum NextRead { |
| ParameterValue(u8), |
| ReportFormat(u8), |
| Report, |
| Timestamp, |
| } |
| |
| // Interface that provides fakes for testing interactions with a Zedmon device. |
| pub struct FakeZedmonInterface { |
| coordinator: Rc<RefCell<Coordinator>>, |
| |
| // The type of read that wil be performed next from this interface, if any. |
| next_read: Option<NextRead>, |
| } |
| |
| impl usb_bulk::Open<FakeZedmonInterface> for FakeZedmonInterface { |
| fn open<F>(_matcher: &mut F) -> Result<FakeZedmonInterface, Error> |
| where |
| F: FnMut(&InterfaceInfo) -> bool, |
| { |
| Err(format_err!("usb_bulk::Open not implemented")) |
| } |
| } |
| |
| impl FakeZedmonInterface { |
| pub fn new(coordinator: Rc<RefCell<Coordinator>>) -> Self { |
| Self { coordinator, next_read: None } |
| } |
| |
| // Populates a ParameterValue packet. |
| fn read_parameter_value(&mut self, index: u8, buffer: &mut [u8]) -> usize { |
| match index { |
| 0 => serialize_parameter_value( |
| ParameterValue { |
| name: "shunt_resistance".to_string(), |
| value: Value::F32( |
| self.coordinator.borrow().get_device_config().shunt_resistance, |
| ), |
| }, |
| buffer, |
| ), |
| 1 => serialize_parameter_value( |
| ParameterValue { name: "".to_string(), value: Value::U8(0) }, |
| buffer, |
| ), |
| _ => panic!("Should only receive 0 or 1 as indices"), |
| } |
| } |
| |
| // Populates a ReportFormat packet. |
| fn read_report_format(&self, index: u8, buffer: &mut [u8]) -> usize { |
| match index { |
| 0 => serialize_report_format( |
| ReportFormat { |
| index, |
| field_type: ScalarType::I16, |
| unit: Unit::Volts, |
| scale: self.coordinator.borrow().get_device_config().v_shunt_scale, |
| name: "v_shunt".to_string(), |
| }, |
| buffer, |
| ), |
| 1 => serialize_report_format( |
| ReportFormat { |
| index, |
| field_type: ScalarType::I16, |
| unit: Unit::Volts, |
| scale: self.coordinator.borrow().get_device_config().v_bus_scale, |
| name: "v_bus".to_string(), |
| }, |
| buffer, |
| ), |
| 2 => serialize_report_format( |
| ReportFormat { |
| index: protocol::REPORT_FORMAT_INDEX_END, |
| field_type: ScalarType::U8, |
| unit: Unit::Volts, |
| scale: 0.0, |
| name: "".to_string(), |
| }, |
| buffer, |
| ), |
| _ => panic!("Should only receive 0, 1, or 2 as indices"), |
| } |
| } |
| |
| // Populates a Report packet. |
| fn read_reports(&mut self, buffer: &mut [u8]) -> usize { |
| let reports = self.coordinator.borrow_mut().get_reports_for_packet(); |
| serialize_reports(&reports, buffer) |
| } |
| |
| // Populates a Timestamp packet. |
| fn read_timestamp(&mut self, buffer: &mut [u8]) -> usize { |
| buffer[0] = PacketType::Timestamp as u8; |
| serialize_timestamp_micros(self.coordinator.borrow().get_timestamp_micros(), buffer) |
| } |
| |
| fn set_output(&self, index: u8, value: u8) { |
| if index == protocol::Output::Relay as u8 { |
| self.coordinator.borrow_mut().set_relay_enabled(value != 0); |
| } |
| } |
| } |
| |
| impl Read for FakeZedmonInterface { |
| fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> { |
| match self.next_read.take() { |
| Some(value) => Ok(match value { |
| NextRead::ParameterValue(index) => self.read_parameter_value(index, buffer), |
| NextRead::ReportFormat(index) => self.read_report_format(index, buffer), |
| NextRead::Report => { |
| self.next_read = Some(NextRead::Report); |
| self.read_reports(buffer) |
| } |
| NextRead::Timestamp => self.read_timestamp(buffer), |
| }), |
| None => Err(std::io::Error::new(std::io::ErrorKind::Other, "Read error: -1")), |
| } |
| } |
| } |
| |
| impl Write for FakeZedmonInterface { |
| fn write(&mut self, data: &[u8]) -> std::io::Result<usize> { |
| let packet_type = PacketType::from_u8(data[0]).unwrap(); |
| match packet_type { |
| PacketType::EnableReporting => self.next_read = Some(NextRead::Report), |
| PacketType::DisableReporting => self.next_read = None, |
| PacketType::QueryParameter => { |
| self.next_read = Some(NextRead::ParameterValue(data[1])) |
| } |
| PacketType::QueryReportFormat => { |
| self.next_read = Some(NextRead::ReportFormat(data[1])) |
| } |
| PacketType::QueryTime => self.next_read = Some(NextRead::Timestamp), |
| PacketType::SetOutput => { |
| assert_eq!(data.len(), 3); |
| self.set_output(data[1], data[2]); |
| } |
| _ => panic!("Not a valid host-to-target packet"), |
| } |
| Ok(data.len()) |
| } |
| fn flush(&mut self) -> std::io::Result<()> { |
| Ok(()) |
| } |
| } |
| } |
| |
| // Helper struct for testing ZedmonClient::record. |
| struct ZedmonRecordRunner { |
| // Maps time in microseconds to (v_shunt, v_bus). |
| voltage_function: Box<dyn Fn(u64) -> (f32, f32)>, |
| |
| shunt_resistance: f32, |
| v_shunt_scale: f32, |
| v_bus_scale: f32, |
| |
| // The length of the test, and interval between report timestamps. The first report is sent |
| // one interval after the starting instant. |
| test_duration: Duration, |
| reporting_interval: Duration, |
| } |
| |
| impl ZedmonRecordRunner { |
| fn run(&self) -> Result<String, Error> { |
| let device_config = fake_device::DeviceConfiguration { |
| shunt_resistance: self.shunt_resistance, |
| v_shunt_scale: self.v_shunt_scale, |
| v_bus_scale: self.v_bus_scale, |
| }; |
| |
| let mut report_queue = VecDeque::new(); |
| |
| let mut elapsed = Duration::from_millis(0); |
| |
| // 1ms of fake time elapses between each report. Reports are batched into groups of 5, |
| // the number that will fit into a single packet. |
| while elapsed <= self.test_duration { |
| let mut reports = Vec::new(); |
| for _ in 0..5 { |
| elapsed = elapsed + self.reporting_interval; |
| if elapsed > self.test_duration { |
| break; |
| } |
| |
| let (v_shunt, v_bus) = (self.voltage_function)(elapsed.as_micros() as u64); |
| reports.push(Report { |
| timestamp_micros: elapsed.as_micros() as u64, |
| values: vec![ |
| Value::I16((v_shunt / self.v_shunt_scale) as i16), |
| Value::I16((v_bus / self.v_bus_scale) as i16), |
| ], |
| }); |
| } |
| if !reports.is_empty() { |
| report_queue.push_back(reports); |
| } |
| } |
| |
| let builder = |
| fake_device::CoordinatorBuilder::new(device_config).with_report_queue(report_queue); |
| let coordinator = builder.build(); |
| let interface = fake_device::FakeZedmonInterface::new(coordinator.clone()); |
| let zedmon = ZedmonClient::new(interface)?; |
| |
| // Implements Write by sending bytes over a channel. The holder of the channel's |
| // Receiver can then inspect the data that was written to test expectations. |
| struct ChannelWriter { |
| sender: mpsc::Sender<Vec<u8>>, |
| buffer: Vec<u8>, |
| } |
| |
| impl std::io::Write for ChannelWriter { |
| fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { |
| self.buffer.extend_from_slice(buf); |
| Ok(buf.len()) |
| } |
| |
| fn flush(&mut self) -> std::io::Result<()> { |
| let mut payload = Vec::new(); |
| std::mem::swap(&mut self.buffer, &mut payload); |
| self.sender.send(payload).unwrap(); |
| Ok(()) |
| } |
| } |
| |
| let (sender, receiver) = mpsc::channel(); |
| let writer = Box::new(ChannelWriter { sender, buffer: Vec::new() }); |
| let stopper = coordinator.borrow().get_stopper(); |
| zedmon.read_reports(writer, stopper)?; |
| |
| let output = receiver.recv()?; |
| Ok(String::from_utf8(output)?) |
| } |
| } |
| |
| // Tests that ZedmonClient will disable reporting and drain enqueued packets on initialization. |
| #[test] |
| fn test_disable_reporting_and_drain_packets() { |
| // Interface that responds to reads with Report packets until reporting is disabled and an |
| // enqueued packet is drained. Afterwards, all calls are forwarded to an inner |
| // FakeZedmonInterface. |
| struct StillReportingInterface { |
| inner: fake_device::FakeZedmonInterface, |
| reporting_enabled: bool, |
| packets_enqueued: usize, |
| } |
| |
| impl StillReportingInterface { |
| fn new(coordinator: Rc<RefCell<fake_device::Coordinator>>) -> Self { |
| Self { |
| inner: fake_device::FakeZedmonInterface::new(coordinator), |
| reporting_enabled: true, |
| packets_enqueued: 1, |
| } |
| } |
| |
| fn make_reports(&self) -> Vec<Report> { |
| let mut reports = Vec::new(); |
| for i in 0..5 { |
| reports.push(Report { |
| timestamp_micros: 1000 * (i as u64), |
| values: vec![Value::I16(i as i16), Value::I16(-(i as i16))], |
| }); |
| } |
| reports |
| } |
| } |
| |
| impl usb_bulk::Open<StillReportingInterface> for StillReportingInterface { |
| fn open<F>(_matcher: &mut F) -> Result<StillReportingInterface, Error> |
| where |
| F: FnMut(&InterfaceInfo) -> bool, |
| { |
| Err(format_err!("usb_bulk::Open not implemented")) |
| } |
| } |
| |
| impl Read for StillReportingInterface { |
| fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> { |
| if self.reporting_enabled { |
| Ok(serialize_reports(&self.make_reports(), buffer)) |
| } else if self.packets_enqueued > 0 { |
| self.packets_enqueued = self.packets_enqueued - 1; |
| Ok(serialize_reports(&self.make_reports(), buffer)) |
| } else { |
| self.inner.read(buffer) |
| } |
| } |
| } |
| |
| impl Write for StillReportingInterface { |
| fn write(&mut self, data: &[u8]) -> std::io::Result<usize> { |
| if self.reporting_enabled |
| && PacketType::from_u8(data[0]).unwrap() == PacketType::DisableReporting |
| { |
| self.reporting_enabled = false; |
| Ok(1) |
| } else { |
| self.inner.write(data) |
| } |
| } |
| |
| fn flush(&mut self) -> std::io::Result<()> { |
| self.inner.flush() |
| } |
| } |
| |
| let device_config = fake_device::DeviceConfiguration { |
| shunt_resistance: 0.01, |
| v_shunt_scale: 1e-5, |
| v_bus_scale: 0.025, |
| }; |
| let builder = fake_device::CoordinatorBuilder::new(device_config); |
| let coordinator = builder.build(); |
| let interface = StillReportingInterface::new(coordinator); |
| |
| assert!(ZedmonClient::new(interface).is_ok()); |
| } |
| |
| #[test] |
| fn test_read_reports() -> Result<(), Error> { |
| // The voltages used are simply time-dependent signals that, combined with shunt_resistance |
| // below, yield power on the order of a few Watts. |
| fn get_voltages(micros: u64) -> (f32, f32) { |
| let seconds = micros as f32 / 1e6; |
| let v_shunt = 1e-3 + 2e-4 * (std::f32::consts::PI * seconds).cos(); |
| let v_bus = 20.0 + 3.0 * (std::f32::consts::PI * seconds).sin(); |
| (v_shunt, v_bus) |
| } |
| |
| // These values are in the same ballpark as those used on Zedmon 2.1. The test shouldn't be |
| // sensitive to them. |
| let shunt_resistance = 0.01; |
| let v_shunt_scale = 1e-5; |
| let v_bus_scale = 0.025; |
| |
| let runner = ZedmonRecordRunner { |
| voltage_function: Box::new(get_voltages), |
| shunt_resistance, |
| v_shunt_scale, |
| v_bus_scale, |
| test_duration: Duration::from_secs(10), |
| reporting_interval: Duration::from_millis(1), |
| }; |
| let output = runner.run()?; |
| |
| let mut num_lines = 0; |
| for line in output.lines() { |
| num_lines = num_lines + 1; |
| |
| let parts: Vec<&str> = line.split(",").collect(); |
| assert_eq!(4, parts.len()); |
| let timestamp: u64 = parts[0].parse()?; |
| let v_shunt_out: f32 = parts[1].parse()?; |
| let v_bus_out: f32 = parts[2].parse()?; |
| |
| let (v_shunt_expected, v_bus_expected) = get_voltages(timestamp); |
| assert_near!(v_shunt_out, v_shunt_expected, v_shunt_scale); |
| assert_near!(v_bus_out, v_bus_expected, v_bus_scale); |
| |
| let power_out: f32 = parts[3].parse()?; |
| assert_near!(power_out, v_shunt_out * v_bus_out / shunt_resistance, 1e-6); |
| } |
| |
| assert_eq!(num_lines, 10000); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_get_time_offset_nanos() -> Result<(), Error> { |
| // This instant is effectively Zedmon's zero timestamp. |
| let zedmon_offset = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; |
| |
| // Values are not used by this test. |
| let device_config = fake_device::DeviceConfiguration { |
| shunt_resistance: 0.0, |
| v_shunt_scale: 0.0, |
| v_bus_scale: 0.0, |
| }; |
| |
| let builder = |
| fake_device::CoordinatorBuilder::new(device_config).with_offset_time(zedmon_offset); |
| let coordinator = builder.build(); |
| let interface = fake_device::FakeZedmonInterface::new(coordinator); |
| let zedmon = ZedmonClient::new(interface)?; |
| |
| let (reported_offset, uncertainty) = zedmon.get_time_offset_nanos()?; |
| assert_near!(zedmon_offset.as_nanos() as i64, reported_offset, uncertainty); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_set_relay() -> Result<(), Error> { |
| // Values are not used by this test. |
| let device_config = fake_device::DeviceConfiguration { |
| shunt_resistance: 0.0, |
| v_shunt_scale: 0.0, |
| v_bus_scale: 0.0, |
| }; |
| |
| let builder = fake_device::CoordinatorBuilder::new(device_config).with_relay_enabled(false); |
| let coordinator = builder.build(); |
| let interface = fake_device::FakeZedmonInterface::new(coordinator.clone()); |
| let zedmon = ZedmonClient::new(interface)?; |
| |
| // Test true->false and false->true transitions, and no-ops in each state. |
| zedmon.set_relay(true)?; |
| assert_eq!(coordinator.borrow().relay_enabled(), true); |
| zedmon.set_relay(true)?; |
| assert_eq!(coordinator.borrow().relay_enabled(), true); |
| zedmon.set_relay(false)?; |
| assert_eq!(coordinator.borrow().relay_enabled(), false); |
| zedmon.set_relay(false)?; |
| assert_eq!(coordinator.borrow().relay_enabled(), false); |
| zedmon.set_relay(true)?; |
| assert_eq!(coordinator.borrow().relay_enabled(), true); |
| |
| Ok(()) |
| } |
| } |