From ff49757abfa6bd9c4272ec0d9fed3ff4239fe7b6 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:23:09 -0700 Subject: [PATCH 01/23] Update embedded-usb-pd --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 9cdfa21fa656a5da5cb961c3393d26db0a7a2702 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 6 Apr 2026 13:22:28 -0700 Subject: [PATCH 02/23] Add Discovered SVIDs register --- src/asynchronous/embassy/mod.rs | 8 ++ src/asynchronous/internal/mod.rs | 19 +++ src/registers/discovered_svids.rs | 185 ++++++++++++++++++++++++++++++ src/registers/mod.rs | 1 + 4 files changed, 213 insertions(+) create mode 100644 src/registers/discovered_svids.rs diff --git a/src/asynchronous/embassy/mod.rs b/src/asynchronous/embassy/mod.rs index d58544d..1e4ec22 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, diff --git a/src/asynchronous/internal/mod.rs b/src/asynchronous/internal/mod.rs index 952db9a..3df91f6 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 diff --git a/src/registers/discovered_svids.rs b/src/registers/discovered_svids.rs new file mode 100644 index 0000000..60be856 --- /dev/null +++ b/src/registers/discovered_svids.rs @@ -0,0 +1,185 @@ +//! 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::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! { + /// Received source/sink capabilities 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. + 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. + 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, 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); + } +} diff --git a/src/registers/mod.rs b/src/registers/mod.rs index 3a67024..f15e824 100644 --- a/src/registers/mod.rs +++ b/src/registers/mod.rs @@ -5,6 +5,7 @@ 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 rx_caps; From 9ec25579ffc63399845ab944303615cf5ccc4d58 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 6 Apr 2026 13:38:39 -0700 Subject: [PATCH 03/23] Added Hard Reset (`HRST`) command --- src/asynchronous/embassy/mod.rs | 5 +++++ src/command/mod.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/asynchronous/embassy/mod.rs b/src/asynchronous/embassy/mod.rs index 1e4ec22..717fdaf 100644 --- a/src/asynchronous/embassy/mod.rs +++ b/src/asynchronous/embassy/mod.rs @@ -748,6 +748,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/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)); From 3956a745359228dc790cb94dc54a8e6a0eff272c Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:15:13 -0700 Subject: [PATCH 04/23] Update Svid import to new module --- src/command/gcdm.rs | 2 +- src/registers/discovered_svids.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/registers/discovered_svids.rs b/src/registers/discovered_svids.rs index 60be856..9642bd5 100644 --- a/src/registers/discovered_svids.rs +++ b/src/registers/discovered_svids.rs @@ -5,7 +5,7 @@ //! This register contains the SVID information returned from `Discover SVIDs REQ` messages. use bitfield::bitfield; -use embedded_usb_pd::vdm::Svid; +use embedded_usb_pd::vdm::structured::Svid; /// The address of the `Discovered SVIDs` register. pub const ADDR: u8 = 0x21; From 493367678ab4034f303ec56657d42506b06fc998 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:15:26 -0700 Subject: [PATCH 05/23] Add Received SOP Identity Data register --- src/registers/mod.rs | 1 + src/registers/received_sop_identity_data.rs | 143 ++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/registers/received_sop_identity_data.rs diff --git a/src/registers/mod.rs b/src/registers/mod.rs index f15e824..e07439d 100644 --- a/src/registers/mod.rs +++ b/src/registers/mod.rs @@ -8,6 +8,7 @@ pub mod boot_flags; pub mod discovered_svids; pub mod dp_status; pub mod port_config; +pub mod received_sop_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..5c2f713 --- /dev/null +++ b/src/registers/received_sop_identity_data.rs @@ -0,0 +1,143 @@ +//! 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, IdHeaderVdo}, + CertStatVdo, ProductTypeVdo, ProductVdo, + }, + 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; + +bitfield! { + /// Received source/sink capabilities 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 [`ResponseType`]. + 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. + pub fn number_valid_vdos(&self) -> usize { + self.0.number_valid_vdos() 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().next()?; + 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(1).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(2).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(3).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)) + } +} From ff2d19dfcc6641e0d33cce0fac9c9649e097fa93 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:18:54 -0700 Subject: [PATCH 06/23] Add Received SOP Prime Identity Data register --- src/registers/mod.rs | 1 + .../received_sop_prime_identity_data.rs | 141 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/registers/received_sop_prime_identity_data.rs diff --git a/src/registers/mod.rs b/src/registers/mod.rs index e07439d..18f4303 100644 --- a/src/registers/mod.rs +++ b/src/registers/mod.rs @@ -9,6 +9,7 @@ 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_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs new file mode 100644 index 0000000..d6af1dd --- /dev/null +++ b/src/registers/received_sop_prime_identity_data.rs @@ -0,0 +1,141 @@ +//! 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::{ + sop_prime::{id_header_vdo, IdHeaderVdo}, + CertStatVdo, ProductTypeVdo, ProductVdo, + }, + 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; + +bitfield! { + /// Received source/sink capabilities 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 [`ResponseType`]. + 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. + pub fn number_valid_vdos(&self) -> usize { + self.0.number_valid_vdos() 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().next()?; + 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(1).map(CertStatVdo) + } + + /// Contains identity information relating to the product. + pub fn product_vdo(&self) -> Option { + self.vdos().nth(2).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(3).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)) + } +} From eb1ef7addcecae32a154b437293529bc9988e122 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:23:04 -0700 Subject: [PATCH 07/23] Add accessor methods --- src/asynchronous/embassy/mod.rs | 16 ++++++++++++++ src/asynchronous/internal/mod.rs | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/asynchronous/embassy/mod.rs b/src/asynchronous/embassy/mod.rs index 717fdaf..c114f3e 100644 --- a/src/asynchronous/embassy/mod.rs +++ b/src/asynchronous/embassy/mod.rs @@ -714,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, diff --git a/src/asynchronous/internal/mod.rs b/src/asynchronous/internal/mod.rs index 3df91f6..8688350 100644 --- a/src/asynchronous/internal/mod.rs +++ b/src/asynchronous/internal/mod.rs @@ -772,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)] From 63f01377a5466acf138aeb7363b50a10025737c9 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 14:33:08 -0700 Subject: [PATCH 08/23] Add conversion to embedded_usb_pd types --- src/registers/received_sop_identity_data.rs | 34 +++++++++++++++++++ .../received_sop_prime_identity_data.rs | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 5c2f713..94f46b2 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -141,3 +141,37 @@ impl From<[u8; LEN]> for ReceivedSopIdentityData { Self(Raw(raw)) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConvertToResponseVdosError { + MissingIdHeader, + InvalidIdHeader, + MissingCertStat, + MissingProductVdo, +} + +impl TryFrom + for embedded_usb_pd::vdm::structured::command::discover_identity::sop::ResponseVdos +{ + type Error = ConvertToResponseVdosError; + + fn try_from(value: ReceivedSopIdentityData) -> Result { + Ok(Self { + id: value + .id_header() + .ok_or(ConvertToResponseVdosError::MissingIdHeader)? + .map_err(|_| ConvertToResponseVdosError::InvalidIdHeader)?, + cert_stat: Some(value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?), + product: Some( + value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo)?, + ), + product_type_vdos: { + let mut iter = value.product_type_vdos(); + core::array::from_fn(|_| iter.next().unwrap_or(ProductTypeVdo(0))) + }, + }) + } +} diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index d6af1dd..6cc10d2 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -139,3 +139,37 @@ impl From<[u8; LEN]> for ReceivedSopPrimeIdentityData { Self(Raw(raw)) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConvertToResponseVdosError { + MissingIdHeader, + InvalidIdHeader, + MissingCertStat, + MissingProductVdo, +} + +impl TryFrom + for embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos +{ + type Error = ConvertToResponseVdosError; + + fn try_from(value: ReceivedSopPrimeIdentityData) -> Result { + Ok(Self { + id: value + .id_header() + .ok_or(ConvertToResponseVdosError::MissingIdHeader)? + .map_err(|_| ConvertToResponseVdosError::InvalidIdHeader)?, + cert_stat: Some(value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?), + product: Some( + value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo)?, + ), + product_type_vdos: { + let mut iter = value.product_type_vdos(); + core::array::from_fn(|_| iter.next().unwrap_or(ProductTypeVdo(0))) + }, + }) + } +} From 90fd1a8c4a9e36972ee9a4d896124ba19aed23be Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 16:37:20 -0700 Subject: [PATCH 09/23] Update to fully parsed types --- src/registers/received_sop_identity_data.rs | 100 ++++++++++++++--- .../received_sop_prime_identity_data.rs | 101 +++++++++++++++--- 2 files changed, 169 insertions(+), 32 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 94f46b2..bb7362d 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -7,7 +7,8 @@ use bitfield::bitfield; use embedded_usb_pd::vdm::structured::{ command::discover_identity::{ - sop::{id_header_vdo, IdHeaderVdo}, + sop::{id_header_vdo, DfpProductTypeVdos, IdHeaderVdo, UfpProductTypeVdos}, + ufp_vdo::ParseUfpVdoError, CertStatVdo, ProductTypeVdo, ProductVdo, }, header::CommandType, @@ -146,9 +147,17 @@ impl From<[u8; LEN]> for ReceivedSopIdentityData { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConvertToResponseVdosError { MissingIdHeader, - InvalidIdHeader, + InvalidIdHeader(id_header_vdo::Raw), MissingCertStat, MissingProductVdo, + MissingProductTypeVdo, + InvalidProductTypeUfpVdo(ParseUfpVdoError), +} + +impl From for ConvertToResponseVdosError { + fn from(value: ParseUfpVdoError) -> Self { + Self::InvalidProductTypeUfpVdo(value) + } } impl TryFrom @@ -157,21 +166,80 @@ impl TryFrom 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)?; + let product = value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo)?; + + 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 { 2 } else { 0 }; + let dfp_vdo = value + .product_type_vdos() + .nth(index) + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .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, + } + } + }; + + 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() + .nth(1) // the second Product Type VDO is the UFP one if both are present + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .try_into()?; + + match product_type_ufp { + id_header_vdo::ProductTypeUfp::Hub => UfpProductTypeVdos::Hub(ufp_vdo), + id_header_vdo::ProductTypeUfp::Peripheral => UfpProductTypeVdos::Peripheral(ufp_vdo), + + // techincally 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, + } + } + }; + Ok(Self { - id: value - .id_header() - .ok_or(ConvertToResponseVdosError::MissingIdHeader)? - .map_err(|_| ConvertToResponseVdosError::InvalidIdHeader)?, - cert_stat: Some(value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?), - product: Some( - value - .product_vdo() - .ok_or(ConvertToResponseVdosError::MissingProductVdo)?, - ), - product_type_vdos: { - let mut iter = value.product_type_vdos(); - core::array::from_fn(|_| iter.next().unwrap_or(ProductTypeVdo(0))) - }, + id: id.into(), + cert_stat, + product, + dfp_product_type_vdos, + ufp_product_type_vdos, }) } } diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index 6cc10d2..b49d1ad 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -7,7 +7,10 @@ use bitfield::bitfield; use embedded_usb_pd::vdm::structured::{ command::discover_identity::{ - sop_prime::{id_header_vdo, IdHeaderVdo}, + active_cable_vdo::{ParseActiveCableVdo1Error, ParseActiveCableVdo2Error}, + passive_cable_vdo::ParsePassiveCableVdoError, + sop_prime::{id_header_vdo, IdHeaderVdo, ProductTypeVdos}, + vpd_vdo::ParseVpdVdoError, CertStatVdo, ProductTypeVdo, ProductVdo, }, header::CommandType, @@ -144,9 +147,38 @@ impl From<[u8; LEN]> for ReceivedSopPrimeIdentityData { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConvertToResponseVdosError { MissingIdHeader, - InvalidIdHeader, + InvalidIdHeader(id_header_vdo::Raw), MissingCertStat, MissingProductVdo, + MissingProductTypeVdo, + InvalidProductTypePassiveCableVdo(ParsePassiveCableVdoError), + InvalidProductTypeActiveCableVdo1(ParseActiveCableVdo1Error), + InvalidProductTypeActiveCableVdo2(ParseActiveCableVdo2Error), + InvalidProductTypeVpdVdo(ParseVpdVdoError), +} + +impl From for ConvertToResponseVdosError { + fn from(value: ParsePassiveCableVdoError) -> Self { + Self::InvalidProductTypePassiveCableVdo(value) + } +} + +impl From for ConvertToResponseVdosError { + fn from(value: ParseActiveCableVdo1Error) -> Self { + Self::InvalidProductTypeActiveCableVdo1(value) + } +} + +impl From for ConvertToResponseVdosError { + fn from(value: ParseActiveCableVdo2Error) -> Self { + Self::InvalidProductTypeActiveCableVdo2(value) + } +} + +impl From for ConvertToResponseVdosError { + fn from(value: ParseVpdVdoError) -> Self { + Self::InvalidProductTypeVpdVdo(value) + } } impl TryFrom @@ -155,21 +187,58 @@ impl TryFrom 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)?; + let product = value + .product_vdo() + .ok_or(ConvertToResponseVdosError::MissingProductVdo)?; + + 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)? + .try_into()?; + + ProductTypeVdos::PassiveCable(vdo) + } + id_header_vdo::ProductType::ActiveCable => { + let vdo1 = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .try_into()?; + + let vdo2 = value + .product_type_vdos() + .nth(1) + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .try_into()?; + + ProductTypeVdos::ActiveCable(vdo1, vdo2) + } + id_header_vdo::ProductType::Vpd => { + let vdo = value + .product_type_vdos() + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .try_into()?; + + ProductTypeVdos::Vpd(vdo) + } + }; + Ok(Self { - id: value - .id_header() - .ok_or(ConvertToResponseVdosError::MissingIdHeader)? - .map_err(|_| ConvertToResponseVdosError::InvalidIdHeader)?, - cert_stat: Some(value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?), - product: Some( - value - .product_vdo() - .ok_or(ConvertToResponseVdosError::MissingProductVdo)?, - ), - product_type_vdos: { - let mut iter = value.product_type_vdos(); - core::array::from_fn(|_| iter.next().unwrap_or(ProductTypeVdo(0))) - }, + id: id.into(), + cert_stat, + product, + product_type_vdos, }) } } From a9bd1fec1122917eda89133a6cd79c6d5c49e9f6 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 16:48:42 -0700 Subject: [PATCH 10/23] Add more information to the parsing error --- src/registers/received_sop_identity_data.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index bb7362d..bf173a3 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -150,7 +150,13 @@ pub enum ConvertToResponseVdosError { InvalidIdHeader(id_header_vdo::Raw), MissingCertStat, MissingProductVdo, - MissingProductTypeVdo, + MissingProductTypeVdo { + /// The number of Product Type VDOs needed based on the ID Header. + needed: usize, + + /// The number of Product Type VDOs actually available. + available: usize, + }, InvalidProductTypeUfpVdo(ParseUfpVdoError), } @@ -197,7 +203,10 @@ impl TryFrom let dfp_vdo = value .product_type_vdos() .nth(index) - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { + needed: index + 1, + available: value.product_type_vdos().count(), + })? .into(); match product_type_dfp { @@ -219,8 +228,11 @@ impl TryFrom product_type_ufp @ (id_header_vdo::ProductTypeUfp::Hub | id_header_vdo::ProductTypeUfp::Peripheral) => { let ufp_vdo = value .product_type_vdos() - .nth(1) // the second Product Type VDO is the UFP one if both are present - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? + .next() + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { + needed: 1, + available: value.product_type_vdos().count(), + })? .try_into()?; match product_type_ufp { From 16d131b44c2538744632639bfa3965432499beea Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 17:26:19 -0700 Subject: [PATCH 11/23] Return results with partial parsing --- src/registers/received_sop_identity_data.rs | 64 +++++-- .../received_sop_prime_identity_data.rs | 167 ++++++++++++++---- 2 files changed, 182 insertions(+), 49 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index bf173a3..54f36c1 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -148,22 +148,49 @@ impl From<[u8; LEN]> for ReceivedSopIdentityData { pub enum ConvertToResponseVdosError { MissingIdHeader, InvalidIdHeader(id_header_vdo::Raw), - MissingCertStat, - MissingProductVdo, + 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, + /// The number of Product Type VDOs needed based on the ID Header. needed: usize, /// The number of Product Type VDOs actually available. available: usize, }, - InvalidProductTypeUfpVdo(ParseUfpVdoError), -} + InvalidProductTypeUfpVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, -impl From for ConvertToResponseVdosError { - fn from(value: ParseUfpVdoError) -> Self { - Self::InvalidProductTypeUfpVdo(value) - } + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + + /// The Product VDO, included for context in debugging. + product: ProductVdo, + + /// The DFP Product Type VDOs, included for context in debugging. + dfp_product_type_vdos: DfpProductTypeVdos, + + /// The inner error encountered when parsing the Product Type (UFP) VDO. + inner: ParseUfpVdoError, + }, } impl TryFrom @@ -177,10 +204,12 @@ impl TryFrom .ok_or(ConvertToResponseVdosError::MissingIdHeader)? .map_err(ConvertToResponseVdosError::InvalidIdHeader)?; - let cert_stat = value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?; + let cert_stat = value + .cert_stat() + .ok_or(ConvertToResponseVdosError::MissingCertStat { id })?; let product = value .product_vdo() - .ok_or(ConvertToResponseVdosError::MissingProductVdo)?; + .ok_or(ConvertToResponseVdosError::MissingProductVdo { id, cert_stat })?; let dfp_product_type_vdos = match id.product_type_dfp { id_header_vdo::ProductTypeDfp::NotADfp => DfpProductTypeVdos::NotADfp, @@ -204,6 +233,9 @@ impl TryFrom .product_type_vdos() .nth(index) .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { + id, + cert_stat, + product, needed: index + 1, available: value.product_type_vdos().count(), })? @@ -230,10 +262,20 @@ impl TryFrom .product_type_vdos() .next() .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { + id, + cert_stat, + product, needed: 1, available: value.product_type_vdos().count(), })? - .try_into()?; + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeUfpVdo { + id, + cert_stat, + product, + dfp_product_type_vdos, + inner, + })?; match product_type_ufp { id_header_vdo::ProductTypeUfp::Hub => UfpProductTypeVdos::Hub(ufp_vdo), diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index b49d1ad..773e526 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -11,7 +11,7 @@ use embedded_usb_pd::vdm::structured::{ passive_cable_vdo::ParsePassiveCableVdoError, sop_prime::{id_header_vdo, IdHeaderVdo, ProductTypeVdos}, vpd_vdo::ParseVpdVdoError, - CertStatVdo, ProductTypeVdo, ProductVdo, + ActiveCableVdo1, CertStatVdo, ProductTypeVdo, ProductVdo, }, header::CommandType, }; @@ -148,37 +148,95 @@ impl From<[u8; LEN]> for ReceivedSopPrimeIdentityData { pub enum ConvertToResponseVdosError { MissingIdHeader, InvalidIdHeader(id_header_vdo::Raw), - MissingCertStat, - MissingProductVdo, - MissingProductTypeVdo, - InvalidProductTypePassiveCableVdo(ParsePassiveCableVdoError), - InvalidProductTypeActiveCableVdo1(ParseActiveCableVdo1Error), - InvalidProductTypeActiveCableVdo2(ParseActiveCableVdo2Error), - InvalidProductTypeVpdVdo(ParseVpdVdoError), -} + MissingCertStat { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, + }, + MissingProductVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, -impl From for ConvertToResponseVdosError { - fn from(value: ParsePassiveCableVdoError) -> Self { - Self::InvalidProductTypePassiveCableVdo(value) - } -} + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, + }, + MissingProductTypeVdo { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, -impl From for ConvertToResponseVdosError { - fn from(value: ParseActiveCableVdo1Error) -> Self { - Self::InvalidProductTypeActiveCableVdo1(value) - } -} + /// The Cert Stat VDO, included for context in debugging. + cert_stat: CertStatVdo, -impl From for ConvertToResponseVdosError { - fn from(value: ParseActiveCableVdo2Error) -> Self { - Self::InvalidProductTypeActiveCableVdo2(value) - } -} + /// The Product VDO, included for context in debugging. + product: ProductVdo, + }, + MissingProductTypeActiveCableVdo2 { + /// The ID Header VDO, included for context in debugging. + id: IdHeaderVdo, -impl From for ConvertToResponseVdosError { - fn from(value: ParseVpdVdoError) -> Self { - Self::InvalidProductTypeVpdVdo(value) - } + /// 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 TryFrom @@ -192,10 +250,13 @@ impl TryFrom .ok_or(ConvertToResponseVdosError::MissingIdHeader)? .map_err(ConvertToResponseVdosError::InvalidIdHeader)?; - let cert_stat = value.cert_stat().ok_or(ConvertToResponseVdosError::MissingCertStat)?; + let cert_stat = value + .cert_stat() + .ok_or(ConvertToResponseVdosError::MissingCertStat { id })?; + let product = value .product_vdo() - .ok_or(ConvertToResponseVdosError::MissingProductVdo)?; + .ok_or(ConvertToResponseVdosError::MissingProductVdo { id, cert_stat })?; let product_type_vdos = match id.product_type { id_header_vdo::ProductType::NotACablePlugVpd => ProductTypeVdos::NotACablePlugVpd, @@ -203,8 +264,14 @@ impl TryFrom let vdo = value .product_type_vdos() .next() - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? - .try_into()?; + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypePassiveCableVdo { + id, + cert_stat, + product, + inner, + })?; ProductTypeVdos::PassiveCable(vdo) } @@ -212,14 +279,32 @@ impl TryFrom let vdo1 = value .product_type_vdos() .next() - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? - .try_into()?; + .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::MissingProductTypeVdo)? - .try_into()?; + .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) } @@ -227,8 +312,14 @@ impl TryFrom let vdo = value .product_type_vdos() .next() - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo)? - .try_into()?; + .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { id, cert_stat, product })? + .try_into() + .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeVpdVdo { + id, + cert_stat, + product, + inner, + })?; ProductTypeVdos::Vpd(vdo) } From bddfc1cb811b3b34ccac1713186462e0a514d886 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 11:06:31 -0700 Subject: [PATCH 12/23] Add partial parsing results to error type These allow callers to handle non-compliant devices, particularly DRDs that don't send a DFP VDO. --- src/registers/received_sop_identity_data.rs | 156 ++++++++++++------ .../received_sop_prime_identity_data.rs | 66 ++++++++ 2 files changed, 175 insertions(+), 47 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 54f36c1..ead8e3c 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -159,7 +159,7 @@ pub enum ConvertToResponseVdosError { /// The Cert Stat VDO, included for context in debugging. cert_stat: CertStatVdo, }, - MissingProductTypeVdo { + MissingProductTypeUfpVdo { /// The ID Header VDO, included for context in debugging. id: IdHeaderVdo, @@ -168,12 +168,6 @@ pub enum ConvertToResponseVdosError { /// The Product VDO, included for context in debugging. product: ProductVdo, - - /// The number of Product Type VDOs needed based on the ID Header. - needed: usize, - - /// The number of Product Type VDOs actually available. - available: usize, }, InvalidProductTypeUfpVdo { /// The ID Header VDO, included for context in debugging. @@ -185,12 +179,84 @@ pub enum ConvertToResponseVdosError { /// The Product VDO, included for context in debugging. product: ProductVdo, - /// The DFP Product Type VDOs, included for context in debugging. - dfp_product_type_vdos: DfpProductTypeVdos, - /// 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 @@ -211,6 +277,37 @@ impl TryFrom .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), + + // techincally 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, @@ -232,10 +329,11 @@ impl TryFrom let dfp_vdo = value .product_type_vdos() .nth(index) - .ok_or(ConvertToResponseVdosError::MissingProductTypeVdo { + .ok_or(ConvertToResponseVdosError::MissingProductTypeDfpVdo { id, cert_stat, product, + ufp_product_type_vdos, needed: index + 1, available: value.product_type_vdos().count(), })? @@ -252,42 +350,6 @@ impl TryFrom } }; - 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::MissingProductTypeVdo { - id, - cert_stat, - product, - needed: 1, - available: value.product_type_vdos().count(), - })? - .try_into() - .map_err(|inner| ConvertToResponseVdosError::InvalidProductTypeUfpVdo { - id, - cert_stat, - product, - dfp_product_type_vdos, - inner, - })?; - - match product_type_ufp { - id_header_vdo::ProductTypeUfp::Hub => UfpProductTypeVdos::Hub(ufp_vdo), - id_header_vdo::ProductTypeUfp::Peripheral => UfpProductTypeVdos::Peripheral(ufp_vdo), - - // techincally 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, - } - } - }; - Ok(Self { id: id.into(), cert_stat, diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index 773e526..980e911 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -239,6 +239,72 @@ pub enum ConvertToResponseVdosError { }, } +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 { From 5b6749ec99863472fd9beaa9e55f6937af43a906 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:05:39 -0700 Subject: [PATCH 13/23] cargo +nightly fmt --- src/registers/discovered_svids.rs | 3 ++- src/registers/received_sop_identity_data.rs | 12 +++++------ .../received_sop_prime_identity_data.rs | 20 ++++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/registers/discovered_svids.rs b/src/registers/discovered_svids.rs index 9642bd5..9b5bc6f 100644 --- a/src/registers/discovered_svids.rs +++ b/src/registers/discovered_svids.rs @@ -145,7 +145,8 @@ impl From<[u8; LEN]> for DiscoveredSvids { mod tests { use super::*; extern crate std; - use std::{vec, vec::Vec}; + use std::vec; + use std::vec::Vec; #[test] fn impl_iterators() { diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index ead8e3c..d706ae2 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -5,14 +5,12 @@ //! 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}, - ufp_vdo::ParseUfpVdoError, - CertStatVdo, ProductTypeVdo, ProductVdo, - }, - header::CommandType, +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; diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index 980e911..c0826f7 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -5,16 +5,18 @@ //! 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}, - passive_cable_vdo::ParsePassiveCableVdoError, - sop_prime::{id_header_vdo, IdHeaderVdo, ProductTypeVdos}, - vpd_vdo::ParseVpdVdoError, - ActiveCableVdo1, CertStatVdo, ProductTypeVdo, ProductVdo, - }, - header::CommandType, +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; From fec20084cd9773a807342259d65a04b7c365657c Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:14:59 -0700 Subject: [PATCH 14/23] Update embedded-usb-pd in examples package --- examples/rt685s-evk/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 19c5277d894e78a0ba7002040ce2273adfa7c4a0 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:26:09 -0700 Subject: [PATCH 15/23] Add comment and tests to clarify clamping behavior --- src/registers/discovered_svids.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/registers/discovered_svids.rs b/src/registers/discovered_svids.rs index 9b5bc6f..3025b33 100644 --- a/src/registers/discovered_svids.rs +++ b/src/registers/discovered_svids.rs @@ -90,6 +90,11 @@ impl DiscoveredSvids { } /// 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(), @@ -112,6 +117,11 @@ impl DiscoveredSvids { } /// 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(), @@ -183,4 +193,18 @@ mod tests { 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); + } } From 46e234e2a357c538978ad46d907b6383668fd8b7 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:27:03 -0700 Subject: [PATCH 16/23] Fix broken rustdoc link --- src/registers/received_sop_identity_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index d706ae2..bbb697d 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -34,7 +34,7 @@ bitfield! { /// Type of response received. /// - /// See [`ResponseType`]. + /// See [`CommandType`] for more details. pub u8, response_type, set_response_type: 7, 6; pub u32, vdo1, set_vdo1: 39, 8; From b72c1b5687020db9d8c6418c273a10c5725920e6 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:27:34 -0700 Subject: [PATCH 17/23] Fix typo --- src/registers/received_sop_identity_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index bbb697d..6586121 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -299,7 +299,7 @@ impl TryFrom id_header_vdo::ProductTypeUfp::Hub => UfpProductTypeVdos::Hub(ufp_vdo), id_header_vdo::ProductTypeUfp::Peripheral => UfpProductTypeVdos::Peripheral(ufp_vdo), - // techincally unreachable since the case was handled above, but we include it for exhaustiveness + // 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, } From e0a1a825310efe3af9bfa4e0a2046ba31f81ebf5 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:28:18 -0700 Subject: [PATCH 18/23] fixup! Fix broken rustdoc link --- src/registers/received_sop_prime_identity_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index c0826f7..ceb21a1 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -40,7 +40,7 @@ bitfield! { /// Type of response received. /// - /// See [`ResponseType`]. + /// See [`CommandType`] for more details. pub u8, response_type, set_response_type: 7, 6; pub u32, vdo1, set_vdo1: 39, 8; From 8ce97dfdda7e7f017752dbfc0a04d4cad7732c18 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:31:26 -0700 Subject: [PATCH 19/23] Cap number_valid_vdos to 6 --- src/registers/received_sop_identity_data.rs | 16 ++++++++++++++-- .../received_sop_prime_identity_data.rs | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 6586121..19d9386 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -53,9 +53,9 @@ 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. + /// Returns the number of valid VDOs in this register (max of 6). pub fn number_valid_vdos(&self) -> usize { - self.0.number_valid_vdos() as usize + self.0.number_valid_vdos().min(6) as usize } /// Returns an iterator over the VDOs. @@ -357,3 +357,15 @@ impl TryFrom }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[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); + } +} diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index ceb21a1..f0a1c24 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -59,9 +59,9 @@ 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. + /// Returns the number of valid VDOs in this register (max of 6). pub fn number_valid_vdos(&self) -> usize { - self.0.number_valid_vdos() as usize + self.0.number_valid_vdos().min(6) as usize } /// Returns an iterator over the VDOs. @@ -401,3 +401,15 @@ impl TryFrom }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[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); + } +} From a4f2390f0469bca6652facb83d82ce59f3f7cb2b Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 09:32:00 -0700 Subject: [PATCH 20/23] Fix copy/pasta --- src/registers/discovered_svids.rs | 2 +- src/registers/received_sop_identity_data.rs | 2 +- src/registers/received_sop_prime_identity_data.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registers/discovered_svids.rs b/src/registers/discovered_svids.rs index 3025b33..2c4edc9 100644 --- a/src/registers/discovered_svids.rs +++ b/src/registers/discovered_svids.rs @@ -16,7 +16,7 @@ pub const ADDR: u8 = 0x21; pub const LEN: usize = 264 / 8; bitfield! { - /// Received source/sink capabilities register + /// Discovered SVIDs register. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct Raw([u8]); diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 19d9386..46f6e84 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -23,7 +23,7 @@ pub const ADDR: u8 = 0x48; pub const LEN: usize = 200 / 8; bitfield! { - /// Received source/sink capabilities register + /// Received SOP Identity Data Object register #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct Raw([u8]); diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index f0a1c24..280515e 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -29,7 +29,7 @@ pub const ADDR: u8 = 0x49; pub const LEN: usize = 200 / 8; bitfield! { - /// Received source/sink capabilities register + /// Received SOP Prime Identity Data Object register #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] struct Raw([u8]); From 5b54b0ebb46b1e34302bc4bf3377f2b431d58644 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 10:19:27 -0700 Subject: [PATCH 21/23] Add tests for impl TryFrom Co-authored-by: Copilot --- src/registers/received_sop_identity_data.rs | 255 ++++++++++++++++++ .../received_sop_prime_identity_data.rs | 206 ++++++++++++++ 2 files changed, 461 insertions(+) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index 46f6e84..ef9d378 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -361,6 +361,24 @@ impl TryFrom #[cfg(test)] mod tests { use super::*; + use embedded_usb_pd::vdm::structured::command::discover_identity::sop::{ + DfpProductTypeVdos, ResponseVdos, UfpProductTypeVdos, + }; + use embedded_usb_pd::vdm::structured::header::CommandType; + + /// 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() { @@ -368,4 +386,241 @@ mod tests { 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 index 280515e..70f8abb 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -405,6 +405,19 @@ impl TryFrom #[cfg(test)] mod tests { use super::*; + use embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::{ProductTypeVdos, ResponseVdos}; + use embedded_usb_pd::vdm::structured::command::discover_identity::PassiveCableVdo; + + #[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() { @@ -412,4 +425,197 @@ mod tests { 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(), + }) + ); + } + } } From 0852c3972f51c48606b797d1c718aec3a9016fa9 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 10:24:33 -0700 Subject: [PATCH 22/23] cargo +nightly fmt --- src/registers/received_sop_identity_data.rs | 3 ++- src/registers/received_sop_prime_identity_data.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index ef9d378..be355b8 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -360,12 +360,13 @@ impl TryFrom #[cfg(test)] mod tests { - use super::*; 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. diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index 70f8abb..b3186fc 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -404,9 +404,9 @@ impl TryFrom #[cfg(test)] mod tests { - use super::*; use embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::{ProductTypeVdos, ResponseVdos}; - use embedded_usb_pd::vdm::structured::command::discover_identity::PassiveCableVdo; + + use super::*; #[test] fn default_has_no_vdos() { From 46a4e8869be38fea5a8ba96138e097348e0b414b Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 28 Apr 2026 17:04:12 -0700 Subject: [PATCH 23/23] Add consts --- src/registers/received_sop_identity_data.rs | 48 +++++++++++++++++-- .../received_sop_prime_identity_data.rs | 28 +++++++++-- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/registers/received_sop_identity_data.rs b/src/registers/received_sop_identity_data.rs index be355b8..c7041ba 100644 --- a/src/registers/received_sop_identity_data.rs +++ b/src/registers/received_sop_identity_data.rs @@ -22,6 +22,40 @@ pub const ADDR: u8 = 0x48; /// 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)] @@ -96,7 +130,7 @@ impl ReceivedSopIdentityData { /// 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().next()?; + 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)), @@ -110,14 +144,14 @@ impl ReceivedSopIdentityData { /// Contains the XID assigned by USB-IF to the product before certification, /// in binary format. pub fn cert_stat(&self) -> Option { - self.vdos().nth(1).map(CertStatVdo) + 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(2).map(ProductVdo::from) + self.vdos().nth(PRODUCT_VDO_INDEX).map(ProductVdo::from) } /// Return an iterator over the Product Type VDOs, if present. @@ -125,7 +159,7 @@ impl ReceivedSopIdentityData { /// 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(3).map(ProductTypeVdo) + self.vdos().skip(PRODUCT_TYPE_VDOS_STARTING_INDEX).map(ProductTypeVdo) } } @@ -323,7 +357,11 @@ impl TryFrom // 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 { 2 } else { 0 }; + 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) diff --git a/src/registers/received_sop_prime_identity_data.rs b/src/registers/received_sop_prime_identity_data.rs index b3186fc..0787c41 100644 --- a/src/registers/received_sop_prime_identity_data.rs +++ b/src/registers/received_sop_prime_identity_data.rs @@ -28,6 +28,26 @@ pub const ADDR: u8 = 0x49; /// 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)] @@ -102,7 +122,7 @@ impl ReceivedSopPrimeIdentityData { /// 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().next()?; + 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)), @@ -116,12 +136,12 @@ impl ReceivedSopPrimeIdentityData { /// Contains the XID assigned by USB-IF to the product before certification, /// in binary format. pub fn cert_stat(&self) -> Option { - self.vdos().nth(1).map(CertStatVdo) + 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(2).map(ProductVdo::from) + self.vdos().nth(PRODUCT_VDO_INDEX).map(ProductVdo::from) } /// Return an iterator over the Product Type VDOs, if present. @@ -129,7 +149,7 @@ impl ReceivedSopPrimeIdentityData { /// 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(3).map(ProductTypeVdo) + self.vdos().skip(PRODUCT_TYPE_VDOS_STARTING_INDEX).map(ProductTypeVdo) } }