| // 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. |
| |
| //! Implements the Zedmon USB protocol, defined by |
| //! https://fuchsia.googlesource.com/zedmon/+/HEAD/docs/usb_proto.md. |
| |
| use { |
| byteorder::{LittleEndian as LE, ReadBytesExt}, |
| num::FromPrimitive, |
| num_derive::FromPrimitive, |
| std::io::{BufRead, Cursor}, |
| std::str, |
| }; |
| |
| pub const MAX_PACKET_SIZE: usize = 64; |
| |
| /// Types of packets that may be sent to or received from Zedmon. |
| #[repr(u8)] |
| #[derive(Debug, FromPrimitive, PartialEq)] |
| pub enum PacketType { |
| QueryReportFormat = 0x0, |
| QueryTime = 0x01, |
| QueryParameter = 0x2, |
| EnableReporting = 0x10, |
| DisableReporting = 0x11, |
| SetOutput = 0x20, |
| ReportFormat = 0x80, |
| Report = 0x81, |
| Timestamp = 0x82, |
| ParameterValue = 0x83, |
| } |
| |
| /// Types for scalars sent over the network. |
| #[repr(u8)] |
| #[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)] |
| pub enum ScalarType { |
| U8 = 0x0, |
| U16 = 0x1, |
| U32 = 0x3, |
| U64 = 0x4, |
| I8 = 0x10, |
| I16 = 0x11, |
| I32 = 0x13, |
| I64 = 0x14, |
| Bool = 0x20, |
| F32 = 0x40, |
| } |
| |
| /// Possible units for scalar vales. |
| #[repr(u8)] |
| #[derive(Clone, Debug, FromPrimitive, PartialEq)] |
| pub enum Unit { |
| Amperes = 0x0, |
| Volts = 0x1, |
| } |
| |
| /// Allowable types of a reported value. |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| pub enum Value { |
| U8(u8), |
| U16(u16), |
| U32(u32), |
| U64(u64), |
| I8(i8), |
| I16(i16), |
| I32(i32), |
| I64(i64), |
| Bool(bool), |
| F32(f32), |
| } |
| |
| /// A fixed parameter of the Zedmon device. |
| #[derive(Debug, PartialEq)] |
| pub struct ParameterValue { |
| pub name: String, |
| pub value: Value, |
| } |
| |
| /// Format of a field in a Report packet. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct ReportFormat { |
| pub index: u8, |
| pub field_type: ScalarType, |
| pub unit: Unit, |
| pub scale: f32, |
| pub name: String, |
| } |
| |
| pub const REPORT_FORMAT_INDEX_END: u8 = 0xff; |
| |
| /// Report of measured data at a timepoint. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct Report { |
| /// Timestamp, in microseconds, in Zedmon's clock domain. |
| /// |
| /// At time of writing the timestamp is zero when Zedmon boots, but that is not a guarantee. |
| // TODO(fxbug.dev/60030): Use one of power_manager's newtypes, or similar. |
| pub timestamp_micros: u64, |
| |
| /// Reported values for this sample. |
| pub values: Vec<Value>, |
| } |
| |
| // Typical Zedmons only support the relay output. |
| #[repr(u8)] |
| pub enum Output { |
| Relay = 0x00, |
| } |
| |
| #[derive(Debug, thiserror::Error, PartialEq)] |
| pub enum Error { |
| #[error("Short data: required {required}, got {received}")] |
| ShortData { required: usize, received: usize }, |
| #[error("Invalid string")] |
| InvalidString, |
| #[error("Wrong packet type; expected {expected:?}, got {received:?}")] |
| WrongPacketType { expected: PacketType, received: PacketType }, |
| #[error("Unknown primitive value {primitive} for enum type {enum_name}")] |
| UnknownEnumValue { primitive: u8, enum_name: &'static str }, |
| #[error("{name} has ScalarType {scalar_type:?}, which cannot be scaled")] |
| InvalidScaledType { name: String, scalar_type: ScalarType }, |
| #[error("Invalid ReportFormats: {reason}")] |
| InvalidReportFormats { reason: String }, |
| #[error("std::io::Error received: {io_error}")] |
| IoError { io_error: String }, |
| } |
| |
| impl From<std::io::Error> for Error { |
| fn from(error: std::io::Error) -> Self { |
| Error::IoError { io_error: error.to_string() } |
| } |
| } |
| |
| /// Result alias for fallible calls in this crate. |
| type Result<T> = std::result::Result<T, Error>; |
| |
| /// Extra reader methods for a Cursor into a byte slice. |
| trait CursorExtras { |
| /// Retrieves a byte slice of the specified length, returning `Error::ShortData` if |
| /// insufficient data is available. *Does not* advance the cursor position. |
| fn get_byte_slice(&mut self, len: usize) -> Result<&[u8]>; |
| |
| /// Reads a UTF-8 string of the specifed `len`. Forwards errors from `get_byte_slice`, |
| /// and returns `Error::InvalidString` if invalid UTF-8 characters are detected. |
| fn read_string(&mut self, len: usize) -> Result<String>; |
| |
| /// Reads a byte and casts it to `EnumType`. Forwards errors from `get_byte_slice`, |
| /// and returns `Error::UnknownEnumValue` if the cast fails. |
| fn read_enum<EnumType: FromPrimitive>(&mut self) -> Result<EnumType>; |
| } |
| |
| impl<T: AsRef<[u8]>> CursorExtras for Cursor<T> { |
| fn get_byte_slice(&mut self, len: usize) -> Result<&[u8]> { |
| // fill_buf() is not fallible for Cursor<T: AsRef<[u8]>>, so unwrap() is safe. |
| let full_slice = &self.fill_buf().unwrap(); |
| if full_slice.len() < len { |
| return Err(Error::ShortData { required: len, received: full_slice.len() }); |
| } |
| Ok(&full_slice[..len]) |
| } |
| |
| fn read_string(&mut self, len: usize) -> Result<String> { |
| let string = { |
| let raw = self.get_byte_slice(len)?; |
| let null_pos = raw.iter().position(|c| *c == b'\0').ok_or(Error::InvalidString)?; |
| let slice = str::from_utf8(&raw[..null_pos]).map_err(|_| Error::InvalidString)?; |
| slice.trim().to_string() |
| }; |
| |
| self.consume(len); |
| Ok(string) |
| } |
| |
| fn read_enum<EnumType: FromPrimitive>(&mut self) -> Result<EnumType> { |
| // Use `get_byte_slice` rather than `read_u8` to clarify possible errors. |
| let primitive = self.get_byte_slice(1)?[0]; |
| self.consume(1); |
| |
| EnumType::from_u8(primitive).ok_or(Error::UnknownEnumValue { |
| primitive, |
| enum_name: std::any::type_name::<EnumType>(), |
| }) |
| } |
| } |
| |
| pub fn encode_disable_reporting() -> [u8; 1] { |
| [PacketType::DisableReporting as u8] |
| } |
| |
| pub fn encode_enable_reporting() -> [u8; 1] { |
| [PacketType::EnableReporting as u8] |
| } |
| |
| pub fn encode_query_report_format(index: u8) -> [u8; 2] { |
| [PacketType::QueryReportFormat as u8, index] |
| } |
| |
| pub fn encode_query_parameter(index: u8) -> [u8; 2] { |
| [PacketType::QueryParameter as u8, index] |
| } |
| |
| pub fn encode_query_time() -> [u8; 1] { |
| [PacketType::QueryTime as u8] |
| } |
| |
| pub fn encode_set_output(index: u8, value: bool) -> [u8; 3] { |
| [PacketType::SetOutput as u8, index, value as u8] |
| } |
| |
| /// Validates that `packet` has the required length and `PacketType`. Returns a cursor to the |
| /// remainder of the packet's data. |
| // NOTE: The `?Sized` relaxation in this function and several others below is needed to support |
| // T=[u8]. |
| fn validate_packet<T: AsRef<[u8]> + ?Sized>( |
| packet: &T, |
| len: usize, |
| expected_type: PacketType, |
| ) -> Result<Cursor<&T>> { |
| let mut cursor = Cursor::new(packet); |
| let packet_type = cursor.read_enum::<PacketType>()?; |
| if packet_type != expected_type { |
| return Err(Error::WrongPacketType { expected: expected_type, received: packet_type }); |
| } |
| |
| // Check for expected length after the packet type, as WrongPacketType is more useful to a |
| // caller. |
| let packet_len = packet.as_ref().len(); |
| if packet_len < len { |
| return Err(Error::ShortData { required: len, received: packet_len }); |
| } |
| |
| Ok(cursor) |
| } |
| |
| const PARAMETER_VALUE_NAME_LENGTH: usize = 54; |
| |
| /// Parses a `PacketType::ParameterValue` packet. |
| pub fn parse_parameter_value<T: AsRef<[u8]>>(packet: &T) -> Result<ParameterValue> { |
| let mut cursor = validate_packet(packet, 64, PacketType::ParameterValue)?; |
| |
| let name = cursor.read_string(PARAMETER_VALUE_NAME_LENGTH)?; |
| let field_type = cursor.read_enum::<ScalarType>()?; |
| |
| let value = match field_type { |
| ScalarType::U8 => Value::U8(cursor.read_u8().unwrap()), |
| ScalarType::U16 => Value::U16(cursor.read_u16::<LE>().unwrap()), |
| ScalarType::U32 => Value::U32(cursor.read_u32::<LE>().unwrap()), |
| ScalarType::U64 => Value::U64(cursor.read_u64::<LE>().unwrap()), |
| ScalarType::I8 => Value::I8(cursor.read_i8().unwrap()), |
| ScalarType::I16 => Value::I16(cursor.read_i16::<LE>().unwrap()), |
| ScalarType::I32 => Value::I32(cursor.read_i32::<LE>().unwrap()), |
| ScalarType::I64 => Value::I64(cursor.read_i64::<LE>().unwrap()), |
| ScalarType::Bool => Value::Bool(cursor.read_u8().unwrap() != 0x00), |
| ScalarType::F32 => Value::F32(cursor.read_f32::<LE>().unwrap()), |
| }; |
| |
| Ok(ParameterValue { name, value }) |
| } |
| |
| const REPORT_FIELD_NAME_LENGTH: usize = 56; |
| |
| /// Parses a `PacketType::ReportFormat` packet. |
| pub fn parse_report_format<T: AsRef<[u8]> + ?Sized>(packet: &T) -> Result<ReportFormat> { |
| let mut cursor = validate_packet(packet, 64, PacketType::ReportFormat)?; |
| |
| let index = cursor.read_u8().unwrap(); |
| let field_type = cursor.read_enum::<ScalarType>()?; |
| let unit = cursor.read_enum::<Unit>()?; |
| let scale = cursor.read_f32::<LE>().unwrap(); |
| let name = cursor.read_string(REPORT_FIELD_NAME_LENGTH)?; |
| |
| Ok(ReportFormat { index, field_type, unit, scale, name }) |
| } |
| |
| pub fn parse_timestamp_micros<T: AsRef<[u8]> + ?Sized>(packet: &T) -> Result<u64> { |
| let mut cursor = validate_packet(packet, 9, PacketType::Timestamp)?; |
| Ok(cursor.read_u64::<LE>()?) |
| } |
| |
| /// Calculates the length of a serialized Report from its field types. |
| fn calc_report_length(field_types: &Vec<ScalarType>) -> usize { |
| let mut len = 8; // u64 timestamp |
| for t in field_types { |
| len += match t { |
| ScalarType::U8 => 1, |
| ScalarType::U16 => 2, |
| ScalarType::U32 => 4, |
| ScalarType::U64 => 8, |
| ScalarType::I8 => 1, |
| ScalarType::I16 => 2, |
| ScalarType::I32 => 4, |
| ScalarType::I64 => 8, |
| ScalarType::Bool => 1, |
| ScalarType::F32 => 4, |
| }; |
| } |
| len |
| } |
| |
| /// ReportParser's internal way of representing the type of a reported value. A field with |
| /// ScalarType of U8, U16, I8, or I16 may be coupled with a nonzero scale, in which case |
| /// ReportParser will parse it into an F32. |
| /// |
| /// For now, only the integer types that may be losslessly converted to f32 are considered scalable. |
| /// Should the need arise, it would be straightforward to support scalable u32 and i32 values as |
| /// well, with parsing into f64. |
| #[derive(Debug)] |
| enum ReportedValueType { |
| Unscaled(ScalarType), |
| ScaledU8(f32), |
| ScaledU16(f32), |
| ScaledI8(f32), |
| ScaledI16(f32), |
| } |
| |
| /// Helper struct for parsing reports. |
| /// |
| /// This is used instead of a bare function to avoid repeatedly re-calculating report length based |
| /// on the fixed value types. |
| #[derive(Debug)] |
| pub struct ReportParser { |
| report_length: usize, |
| value_types: Vec<ReportedValueType>, |
| } |
| |
| impl ReportParser { |
| pub fn new(format: &Vec<ReportFormat>) -> Result<ReportParser> { |
| // Sort the provided field formats by index, and make sure that the resulting indices |
| // are 0..format.len(). |
| let format = { |
| let mut sorted = format.clone(); |
| let comparator = |a: &ReportFormat, b: &ReportFormat| { |
| if a.index < b.index { |
| std::cmp::Ordering::Less |
| } else if a.index == b.index { |
| std::cmp::Ordering::Equal |
| } else { |
| std::cmp::Ordering::Greater |
| } |
| }; |
| sorted.sort_by(comparator); |
| |
| let indices: Vec<u8> = sorted.iter().map(|f| f.index).collect(); |
| let expected: Vec<u8> = (0..sorted.len()).map(|x| x as u8).collect(); |
| if indices != expected { |
| return Err(Error::InvalidReportFormats { |
| reason: format!( |
| "ReportFormat indices {:?} must sort to {:?}", |
| indices, expected |
| ), |
| }); |
| } |
| |
| sorted |
| }; |
| |
| // Determine the ScalarType and ReportedValueType of each field. |
| let mut scalar_types = Vec::new(); |
| let mut reported_types = Vec::new(); |
| for field_format in format { |
| scalar_types.push(field_format.field_type); |
| |
| let scale = field_format.scale; |
| let reported_type = if scale == 0.0 { |
| ReportedValueType::Unscaled(field_format.field_type) |
| } else { |
| match field_format.field_type { |
| ScalarType::U8 => ReportedValueType::ScaledU8(scale), |
| ScalarType::U16 => ReportedValueType::ScaledU16(scale), |
| ScalarType::I8 => ReportedValueType::ScaledI8(scale), |
| ScalarType::I16 => ReportedValueType::ScaledI16(scale), |
| _ => { |
| return Err(Error::InvalidScaledType { |
| name: field_format.name.clone(), |
| scalar_type: field_format.field_type, |
| }) |
| } |
| } |
| }; |
| reported_types.push(reported_type); |
| } |
| |
| Ok(ReportParser { |
| report_length: calc_report_length(&scalar_types), |
| value_types: reported_types, |
| }) |
| } |
| |
| /// Parses a Report packet to a Vec<Report>. |
| pub fn parse_reports<T: AsRef<[u8]> + ?Sized>(&self, packet: &T) -> Result<Vec<Report>> { |
| let num_bytes = packet.as_ref().len(); |
| |
| // Determine how many reports follow the leading PacketType byte. |
| let num_reports = (num_bytes - 1) / self.report_length; |
| |
| let mut cursor = validate_packet(packet, num_bytes, PacketType::Report)?; |
| |
| let mut reports = Vec::with_capacity(num_reports); |
| |
| for _ in 0..num_reports { |
| let timestamp = cursor.read_u64::<LE>()?; |
| let mut values = Vec::new(); |
| for t in &self.value_types { |
| let value = match *t { |
| ReportedValueType::Unscaled(st) => match st { |
| ScalarType::U8 => Value::U8(cursor.read_u8()?), |
| ScalarType::U16 => Value::U16(cursor.read_u16::<LE>()?), |
| ScalarType::U32 => Value::U32(cursor.read_u32::<LE>()?), |
| ScalarType::U64 => Value::U64(cursor.read_u64::<LE>()?), |
| ScalarType::I8 => Value::I8(cursor.read_i8()?), |
| ScalarType::I16 => Value::I16(cursor.read_i16::<LE>()?), |
| ScalarType::I32 => Value::I32(cursor.read_i32::<LE>()?), |
| ScalarType::I64 => Value::I64(cursor.read_i64::<LE>()?), |
| ScalarType::Bool => Value::Bool(cursor.read_u8()? != 0), |
| ScalarType::F32 => Value::F32(cursor.read_f32::<LE>()?), |
| }, |
| ReportedValueType::ScaledU8(scale) => { |
| Value::F32(cursor.read_u8()? as f32 * scale) |
| } |
| ReportedValueType::ScaledU16(scale) => { |
| Value::F32(cursor.read_u16::<LE>()? as f32 * scale) |
| } |
| ReportedValueType::ScaledI8(scale) => { |
| Value::F32(cursor.read_i8()? as f32 * scale) |
| } |
| ReportedValueType::ScaledI16(scale) => { |
| Value::F32(cursor.read_i16::<LE>()? as f32 * scale) |
| } |
| }; |
| values.push(value); |
| } |
| reports.push(Report { timestamp_micros: timestamp, values }); |
| } |
| Ok(reports) |
| } |
| } |
| |
| /// This module is public to expose serialization functions to tests in other modules. |
| #[cfg(test)] |
| pub mod tests { |
| use {super::*, byteorder::WriteBytesExt, matches::assert_matches, std::io::Write}; |
| |
| fn get_scalar_type(value: &Value) -> ScalarType { |
| match *value { |
| Value::U8(_) => ScalarType::U8, |
| Value::U16(_) => ScalarType::U16, |
| Value::U32(_) => ScalarType::U32, |
| Value::U64(_) => ScalarType::U64, |
| Value::I8(_) => ScalarType::I8, |
| Value::I16(_) => ScalarType::I16, |
| Value::I32(_) => ScalarType::I32, |
| Value::I64(_) => ScalarType::I64, |
| Value::Bool(_) => ScalarType::Bool, |
| Value::F32(_) => ScalarType::F32, |
| } |
| } |
| |
| /// Serializes a ReportFormat into a buffer, returning the number of bytes written. |
| pub fn serialize_report_format(format: ReportFormat, buffer: &mut [u8]) -> usize { |
| let mut buffer = &mut buffer[..MAX_PACKET_SIZE]; |
| |
| buffer |
| .write(&[ |
| PacketType::ReportFormat as u8, |
| format.index, |
| format.field_type as u8, |
| format.unit as u8, |
| ]) |
| .unwrap(); |
| buffer.write_f32::<LE>(format.scale).unwrap(); |
| |
| // Make sure the name leaves room for a null terminator, write it, then zero the rest of the |
| // buffer. |
| let name = &format.name; |
| assert!(name.len() < buffer.len()); |
| buffer.write(name.as_bytes()).unwrap(); |
| for i in 0..buffer.len() { |
| buffer[i] = 0; |
| } |
| |
| MAX_PACKET_SIZE |
| } |
| |
| /// Serializes a ParameterValue into a buffer, returning the number of bytes written. |
| pub fn serialize_parameter_value(parameter: ParameterValue, buffer: &mut [u8]) -> usize { |
| let buffer = &mut buffer[..MAX_PACKET_SIZE]; |
| |
| buffer[0] = PacketType::ParameterValue as u8; |
| |
| // Write the parameter name, making sure there's room for a null terminator. |
| let name_length = parameter.name.len(); |
| assert!(name_length < PARAMETER_VALUE_NAME_LENGTH); |
| buffer[1..name_length + 1].copy_from_slice(parameter.name.as_bytes()); |
| |
| // Zero out the remainder of the name region, and the value region as well, so we don't have |
| // to worry about the number of bytes to zero for each ScalarType. |
| for i in name_length + 1..MAX_PACKET_SIZE { |
| buffer[i] = 0; |
| } |
| |
| // Write the value. |
| let mut value_buffer = &mut buffer[1 + PARAMETER_VALUE_NAME_LENGTH..]; |
| value_buffer.write_u8(get_scalar_type(¶meter.value) as u8).unwrap(); |
| match parameter.value { |
| Value::U8(v) => value_buffer.write_u8(v).unwrap(), |
| Value::U16(v) => value_buffer.write_u16::<LE>(v).unwrap(), |
| Value::U32(v) => value_buffer.write_u32::<LE>(v).unwrap(), |
| Value::U64(v) => value_buffer.write_u64::<LE>(v).unwrap(), |
| Value::I8(v) => value_buffer.write_i8(v).unwrap(), |
| Value::I16(v) => value_buffer.write_i16::<LE>(v).unwrap(), |
| Value::I32(v) => value_buffer.write_i32::<LE>(v).unwrap(), |
| Value::I64(v) => value_buffer.write_i64::<LE>(v).unwrap(), |
| Value::Bool(v) => value_buffer.write_u8(v as u8).unwrap(), |
| Value::F32(v) => value_buffer.write_f32::<LE>(v).unwrap(), |
| } |
| MAX_PACKET_SIZE |
| } |
| |
| /// Serializes a Vec<Report> into a buffer, returning the number of bytes written. |
| /// |
| /// If the Values in a given Report are to be scaled, they should be sent to this function in |
| /// their *unscaled* form. Note that serialize_reports is not the inverse of ReportParser.parse |
| /// for scaled Values. |
| pub fn serialize_reports(reports: &Vec<Report>, mut buffer: &mut [u8]) -> usize { |
| let field_types = reports[0].values.iter().map(|v| get_scalar_type(v)).collect(); |
| |
| // Make sure the buffer has enough space for the reports. If this fails, the test probably isn't |
| // configured correctly. |
| let num_bytes = calc_report_length(&field_types) * reports.len() + 1; |
| assert!(num_bytes <= buffer.len()); |
| |
| buffer.write_u8(PacketType::Report as u8).unwrap(); |
| for report in reports { |
| buffer.write_u64::<LE>(report.timestamp_micros).unwrap(); |
| for (value, field_type) in report.values.iter().zip(field_types.iter()) { |
| assert_eq!( |
| get_scalar_type(value), |
| *field_type, |
| "Value {:?} has wrong type (need {:?})", |
| *value, |
| *field_type |
| ); |
| match *value { |
| Value::U8(v) => buffer.write_u8(v).unwrap(), |
| Value::U16(v) => buffer.write_u16::<LE>(v).unwrap(), |
| Value::U32(v) => buffer.write_u32::<LE>(v).unwrap(), |
| Value::U64(v) => buffer.write_u64::<LE>(v).unwrap(), |
| Value::I8(v) => buffer.write_i8(v).unwrap(), |
| Value::I16(v) => buffer.write_i16::<LE>(v).unwrap(), |
| Value::I32(v) => buffer.write_i32::<LE>(v).unwrap(), |
| Value::I64(v) => buffer.write_i64::<LE>(v).unwrap(), |
| Value::Bool(v) => buffer.write_u8(v as u8).unwrap(), |
| Value::F32(v) => buffer.write_f32::<LE>(v).unwrap(), |
| } |
| } |
| } |
| |
| num_bytes |
| } |
| |
| /// Serializes a timestamp into a buffer, returning the number of bytes written. |
| pub fn serialize_timestamp_micros(timestamp: u64, mut buffer: &mut [u8]) -> usize { |
| buffer.write_u8(PacketType::Timestamp as u8).unwrap(); |
| buffer.write_u64::<LE>(timestamp).unwrap(); |
| 9 |
| } |
| |
| #[test] |
| fn test_validate_packet() { |
| // Successful validation |
| assert!(validate_packet( |
| &[PacketType::ParameterValue as u8, 0x00], |
| 2, |
| PacketType::ParameterValue |
| ) |
| .is_ok()); |
| |
| // Bad PacketType |
| assert_matches!( |
| validate_packet( |
| &[0xff, 0x00], 2, PacketType::ParameterValue), |
| Err(Error::UnknownEnumValue { |
| primitive: 0xff, |
| enum_name |
| }) if enum_name.contains("PacketType") |
| ); |
| |
| // Wrong packet length |
| assert_eq!( |
| validate_packet( |
| &[PacketType::ParameterValue as u8, 0x00], |
| 3, |
| PacketType::ParameterValue |
| ), |
| Err(Error::ShortData { required: 3, received: 2 }) |
| ); |
| |
| // Wrong PacketType |
| assert_eq!( |
| validate_packet(&[PacketType::ReportFormat as u8, 0x00], 2, PacketType::ParameterValue), |
| Err(Error::WrongPacketType { |
| expected: PacketType::ParameterValue, |
| received: PacketType::ReportFormat |
| }) |
| ); |
| } |
| |
| #[test] |
| fn test_parse_report_format() { |
| let mut base_packet = [0; MAX_PACKET_SIZE]; |
| serialize_report_format( |
| ReportFormat { |
| index: 1, |
| field_type: ScalarType::F32, |
| unit: Unit::Volts, |
| scale: 0.00125, |
| name: "v_bus".to_string(), |
| }, |
| &mut base_packet, |
| ); |
| |
| // Base packet parses successfully |
| assert_eq!( |
| parse_report_format(&base_packet), |
| Ok(ReportFormat { |
| index: 1, |
| field_type: ScalarType::F32, |
| unit: Unit::Volts, |
| scale: 0.00125, |
| name: "v_bus".to_string(), |
| }) |
| ); |
| |
| // Wrong packet type |
| let mut packet = base_packet.clone(); |
| packet[0] = PacketType::ParameterValue as u8; |
| assert_eq!( |
| parse_report_format(&packet), |
| Err(Error::WrongPacketType { |
| expected: PacketType::ReportFormat, |
| received: PacketType::ParameterValue, |
| }) |
| ); |
| |
| // Bad field type |
| let mut packet = base_packet.clone(); |
| packet[2] = 0xff; // Invalid ScalarType |
| assert_matches!( |
| parse_report_format(&packet), |
| Err(Error::UnknownEnumValue { |
| primitive: 0xff, |
| enum_name |
| }) if enum_name.contains("ScalarType") |
| ); |
| |
| // Bad unit |
| let mut packet = base_packet.clone(); |
| packet[3] = 0xff; // Invalid Unit |
| assert_matches!( |
| parse_report_format(&packet), |
| Err(Error::UnknownEnumValue { |
| primitive: 0xff, |
| enum_name |
| }) if enum_name.contains("Unit") |
| ); |
| |
| // Invalid string |
| let mut packet = base_packet.clone(); |
| packet[8] = 0xff; // Invalid UTF-8 character |
| assert_eq!(parse_report_format(&packet), Err(Error::InvalidString)); |
| } |
| |
| #[test] |
| fn test_parse_parameter_value() { |
| let mut base_packet = [0; MAX_PACKET_SIZE]; |
| serialize_parameter_value( |
| ParameterValue { name: "shunt_resistance".to_string(), value: Value::F32(0.01) }, |
| &mut base_packet, |
| ); |
| |
| // Base packet parses successfully |
| assert_eq!( |
| parse_parameter_value(&base_packet), |
| Ok(ParameterValue { name: "shunt_resistance".to_string(), value: Value::F32(0.01) }) |
| ); |
| |
| // Wrong packet type |
| let mut packet = base_packet.clone(); |
| packet[0] = PacketType::ReportFormat as u8; |
| assert_eq!( |
| parse_parameter_value(&packet), |
| Err(Error::WrongPacketType { |
| expected: PacketType::ParameterValue, |
| received: PacketType::ReportFormat, |
| }) |
| ); |
| |
| // Invalid string |
| let mut packet = base_packet.clone(); |
| packet[1] = 0xff; // Invalid UTF-8 character |
| assert_eq!(parse_parameter_value(&packet), Err(Error::InvalidString)); |
| |
| // Bad field type |
| let mut packet = base_packet.clone(); |
| packet[1 + PARAMETER_VALUE_NAME_LENGTH] = 0xff; // Invalid ScalarType |
| assert_matches!( |
| parse_parameter_value(&packet), |
| Err(Error::UnknownEnumValue { |
| primitive: 0xff, |
| enum_name |
| }) if enum_name.contains("ScalarType") |
| ); |
| } |
| |
| #[test] |
| fn test_parse_timestamp_micros() { |
| let mut base_packet = [0; 9]; |
| serialize_timestamp_micros(1234567, &mut base_packet); |
| |
| // Base packet parses successfully |
| assert_eq!(parse_timestamp_micros(&base_packet), Ok(1234567)); |
| |
| let mut packet = base_packet.clone(); |
| packet[0] = PacketType::ParameterValue as u8; |
| assert_eq!( |
| parse_timestamp_micros(&packet), |
| Err(Error::WrongPacketType { |
| expected: PacketType::Timestamp, |
| received: PacketType::ParameterValue, |
| }) |
| ); |
| } |
| |
| #[test] |
| fn test_report_parser() -> Result<()> { |
| /// Makes a Vec<ReportFormat> from an array or Vec of tuples. |
| fn make_format<V>(field_formats: V) -> Vec<ReportFormat> |
| where |
| for<'a> &'a V: IntoIterator<Item = &'a (u8, ScalarType, f32)>, |
| { |
| field_formats |
| .into_iter() |
| .map(|v| ReportFormat { |
| index: v.0, |
| field_type: v.1, |
| unit: Unit::Volts, |
| scale: v.2, |
| name: "foo".to_string(), |
| }) |
| .collect() |
| } |
| |
| fn serialize_and_parse( |
| reports_in: &Vec<Report>, |
| parser: &ReportParser, |
| ) -> Result<Vec<Report>> { |
| let mut packet = [0; MAX_PACKET_SIZE]; |
| let bytes_used = serialize_reports(&reports_in, &mut packet[..]); |
| parser.parse_reports(&packet[..bytes_used]) |
| } |
| |
| // Test all unscaled types. |
| let format = make_format([ |
| (0, ScalarType::U8, 0.0), |
| (1, ScalarType::U16, 0.0), |
| (2, ScalarType::U32, 0.0), |
| (3, ScalarType::U64, 0.0), |
| (4, ScalarType::I8, 0.0), |
| (5, ScalarType::I16, 0.0), |
| (6, ScalarType::I32, 0.0), |
| (7, ScalarType::I64, 0.0), |
| (8, ScalarType::Bool, 0.0), |
| (9, ScalarType::F32, 0.0), |
| ]); |
| let parser = ReportParser::new(&format)?; |
| let reports_in = vec![Report { |
| timestamp_micros: 1000, |
| values: vec![ |
| Value::U8(u8::MAX), |
| Value::U16(u16::MAX), |
| Value::U32(u32::MAX), |
| Value::U64(u64::MAX), |
| Value::I8(i8::MIN), |
| Value::I16(i16::MIN), |
| Value::I32(i32::MIN), |
| Value::I64(i64::MIN), |
| Value::Bool(true), |
| Value::F32(f32::MAX), |
| ], |
| }]; |
| let reports_out = serialize_and_parse(&reports_in, &parser)?; |
| assert_eq!(reports_in, reports_out); |
| |
| // Test all scaled types. |
| let format = make_format([ |
| (0, ScalarType::U8, 0.1), |
| (1, ScalarType::U16, 0.2), |
| (2, ScalarType::I8, 0.3), |
| (3, ScalarType::I16, 0.4), |
| ]); |
| let parser = ReportParser::new(&format)?; |
| let reports_in = vec![Report { |
| timestamp_micros: 1000, |
| values: vec![ |
| Value::U8(u8::MAX), |
| Value::U16(u16::MAX), |
| Value::I8(i8::MIN), |
| Value::I16(i16::MIN), |
| ], |
| }]; |
| let reports_out = serialize_and_parse(&reports_in, &parser)?; |
| let expected = vec![Report { |
| timestamp_micros: 1000, |
| values: vec![ |
| Value::F32(u8::MAX as f32 * 0.1), |
| Value::F32(u16::MAX as f32 * 0.2), |
| Value::F32(i8::MIN as f32 * 0.3), |
| Value::F32(i16::MIN as f32 * 0.4), |
| ], |
| }]; |
| assert_eq!(reports_out, expected); |
| |
| // Test multiple reports. |
| let format = make_format([(0, ScalarType::U8, 0.0), (1, ScalarType::I16, 0.0)]); |
| let parser = ReportParser::new(&format)?; |
| let reports_in = vec![ |
| Report { timestamp_micros: 1000, values: vec![Value::U8(1), Value::I16(-1)] }, |
| Report { timestamp_micros: 2000, values: vec![Value::U8(2), Value::I16(-2)] }, |
| Report { timestamp_micros: 3000, values: vec![Value::U8(3), Value::I16(-3)] }, |
| Report { timestamp_micros: 4000, values: vec![Value::U8(4), Value::I16(-4)] }, |
| ]; |
| let reports_out = serialize_and_parse(&reports_in, &parser)?; |
| assert_eq!(reports_in, reports_out); |
| |
| // Test sorting of field formats by index |
| let format = make_format([ |
| (2, ScalarType::F32, 0.0), |
| (0, ScalarType::U8, 0.0), |
| (1, ScalarType::I16, 0.0), |
| ]); |
| let parser = ReportParser::new(&format)?; |
| let reports_in = vec![Report { |
| timestamp_micros: 1000, |
| values: vec![Value::U8(u8::MAX), Value::I16(i16::MIN), Value::F32(f32::MIN)], |
| }]; |
| let reports_out = serialize_and_parse(&reports_in, &parser)?; |
| assert_eq!(reports_in, reports_out); |
| |
| // Test error on invalid ReportFormat indices |
| let format = make_format([ |
| (0, ScalarType::U8, 0.0), |
| (1, ScalarType::I16, 0.0), |
| (3, ScalarType::F32, 0.0), |
| ]); |
| let result = ReportParser::new(&format); |
| assert_matches!(result, Err(Error::InvalidReportFormats { reason: _ })); |
| |
| // Test error on invalid scaled type |
| let format = make_format([(0, ScalarType::Bool, 0.1)]); |
| let result = ReportParser::new(&format); |
| assert_matches!(result, Err(Error::InvalidScaledType { name: _, scalar_type: _ })); |
| |
| Ok(()) |
| } |
| } |