diff --git a/Cargo.lock b/Cargo.lock index dc0cf8d..7592a97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#ef0a85e2708af97849940dcc14fc12cca95e8b1c" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" dependencies = [ "aquamarine", "bincode", diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 6c21c8e..e8880fa 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -561,7 +561,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#98ca3abe6e5014aa63826a3e7ca4eb59ed2b059e" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" dependencies = [ "aquamarine", "bincode", diff --git a/src/asynchronous/embassy/mod.rs b/src/asynchronous/embassy/mod.rs index d58544d..c114f3e 100644 --- a/src/asynchronous/embassy/mod.rs +++ b/src/asynchronous/embassy/mod.rs @@ -583,6 +583,14 @@ impl<'a, M: RawMutex, B: I2c> Tps6699x<'a, M, B> { inner.set_sx_app_config(port, state).await } + /// Get the discovered SVIDs on a port returned from `Discover SVIDs REQ` messages. + pub async fn get_discovered_svids( + &mut self, + port: LocalPortId, + ) -> Result> { + self.lock_inner().await.get_discovered_svids(port).await + } + /// Get Rx ADO pub async fn get_rx_ado( &mut self, @@ -706,6 +714,22 @@ impl<'a, M: RawMutex, B: I2c> Tps6699x<'a, M, B> { self.lock_inner().await.modify_tx_identity(port, f).await } + /// Get the latest received SOP identity data + pub async fn get_received_sop_identity_data( + &mut self, + port: LocalPortId, + ) -> Result> { + self.lock_inner().await.get_received_sop_identity_data(port).await + } + + /// Get the latest received SOP Prime identity data + pub async fn get_received_sop_prime_identity_data( + &mut self, + port: LocalPortId, + ) -> Result> { + self.lock_inner().await.get_received_sop_prime_identity_data(port).await + } + /// Get DP config pub async fn get_dp_config( &mut self, @@ -740,6 +764,11 @@ impl<'a, M: RawMutex, B: I2c> Tps6699x<'a, M, B> { self.execute_command(port, Command::Drst, None, None).await } + /// Execute the [`Command::HRST`] command. + pub async fn execute_hrst(&mut self, port: LocalPortId) -> Result> { + self.execute_command(port, Command::HRST, None, None).await + } + /// Get Rx discovered custom modes pub async fn execute_gcdm( &mut self, diff --git a/src/asynchronous/internal/mod.rs b/src/asynchronous/internal/mod.rs index 952db9a..8688350 100644 --- a/src/asynchronous/internal/mod.rs +++ b/src/asynchronous/internal/mod.rs @@ -598,6 +598,25 @@ impl Tps6699x { .await } + /// Get the discovered SVIDs on a port returned from `Discover SVIDs REQ` messages. + pub async fn get_discovered_svids( + &mut self, + port: LocalPortId, + ) -> Result> { + let mut buf = [0u8; registers::discovered_svids::LEN]; + self.borrow_port(port)? + .into_registers() + .interface() + .read_register( + registers::discovered_svids::ADDR, + (registers::discovered_svids::LEN * 8) as u32, + &mut buf, + ) + .await?; + + Ok(buf.into()) + } + /// Get Rx ADO pub async fn get_rx_ado(&mut self, port: LocalPortId) -> Result> { self.borrow_port(port)?.into_registers().rx_ado().read_async().await @@ -753,6 +772,42 @@ impl Tps6699x { self.set_tx_identity(port, reg.clone()).await?; Ok(reg) } + + /// Get the latest received SOP identity data + pub async fn get_received_sop_identity_data( + &mut self, + port: LocalPortId, + ) -> Result> { + let mut buf = [0u8; registers::received_sop_identity_data::LEN]; + self.borrow_port(port)? + .into_registers() + .interface() + .read_register( + registers::received_sop_identity_data::ADDR, + (registers::received_sop_identity_data::LEN * 8) as u32, + &mut buf, + ) + .await?; + Ok(buf.into()) + } + + /// Get the latest received SOP Prime identity data + pub async fn get_received_sop_prime_identity_data( + &mut self, + port: LocalPortId, + ) -> Result> { + let mut buf = [0u8; registers::received_sop_prime_identity_data::LEN]; + self.borrow_port(port)? + .into_registers() + .interface() + .read_register( + registers::received_sop_prime_identity_data::ADDR, + (registers::received_sop_prime_identity_data::LEN * 8) as u32, + &mut buf, + ) + .await?; + Ok(buf.into()) + } } #[cfg(test)] diff --git a/src/command/gcdm.rs b/src/command/gcdm.rs index c714e90..dcdba00 100644 --- a/src/command/gcdm.rs +++ b/src/command/gcdm.rs @@ -1,5 +1,5 @@ //! Get custom discovered modes command -use embedded_usb_pd::vdm::Svid; +use embedded_usb_pd::vdm::structured::Svid; /// Input data length pub const INPUT_LEN: usize = 3; diff --git a/src/command/mod.rs b/src/command/mod.rs index 0466efb..8c4afa1 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -104,6 +104,15 @@ pub enum Command { /// [`ReturnValue`] Drst = u32_from_str(*b"DRST"), + /// Hard Reset + /// + /// # Input + /// None + /// + /// # Output + /// [`ReturnValue`] + HRST = u32_from_str(*b"HRST"), + /// Send VDM. /// /// # Input @@ -170,6 +179,8 @@ impl TryFrom for Command { Ok(Command::Muxr) } else if Command::Drst == value { Ok(Command::Drst) + } else if Command::HRST == value { + Ok(Command::HRST) } else if Command::VDMs == value { Ok(Command::VDMs) } else if Command::Ucsi == value { @@ -718,6 +729,7 @@ mod test { assert_eq!(Command::try_from(Command::Dbfg as u32).unwrap(), Command::Dbfg); assert_eq!(Command::try_from(Command::Muxr as u32).unwrap(), Command::Muxr); assert_eq!(Command::try_from(Command::Drst as u32).unwrap(), Command::Drst); + assert_eq!(Command::try_from(Command::HRST as u32).unwrap(), Command::HRST); assert_eq!(Command::try_from(Command::VDMs as u32).unwrap(), Command::VDMs); assert_eq!(Command::try_from(Command::Ucsi as u32).unwrap(), Command::Ucsi); assert_eq!(Command::try_from(0xFFFFFFFFu32), Err(PdError::InvalidParams)); diff --git a/src/registers/discovered_svids.rs b/src/registers/discovered_svids.rs new file mode 100644 index 0000000..2c4edc9 --- /dev/null +++ b/src/registers/discovered_svids.rs @@ -0,0 +1,210 @@ +//! Discovered SVIDs register (`0x21`). +//! +//! This register's size exceeds the maximum supported length by the [`device_driver`] crate. +//! +//! This register contains the SVID information returned from `Discover SVIDs REQ` messages. + +use bitfield::bitfield; +use embedded_usb_pd::vdm::structured::Svid; + +/// The address of the `Discovered SVIDs` register. +pub const ADDR: u8 = 0x21; + +/// The length of the `Discovered SVIDs` register, in bytes. +/// +/// This exceeds the maximum supported length by the [`device_driver`] crate. +pub const LEN: usize = 264 / 8; + +bitfield! { + /// Discovered SVIDs register. + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + struct Raw([u8]); + impl Debug; + + /// Number of SVIDs discovered on SOP. + pub u8, number_sop_svids, set_number_sop_svids: 3, 0; + /// Number of SVIDs discovered on SOP'. + pub u8, number_sop_prime_svids, set_number_sop_prime_svids: 7, 4; + + /// First SVID supported by SOP port partner. + pub u16, svid_sop0, set_svid_sop0: 23, 8; + + /// Second SVID supported by SOP port partner. + pub u16, svid_sop1, set_svid_sop1: 39, 24; + + /// Third SVID supported by SOP port partner. + pub u16, svid_sop2, set_svid_sop2: 55, 40; + + /// Fourth SVID supported by SOP port partner. + pub u16, svid_sop3, set_svid_sop3: 71, 56; + + /// Fifth SVID supported by SOP port partner. + pub u16, svid_sop4, set_svid_sop4: 87, 72; + + /// Sixth SVID supported by SOP port partner. + pub u16, svid_sop5, set_svid_sop5: 103, 88; + + /// Seventh SVID supported by SOP port partner. + pub u16, svid_sop6, set_svid_sop6: 119, 104; + + /// Eighth SVID supported by SOP port partner. + pub u16, svid_sop7, set_svid_sop7: 135, 120; + + /// First SVID supported by SOP' cable plug + pub u16, svid_sop_prime0, set_svid_sop_prime0: 151, 136; + + /// Second SVID supported by SOP' cable plug + pub u16, svid_sop_prime1, set_svid_sop_prime1: 167, 152; + + /// Third SVID supported by SOP' cable plug + pub u16, svid_sop_prime2, set_svid_sop_prime2: 183, 168; + + /// Fourth SVID supported by SOP' cable plug + pub u16, svid_sop_prime3, set_svid_sop_prime3: 199, 184; + + /// Fifth SVID supported by SOP' cable plug + pub u16, svid_sop_prime4, set_svid_sop_prime4: 215, 200; + + /// Sixth SVID supported by SOP' cable plug + pub u16, svid_sop_prime5, set_svid_sop_prime5: 231, 216; + + /// Seventh SVID supported by SOP' cable plug + pub u16, svid_sop_prime6, set_svid_sop_prime6: 247, 232; + + /// Eighth SVID supported by SOP' cable plug + pub u16, svid_sop_prime7, set_svid_sop_prime7: 263, 248; +} + +/// Discovered SVIDs register, containing the SVID information returned from `Discover SVIDs REQ` messages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DiscoveredSvids(Raw<[u8; LEN]>); + +impl DiscoveredSvids { + pub const DEFAULT: Self = Self(Raw([0; LEN])); + + /// Returns the number of SVIDs discovered on the SOP port partner. + pub fn number_sop_svids(&self) -> usize { + self.0.number_sop_svids() as usize + } + + /// Returns an iterator over the SVIDs discovered on the SOP port partner. + /// + /// This will return at most 8 SVIDs, even if [`Self::number_sop_svids`] returns + /// a larger number. Neither the data sheet nor the USB PD specification explicitly + /// state that there can be at most 8 SVIDs, but the register only has space + /// for 8. + pub fn svid_sop(&self) -> impl ExactSizeIterator { + [ + self.0.svid_sop0(), + self.0.svid_sop1(), + self.0.svid_sop2(), + self.0.svid_sop3(), + self.0.svid_sop4(), + self.0.svid_sop5(), + self.0.svid_sop6(), + self.0.svid_sop7(), + ] + .into_iter() + .take(self.number_sop_svids()) + .map(Svid) + } + + /// Returns the number of SVIDs discovered on the SOP' cable plug. + pub fn number_sop_prime_svids(&self) -> usize { + self.0.number_sop_prime_svids() as usize + } + + /// Returns an iterator over the SVIDs discovered on the SOP' cable plug. + /// + /// This will return at most 8 SVIDs, even if [`Self::number_sop_prime_svids`] + /// returns a larger number. Neither the data sheet nor the USB PD specification + /// explicitly state that there can be at most 8 SVIDs, but the register only + /// has space for 8. + pub fn svid_sop_prime(&self) -> impl ExactSizeIterator { + [ + self.0.svid_sop_prime0(), + self.0.svid_sop_prime1(), + self.0.svid_sop_prime2(), + self.0.svid_sop_prime3(), + self.0.svid_sop_prime4(), + self.0.svid_sop_prime5(), + self.0.svid_sop_prime6(), + self.0.svid_sop_prime7(), + ] + .into_iter() + .take(self.number_sop_prime_svids()) + .map(Svid) + } +} + +impl Default for DiscoveredSvids { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From<[u8; LEN]> for DiscoveredSvids { + fn from(raw: [u8; LEN]) -> Self { + Self(Raw(raw)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + extern crate std; + use std::vec; + use std::vec::Vec; + + #[test] + fn impl_iterators() { + let mut reg = DiscoveredSvids::default(); + + reg.0.set_number_sop_svids(3); + reg.0.set_svid_sop0(0x1234); + reg.0.set_svid_sop1(0x5678); + reg.0.set_svid_sop2(0x9ABC); + + reg.0.set_number_sop_prime_svids(2); + reg.0.set_svid_sop_prime0(0xDEF0); + reg.0.set_svid_sop_prime1(0xFFFF); + + assert_eq!(reg.number_sop_svids(), 3); + assert_eq!( + reg.svid_sop().collect::>(), + vec![Svid(0x1234), Svid(0x5678), Svid(0x9ABC)] + ); + + assert_eq!(reg.number_sop_prime_svids(), 2); + assert_eq!( + reg.svid_sop_prime().collect::>(), + vec![Svid(0xDEF0), Svid(0xFFFF)] + ); + } + + #[test] + fn default_has_no_svids() { + let reg = DiscoveredSvids::default(); + assert_eq!(reg.number_sop_svids(), 0); + assert_eq!(reg.svid_sop().len(), 0); + + assert_eq!(reg.number_sop_prime_svids(), 0); + assert_eq!(reg.svid_sop_prime().len(), 0); + } + + /// There's only space fo r8 SVIDs in the register but the number of SVIDs could + /// be larger, so the iterators should clamp to 8. + #[test] + fn iterators_clamp_to_8() { + let mut reg = DiscoveredSvids::default(); + + reg.0.set_number_sop_svids(9); + reg.0.set_number_sop_prime_svids(10); + assert_eq!(reg.number_sop_svids(), 9); + assert_eq!(reg.number_sop_prime_svids(), 10); + assert_eq!(reg.svid_sop().len(), 8); + assert_eq!(reg.svid_sop_prime().len(), 8); + } +} diff --git a/src/registers/mod.rs b/src/registers/mod.rs index 3a67024..18f4303 100644 --- a/src/registers/mod.rs +++ b/src/registers/mod.rs @@ -5,8 +5,11 @@ use crate::Mode; pub mod autonegotiate_sink; pub mod boot_flags; +pub mod discovered_svids; pub mod dp_status; pub mod port_config; +pub mod received_sop_identity_data; +pub mod received_sop_prime_identity_data; pub mod rx_caps; pub mod rx_other_vdm; pub mod tx_identity; diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs new file mode 100644 index 0000000..c7041ba --- /dev/null +++ b/src/registers/received_sop_identity_data.rs @@ -0,0 +1,665 @@ +//! Received SOP Identity Data Object register (`0x48`). +//! +//! This register's size exceeds the maximum supported length by the [`device_driver`] crate. +//! +//! This register contains the response to Discover Identity command sent to the SOP port partner. + +use bitfield::bitfield; +use embedded_usb_pd::vdm::structured::command::discover_identity::sop::{ + id_header_vdo, DfpProductTypeVdos, IdHeaderVdo, UfpProductTypeVdos, +}; +use embedded_usb_pd::vdm::structured::command::discover_identity::ufp_vdo::ParseUfpVdoError; +use embedded_usb_pd::vdm::structured::command::discover_identity::{CertStatVdo, ProductTypeVdo, ProductVdo}; +use embedded_usb_pd::vdm::structured::header::CommandType; + +use crate::debug; + +/// The address of the `Received SOP Identity Data Object` register. +pub const ADDR: u8 = 0x48; + +/// The length of the `Received SOP Identity Data Object` register, in bytes. +/// +/// This exceeds the maximum supported length by the [`device_driver`] crate. +pub const LEN: usize = 200 / 8; + +/// Index of the DFP VDO in the Received SOP Identity Data Object's Product Type VDOs +/// when the port partner supports dual-role (UFP and DFP) functionality. +/// +/// - See [`SINGLE_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX`]. +/// - See PD spec 6.4.4.3.1 Discover Identity. +const DUAL_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX: usize = 2; + +/// Index of the DFP VDO in the Received SOP Identity Data Object's Product Type VDOs +/// when the port partner only supports single-role (DFP only) functionality. +/// +/// - See [`DUAL_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX`]. +/// - See PD spec 6.4.4.3.1 Discover Identity. +const SINGLE_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX: usize = 0; + +/// Index of the ID Header VDO in the Received SOP Identity Data Object's VDO list. +/// +/// See [`ReceivedSopIdentityData::id_header`]. +const ID_HEADER_VDO_INDEX: usize = 0; + +/// Index of the Cert Stat VDO in the Received SOP Identity Data Object's VDO list. +/// +/// See [`ReceivedSopIdentityData::cert_stat`]. +const CERT_STAT_VDO_INDEX: usize = 1; + +/// Index of the Product VDO in the Received SOP Identity Data Object's VDO list. +/// +/// See [`ReceivedSopIdentityData::product_vdo`]. +const PRODUCT_VDO_INDEX: usize = 2; + +/// Index of the first Product Type VDO in the Received SOP Identity Data Object's VDO list. +/// +/// See [`ReceivedSopIdentityData::product_type_vdos`]. +const PRODUCT_TYPE_VDOS_STARTING_INDEX: usize = 3; + +bitfield! { + /// Received SOP Identity Data Object register + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + struct Raw([u8]); + impl Debug; + + /// Number of valid VDOs in this register (max of 6). + pub u8, number_valid_vdos, set_number_valid_vdos: 2, 0; + + /// Type of response received. + /// + /// See [`CommandType`] for more details. + pub u8, response_type, set_response_type: 7, 6; + + pub u32, vdo1, set_vdo1: 39, 8; + pub u32, vdo2, set_vdo2: 71, 40; + pub u32, vdo3, set_vdo3: 103, 72; + pub u32, vdo4, set_vdo4: 135, 104; + pub u32, vdo5, set_vdo5: 167, 136; + pub u32, vdo6, set_vdo6: 199, 168; +} + +/// Received SOP Identity Data Object register, containing the identity information returned from `Discover Identity REQ` messages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ReceivedSopIdentityData(Raw<[u8; LEN]>); + +impl ReceivedSopIdentityData { + pub const DEFAULT: Self = Self(Raw([0; LEN])); + + /// Returns the number of valid VDOs in this register (max of 6). + pub fn number_valid_vdos(&self) -> usize { + self.0.number_valid_vdos().min(6) as usize + } + + /// Returns an iterator over the VDOs. + /// + /// Each response usually contains an ID Header VDO, a Cert Stat VDO, a Product VDO, + /// and up to 3 Product Type VDOs whose types are context-specific. Specific + /// methods are available to parse the first 3 VDOs and to retrieve the + /// Product Type VDOs. + /// + /// - ID Header VDO: [`Self::id_header`] + /// - Cert Stat VDO: [`Self::cert_stat`] + /// - Product VDO: [`Self::product_vdo`] + /// - Product Type VDOs: [`Self::product_type_vdos`] + pub fn vdos(&self) -> impl ExactSizeIterator { + [ + self.0.vdo1(), + self.0.vdo2(), + self.0.vdo3(), + self.0.vdo4(), + self.0.vdo5(), + self.0.vdo6(), + ] + .into_iter() + .take(self.number_valid_vdos()) + } + + /// The type of response received for the Discover Identity command sent to + /// the SOP port partner. + /// + /// See [`CommandType`] for more details. + pub fn response_type(&self) -> CommandType { + self.0.response_type().into() + } + + /// Contains information corresponding to the Power Delivery Product. + /// + /// Returns [`None`] if there isn't enough valid VDOs to contain an ID Header VDO. + /// If there are, attempts to parse it as an [`IdHeaderVdo`] and returns the result. + /// If that fails, returns the raw VDO for further analysis. + pub fn id_header(&self) -> Option> { + let raw = self.vdos().nth(ID_HEADER_VDO_INDEX)?; + let raw = id_header_vdo::Raw(raw); + match IdHeaderVdo::try_from(raw) { + Ok(id_header) => Some(Ok(id_header)), + Err(e) => { + debug!("Failed to parse ID Header VDO: {:?}", e); + Some(Err(raw)) + } + } + } + + /// Contains the XID assigned by USB-IF to the product before certification, + /// in binary format. + pub fn cert_stat(&self) -> Option { + self.vdos().nth(CERT_STAT_VDO_INDEX).map(CertStatVdo) + } + + /// Contains identity information relating to the product. + /// + /// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. + pub fn product_vdo(&self) -> Option { + self.vdos().nth(PRODUCT_VDO_INDEX).map(ProductVdo::from) + } + + /// Return an iterator over the Product Type VDOs, if present. + /// + /// The interpretation of these VDOs is context-specific based on the contents + /// of the [`Self::id_header`]. Some or all may be padding with the value of `0x00000000`. + pub fn product_type_vdos(&self) -> impl Iterator { + self.vdos().skip(PRODUCT_TYPE_VDOS_STARTING_INDEX).map(ProductTypeVdo) + } +} + +impl Default for ReceivedSopIdentityData { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From<[u8; LEN]> for ReceivedSopIdentityData { + fn from(raw: [u8; LEN]) -> Self { + Self(Raw(raw)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConvertToResponseVdosError { + MissingIdHeader, + InvalidIdHeader(id_header_vdo::Raw), + MissingCertStat { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + }, + MissingProductVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + }, + MissingProductTypeUfpVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + }, + InvalidProductTypeUfpVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The inner error encountered when parsing the Product Type (UFP) VDO. + inner: ParseUfpVdoError, + }, + MissingProductTypeDfpVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The UFP Product Type VDO, included for context in debugging. + ufp_product_type_vdos: UfpProductTypeVdos, + + /// The number of Product Type VDOs needed based on the ID Header. + needed: usize, + + /// The number of Product Type VDOs actually available. + available: usize, + }, +} + +impl ConvertToResponseVdosError { + /// Get the ID Header VDO if it was parsed successfully. + pub const fn id(&self) -> Option { + match self { + Self::MissingIdHeader | Self::InvalidIdHeader(_) => None, + Self::MissingCertStat { id } + | Self::MissingProductVdo { id, .. } + | Self::MissingProductTypeUfpVdo { id, .. } + | Self::InvalidProductTypeUfpVdo { id, .. } + | Self::MissingProductTypeDfpVdo { id, .. } => Some(*id), + } + } + + /// Get the Cert Stat VDO if it was parsed successfully. + pub const fn cert_stat(&self) -> Option { + match self { + Self::MissingIdHeader | Self::InvalidIdHeader(_) | Self::MissingCertStat { .. } => None, + Self::MissingProductVdo { cert_stat, .. } + | Self::MissingProductTypeUfpVdo { cert_stat, .. } + | Self::InvalidProductTypeUfpVdo { cert_stat, .. } + | Self::MissingProductTypeDfpVdo { cert_stat, .. } => Some(*cert_stat), + } + } + + /// Get the Product VDO if it was parsed successfully. + pub const fn product(&self) -> Option { + match self { + Self::MissingIdHeader + | Self::InvalidIdHeader(_) + | Self::MissingCertStat { .. } + | Self::MissingProductVdo { .. } => None, + Self::MissingProductTypeUfpVdo { product, .. } + | Self::InvalidProductTypeUfpVdo { product, .. } + | Self::MissingProductTypeDfpVdo { product, .. } => Some(*product), + } + } + + /// Get the UFP Product Type VDOs if they were parsed successfully. + /// + /// If the DFP Product Type VDO was parsed successfully, it, and the UFP VDO, + /// are available in the [`Ok`] return value of the [`TryFrom`] implementation. + pub const fn ufp_product_type_vdos(&self) -> Option { + match self { + Self::MissingIdHeader + | Self::InvalidIdHeader(_) + | Self::MissingCertStat { .. } + | Self::MissingProductVdo { .. } + | Self::MissingProductTypeUfpVdo { .. } + | Self::InvalidProductTypeUfpVdo { .. } => None, + Self::MissingProductTypeDfpVdo { + ufp_product_type_vdos, .. + } => Some(*ufp_product_type_vdos), + } + } +} + +impl TryFrom + for embedded_usb_pd::vdm::structured::command::discover_identity::sop::ResponseVdos +{ + type Error = ConvertToResponseVdosError; + + fn try_from(value: ReceivedSopIdentityData) -> Result { + let id = value + .id_header() + .ok_or(ConvertToResponseVdosError::MissingIdHeader)? + .map_err(ConvertToResponseVdosError::InvalidIdHeader)?; + + let cert_stat = value + .cert_stat() + .ok_or(ConvertToResponseVdosError::MissingCertStat { id })?; + let product = value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo { id, cert_stat })?; + + // parse UFP first since it always comes first in the VDO list for DRDs (see DFP parsing below) + // this provides the UFP VDO to callers in the case that DFP parsing fails, whereas parsing DFP first would not + let ufp_product_type_vdos = match id.product_type_ufp { + id_header_vdo::ProductTypeUfp::NotAUfp => UfpProductTypeVdos::NotAUfp, + id_header_vdo::ProductTypeUfp::Psd => UfpProductTypeVdos::Psd, + + // these all parse the same way, so combine to reduce code duplication + product_type_ufp @ (id_header_vdo::ProductTypeUfp::Hub | id_header_vdo::ProductTypeUfp::Peripheral) => { + let ufp_vdo = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeUfpVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeUfpVdo { + id, + cert_stat, + product, + inner, + })?; + + match product_type_ufp { + id_header_vdo::ProductTypeUfp::Hub => UfpProductTypeVdos::Hub(ufp_vdo), + id_header_vdo::ProductTypeUfp::Peripheral => UfpProductTypeVdos::Peripheral(ufp_vdo), + + // technically unreachable since the case was handled above, but we include it for exhaustiveness + id_header_vdo::ProductTypeUfp::NotAUfp => UfpProductTypeVdos::NotAUfp, + id_header_vdo::ProductTypeUfp::Psd => UfpProductTypeVdos::Psd, + } + } + }; + + let dfp_product_type_vdos = match id.product_type_dfp { + id_header_vdo::ProductTypeDfp::NotADfp => DfpProductTypeVdos::NotADfp, + + // these all parse the same way, so combine to reduce code duplication + product_type_dfp @ (id_header_vdo::ProductTypeDfp::Hub + | id_header_vdo::ProductTypeDfp::Host + | id_header_vdo::ProductTypeDfp::Charger) => { + /* PD 6.4.4.3.1 Discover Identity + + If the product is a DRD both a Product Type (UFP) and a Product Type (DFP) are declared in the ID Header. These + products Shall return Product Type VDOs for both UFP and DFP beginning with the UFP VDO, then by a 32-bit Pad + Object (defined as all '0's), followed by the DFP VDO as shown in Figure 6.17, "Discover Identity Command response + for a DRD". + */ + + // we're already a DFP at this scope, so we're DRD if we're also a UFP + let is_dual_role = !matches!(id.product_type_ufp, id_header_vdo::ProductTypeUfp::NotAUfp); + let index = if is_dual_role { + DUAL_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX + } else { + SINGLE_ROLE_DFP_PRODUCT_TYPE_VDOS_INDEX + }; + let dfp_vdo = value + .product_type_vdos() + .nth(index) + .ok_or(ConvertToResponseVdosError::MissingProductTypeDfpVdo { + id, + cert_stat, + product, + ufp_product_type_vdos, + needed: index + 1, + available: value.product_type_vdos().count(), + })? + .into(); + + match product_type_dfp { + id_header_vdo::ProductTypeDfp::Hub => DfpProductTypeVdos::Hub(dfp_vdo), + id_header_vdo::ProductTypeDfp::Host => DfpProductTypeVdos::Host(dfp_vdo), + id_header_vdo::ProductTypeDfp::Charger => DfpProductTypeVdos::Charger(dfp_vdo), + + // techincally unreachable since the case was handled above, but we include it for exhaustiveness + id_header_vdo::ProductTypeDfp::NotADfp => DfpProductTypeVdos::NotADfp, + } + } + }; + + Ok(Self { + id: id.into(), + cert_stat, + product, + dfp_product_type_vdos, + ufp_product_type_vdos, + }) + } +} + +#[cfg(test)] +mod tests { + use embedded_usb_pd::vdm::structured::command::discover_identity::sop::{ + DfpProductTypeVdos, ResponseVdos, UfpProductTypeVdos, + }; + use embedded_usb_pd::vdm::structured::header::CommandType; + + use super::*; + + /// Build a raw register byte array for testing. + /// + /// Byte 0 encodes `num_vdos` in bits 2:0 and `response_type` in bits 7:6. + /// VDO values are stored little-endian starting at byte 1, 4 bytes each. + fn make_raw(num_vdos: u8, response_type: u8, vdos: &[u32]) -> [u8; LEN] { + let mut raw = [0u8; LEN]; + raw[0] = (num_vdos & 0b111) | ((response_type & 0b11) << 6); + for (i, &vdo) in vdos.iter().enumerate().take(6) { + let offset = 1 + i * 4; + raw[offset..offset + 4].copy_from_slice(&vdo.to_le_bytes()); + } + raw + } + + #[test] + fn number_valid_vdos_is_capped_at_6() { + let mut reg = ReceivedSopIdentityData::default(); + reg.0.set_number_valid_vdos(7); + assert_eq!(reg.number_valid_vdos(), 6); + } + + #[test] + fn vdos_returns_correct_count() { + for n in 0..=6u8 { + let raw = make_raw(n, 0, &[0; 6]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!(reg.vdos().len(), n as usize, "n={n}"); + } + } + + #[test] + fn vdos_returns_correct_values() { + let expected = [0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666]; + let raw = make_raw(6, 0, &expected); + let reg = ReceivedSopIdentityData::from(raw); + let mut iter = reg.vdos(); + for &e in &expected { + assert_eq!(iter.next(), Some(e)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn response_type_maps_all_variants() { + let cases = [ + (0b00u8, CommandType::Request), + (0b01, CommandType::Ack), + (0b10, CommandType::Nak), + (0b11, CommandType::Busy), + ]; + for (raw_bits, expected) in cases { + let raw = make_raw(0, raw_bits, &[]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!(reg.response_type(), expected, "raw_bits={raw_bits:#04b}"); + } + } + + #[test] + fn id_header_returns_none_when_no_vdos() { + let reg = ReceivedSopIdentityData::default(); + assert!(reg.id_header().is_none()); + } + + #[test] + fn cert_stat_returns_none_when_fewer_than_2_vdos() { + let raw = make_raw(1, 0, &[0]); + let reg = ReceivedSopIdentityData::from(raw); + assert!(reg.cert_stat().is_none()); + } + + #[test] + fn product_vdo_returns_none_when_fewer_than_3_vdos() { + let raw = make_raw(2, 0, &[0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + assert!(reg.product_vdo().is_none()); + } + + #[test] + fn product_type_vdos_skips_first_three() { + let raw = make_raw( + 6, + 0, + &[0x11111111, 0x22222222, 0x33333333, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC], + ); + let reg = ReceivedSopIdentityData::from(raw); + let mut iter = reg.product_type_vdos(); + assert_eq!(iter.next().map(|v| v.0), Some(0xAAAAAAAA)); + assert_eq!(iter.next().map(|v| v.0), Some(0xBBBBBBBB)); + assert_eq!(iter.next().map(|v| v.0), Some(0xCCCCCCCC)); + assert_eq!(iter.next(), None); + } + + mod try_from { + use super::*; + + // connector_type=Receptacle (0b10) at bits 22:21, product_type_dfp=NotADfp (0b000) at bits 25:23, + // product_type_ufp=NotAUfp (0b000) at bits 29:27. + const SIMPLE_RECEPTACLE_ID_HEADER: u32 = 0b10 << 21; // 0x00400000 + + #[test] + fn missing_id_header() { + let reg = ReceivedSopIdentityData::default(); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingIdHeader) + ); + } + + #[test] + fn invalid_id_header() { + // connector_type bits 22:21 = 0b00 is invalid (valid: 0b10=Receptacle, 0b11=Plug) + let raw = make_raw(1, 0b01, &[0x00000000]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::InvalidIdHeader(id_header_vdo::Raw( + 0x00000000 + ))) + ); + } + + #[test] + fn missing_cert_stat() { + let raw = make_raw(1, 0b01, &[SIMPLE_RECEPTACLE_ID_HEADER]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingCertStat { + id: SIMPLE_RECEPTACLE_ID_HEADER.try_into().unwrap(), + }) + ); + } + + #[test] + fn missing_product_vdo() { + let raw = make_raw(2, 0b01, &[SIMPLE_RECEPTACLE_ID_HEADER, 0]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductVdo { + id: SIMPLE_RECEPTACLE_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + }) + ); + } + + #[test] + fn success_not_ufp_not_dfp() { + // A device that is neither UFP nor DFP requires only the base 3 VDOs. + let raw = make_raw(3, 0b01, &[SIMPLE_RECEPTACLE_ID_HEADER, 0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!(vdos.ufp_product_type_vdos, UfpProductTypeVdos::NotAUfp); + assert_eq!(vdos.dfp_product_type_vdos, DfpProductTypeVdos::NotADfp); + } + + // connector_type=Receptacle (0b10), product_type_ufp=Hub (0b001) at bits 29:27, + // product_type_dfp=NotADfp (0b000) at bits 25:23. + const UFP_HUB_ID_HEADER: u32 = (0b10 << 21) | (0b001 << 27); // 0x08400000 + + #[test] + fn missing_ufp_product_type_vdo() { + // Hub UFP requires a product type VDO, but we only have the base 3. + let raw = make_raw(3, 0b01, &[UFP_HUB_ID_HEADER, 0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductTypeUfpVdo { + id: UFP_HUB_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + }) + ); + } + + #[test] + fn success_ufp_hub() { + // 0x00000000 is a valid UfpVdo (usb_highest_speed=Usb2p0, vconn_power=OneW, etc.) + let raw = make_raw(4, 0b01, &[UFP_HUB_ID_HEADER, 0, 0, 0x00000000]); + let reg = ReceivedSopIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!( + vdos.ufp_product_type_vdos, + UfpProductTypeVdos::Hub(0.try_into().unwrap()) + ); + assert_eq!(vdos.dfp_product_type_vdos, DfpProductTypeVdos::NotADfp); + } + + // connector_type=Receptacle (0b10), product_type_dfp=Host (0b010) at bits 25:23, + // product_type_ufp=NotAUfp (0b000) at bits 29:27. + const DFP_HOST_ID_HEADER: u32 = (0b10 << 21) | (0b010 << 23); // 0x01400000 + + #[test] + fn missing_dfp_product_type_vdo() { + // Host DFP requires a product type VDO, but we only have the base 3. + let raw = make_raw(3, 0b01, &[DFP_HOST_ID_HEADER, 0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductTypeDfpVdo { + id: DFP_HOST_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + ufp_product_type_vdos: UfpProductTypeVdos::NotAUfp, + needed: 1, + available: 0, + }) + ); + } + + #[test] + fn success_dfp_host() { + // DfpVdo uses From (infallible), so 0x00000000 is valid. + let raw = make_raw(4, 0b01, &[DFP_HOST_ID_HEADER, 0, 0, 0x00000000]); + let reg = ReceivedSopIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!(vdos.dfp_product_type_vdos, DfpProductTypeVdos::Host(0.into())); + assert_eq!(vdos.ufp_product_type_vdos, UfpProductTypeVdos::NotAUfp); + } + + // DRD: connector_type=Receptacle (0b10), product_type_ufp=Hub (0b001) at bits 29:27, + // product_type_dfp=Host (0b010) at bits 25:23. + // PD spec: DRD response has [ufp_vdo, pad(0), dfp_vdo] in product type VDOs. + const DRD_HUB_HOST_ID_HEADER: u32 = (0b10 << 21) | (0b001 << 27) | (0b010 << 23); // 0x09400000 + + #[test] + fn drd_missing_dfp_product_type_vdo() { + // DRD DFP VDO is at product_type_vdos index 2, but with only 5 total VDOs + // product_type_vdos() yields 2 items (indices 0 and 1), so nth(2) returns None. + let raw = make_raw(5, 0b01, &[DRD_HUB_HOST_ID_HEADER, 0, 0, 0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductTypeDfpVdo { + id: DRD_HUB_HOST_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + ufp_product_type_vdos: UfpProductTypeVdos::Hub(0.try_into().unwrap()), + needed: 3, + available: 2, + }) + ); + } + + #[test] + fn success_drd() { + // 6 VDOs: id, cert_stat, product_vdo, ufp_vdo, pad(0), dfp_vdo. + let raw = make_raw(6, 0b01, &[DRD_HUB_HOST_ID_HEADER, 0, 0, 0, 0, 0]); + let reg = ReceivedSopIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!( + vdos.ufp_product_type_vdos, + UfpProductTypeVdos::Hub(0.try_into().unwrap()) + ); + assert_eq!(vdos.dfp_product_type_vdos, DfpProductTypeVdos::Host(0.into())); + } + } +} diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs new file mode 100644 index 0000000..0787c41 --- /dev/null +++ b/src/registers/received_sop_prime_identity_data.rs @@ -0,0 +1,641 @@ +//! Received SOP Prime Identity Data Object register (`0x49`). +//! +//! This register's size exceeds the maximum supported length by the [`device_driver`] crate. +//! +//! This register contains the response to Discover Identity command sent to the SOP' or SOP'' cable plug. + +use bitfield::bitfield; +use embedded_usb_pd::vdm::structured::command::discover_identity::active_cable_vdo::{ + ParseActiveCableVdo1Error, ParseActiveCableVdo2Error, +}; +use embedded_usb_pd::vdm::structured::command::discover_identity::passive_cable_vdo::ParsePassiveCableVdoError; +use embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::{ + id_header_vdo, IdHeaderVdo, ProductTypeVdos, +}; +use embedded_usb_pd::vdm::structured::command::discover_identity::vpd_vdo::ParseVpdVdoError; +use embedded_usb_pd::vdm::structured::command::discover_identity::{ + ActiveCableVdo1, CertStatVdo, ProductTypeVdo, ProductVdo, +}; +use embedded_usb_pd::vdm::structured::header::CommandType; + +use crate::debug; + +/// The address of the `Received SOP Prime Identity Data Object` register. +pub const ADDR: u8 = 0x49; + +/// The length of the `Received SOP Prime Identity Data Object` register, in bytes. +/// +/// This exceeds the maximum supported length by the [`device_driver`] crate. +pub const LEN: usize = 200 / 8; + +/// Index of the ID Header VDO in the Received SOP Prime Identity Data Object's VDO list. +/// +/// See [`ReceivedSopPrimeIdentityData::id_header`]. +const ID_HEADER_VDO_INDEX: usize = 0; + +/// Index of the Cert Stat VDO in the Received SOP Prime Identity Data Object's VDO list. +/// +/// See [`ReceivedSopPrimeIdentityData::cert_stat`]. +const CERT_STAT_VDO_INDEX: usize = 1; + +/// Index of the Product VDO in the Received SOP Prime Identity Data Object's VDO list. +/// +/// See [`ReceivedSopPrimeIdentityData::product_vdo`]. +const PRODUCT_VDO_INDEX: usize = 2; + +/// Index of the first Product Type VDO in the Received SOP Prime Identity Data Object's VDO list. +/// +/// See [`ReceivedSopPrimeIdentityData::product_type_vdos`]. +const PRODUCT_TYPE_VDOS_STARTING_INDEX: usize = 3; + +bitfield! { + /// Received SOP Prime Identity Data Object register + #[derive(Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + struct Raw([u8]); + impl Debug; + + /// Number of valid VDOs in this register (max of 6). + pub u8, number_valid_vdos, set_number_valid_vdos: 2, 0; + + /// Type of response received. + /// + /// See [`CommandType`] for more details. + pub u8, response_type, set_response_type: 7, 6; + + pub u32, vdo1, set_vdo1: 39, 8; + pub u32, vdo2, set_vdo2: 71, 40; + pub u32, vdo3, set_vdo3: 103, 72; + pub u32, vdo4, set_vdo4: 135, 104; + pub u32, vdo5, set_vdo5: 167, 136; + pub u32, vdo6, set_vdo6: 199, 168; +} + +/// Received SOP Prime Identity Data Object register, containing the identity information returned from `Discover Identity REQ` messages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ReceivedSopPrimeIdentityData(Raw<[u8; LEN]>); + +impl ReceivedSopPrimeIdentityData { + pub const DEFAULT: Self = Self(Raw([0; LEN])); + + /// Returns the number of valid VDOs in this register (max of 6). + pub fn number_valid_vdos(&self) -> usize { + self.0.number_valid_vdos().min(6) as usize + } + + /// Returns an iterator over the VDOs. + /// + /// Each response usually contains an ID Header VDO, a Cert Stat VDO, a Product VDO, + /// and up to 3 Product Type VDOs whose types are context-specific. Specific + /// methods are available to parse the first 3 VDOs and to retrieve the + /// Product Type VDOs. + /// + /// - ID Header VDO: [`Self::id_header`] + /// - Cert Stat VDO: [`Self::cert_stat`] + /// - Product VDO: [`Self::product_vdo`] + /// - Product Type VDOs: [`Self::product_type_vdos`] + pub fn vdos(&self) -> impl ExactSizeIterator { + [ + self.0.vdo1(), + self.0.vdo2(), + self.0.vdo3(), + self.0.vdo4(), + self.0.vdo5(), + self.0.vdo6(), + ] + .into_iter() + .take(self.number_valid_vdos()) + } + + /// The type of response received for the Discover Identity command sent to + /// the SOP' or SOP'' cable plug. + /// + /// See [`CommandType`] for more details. + pub fn response_type(&self) -> CommandType { + self.0.response_type().into() + } + + /// Contains information corresponding to the Power Delivery Product. + /// + /// Returns [`None`] if there isn't enough valid VDOs to contain an ID Header VDO. + /// If there are, attempts to parse it as an [`IdHeaderVdo`] and returns the result. + /// If that fails, returns the raw VDO for further analysis. + pub fn id_header(&self) -> Option> { + let raw = self.vdos().nth(ID_HEADER_VDO_INDEX)?; + let raw = id_header_vdo::Raw(raw); + match IdHeaderVdo::try_from(raw) { + Ok(id_header) => Some(Ok(id_header)), + Err(e) => { + debug!("Failed to parse ID Header VDO: {:?}", e); + Some(Err(raw)) + } + } + } + + /// Contains the XID assigned by USB-IF to the product before certification, + /// in binary format. + pub fn cert_stat(&self) -> Option { + self.vdos().nth(CERT_STAT_VDO_INDEX).map(CertStatVdo) + } + + /// Contains identity information relating to the product. + pub fn product_vdo(&self) -> Option { + self.vdos().nth(PRODUCT_VDO_INDEX).map(ProductVdo::from) + } + + /// Return an iterator over the Product Type VDOs, if present. + /// + /// The interpretation of these VDOs is context-specific based on the contents + /// of the [`Self::id_header`]. Some or all may be padding with the value of `0x00000000`. + pub fn product_type_vdos(&self) -> impl Iterator { + self.vdos().skip(PRODUCT_TYPE_VDOS_STARTING_INDEX).map(ProductTypeVdo) + } +} + +impl Default for ReceivedSopPrimeIdentityData { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From<[u8; LEN]> for ReceivedSopPrimeIdentityData { + fn from(raw: [u8; LEN]) -> Self { + Self(Raw(raw)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConvertToResponseVdosError { + MissingIdHeader, + InvalidIdHeader(id_header_vdo::Raw), + MissingCertStat { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + }, + MissingProductVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + }, + MissingProductTypeVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + }, + MissingProductTypeActiveCableVdo2 { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The first Product Type (Active Cable) VDO, included for context in debugging. + active_cable_vdo1: ActiveCableVdo1, + }, + InvalidProductTypePassiveCableVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The inner error encountered when parsing the Product Type (Passive Cable) VDO. + inner: ParsePassiveCableVdoError, + }, + InvalidProductTypeActiveCableVdo1 { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The inner error encountered when parsing the first Product Type (Active Cable) VDO. + inner: ParseActiveCableVdo1Error, + }, + InvalidProductTypeActiveCableVdo2 { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The first Product Type (Active Cable) VDO, included for context in debugging. + active_cable_vdo1: ActiveCableVdo1, + + /// The inner error encountered when parsing the second Product Type (Active Cable) VDO. + inner: ParseActiveCableVdo2Error, + }, + InvalidProductTypeVpdVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The inner error encountered when parsing the Product Type (VPD) VDO. + inner: ParseVpdVdoError, + }, +} + +impl ConvertToResponseVdosError { + /// Get the ID Header VDO if it was parsed successfully. + pub const fn id(&self) -> Option { + match self { + Self::MissingIdHeader | Self::InvalidIdHeader(_) => None, + Self::MissingCertStat { id } + | Self::MissingProductVdo { id, .. } + | Self::MissingProductTypeVdo { id, .. } + | Self::MissingProductTypeActiveCableVdo2 { id, .. } + | Self::InvalidProductTypePassiveCableVdo { id, .. } + | Self::InvalidProductTypeActiveCableVdo1 { id, .. } + | Self::InvalidProductTypeActiveCableVdo2 { id, .. } + | Self::InvalidProductTypeVpdVdo { id, .. } => Some(*id), + } + } + + /// Get the Cert Stat VDO if it was parsed successfully. + pub const fn cert_stat(&self) -> Option { + match self { + Self::MissingIdHeader | Self::InvalidIdHeader(_) | Self::MissingCertStat { .. } => None, + Self::MissingProductVdo { cert_stat, .. } + | Self::MissingProductTypeVdo { cert_stat, .. } + | Self::MissingProductTypeActiveCableVdo2 { cert_stat, .. } + | Self::InvalidProductTypePassiveCableVdo { cert_stat, .. } + | Self::InvalidProductTypeActiveCableVdo1 { cert_stat, .. } + | Self::InvalidProductTypeActiveCableVdo2 { cert_stat, .. } + | Self::InvalidProductTypeVpdVdo { cert_stat, .. } => Some(*cert_stat), + } + } + + /// Get the Product VDO if it was parsed successfully. + pub const fn product(&self) -> Option { + match self { + Self::MissingIdHeader + | Self::InvalidIdHeader(_) + | Self::MissingCertStat { .. } + | Self::MissingProductVdo { .. } => None, + Self::MissingProductTypeVdo { product, .. } + | Self::MissingProductTypeActiveCableVdo2 { product, .. } + | Self::InvalidProductTypePassiveCableVdo { product, .. } + | Self::InvalidProductTypeActiveCableVdo1 { product, .. } + | Self::InvalidProductTypeActiveCableVdo2 { product, .. } + | Self::InvalidProductTypeVpdVdo { product, .. } => Some(*product), + } + } + + /// Get the Active Cable VDO1 if it was parsed successfully. + /// + /// If the Active Cable VDO2 was parsed successfully, it, and the VDO1, are + /// available in the [`Ok`] return value of the [`TryFrom`] implementation. + pub const fn active_cable_vdo1(&self) -> Option { + match self { + Self::MissingIdHeader + | Self::InvalidIdHeader(_) + | Self::MissingCertStat { .. } + | Self::MissingProductVdo { .. } + | Self::MissingProductTypeVdo { .. } + | Self::InvalidProductTypePassiveCableVdo { .. } + | Self::InvalidProductTypeActiveCableVdo1 { .. } + | Self::InvalidProductTypeVpdVdo { .. } => None, + Self::MissingProductTypeActiveCableVdo2 { active_cable_vdo1, .. } + | Self::InvalidProductTypeActiveCableVdo2 { active_cable_vdo1, .. } => Some(*active_cable_vdo1), + } + } +} + +impl TryFrom + for embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos +{ + type Error = ConvertToResponseVdosError; + + fn try_from(value: ReceivedSopPrimeIdentityData) -> Result { + let id = value + .id_header() + .ok_or(ConvertToResponseVdosError::MissingIdHeader)? + .map_err(ConvertToResponseVdosError::InvalidIdHeader)?; + + let cert_stat = value + .cert_stat() + .ok_or(ConvertToResponseVdosError::MissingCertStat { id })?; + + let product = value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo { id, cert_stat })?; + + let product_type_vdos = match id.product_type { + id_header_vdo::ProductType::NotACablePlugVpd => ProductTypeVdos::NotACablePlugVpd, + id_header_vdo::ProductType::PassiveCable => { + let vdo = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypePassiveCableVdo { + id, + cert_stat, + product, + inner, + })?; + + ProductTypeVdos::PassiveCable(vdo) + } + id_header_vdo::ProductType::ActiveCable => { + let vdo1 = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeActiveCableVdo1 { + id, + cert_stat, + product, + inner, + })?; + + let vdo2 = value + .product_type_vdos() + .nth(1) + .ok_or(ConvertToResponseVdosError::MissingProductTypeActiveCableVdo2 { + id, + cert_stat, + product, + active_cable_vdo1: vdo1, + })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeActiveCableVdo2 { + id, + cert_stat, + product, + active_cable_vdo1: vdo1, + inner, + })?; + + ProductTypeVdos::ActiveCable(vdo1, vdo2) + } + id_header_vdo::ProductType::Vpd => { + let vdo = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeVpdVdo { + id, + cert_stat, + product, + inner, + })?; + + ProductTypeVdos::Vpd(vdo) + } + }; + + Ok(Self { + id: id.into(), + cert_stat, + product, + product_type_vdos, + }) + } +} + +#[cfg(test)] +mod tests { + use embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::{ProductTypeVdos, ResponseVdos}; + + use super::*; + + #[test] + fn default_has_no_vdos() { + let reg = ReceivedSopPrimeIdentityData::default(); + assert_eq!(reg.number_valid_vdos(), 0); + assert_eq!(reg.vdos().count(), 0); + assert_eq!(reg.id_header(), None); + assert_eq!(reg.cert_stat(), None); + assert_eq!(reg.product_vdo(), None); + assert_eq!(reg.product_type_vdos().count(), 0); + } + + #[test] + fn number_valid_vdos_is_capped_at_6() { + let mut reg = ReceivedSopPrimeIdentityData::default(); + reg.0.set_number_valid_vdos(7); + assert_eq!(reg.number_valid_vdos(), 6); + } + + /// Build a raw register byte array for testing. + /// + /// Byte 0 encodes `num_vdos` in bits 2:0 and `response_type` in bits 7:6. + /// VDO values are stored little-endian starting at byte 1, 4 bytes each. + fn make_raw(num_vdos: u8, response_type: u8, vdos: &[u32]) -> [u8; LEN] { + let mut raw = [0u8; LEN]; + raw[0] = (num_vdos & 0b111) | ((response_type & 0b11) << 6); + for (i, &vdo) in vdos.iter().enumerate().take(6) { + let offset = 1 + i * 4; + raw[offset..offset + 4].copy_from_slice(&vdo.to_le_bytes()); + } + raw + } + + #[test] + fn vdos_returns_correct_count() { + for n in 0..=6u8 { + let raw = make_raw(n, 0, &[0; 6]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!(reg.vdos().len(), n as usize); + } + } + + #[test] + fn vdos_returns_correct_values() { + let expected = [0x11111111, 0x22222222, 0x33333333, 0x44444444, 0x55555555, 0x66666666]; + let raw = make_raw(6, 0, &expected); + let reg = ReceivedSopPrimeIdentityData::from(raw); + let mut iter = reg.vdos(); + for &e in &expected { + assert_eq!(iter.next(), Some(e)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn product_type_vdos_skips_first_three() { + let raw = make_raw( + 6, + 0, + &[0x11111111, 0x22222222, 0x33333333, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC], + ); + let reg = ReceivedSopPrimeIdentityData::from(raw); + let mut iter = reg.product_type_vdos(); + assert_eq!(iter.next().map(|v| v.0), Some(0xAAAAAAAA)); + assert_eq!(iter.next().map(|v| v.0), Some(0xBBBBBBBB)); + assert_eq!(iter.next().map(|v| v.0), Some(0xCCCCCCCC)); + assert_eq!(iter.next(), None); + } + + mod try_from { + use super::*; + + // connector_type=Plug (0b11) at bits 22:21, product_type=NotACablePlugVpd (0b000) at bits 29:27. + const SIMPLE_PLUG_ID_HEADER: u32 = 0b11 << 21; // 0x00600000 + + #[test] + fn missing_id_header() { + let reg = ReceivedSopPrimeIdentityData::default(); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingIdHeader) + ); + } + + #[test] + fn invalid_id_header() { + // connector_type bits 22:21 = 0b00 is invalid (valid: 0b10=Receptacle, 0b11=Plug) + let raw = make_raw(1, 0b01, &[0x00000000]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::InvalidIdHeader(id_header_vdo::Raw( + 0x00000000 + ))) + ); + } + + #[test] + fn missing_cert_stat() { + let raw = make_raw(1, 0b01, &[SIMPLE_PLUG_ID_HEADER]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingCertStat { + id: SIMPLE_PLUG_ID_HEADER.try_into().unwrap(), + }) + ); + } + + #[test] + fn missing_product_vdo() { + let raw = make_raw(2, 0b01, &[SIMPLE_PLUG_ID_HEADER, 0]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductVdo { + id: SIMPLE_PLUG_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + }) + ); + } + + #[test] + fn success_not_a_cable_plug_vpd() { + // NotACablePlugVpd requires no product type VDOs, only the base 3. + let raw = make_raw(3, 0b01, &[SIMPLE_PLUG_ID_HEADER, 0, 0]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!(vdos.product_type_vdos, ProductTypeVdos::NotACablePlugVpd); + } + + // connector_type=Plug (0b11), product_type=PassiveCable (0b011) at bits 29:27. + const PASSIVE_CABLE_ID_HEADER: u32 = (0b11 << 21) | (0b011 << 27); // 0x18600000 + + // A valid PassiveCableVdo raw value: + // vbus_current_handling_capability=ThreeAmps (0b01) at bits 6:5 + // cable_latency=LessThan10ns (0b0001) at bits 16:13 + // usb_type_c_or_captive=UsbTypeC (0b10) at bits 19:18 + // all other fields at their zero-value (usb_highest_speed=Usb2p0, maximum_vbus_voltage=TwentyVolt, etc.) + const VALID_PASSIVE_CABLE_VDO: u32 = (0b01 << 5) | (0b0001 << 13) | (0b10 << 18); // 0x82020 + + #[test] + fn missing_product_type_vdo_for_passive_cable() { + // PassiveCable requires 1 product type VDO, but we only have the base 3. + let raw = make_raw(3, 0b01, &[PASSIVE_CABLE_ID_HEADER, 0, 0]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductTypeVdo { + id: PASSIVE_CABLE_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + }) + ); + } + + #[test] + fn invalid_product_type_passive_cable_vdo() { + // 0x00000000 has vbus_current_handling_capability=0b00, which is invalid. + let raw = make_raw(4, 0b01, &[PASSIVE_CABLE_ID_HEADER, 0, 0, 0x00000000]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::InvalidProductTypePassiveCableVdo { + id: PASSIVE_CABLE_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + inner: ParsePassiveCableVdoError::InvalidVbusCurrentHandlingCapability, + }) + ); + } + + #[test] + fn success_passive_cable() { + let raw = make_raw(4, 0b01, &[PASSIVE_CABLE_ID_HEADER, 0, 0, VALID_PASSIVE_CABLE_VDO]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + let vdos = ResponseVdos::try_from(reg).unwrap(); + assert_eq!( + vdos.product_type_vdos, + ProductTypeVdos::PassiveCable(VALID_PASSIVE_CABLE_VDO.try_into().unwrap()) + ); + } + + // connector_type=Plug (0b11), product_type=ActiveCable (0b100) at bits 29:27. + const ACTIVE_CABLE_ID_HEADER: u32 = (0b11 << 21) | (0b100 << 27); // 0x20600000 + + // A valid ActiveCableVdo1 raw value: + // vbus_current_handling_capability=ThreeAmps (0b01) at bits 6:5 + // cable_termination_type=OneEndActive (0b10) at bits 12:11 + // (unlike PassiveCableVdo, active cable's CableTerminationType only accepts 0b10/0b11) + // cable_latency=LessThan10ns (0b0001) at bits 16:13 + // usb_type_c_or_captive=UsbTypeC (0b10) at bits 19:18 + // all other fields at their zero-value + const VALID_ACTIVE_CABLE_VDO1: u32 = (0b01 << 5) | (0b10 << 11) | (0b0001 << 13) | (0b10 << 18); // 0x83020 + + #[test] + fn missing_active_cable_vdo2() { + // ActiveCable needs 2 product type VDOs; we provide a valid VDO1 but no VDO2. + let raw = make_raw(4, 0b01, &[ACTIVE_CABLE_ID_HEADER, 0, 0, VALID_ACTIVE_CABLE_VDO1]); + let reg = ReceivedSopPrimeIdentityData::from(raw); + assert_eq!( + ResponseVdos::try_from(reg), + Err(ConvertToResponseVdosError::MissingProductTypeActiveCableVdo2 { + id: ACTIVE_CABLE_ID_HEADER.try_into().unwrap(), + cert_stat: CertStatVdo(0), + product: 0.into(), + active_cable_vdo1: ActiveCableVdo1::try_from(VALID_ACTIVE_CABLE_VDO1).unwrap(), + }) + ); + } + } +}