blob: d0307d9b3091268081b3adbe0062b073b7c03e29 [file] [log] [blame]
// 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, Error},
std::{
cell::RefCell,
collections::HashMap,
io::{Read, Write},
os::raw::{c_uchar, c_ushort},
},
usb_bulk::InterfaceInfo,
};
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>;
}
/// Writes a Report to a CSV-formatted line of output.
fn report_to_string(
report: protocol::Report,
shunt_resistance: f32,
v_shunt_index: usize,
v_bus_index: usize,
) -> String {
let v_shunt = report.values[v_shunt_index];
let v_bus = report.values[v_bus_index];
let (v_shunt, v_bus) = match (v_shunt, v_bus) {
(Value::F32(x), Value::F32(y)) => (x, y),
t => panic!("Got wrong value types for (v_shunt, v_bus): {:?}", t),
};
let power = v_bus * v_shunt / shunt_resistance;
format!("{},{},{},{}\n", report.timestamp, v_shunt, v_bus, power)
}
/// 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
}
/// Creates a new ZedmonClient instance.
// TODO(fxbug.dev/61148): Make the behavior predictable if multiple Zedmons are attached.
fn new() -> Result<Self, Error> {
let mut interface = InterfaceType::open(&mut zedmon_match).unwrap();
let parameters = Self::get_parameters(&mut interface).unwrap();
let field_formats = Self::get_field_formats(&mut interface).unwrap();
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,
})
}
/// 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(&self) -> Result<(), Error> {
let request = protocol::encode_disable_reporting();
self.interface.borrow_mut().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,
mut 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> {
for buffer in packet_receiver.iter() {
let reports = parser.parse_reports(&buffer).unwrap();
for report in reports.into_iter() {
write!(
*writer,
"{}",
report_to_string(report, shunt_resistance, v_shunt_index, v_bus_index,)
)?;
}
}
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()?;
drop(packet_sender);
break;
}
}
// Wait for the parsing thread to complete upon draining the channel buffer.
output_thread.join().unwrap().unwrap();
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 result = ZedmonClient::<usb_bulk::Interface>::new();
if result.is_err() {
eprintln!("Error initializing ZedmonClient: {:?}", result);
}
result.unwrap()
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::{format_err, Error},
lazy_static::lazy_static,
protocol::{Report, ScalarType},
std::collections::VecDeque,
std::sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, RwLock,
},
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.
//
// FakeZedmonInterface provides test support at the USB interface level. However, a test does
// not have direct access to ZedmonClient's FakeZedmonInterface instance.
//
// This module allows communication between the test and FakeZedmonInterface through a static
// instance of the Coordinator struct. For reporting, the process is as follows:
// - The test calls `init` to populate the static COORDINATOR, passing in a DeviceConfiguration
// and a queue of Vec<Report> that FakeZedmonInterface will stream to zedmon. The test
// receives a CoordinatorHandle that will destroy COORDINATOR when it goes out of scope.
// - The test calls ZedmonClient::read_reports with a stop signal provided by the
// CoorinatorHandle.
// - Each time Zedmon reads a report packet, FakeZedmonInterface will provide it with a
// serialized Vec<Report> from the COORDINATOR's queue.
// - When the queue is exhausted, COORDINATOR triggers the stop signal, ending reporting.
mod fake_device {
use {
super::*,
num::FromPrimitive,
protocol::{tests::*, PacketType, Unit},
};
// StopSignal implementer for testing ZedmonClient::read_reports. The state is set by
// 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.
//
// There is meant to be only one instance of this struct, the static COORDINATOR. Within
// this module, COORDINATOR is accessed by a number of helper functions. Outside of this
// module (i.e. in a test), COORDINATOR is accessed in RAII fashion via a CoordinatorHandle.
struct Coordinator {
device_config: DeviceConfiguration,
report_queue: VecDeque<Vec<Report>>,
stop_signal: Arc<AtomicBool>,
}
// Constants that are inherent to a Zedmon device.
#[derive(Clone, Debug)]
pub struct DeviceConfiguration {
pub shunt_resistance: f32,
pub v_shunt_scale: f32,
pub v_bus_scale: f32,
}
// Used to provide tests with RAII access to COORDINATOR.
pub struct CoordinatorHandle {}
impl CoordinatorHandle {
pub fn get_stopper(&self) -> Stopper {
let lock = COORDINATOR.read().unwrap();
let coordinator = lock.as_ref().unwrap();
Stopper { signal: coordinator.stop_signal.clone() }
}
}
impl Drop for CoordinatorHandle {
fn drop(&mut self) {
let mut lock = COORDINATOR.write().unwrap();
lock.take();
}
}
// At time of writing, COORDINATOR is only accessed on the same thread on which it was
// created, so it could be made thread-local. However, if ZedmonClient were to perform USB
// reading on a child thread and Report-parsing on its main thread, that would no longer be
// the case. So for robustness, COORDINATOR is not thread-local.
lazy_static! {
static ref COORDINATOR: RwLock<Option<Coordinator>> = RwLock::new(None);
}
// Entry point for tests. Populates COORDINATOR and returns a CoordinatorHandle used to
// access it.
pub fn init(
device_config: DeviceConfiguration,
report_queue: VecDeque<Vec<Report>>,
) -> CoordinatorHandle {
let stop_signal = Arc::new(AtomicBool::new(false));
let mut lock = COORDINATOR.write().unwrap();
assert!(
lock.is_none(),
"COORDINATOR was not properly cleared; this should happen automatically."
);
lock.replace(Coordinator { device_config, report_queue, stop_signal });
CoordinatorHandle {}
}
// Gets COORDINATOR's device_config.
fn device_config() -> DeviceConfiguration {
let lock = COORDINATOR.read().unwrap();
let coordinator = lock.as_ref().unwrap();
coordinator.device_config.clone()
}
// Gets the next packet's worth of Reports from COORDINATOR.
fn get_reports_for_packet() -> Vec<Report> {
let mut lock = COORDINATOR.write().unwrap();
let coordinator = lock.as_mut().unwrap();
assert!(coordinator.report_queue.len() > 0, "No reports left in queue");
if coordinator.report_queue.len() == 1 {
coordinator.stop_signal.store(true, Ordering::SeqCst);
}
coordinator.report_queue.pop_front().unwrap()
}
// Indicates the contents of the next read from FakeZedmonInterface.
enum NextRead {
ParameterValue(u8),
ReportFormat(u8),
Report,
}
// Interface that provides fakes for testing interactions with a Zedmon device.
pub struct FakeZedmonInterface {
// 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,
{
Ok(FakeZedmonInterface { next_read: None })
}
}
impl FakeZedmonInterface {
// 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(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: 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: 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 = get_reports_for_packet();
serialize_reports(&reports, buffer)
}
}
impl Read for FakeZedmonInterface {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
let bytes_read = match self.next_read.take().unwrap() {
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)
}
};
Ok(bytes_read)
}
}
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]))
}
_ => 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 nanoseconds 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_nanos() as u64);
reports.push(Report {
timestamp: elapsed.as_nanos() 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 coordinator = fake_device::init(device_config, report_queue);
let zedmon = ZedmonClient::<fake_device::FakeZedmonInterface>::new()?;
// 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() });
zedmon.read_reports(writer, coordinator.get_stopper())?;
let output = receiver.recv()?;
Ok(String::from_utf8(output)?)
}
}
#[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(nanos: u64) -> (f32, f32) {
let seconds = nanos as f32 / 1e9;
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(())
}
}