| // 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. |
| |
| // TODO(claridge): Remove once the protocol is in use. This is needed temporarily while building |
| // up this crate in isolation. |
| #![allow(dead_code)] |
| |
| use { |
| byteorder::{LittleEndian as LE, ReadBytesExt}, |
| num::FromPrimitive, |
| num_derive::FromPrimitive, |
| std::io::{BufRead, Cursor}, |
| std::str, |
| }; |
| |
| /// 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(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(Debug, FromPrimitive, PartialEq)] |
| pub enum Unit { |
| Amperes = 0x0, |
| Volts = 0x1, |
| } |
| |
| /// A value corresponding to a ScalarType. |
| #[derive(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(Debug, PartialEq)] |
| pub struct ReportFormat { |
| pub index: u8, |
| pub field_type: ScalarType, |
| pub unit: Unit, |
| pub scale: f32, |
| pub name: String, |
| } |
| |
| #[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 }, |
| } |
| |
| /// 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>(), |
| }) |
| } |
| } |
| |
| /// Validates that `packet` has the required length and `PacketType`. Returns a cursor to the |
| /// remainder of the packet's data. |
| fn validate_packet<T: AsRef<[u8]>>( |
| packet: &T, |
| len: usize, |
| expected_type: PacketType, |
| ) -> Result<Cursor<&T>> { |
| let packet_len = packet.as_ref().len(); |
| if packet_len < len { |
| return Err(Error::ShortData { required: len, received: packet_len }); |
| } |
| |
| 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 }); |
| } |
| 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]>>(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 }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use matches::assert_matches; |
| |
| #[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(&[0xff, 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() { |
| fn base_packet() -> Vec<u8> { |
| let mut packet = Vec::new(); |
| packet.push(PacketType::ReportFormat as u8); |
| packet.push(1); |
| packet.push(ScalarType::F32 as u8); |
| packet.push(Unit::Volts as u8); |
| packet.extend(0.00125f32.to_le_bytes().as_ref()); |
| packet.extend(b"v_bus".as_ref()); |
| packet.extend(&vec![0u8; REPORT_FIELD_NAME_LENGTH - "v_bus".len()]); |
| 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(); |
| 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(); |
| 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(); |
| 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(); |
| packet[8] = 0xff; // Invalid UTF-8 character |
| assert_eq!(parse_report_format(&packet), Err(Error::InvalidString)); |
| } |
| |
| #[test] |
| fn test_parse_parameter_value() { |
| fn base_packet() -> Vec<u8> { |
| let mut packet = Vec::new(); |
| packet.push(PacketType::ParameterValue as u8); |
| packet.extend(b"shunt_resistance".as_ref()); |
| packet.extend(vec![0u8; PARAMETER_VALUE_NAME_LENGTH - "shunt_resistance".len()]); |
| packet.push(ScalarType::F32 as u8); |
| packet.extend(0.01f32.to_le_bytes().as_ref()); |
| packet.extend([0x00; 4].as_ref()); // 4 bytes padding |
| 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(); |
| 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(); |
| packet[1] = 0xff; // Invalid UTF-8 character |
| assert_eq!(parse_parameter_value(&packet), Err(Error::InvalidString)); |
| |
| // Bad field type |
| let mut packet = base_packet(); |
| 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") |
| ); |
| } |
| } |