From 67ca4d1a3f85a9f112ab462ed23b6528ab086cf6 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Tue, 7 Apr 2026 13:51:17 -0700 Subject: [PATCH 01/26] Add SVID constants --- src/{vdm.rs => vdm/mod.rs} | 9 ++++----- src/vdm/svid.rs | 11 +++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) rename src/{vdm.rs => vdm/mod.rs} (82%) create mode 100644 src/vdm/svid.rs diff --git a/src/vdm.rs b/src/vdm/mod.rs similarity index 82% rename from src/vdm.rs rename to src/vdm/mod.rs index ea08f8b..83e656b 100644 --- a/src/vdm.rs +++ b/src/vdm/mod.rs @@ -1,3 +1,7 @@ +pub mod svid; + +pub use svid::Svid; + pub const DATA_OBJ_SIZE: usize = 4; pub const MAX_VDOS: usize = 6; pub const MAX_NUM_DATA_OBJECTS: usize = 7; @@ -25,11 +29,6 @@ pub enum Cmd { SvidCmdStart = 16, } -/// Standard or Vendor ID (SVID) newtype, see PD spec 6.4.4.2.1 -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Svid(pub u16); - /// Altmode ID newtype for discover modes command and others, see PD spec 6.4.4.3.3 #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/vdm/svid.rs b/src/vdm/svid.rs new file mode 100644 index 0000000..649eacb --- /dev/null +++ b/src/vdm/svid.rs @@ -0,0 +1,11 @@ +/// Standard or Vendor ID (SVID) newtype, see PD spec 6.4.4.2.1 +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Svid(pub u16); + +impl Svid { + pub const PD: Self = Self(0xFF00); + pub const DISPLAY_PORT_TYPE_C: Self = Self(0xFF01); + pub const USB4: Self = Self(0xFF03); + pub const THUNDERBOLT: Self = Self(0x8087); +} From 7370000615d8321728774f559aeeeabf070696c6 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Wed, 8 Apr 2026 13:30:47 -0700 Subject: [PATCH 02/26] Remove USB4 SVID --- src/vdm/svid.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vdm/svid.rs b/src/vdm/svid.rs index 649eacb..f0fb23e 100644 --- a/src/vdm/svid.rs +++ b/src/vdm/svid.rs @@ -6,6 +6,5 @@ pub struct Svid(pub u16); impl Svid { pub const PD: Self = Self(0xFF00); pub const DISPLAY_PORT_TYPE_C: Self = Self(0xFF01); - pub const USB4: Self = Self(0xFF03); pub const THUNDERBOLT: Self = Self(0x8087); } From b6aa096dc8b70e465404724223808f24df8f45df Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Wed, 8 Apr 2026 17:23:41 -0700 Subject: [PATCH 03/26] Add Discover Identity types --- src/ucsi/lpm/get_alternate_modes.rs | 2 +- src/vdm/mod.rs | 6 +- .../discover_identity/active_cable_vdo.rs | 783 ++++++++++++++++++ .../command/discover_identity/dfp_vdo.rs | 91 ++ .../command/discover_identity/mod.rs | 60 ++ .../discover_identity/passive_cable_vdo.rs | 357 ++++++++ .../command/discover_identity/sop.rs | 139 ++++ .../command/discover_identity/sop_prime.rs | 91 ++ .../command/discover_identity/ufp_vdo.rs | 248 ++++++ .../command/discover_identity/vpd_vdo.rs | 176 ++++ src/vdm/structured/command/mod.rs | 5 + src/vdm/structured/header.rs | 125 +++ src/vdm/structured/mod.rs | 10 + src/vdm/structured/svid.rs | 18 + src/vdm/svid.rs | 10 - 15 files changed, 2108 insertions(+), 13 deletions(-) create mode 100644 src/vdm/structured/command/discover_identity/active_cable_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/dfp_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/mod.rs create mode 100644 src/vdm/structured/command/discover_identity/passive_cable_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/sop.rs create mode 100644 src/vdm/structured/command/discover_identity/sop_prime.rs create mode 100644 src/vdm/structured/command/discover_identity/ufp_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/vpd_vdo.rs create mode 100644 src/vdm/structured/command/mod.rs create mode 100644 src/vdm/structured/header.rs create mode 100644 src/vdm/structured/mod.rs create mode 100644 src/vdm/structured/svid.rs delete mode 100644 src/vdm/svid.rs diff --git a/src/ucsi/lpm/get_alternate_modes.rs b/src/ucsi/lpm/get_alternate_modes.rs index 71fe74a..2debf94 100644 --- a/src/ucsi/lpm/get_alternate_modes.rs +++ b/src/ucsi/lpm/get_alternate_modes.rs @@ -9,7 +9,7 @@ use bitfield::bitfield; use super::Recipient; use crate::ucsi::lpm::InvalidRecipient; use crate::ucsi::{CommandHeaderRaw, COMMAND_LEN}; -use crate::vdm::{AltModeId, Svid}; +use crate::vdm::{structured::Svid, AltModeId}; /// Data length for the GET_ALTERNATE_MODES command response pub const RESPONSE_DATA_LEN: usize = 12; diff --git a/src/vdm/mod.rs b/src/vdm/mod.rs index 83e656b..0b3b91b 100644 --- a/src/vdm/mod.rs +++ b/src/vdm/mod.rs @@ -1,6 +1,8 @@ -pub mod svid; +//! Vendor Defined Messages (VDM) allow vendors to exchange information outside of that defined by the PD spec. +//! +//! See PD spect 6.4.4 Vendor Defined Message. -pub use svid::Svid; +pub mod structured; pub const DATA_OBJ_SIZE: usize = 4; pub const MAX_VDOS: usize = 6; diff --git a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs new file mode 100644 index 0000000..2a72194 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs @@ -0,0 +1,783 @@ +//! An Active Cable has a USB Plug on each end, at least one of which is a Cable +//! Plug supporting SOP' Communication. It incorporates data bus signal conditioning +//! circuits. +//! +//! See PD spec 6.4.4.3.1.7 Active Cable VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// An Active Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. It incorporates data bus signal conditioning +/// circuits. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.7 Active Cable VDO, table 6.42 Active Cable VDO1. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ActiveCableVdo1 { + /// The highest rate the cable supports. + pub usb_highest_speed: UsbHighestSpeed, + + /// Whether one of the Cable Plugs is capable of SOP'' Communication in addition + /// to the normative SOP' Communication. + pub soppp_controller_present: bool, + + /// Whether the cable contains an end-to-end `VBUS` wire. + pub vbus_through_cable: bool, + + /// Whether the cable is capable of carrying 3A or 5A. + pub vbus_current_handling_capability: VbusCurrentHandlingCapability, + + /// Whether the SBUs are passive or active (e.g., digital). + pub sbu_type: SbuType, + + /// Whether the cable supports the SBUs in the cable. + pub sbu_supported: bool, + + /// The maximum voltage that shall be negotiated as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// Whether the Active Cable has one or two Cable Plugs requiring power from + /// `VCONN`. + pub cable_termination_type: CableTerminationType, + + /// The signal latency through the cable, which can be used as an approximation + /// for its length. + pub cable_latency: CableLatency, + + /// The cable is specifically designed for safe operation when carrying up to + /// 48 volts at 5 amps. + pub epr_capable: bool, + + /// Indicates whether the opposite end from the USB Type-C plug is another USB + /// Type-C plug or is a Captive Cable Assembly. + pub usb_type_c_or_captive: UsbTypeCOrCaptive, + + /// FW version assigned by the VID owner. + pub firmware_version: u8, + + /// HW version assigned by the VID owner. + pub hw_version: u8, +} + +/// An Active Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. It incorporates data bus signal conditioning +/// circuits. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.7 Active Cable VDO, table 6.43 Active Cable VDO2. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ActiveCableVdo2 { + /// The signaling generation the cable supports. + pub usb_gen: UsbGen, + + /// Whether or not the cable supports asymmetric mode, as defined by the USB4 + /// and USB Type-C 2.4 specifications. + pub usb4_asymmetric_mode_supported: bool, + + /// Whether or not the cable is optically isolated, as defined by the USB Type-C + /// 2.4 specification. + pub optically_isolated_active_cable: bool, + + /// The number of lanes the cable supports. + pub usb_lanes_supported: UsbLanesSupported, + + /// Whether or not the cable supports USB 3.2 SuperSpeed signaling. + pub usb3p2_supported: bool, + + /// Whether or not the cable supports USB 2.0 only signaling. + pub usb2p0_supported: bool, + + /// The number of USB 2.0 "hub hops" that are lost due to the transmission time + /// of the cable. + pub usb2p0_hub_hops_consumed: u8, + + /// Whether or not the cable supports USB4 operation. + pub usb4_supported: bool, + + /// The cable's active element. + pub active_element: ActiveElement, + + /// The cable's construction between the active elements. + pub physical_connection: PhysicalConnection, + + /// Which U3 to U0 mode the cable supports. + pub u3_to_u0_transition_mode: U3ToU0TransitionMode, + + /// The power the cable consumes while in USB 3.2 U3 or USB4 CLd. + pub u3_cld_power: U3CldPower, + + /// The temperature, in degrees Celsius, at which the plug will shut down its + /// active signaling components. + pub shutdown_temperature: u8, + + /// The maximum allowable operating temperature inside the plug, in degrees Celsius. + pub maximum_operating_temperature: u8, +} + +/// Errors that can occur when parsing an [`ActiveCableVdo1`] from a raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseActiveCableVdo1Error { + /// [`ActiveCableVdo1::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`ActiveCableVdo1::vbus_current_handling_capability`] contains an invalid value. + InvalidVbusCurrentHandlingCapability, + + /// [`ActiveCableVdo1::sbu_type`] contains an invalid value. + InvalidSbuType, + + /// [`ActiveCableVdo1::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, + + /// [`ActiveCableVdo1::cable_termination_type`] contains an invalid value. + InvalidCableTerminationType, + + /// [`ActiveCableVdo1::cable_latency`] contains an invalid value. + InvalidCableLatency, + + /// [`ActiveCableVdo1::usb_type_c_or_captive`] contains an invalid value. + InvalidUsbTypeCOrCaptive, +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(raw: Raw1) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidUsbHighestSpeed)?, + soppp_controller_present: raw.soppp_controller_present(), + vbus_through_cable: raw.vbus_through_cable(), + vbus_current_handling_capability: raw + .vbus_current_handling_capability() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidVbusCurrentHandlingCapability)?, + sbu_type: raw + .sbu_type() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidSbuType)?, + sbu_supported: !raw.sbu_supported_n(), + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidMaximumVbusVoltage)?, + cable_termination_type: raw + .cable_termination_type() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidCableTerminationType)?, + cable_latency: raw + .cable_latency() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidCableLatency)?, + epr_capable: raw.epr_capable(), + usb_type_c_or_captive: raw + .usb_type_c_or_captive() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidUsbTypeCOrCaptive)?, + firmware_version: raw.firmware_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(value: u32) -> Result { + Raw1(value).try_into() + } +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(value: ProductTypeVdo) -> Result { + Raw1(value.0).try_into() + } +} + +impl TryFrom<[u8; 4]> for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(bytes: [u8; 4]) -> Result { + let value = u32::from_le_bytes(bytes); + Raw1(value).try_into() + } +} + +/// Errors that can occur when parsing an [`ActiveCableVdo2`] from a raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseActiveCableVdo2Error { + /// [`ActiveCableVdo2::usb_gen`] contains an invalid value. + InvalidUsbGen, + + /// [`ActiveCableVdo2::usb4_asymmetric_mode_supported`] contains an invalid value. + InvalidUsb4AsymmetricModeSupported, + + /// [`ActiveCableVdo2::optically_isolated_active_cable`] contains an invalid value. + InvalidOpticallyIsolatedActiveCable, + + /// [`ActiveCableVdo2::usb_lanes_supported`] contains an invalid value. + InvalidUsbLanesSupported, + + /// [`ActiveCableVdo2::usb3p2_supported`] contains an invalid value. + InvalidUsb3p2Supported, + + /// [`ActiveCableVdo2::usb2p0_supported`] contains an invalid value. + InvalidUsb2p0Supported, + + /// [`ActiveCableVdo2::usb2p0_hub_hops_consumed`] contains an invalid value. + InvalidUsb2p0HubHopsConsumed, + + /// [`ActiveCableVdo2::usb4_supported`] contains an invalid value. + InvalidUsb4Supported, + + /// [`ActiveCableVdo2::active_element`] contains an invalid value. + InvalidActiveElement, + + /// [`ActiveCableVdo2::physical_connection`] contains an invalid value. + InvalidPhysicalConnection, + + /// [`ActiveCableVdo2::u3_to_u0_transition_mode`] contains an invalid value. + InvalidU3ToU0TransitionMode, + + /// [`ActiveCableVdo2::u3_cld_power`] contains an invalid value. + InvalidU3CldPower, +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(raw: Raw2) -> Result { + Ok(Self { + usb_gen: raw + .usb_gen() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidUsbGen)?, + usb4_asymmetric_mode_supported: raw.usb4_asymmetric_mode_supported(), + optically_isolated_active_cable: raw.optically_isolated_active_cable(), + usb_lanes_supported: raw + .usb_lanes_supported() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidUsbLanesSupported)?, + usb3p2_supported: !raw.usb3p2_supported_n(), + usb2p0_supported: !raw.usb2p0_supported_n(), + usb2p0_hub_hops_consumed: raw.usb2p0_hub_hops_consumed(), + usb4_supported: !raw.usb4_supported_n(), + active_element: raw + .active_element() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidActiveElement)?, + physical_connection: raw + .physical_connection() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidPhysicalConnection)?, + u3_to_u0_transition_mode: raw + .u3_to_u0_transition_mode() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidU3ToU0TransitionMode)?, + u3_cld_power: raw + .u3_cld_power() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidU3CldPower)?, + shutdown_temperature: raw.shutdown_temperature(), + maximum_operating_temperature: raw.maximum_operating_temperature(), + }) + } +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(value: u32) -> Result { + Raw2(value).try_into() + } +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +bitfield::bitfield! { + /// The raw value of an [`ActiveCableVdo1`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw1(u32); + impl Debug; + + /// See [`ActiveCableVdo1::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`ActiveCableVdo1::soppp_controller_present`]. + pub bool, soppp_controller_present, set_soppp_controller_present: 3; + + /// See [`ActiveCableVdo1::vbus_through_cable`]. + pub bool, vbus_through_cable, set_vbus_through_cable: 4; + + /// See [`ActiveCableVdo1::vbus_current_handling_capability`]. + pub u8, vbus_current_handling_capability, set_vbus_current_handling_capability: 6, 5; + + /// See [`ActiveCableVdo1::sbu_type`]. + pub u8, sbu_type, set_sbu_type: 7, 7; + + /// See [`ActiveCableVdo1::sbu_supported`]. + pub bool, sbu_supported_n, set_sbu_supported_n: 8; + + /// See [`ActiveCableVdo1::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 10, 9; + + /// See [`ActiveCableVdo1::cable_termination_type`]. + pub u8, cable_termination_type, set_cable_termination_type: 12, 11; + + /// See [`ActiveCableVdo1::cable_latency`]. + pub u8, cable_latency, set_cable_latency: 16, 13; + + /// See [`ActiveCableVdo1::epr_capable`]. + pub bool, epr_capable, set_epr_capable: 17; + + /// See [`ActiveCableVdo1::usb_type_c_or_captive`]. + pub u8, usb_type_c_or_captive, set_usb_type_c_or_captive: 19, 18; + + /// See [`ActiveCableVdo1::firmware_version`]. + pub u8, firmware_version, set_firmware_version: 27, 24; + + /// See [`ActiveCableVdo1::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +bitfield::bitfield! { + /// The raw value of an [`ActiveCableVdo2`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw2(u32); + impl Debug; + + /// See [`ActiveCableVdo2::usb_gen`]. + pub u8, usb_gen, set_usb_gen: 0, 0; + + /// See [`ActiveCableVdo2::usb4_asymmetric_mode_supported`]. + pub bool, usb4_asymmetric_mode_supported, set_usb4_asymmetric_mode_supported: 1; + + /// See [`ActiveCableVdo2::optically_isolated_active_cable`]. + pub bool, optically_isolated_active_cable, set_optically_isolated_active_cable: 2; + + /// See [`ActiveCableVdo2::usb_lanes_supported`]. + pub u8, usb_lanes_supported, set_usb_lanes_supported: 3, 3; + + /// See [`ActiveCableVdo2::usb3p2_supported`]. + pub bool, usb3p2_supported_n, set_usb3p2_supported_n: 4; + + /// See [`ActiveCableVdo2::usb2p0_supported`]. + pub bool, usb2p0_supported_n, set_usb2p0_supported_n: 5; + + /// See [`ActiveCableVdo2::usb2p0_hub_hops_consumed`]. + pub u8, usb2p0_hub_hops_consumed, set_usb2p0_hub_hops_consumed: 7, 6; + + /// See [`ActiveCableVdo2::usb4_supported`]. + pub bool, usb4_supported_n, set_usb4_supported_n: 8; + + /// See [`ActiveCableVdo2::active_element`]. + pub u8, active_element, set_active_element: 9, 9; + + /// See [`ActiveCableVdo2::physical_connection`]. + pub u8, physical_connection, set_physical_connection: 10, 10; + + /// See [`ActiveCableVdo2::u3_to_u0_transition_mode`]. + pub u8, u3_to_u0_transition_mode, set_u3_to_u0_transition_mode: 11, 11; + + /// See [`ActiveCableVdo2::u3_cld_power`]. + pub u8, u3_cld_power, set_u3_cld_power: 14, 12; + + /// See [`ActiveCableVdo2::shutdown_temperature`]. + pub u8, shutdown_temperature, set_shutdown_temperature: 23, 16; + + /// See [`ActiveCableVdo2::maximum_operating_temperature`]. + pub u8, maximum_operating_temperature, set_maximum_operating_temperature: 31, 24; +} + +/// The highest rate the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1 + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2 + Usb3p2, + + /// USB4 Gen3 + Usb4Gen3, + + /// USB4 Gen4 + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// Whether the cable is capable of carrying 3A or 5A. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VbusCurrentHandlingCapability { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for VbusCurrentHandlingCapability { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b01 => Ok(Self::ThreeAmps), + 0b10 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +/// Whether the SBUs are passive or active (e.g., digital). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SbuType { + /// SBU is passive. + Passive, + + /// SBU is active (e.g., digital). + Active, +} + +impl TryFrom for SbuType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Passive), + 1 => Ok(Self::Active), + _ => Err(()), + } + } +} + +/// The maximum voltage that shall be negotiated as part of an Explicit Contract. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaximumVbusVoltage { + /// 20V + TwentyV, + + /// 30V + ThirtyV, + + /// 40V + FortyV, + + /// 50V + FiftyV, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyV), + 0b01 => Ok(Self::ThirtyV), + 0b10 => Ok(Self::FortyV), + 0b11 => Ok(Self::FiftyV), + _ => Err(()), + } + } +} + +/// Whether the Active Cable has one or two Cable Plugs requiring power from +/// `VCONN`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableTerminationType { + /// One end of the cable is active. + OneEndActive, + + /// Both ends of the cable are active. + BothEndsActive, +} + +impl TryFrom for CableTerminationType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::OneEndActive), + 0b11 => Ok(Self::BothEndsActive), + _ => Err(()), + } + } +} + +/// The signal latency through the cable, which can be used as an approximation +/// for its length. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableLatency { + /// <10ns (~1m) + LessThan10ns, + + /// 10ns to 20ns (~2m) + LessThan20ns, + + /// 20ns to 30ns (~3m) + LessThan30ns, + + /// 30ns to 40ns (~4m) + LessThan40ns, + + /// 40ns to 50ns (~5m) + LessThan50ns, + + /// 50ns to 60ns (~6m) + LessThan60ns, + + /// 60ns to 70ns (~7m) + LessThan70ns, + + /// 1000ns (~100m) + LessThan1000ns, + + /// 2000ns (~200m) + LessThan2000ns, + + /// 3000ns (~300m) + LessThan3000ns, +} + +impl TryFrom for CableLatency { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0001 => Ok(Self::LessThan10ns), + 0b0010 => Ok(Self::LessThan20ns), + 0b0011 => Ok(Self::LessThan30ns), + 0b0100 => Ok(Self::LessThan40ns), + 0b0101 => Ok(Self::LessThan50ns), + 0b0110 => Ok(Self::LessThan60ns), + 0b0111 => Ok(Self::LessThan70ns), + 0b1000 => Ok(Self::LessThan1000ns), + 0b1001 => Ok(Self::LessThan2000ns), + 0b1010 => Ok(Self::LessThan3000ns), + _ => Err(()), + } + } +} + +/// Indicates whether the opposite end from the USB Type-C plug is another USB +/// Type-C plug or is a Captive Cable Assembly. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbTypeCOrCaptive { + /// The opposite end from the USB Type-C plug is another USB Type-C plug. + UsbTypeC, + + /// The opposite end from the USB Type-C plug is a Captive Cable Assembly. + Captive, +} + +impl TryFrom for UsbTypeCOrCaptive { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::UsbTypeC), + 0b11 => Ok(Self::Captive), + _ => Err(()), + } + } +} + +/// The signaling generation the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbGen { + /// Gen1 + Gen1, + + /// Gen2 or higher. + Gen2OrHigher, +} + +impl TryFrom for UsbGen { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Gen1), + 1 => Ok(Self::Gen2OrHigher), + _ => Err(()), + } + } +} + +/// The number of lanes the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbLanesSupported { + /// The cable supports one lane. + OneLane, + + /// The cable supports two lanes. + TwoLanes, +} + +impl TryFrom for UsbLanesSupported { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::OneLane), + 1 => Ok(Self::TwoLanes), + _ => Err(()), + } + } +} + +/// The cable's active element. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ActiveElement { + /// The active element is a re-driver. + Redriver, + + /// The active element is a re-timer. + Retimer, +} + +impl TryFrom for ActiveElement { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Redriver), + 1 => Ok(Self::Retimer), + _ => Err(()), + } + } +} + +/// The cable's construction between the active elements. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PhysicalConnection { + /// The connection between the active elements is made of copper. + Copper, + + /// The connection between the active elements is made of optical fiber. + Optical, +} + +impl TryFrom for PhysicalConnection { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Copper), + 1 => Ok(Self::Optical), + _ => Err(()), + } + } +} + +/// Which U3 to U0 mode the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum U3ToU0TransitionMode { + /// The cable supports a direct transition from U3 to U0. + Direct, + + /// The cable supports a transition from U3 to U0 through U3S. + ThroughU3S, +} + +impl TryFrom for U3ToU0TransitionMode { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Direct), + 1 => Ok(Self::ThroughU3S), + _ => Err(()), + } + } +} + +/// The power the cable consumes while in USB 3.2 U3 or USB4 CLd. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum U3CldPower { + /// >10mW + GreaterThan10Milliwatts, + + /// 5-10mW + FiveToTenMilliwatts, + + /// 1-5mW + OneToFiveMilliwatts, + + /// 0.5-1mW + P5To1Milliwatt, + + /// 0.2-0.5mW + P2ToP5Milliwatt, + + /// 50-200µW + FiftyToTwoHundredMicrowatts, + + /// <50µW + LessThanFiftyMicrowatts, +} + +impl TryFrom for U3CldPower { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::GreaterThan10Milliwatts), + 0b001 => Ok(Self::FiveToTenMilliwatts), + 0b010 => Ok(Self::OneToFiveMilliwatts), + 0b011 => Ok(Self::P5To1Milliwatt), + 0b100 => Ok(Self::P2ToP5Milliwatt), + 0b101 => Ok(Self::FiftyToTwoHundredMicrowatts), + 0b110 => Ok(Self::LessThanFiftyMicrowatts), + _ => Err(()), + } + } +} diff --git a/src/vdm/structured/command/discover_identity/dfp_vdo.rs b/src/vdm/structured/command/discover_identity/dfp_vdo.rs new file mode 100644 index 0000000..ba7e0af --- /dev/null +++ b/src/vdm/structured/command/discover_identity/dfp_vdo.rs @@ -0,0 +1,91 @@ +//! A DFP (Downward Facing Port) is a Port that provides power and/or data to a +//! UFP. DFPs include Hosts, Hubs, and Power Bricks. +//! +//! See PD spec 6.4.4.3.1.5 DFP VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// Returned by Ports capable as operating as a DFP, including those implemented +/// by Hosts, Hubs, and Power Bricks. +/// +/// Sent based on the value of [`sop::IdHeaderVdo::product_type_dfp`][super::sop::IdHeaderVdo::product_type_dfp]. +/// +/// See PD spec 6.4.4.3.1.5 DFP VDO, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DfpVdo { + /// A unique number that unambiguously identifies each USB Type-C 2.4 DFP, including + /// DRPs, on the device. + /// + /// This number is independent of the USB Port number. + pub port_number: u8, + + /// Whether the FDP can operate as a PDUSB Host, and its Capabilities when operating + /// as such. + pub host_capability: HostCapability, +} + +impl From for DfpVdo { + fn from(raw: Raw) -> Self { + Self { + port_number: raw.port_number(), + host_capability: raw.host_capability().into(), + } + } +} + +impl From for DfpVdo { + fn from(value: u32) -> Self { + Raw(value).into() + } +} + +impl From for DfpVdo { + fn from(value: ProductTypeVdo) -> Self { + value.0.into() + } +} + +impl From<[u8; 4]> for DfpVdo { + fn from(bytes: [u8; 4]) -> Self { + u32::from_le_bytes(bytes).into() + } +} + +bitfield::bitfield! { + /// The raw value of a [`DfpVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`DfpVdo::port_number`]. + pub u8, port_number, set_port_number: 4, 0; + + /// See [`DfpVdo::host_capability`]. + pub u8, host_capability, set_host_capability: 26, 24; +} + +/// A DFP's Capabilities when operating as a PDUSB Host. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HostCapability { + /// USB 2.0 Host Capable. + pub usb2p0: bool, + + /// USB 3.2 Host Capable. + pub usb3p2: bool, + + /// USB4 Host Capable. + pub usb4: bool, +} + +impl From for HostCapability { + fn from(value: u8) -> Self { + Self { + usb2p0: value & 0b001 != 0, + usb3p2: value & 0b010 != 0, + usb4: value & 0b100 != 0, + } + } +} diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs new file mode 100644 index 0000000..85c6526 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -0,0 +1,60 @@ +//! The Discover Identity Command is used to identify a Port Partner and the Responder (Cable Plug or VPD). +//! +//! See PD spec 6.4.4.3.1 Discover Identity. + +pub mod active_cable_vdo; +pub mod dfp_vdo; +pub mod passive_cable_vdo; +pub mod sop; +pub mod sop_prime; +pub mod ufp_vdo; +pub mod vpd_vdo; + +pub use active_cable_vdo::{ActiveCableVdo1, ActiveCableVdo2}; +pub use dfp_vdo::DfpVdo; +pub use passive_cable_vdo::PassiveCableVdo; +pub use ufp_vdo::UfpVdo; +pub use vpd_vdo::VpdVdo; + +/// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. +/// +/// See PD spec 6.4.4.3.1.1.7 Connector Type Field. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConnectorType { + /// The device is a USB Type-C receptacle. + Receptacle, + + /// The device is a USB Type-C plug. + Plug, +} + +/// Contains the XID assigned by USB-IF to the product before certification in binary format. +/// +/// See PD spec 6.4.4.3.1.2 Cert Stat VDO, table 6.37 Cert Stat VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CertStatVdo(pub u32); + +/// The Product VDO contains identity information relating to the product. +/// +/// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductVdo { + /// The USB Product ID, as defined by the USB 2.0 / USB 3.2 specifications. + pub usb_product_id: u16, + + /// The USB Device Release Number, as defined by the USB 2.0 / USB 3.2 specifications. + pub bcd_device: u16, +} + +/// An unspecified Product Type VDO in the Product Type VDO(s) of the Discover +/// Identity Command response. +/// +/// The type of this VDO is determined by the ID Header VDO and whether targetting +/// SOP or SOP'. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductTypeVdo(pub u32); diff --git a/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs new file mode 100644 index 0000000..8664308 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs @@ -0,0 +1,357 @@ +//! A Passive Cable has a USB Plug on each end, at least one of which is a Cable +//! Plug supporting SOP' Communication. +//! +//! See PD spec 6.4.4.3.1.6 Passive Cable VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// A Passive Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.6 Passive Cable VDO, table 6.41 Passive Cable VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PassiveCableVdo { + /// The highest rate the cable supports. + pub usb_highest_speed: UsbHighestSpeed, + + /// Indicates whether the cable can carry 3A or 5A. + pub vbus_current_handling_capability: VbusCurrentHandlingCapability, + + /// The maximum voltage that shall be negotiated using a Fixed Supply over the + /// cable as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// Whether the cable needs `VCONN` only initially in order to support the Discover + /// Identity Command, after which it can be removed, or if it needs `VCONN` + /// to be continuously applied to power some feature of the Cable Plug. + pub cable_termination_type: CableTerminationType, + + /// The signal latency through the cable, which can be used as an approximation + /// for its length. + pub cable_latency: CableLatency, + + /// Whether or not the cable is designed for safe operation when carrying up + /// to 48 volts at 5 amps. + pub epr_capable: bool, + + /// Whether or not the opposite end from the USB Type-C plug is another USB + /// Type-C plug or a Captive Cable Assembly. + pub usb_type_c_or_captive: UsbTypeCPlugOrCaptive, + + /// The FW version assigned by the VID owner. + pub firmware_version: u8, + + /// The HW version assigned by the VID owner. + pub hw_version: u8, +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidUsbHighestSpeed)?, + vbus_current_handling_capability: raw + .vbus_current_handling_capability() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidVbusCurrentHandlingCapability)?, + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidMaximumVbusVoltage)?, + cable_termination_type: raw + .cable_termination_type() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidCableTerminationType)?, + cable_latency: raw + .cable_latency() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidCableLatency)?, + epr_capable: raw.epr_capable(), + usb_type_c_or_captive: raw + .usb_type_c_or_captive() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidUsbTypeCOrCaptive)?, + firmware_version: raw.firmware_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// Errors that can occur when parsing a [`PassiveCableVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParsePassiveCableVdoError { + /// [`PassiveCableVdo::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`PassiveCableVdo::vbus_current_handling_capability`] contains an invalid value. + InvalidVbusCurrentHandlingCapability, + + /// [`PassiveCableVdo::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, + + /// [`PassiveCableVdo::cable_termination_type`] contains an invalid value. + InvalidCableTerminationType, + + /// [`PassiveCableVdo::cable_latency`] contains an invalid value. + InvalidCableLatency, + + /// [`PassiveCableVdo::usb_type_c_or_captive`] contains an invalid value. + InvalidUsbTypeCOrCaptive, +} + +bitfield::bitfield! { + /// The raw value of a [`PassiveCableVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`PassiveCableVdo::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`PassiveCableVdo::vbus_current_handling_capability`]. + pub u8, vbus_current_handling_capability, set_vbus_current_handling_capability: 6, 5; + + /// See [`PassiveCableVdo::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 10, 9; + + /// See [`PassiveCableVdo::cable_termination_type`]. + pub u8, cable_termination_type, set_cable_termination_type: 12, 11; + + /// See [`PassiveCableVdo::cable_latency`]. + pub u8, cable_latency, set_cable_latency: 16, 13; + + /// See [`PassiveCableVdo::epr_capable`]. + pub bool, epr_capable, set_epr_capable: 17; + + /// See [`PassiveCableVdo::usb_type_c_or_captive`]. + pub u8, usb_type_c_or_captive, set_usb_type_c_or_captive: 19, 18; + + /// See [`PassiveCableVdo::firmware_version`]. + pub u8, firmware_version, set_firmware_version: 27, 24; + + /// See [`PassiveCableVdo::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +/// The highest rate the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1. + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2. + Usb3p2, + + /// USB4 Gen3. + Usb4Gen3, + + /// USB4 Gen4. + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// Indicates whether the cable can carry 3A or 5A. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VbusCurrentHandlingCapability { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for VbusCurrentHandlingCapability { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b01 => Ok(Self::ThreeAmps), + 0b10 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +/// The maximum voltage that shall be negotiated using a Fixed Supply over the +/// cable as part of an Explicit Contract. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaximumVbusVoltage { + /// 20V + TwentyVolt, + + /// 30V + ThirtyVolt, + + /// 40V + FortyVolt, + + /// 50V + FiftyVolt, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyVolt), + 0b01 => Ok(Self::ThirtyVolt), + 0b10 => Ok(Self::FortyVolt), + 0b11 => Ok(Self::FiftyVolt), + _ => Err(()), + } + } +} + +/// Whether the cable needs `VCONN` only initially in order to support the Discover +/// Identity Command, after which it can be removed, or if it needs `VCONN` +/// to be continuously applied to power some feature of the Cable Plug. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableTerminationType { + /// The cable only requires `VCONN` to support the Discover Identity Command, + /// after which it can be removed. + VconnNotRequired, + + /// The cable requires `VCONN` to be continuously applied to power some feature + /// of the Cable Plug, in addition to needing `VCONN` to support the Discover + /// Identity Command. + VconnRequired, +} + +impl TryFrom for CableTerminationType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::VconnNotRequired), + 0b01 => Ok(Self::VconnRequired), + _ => Err(()), + } + } +} + +/// The signal latency through the cable, which can be used as an approximation +/// for its length. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableLatency { + /// <10ns (~1m) + LessThan10ns, + + /// 10ns to 20ns (~2m) + LessThan20ns, + + /// 20ns to 30ns (~3m) + LessThan30ns, + + /// 30ns to 40ns (~4m) + LessThan40ns, + + /// 40ns to 50ns (~5m) + LessThan50ns, + + /// 50ns to 60ns (~6m) + LessThan60ns, + + /// 60ns to 70ns (~7m) + LessThan70ns, + + /// >70ns (>~7m) + GreaterThan70ns, +} + +impl TryFrom for CableLatency { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0001 => Ok(Self::LessThan10ns), + 0b0010 => Ok(Self::LessThan20ns), + 0b0011 => Ok(Self::LessThan30ns), + 0b0100 => Ok(Self::LessThan40ns), + 0b0101 => Ok(Self::LessThan50ns), + 0b0110 => Ok(Self::LessThan60ns), + 0b0111 => Ok(Self::LessThan70ns), + 0b1000 => Ok(Self::GreaterThan70ns), + _ => Err(()), + } + } +} + +/// Whether or not the opposite end from the USB Type-C plug is another USB +/// Type-C plug or a Captive Cable Assembly. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbTypeCPlugOrCaptive { + /// Opposite end from the plug is another USB Type-C plug. + UsbTypeC, + + /// Opposite end from the plug is a Captive Cable Assembly. + Captive, +} + +impl TryFrom for UsbTypeCPlugOrCaptive { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::UsbTypeC), + 0b11 => Ok(Self::Captive), + _ => Err(()), + } + } +} diff --git a/src/vdm/structured/command/discover_identity/sop.rs b/src/vdm/structured/command/discover_identity/sop.rs new file mode 100644 index 0000000..d699f05 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop.rs @@ -0,0 +1,139 @@ +//! [`Response`] contains the response to a Discover Identity Command targetting SOP. + +use super::{CertStatVdo, ConnectorType, ProductVdo}; +use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; + +/// The response to a Discover Identity Command using SOP. +/// +/// Each response contains up to 7 VDOs. +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Response { + /// The header for this Structured VDM Message. + pub header: Header, + + /// Information corresponding to the Product. + pub id: IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: Option, + + /// Identity information relating to the product. + pub product: Option, + + /// The Product-specific VDOs. + /// + /// The types of these VDOs are determined by fields in the [`Self::id`] field. + pub product_type_vdos: [ProductTypeVdo; 3], +} + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates the type of Product when in DFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type_dfp: ProductTypeDfp, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when in UFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type_ufp: ProductTypeUfp, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +/// The [`IdHeaderVdo::product_type_dfp`] field indicates the type of Product when +/// in DFP Data Role, whether a VDO will be returned, and if so, the type of VDO +/// to be returned. +/// +/// See PD spec 6.4.4.3.1.1.6 Product Type (DFP), table 6.36 Product Types (DFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeDfp { + /// This is not a DFP. + /// + /// [`Response::product_type_vdos`] is empty. + NotADfp, + + /// The product is a PDUSB Hub. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Hub, + + /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate + /// Modes as an AMC. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Host, + + /// The product is a charger / power brick. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Charger, +} + +/// The [`IdHeaderVdo::product_type_ufp`] field indicates the type of Product when +/// in the UFP Data Role, whether a VDO will be returned, and if so, the type of +/// VDO to be returned. +/// +/// See PD spec 6.4.4.3.1.1.3 Product Type (UFP), table 6.34 Product Types (UFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeUfp { + /// This is not a UFP. + /// + /// [`Response::product_type_vdos`] is empty. + NotAUfp, + + /// The product is a PDUSB Hub. + /// + /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + Hub, + + /// The product is a PDUSB Device other than a Hub. + /// + /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + Peripheral, + + /// The product is a PSD, e.g., power bank. + /// + /// [`Response::product_type_vdos`] is empty. + Psd, +} diff --git a/src/vdm/structured/command/discover_identity/sop_prime.rs b/src/vdm/structured/command/discover_identity/sop_prime.rs new file mode 100644 index 0000000..7c1c71b --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop_prime.rs @@ -0,0 +1,91 @@ +//! [`Response`] contains the response to a Discover Identity Command targetting SOP'. + +use super::{CertStatVdo, ConnectorType, ProductVdo}; +use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; + +/// The response to a Discover Identity Command using `SOP'`. +/// +/// Each response contains up to 7 VDOs. The first three +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Response { + /// The header for this Structured VDM Message. + pub header: Header, + + /// Information corresponding to the Product. + pub id: IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: Option, + + /// Identity information relating to the product. + pub product: Option, + + /// The Product-specific VDOs. + /// + /// The types of these VDOs are determined by fields in the [`Self::id`] field. + pub product_type_vdos: [ProductTypeVdo; 3], +} + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when the Product is a Cable Plug or VPD, whether + /// a VDO will be returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type: ProductType, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +/// The `SOP'` Product Type (Cable Plug/VPD) field indicates the type of Product +/// when the Product is a Cable Plug or VPD, whether a VDO will be returned, and +/// if so, the type of VDO to be returned. +/// +/// The type changes how [`Response::product_type_vdos`] is interpreted. +/// +/// See PD spec 6.4.4.3.1.1.4 Product Type (Cable Plug), table 6.35 Product Types (Cable Plug/VPD). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductType { + /// No other Product Type is appropriate. + /// + /// [`Response::product_type_vdos`] is empty. + NotACablePlugVpd, + + /// The Product is a cable that does not incorporate signal conditioning circuits. + /// + /// The first item in [`Response::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. + PassiveCable, + + /// The Product is a cable that incorporates signal conditioning circuits. + /// + /// The first item in [`Response::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. + /// The second item is a [`ActiveCableVdo2`][`super::ActiveCableVdo2`]. + ActiveCable, + + /// The Product is a `VCONN`-powered USB device. + /// + /// The first item in [`Response::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. + Vpd, +} diff --git a/src/vdm/structured/command/discover_identity/ufp_vdo.rs b/src/vdm/structured/command/discover_identity/ufp_vdo.rs new file mode 100644 index 0000000..f618cff --- /dev/null +++ b/src/vdm/structured/command/discover_identity/ufp_vdo.rs @@ -0,0 +1,248 @@ +//! A UFP (Upward Facing Port) is a Port that consumes power and/or data from a +//! DFP. UFPs include traditional USB peripherals, USB Hub's upstream Port, and +//! DRD-capable host Ports. +//! +//! See PD spec 6.4.4.3.1.4 UFP VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// Returned by Ports capable of operating as a UFP, including traditional USB peripherals, +/// USB Hub's upstream Port, and DRD-capable host Ports. +/// +/// Sent based on the value of [`sop::IdHeaderVdo::product_type_ufp`][super::sop::IdHeaderVdo::product_type_ufp]. +/// +/// See PD spec 6.4.4.3.1.4 UFP VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct UfpVdo { + /// The port's highest speed capability. + pub usb_highest_speed: UsbHighestSpeed, + + /// All types of Alternate Modes, if any, the device supports. + pub alternate_modes: AlternateModes, + pub vbus_required: bool, + pub vconn_required: bool, + + /// Whether `VCONN` is needed for the AMA to operate. + pub vconn_power: VconnPower, + + /// The UFP's Capabilities when operating as either a PDUSB Device or Hub. + pub device_capability: DeviceCapability, +} + +bitfield::bitfield! { + /// The raw value of a [`UfpVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`UfpVdo::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`UfpVdo::alternate_modes`]. + pub u8, alternate_modes, set_alternate_modes: 5, 3; + + /// See [`UfpVdo::vbus_required`]. + pub bool, vbus_required_n, set_vbus_required_n: 6; + + /// See [`UfpVdo::vconn_required`]. + pub bool, vconn_required, set_vconn_required: 7; + + /// See [`UfpVdo::vconn_power`]. + pub u8, vconn_power, set_vconn_power: 10, 8; + + /// See [`UfpVdo::device_capability`]. + pub u8, device_capability, set_device_capability: 27, 24; +} + +/// Errors that can occur when parsing a [`UfpVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseUfpVdoError { + /// [`UfpVdo::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`UfpVdo::vconn_power`] contains an invalid value. + InvalidVconnPower, + + /// [`UfpVdo::device_capability`] contains an invalid value. + InvalidDeviceCapability, +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParseUfpVdoError::InvalidUsbHighestSpeed)?, + alternate_modes: raw.alternate_modes().into(), + vbus_required: !raw.vbus_required_n(), + vconn_required: raw.vconn_required(), + vconn_power: raw + .vconn_power() + .try_into() + .map_err(|()| ParseUfpVdoError::InvalidVconnPower)?, + device_capability: raw.device_capability().into(), + }) + } +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// The port's highest speed capability. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1. + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2. + Usb3p2Gen2, + + /// USB4 Gen3. + Usb4Gen3, + + /// USB4 Gen4. + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2Gen2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// All types of Alternate Modes, if any, the device supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AlternateModes { + /// Supports Thunderbolt 3 Alternate Mode. + pub tbt3: bool, + + /// Supports Alternate Modes that reconfigure the signals on the USB Type-C + /// 2.4 connector, except for Thunderbolt 3. + pub reconfigures_type_c: bool, + + /// Supports Alternate Modes that do not reconfigure the signals on the + /// USB Type-C 2.4 connector. + pub does_not_reconfigure_type_c: bool, +} + +impl From for AlternateModes { + fn from(value: u8) -> Self { + Self { + tbt3: value & 0b001 != 0, + reconfigures_type_c: value & 0b010 != 0, + does_not_reconfigure_type_c: value & 0b100 != 0, + } + } +} + +/// Whether `VCONN` is needed for the AMA to operate. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VconnPower { + /// 1W + OneW, + + /// 1.5W + OnePointFiveW, + + /// 2W + TwoW, + + /// 3W + ThreeW, + + /// 4W + FourW, + + /// 5W + FiveW, + + /// 6W + SixW, +} + +impl TryFrom for VconnPower { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::OneW), + 0b001 => Ok(Self::OnePointFiveW), + 0b010 => Ok(Self::TwoW), + 0b011 => Ok(Self::ThreeW), + 0b100 => Ok(Self::FourW), + 0b101 => Ok(Self::FiveW), + 0b110 => Ok(Self::SixW), + _ => Err(()), + } + } +} + +/// The UFP's Capabilities when operating as either a PDUSB Device or Hub. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DeviceCapability { + /// USB 2.0 Device Capable + pub usb2p0: bool, + + /// USB 2.0 Device Capable (Billboard only) + pub usb2p0_billboard_only: bool, + + /// USB 3.2 Device Capable + pub usb3p2: bool, + + /// USB4 Device Capable + pub usb4: bool, +} + +impl From for DeviceCapability { + fn from(value: u8) -> Self { + Self { + usb2p0: value & 0b0001 != 0, + usb2p0_billboard_only: value & 0b0010 != 0, + usb3p2: value & 0b0100 != 0, + usb4: value & 0b1000 != 0, + } + } +} diff --git a/src/vdm/structured/command/discover_identity/vpd_vdo.rs b/src/vdm/structured/command/discover_identity/vpd_vdo.rs new file mode 100644 index 0000000..1d01042 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/vpd_vdo.rs @@ -0,0 +1,176 @@ +//! VCONN-Powered USB Devices (VPDs) are Ports that consume power from a DFP. +//! +//! See PD spec 6.4.4.3.1.9 VCONN Powered USB Device VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// A `VCONN`-Powered USB Device (VPD). +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.9 VCONN Powered USB Device VDO, table 6.44 VPD VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct VpdVdo { + /// Whether the VPD supports Charge Through. + pub charge_through_support: bool, + + /// The impedance through ground the VPD adds in series between the Source and Sink. + pub ground_impedance: GroundImpedance, + + /// The impedance through `VBUS` the VPD adds in series between the Source and Sink. + pub vbus_impedance: VbusImpedance, + pub charge_through_current_support: ChargeThroughCurrentSupport, + + /// The maximum voltage that a Sink shall negotiate through the VPD Charge Through + /// Port as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// The FW version assigned by the VID owner. + pub fw_version: u8, + + /// The HW version assigned by the VID owner. + pub hw_version: u8, +} + +/// Errors that can occur when parsing a [`VpdVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseVpdVdoError { + /// [`VpdVdo::charge_through_current_support`] contains an invalid value. + InvalidChargeThroughCurrentSupport, + + /// [`VpdVdo::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + charge_through_support: raw.charge_through_support(), + ground_impedance: GroundImpedance(raw.ground_impedance()), + vbus_impedance: VbusImpedance(raw.vbus_impedance()), + charge_through_current_support: raw + .charge_through_current_support() + .try_into() + .map_err(|_| ParseVpdVdoError::InvalidChargeThroughCurrentSupport)?, + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|_| ParseVpdVdoError::InvalidMaximumVbusVoltage)?, + fw_version: raw.fw_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +bitfield::bitfield! { + /// The raw value of a [`VpdVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`VpdVdo::charge_through_support`]. + pub bool, charge_through_support, set_charge_through_support: 0; + + /// See [`VpdVdo::ground_impedance`]. + pub u8, ground_impedance, set_ground_impedance: 6, 1; + + /// See [`VpdVdo::vbus_impedance`]. + pub u8, vbus_impedance, set_vbus_impedance: 12, 7; + + /// See [`VpdVdo::charge_through_current_support`]. + pub u8, charge_through_current_support, set_charge_through_current_support: 14, 14; + + /// See [`VpdVdo::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 16, 15; + + /// See [`VpdVdo::fw_version`]. + pub u8, fw_version, set_fw_version: 27, 24; + + /// See [`VpdVdo::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Ground impedance through the VPD in 2mΩ increments. +pub struct GroundImpedance(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// `VBUS` impedance through the VPD in 2mΩ increments. +pub struct VbusImpedance(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ChargeThroughCurrentSupport { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for ChargeThroughCurrentSupport { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0 => Ok(Self::ThreeAmps), + 0b1 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// The maximum voltage that a Sink shall negotiate through the VPD Charge Through +/// Port as part of an Explicit Contract. +pub enum MaximumVbusVoltage { + /// 20V + TwentyVolts, + + /// 30V + ThirtyVolts, + + /// 40V + FortyVolts, + + /// 50V + FiftyVolts, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyVolts), + 0b01 => Ok(Self::ThirtyVolts), + 0b10 => Ok(Self::FortyVolts), + 0b11 => Ok(Self::FiftyVolts), + _ => Err(()), + } + } +} diff --git a/src/vdm/structured/command/mod.rs b/src/vdm/structured/command/mod.rs new file mode 100644 index 0000000..f06f848 --- /dev/null +++ b/src/vdm/structured/command/mod.rs @@ -0,0 +1,5 @@ +//! The VDM Header for a Structured VDM Message defines Commands used to retrieve +//! information from the device. +//! +//! See PD spec 6.4.4.3 Use of Commands. +pub mod discover_identity; diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs new file mode 100644 index 0000000..eb1e663 --- /dev/null +++ b/src/vdm/structured/header.rs @@ -0,0 +1,125 @@ +//! [`Header]` defines the VDM Header for a Structured VDM Message. + +use crate::vdm::structured::Svid; + +/// The VDM Header for a Structured VDM Message. +/// +/// See PD spec 6.4.4.2 Structured VDM, table 6.29 Structured VDM Header. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Header { + pub command: Command, + pub command_type: CommandType, + pub object_position: ObjectPosition, + pub structured_vdm_version: StructuredVdmVersion, + pub svid: Svid, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Command { + DiscoverIdentity, + DiscoverSvids, + DiscoverModes, + EnterMode, + ExitMode, + Attention, + + /// SVID-specific Commands as defined by the vendor in [`Header::svid`]. + SvidSpecific(u8), +} + +impl TryFrom for Command { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::DiscoverIdentity), + 2 => Ok(Self::DiscoverSvids), + 3 => Ok(Self::DiscoverModes), + 4 => Ok(Self::EnterMode), + 5 => Ok(Self::ExitMode), + 6 => Ok(Self::Attention), + cmd if cmd >= 16 => Ok(Self::SvidSpecific(cmd)), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CommandType { + Request, + Ack, + Nak, + Busy, +} + +impl TryFrom for CommandType { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Request), + 1 => Ok(Self::Ack), + 2 => Ok(Self::Nak), + 3 => Ok(Self::Busy), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ObjectPosition(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Version number of the Structured VDM (not the specification). +pub struct StructuredVdmVersion(pub u8); + +bitfield::bitfield! { + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + pub u8, command, set_command: 4, 0; + pub u8, command_type, set_command_type: 7, 6; + pub u8, object_position, set_object_position: 10, 8; + pub u8, structured_vdm_version, set_structured_vdm_version: 14, 11; + pub u16, svid, set_svid: 30, 16; +} + +pub enum ParseError { + InvalidCommand, + InvalidCommandType, +} + +impl TryFrom for Header { + type Error = ParseError; + fn try_from(raw: Raw) -> Result { + Ok(Self { + command: raw.command().try_into().map_err(|()| ParseError::InvalidCommand)?, + command_type: raw + .command_type() + .try_into() + .map_err(|()| ParseError::InvalidCommandType)?, + object_position: ObjectPosition(raw.object_position()), + structured_vdm_version: StructuredVdmVersion(raw.structured_vdm_version()), + svid: Svid(raw.svid()), + }) + } +} + +impl TryFrom for Header { + type Error = ParseError; + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for Header { + type Error = ParseError; + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} diff --git a/src/vdm/structured/mod.rs b/src/vdm/structured/mod.rs new file mode 100644 index 0000000..082aabf --- /dev/null +++ b/src/vdm/structured/mod.rs @@ -0,0 +1,10 @@ +//! Structured VDMs define the contents of bits `[0, 14]` in the VDM Header. +//! +//! See PD spec 6.4.4.2 Structured VDM. + +pub mod command; +mod header; +mod svid; + +pub use header::Header; +pub use svid::Svid; diff --git a/src/vdm/structured/svid.rs b/src/vdm/structured/svid.rs new file mode 100644 index 0000000..4dbc545 --- /dev/null +++ b/src/vdm/structured/svid.rs @@ -0,0 +1,18 @@ +/// The Standard or Vendor ID (SVID) field Shall contain either a 16-bit USB Standard +/// ID value (SID) or the 16-bit Vendor ID (VID) assigned to the vendor by the USB-IF. +/// +/// See PD spec 6.4.4.2.1 SVID. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Svid(pub u16); + +impl Svid { + /// The Standard ID allocated to the PD specification by USB-IF. + pub const PD: Self = Self(0xFF00); + + /// The Standard ID allocated to DisplayPort Type-C (DPTC) by USB-IF. + pub const DISPLAY_PORT_TYPE_C: Self = Self(0xFF01); + + /// The Vendor ID assigned to Thunderbolt by USB-IF. + pub const THUNDERBOLT: Self = Self(0x8087); +} diff --git a/src/vdm/svid.rs b/src/vdm/svid.rs deleted file mode 100644 index f0fb23e..0000000 --- a/src/vdm/svid.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Standard or Vendor ID (SVID) newtype, see PD spec 6.4.4.2.1 -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Svid(pub u16); - -impl Svid { - pub const PD: Self = Self(0xFF00); - pub const DISPLAY_PORT_TYPE_C: Self = Self(0xFF01); - pub const THUNDERBOLT: Self = Self(0x8087); -} From acd080ece6e587100728a64ca92d2ed0eda75df1 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Wed, 8 Apr 2026 17:31:47 -0700 Subject: [PATCH 04/26] Add tests for enum parsing --- .../discover_identity/active_cable_vdo.rs | 343 ++++++++++++++++++ .../discover_identity/passive_cable_vdo.rs | 152 ++++++++ .../command/discover_identity/ufp_vdo.rs | 60 +++ .../command/discover_identity/vpd_vdo.rs | 54 +++ src/vdm/structured/header.rs | 65 ++++ 5 files changed, 674 insertions(+) diff --git a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs index 2a72194..d9c4bdc 100644 --- a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs +++ b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs @@ -781,3 +781,346 @@ impl TryFrom for U3CldPower { } } } + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!( + UsbHighestSpeed::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod vbus_current_handling_capability { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VbusCurrentHandlingCapability); 2] = [ + (0b01, VbusCurrentHandlingCapability::ThreeAmps), + (0b10, VbusCurrentHandlingCapability::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!( + VbusCurrentHandlingCapability::try_from(raw), + Ok(expected), + "raw={raw}" + ); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 3] { + assert!( + VbusCurrentHandlingCapability::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod sbu_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, SbuType); 2] = [(0, SbuType::Passive), (1, SbuType::Active)]; + for (raw, expected) in cases { + assert_eq!(SbuType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(SbuType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyV), + (0b01, MaximumVbusVoltage::ThirtyV), + (0b10, MaximumVbusVoltage::FortyV), + (0b11, MaximumVbusVoltage::FiftyV), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!( + MaximumVbusVoltage::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod cable_termination_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableTerminationType); 2] = [ + (0b10, CableTerminationType::OneEndActive), + (0b11, CableTerminationType::BothEndsActive), + ]; + for (raw, expected) in cases { + assert_eq!( + CableTerminationType::try_from(raw), + Ok(expected), + "raw={raw}" + ); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!( + CableTerminationType::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod cable_latency { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableLatency); 10] = [ + (0b0001, CableLatency::LessThan10ns), + (0b0010, CableLatency::LessThan20ns), + (0b0011, CableLatency::LessThan30ns), + (0b0100, CableLatency::LessThan40ns), + (0b0101, CableLatency::LessThan50ns), + (0b0110, CableLatency::LessThan60ns), + (0b0111, CableLatency::LessThan70ns), + (0b1000, CableLatency::LessThan1000ns), + (0b1001, CableLatency::LessThan2000ns), + (0b1010, CableLatency::LessThan3000ns), + ]; + for (raw, expected) in cases { + assert_eq!(CableLatency::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + assert!(CableLatency::try_from(0u8).is_err()); + for v in 11..=255u8 { + assert!( + CableLatency::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod usb_type_c_or_captive { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbTypeCOrCaptive); 2] = [ + (0b10, UsbTypeCOrCaptive::UsbTypeC), + (0b11, UsbTypeCOrCaptive::Captive), + ]; + for (raw, expected) in cases { + assert_eq!(UsbTypeCOrCaptive::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!( + UsbTypeCOrCaptive::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod usb_gen { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbGen); 2] = [(0, UsbGen::Gen1), (1, UsbGen::Gen2OrHigher)]; + for (raw, expected) in cases { + assert_eq!(UsbGen::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(UsbGen::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_lanes_supported { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbLanesSupported); 2] = + [(0, UsbLanesSupported::OneLane), (1, UsbLanesSupported::TwoLanes)]; + for (raw, expected) in cases { + assert_eq!(UsbLanesSupported::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + UsbLanesSupported::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod active_element { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ActiveElement); 2] = + [(0, ActiveElement::Redriver), (1, ActiveElement::Retimer)]; + for (raw, expected) in cases { + assert_eq!(ActiveElement::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + ActiveElement::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod physical_connection { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, PhysicalConnection); 2] = + [(0, PhysicalConnection::Copper), (1, PhysicalConnection::Optical)]; + for (raw, expected) in cases { + assert_eq!(PhysicalConnection::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + PhysicalConnection::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod u3_to_u0_transition_mode { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, U3ToU0TransitionMode); 2] = [ + (0, U3ToU0TransitionMode::Direct), + (1, U3ToU0TransitionMode::ThroughU3S), + ]; + for (raw, expected) in cases { + assert_eq!( + U3ToU0TransitionMode::try_from(raw), + Ok(expected), + "raw={raw}" + ); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + U3ToU0TransitionMode::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod u3_cld_power { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, U3CldPower); 7] = [ + (0b000, U3CldPower::GreaterThan10Milliwatts), + (0b001, U3CldPower::FiveToTenMilliwatts), + (0b010, U3CldPower::OneToFiveMilliwatts), + (0b011, U3CldPower::P5To1Milliwatt), + (0b100, U3CldPower::P2ToP5Milliwatt), + (0b101, U3CldPower::FiftyToTwoHundredMicrowatts), + (0b110, U3CldPower::LessThanFiftyMicrowatts), + ]; + for (raw, expected) in cases { + assert_eq!(U3CldPower::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 7..=255u8 { + assert!( + U3CldPower::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs index 8664308..15b07de 100644 --- a/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs +++ b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs @@ -355,3 +355,155 @@ impl TryFrom for UsbTypeCPlugOrCaptive { } } } + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod vbus_current_handling_capability { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VbusCurrentHandlingCapability); 2] = [ + (0b01, VbusCurrentHandlingCapability::ThreeAmps), + (0b10, VbusCurrentHandlingCapability::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!(VbusCurrentHandlingCapability::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 3] { + assert!( + VbusCurrentHandlingCapability::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyVolt), + (0b01, MaximumVbusVoltage::ThirtyVolt), + (0b10, MaximumVbusVoltage::FortyVolt), + (0b11, MaximumVbusVoltage::FiftyVolt), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_termination_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableTerminationType); 2] = [ + (0b00, CableTerminationType::VconnNotRequired), + (0b01, CableTerminationType::VconnRequired), + ]; + for (raw, expected) in cases { + assert_eq!(CableTerminationType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(CableTerminationType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_latency { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableLatency); 8] = [ + (0b0001, CableLatency::LessThan10ns), + (0b0010, CableLatency::LessThan20ns), + (0b0011, CableLatency::LessThan30ns), + (0b0100, CableLatency::LessThan40ns), + (0b0101, CableLatency::LessThan50ns), + (0b0110, CableLatency::LessThan60ns), + (0b0111, CableLatency::LessThan70ns), + (0b1000, CableLatency::GreaterThan70ns), + ]; + for (raw, expected) in cases { + assert_eq!(CableLatency::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + assert!(CableLatency::try_from(0u8).is_err()); + for v in 9..=255u8 { + assert!(CableLatency::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_type_c_plug_or_captive { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbTypeCPlugOrCaptive); 2] = [ + (0b10, UsbTypeCPlugOrCaptive::UsbTypeC), + (0b11, UsbTypeCPlugOrCaptive::Captive), + ]; + for (raw, expected) in cases { + assert_eq!(UsbTypeCPlugOrCaptive::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!(UsbTypeCPlugOrCaptive::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/ufp_vdo.rs b/src/vdm/structured/command/discover_identity/ufp_vdo.rs index f618cff..20be4ac 100644 --- a/src/vdm/structured/command/discover_identity/ufp_vdo.rs +++ b/src/vdm/structured/command/discover_identity/ufp_vdo.rs @@ -246,3 +246,63 @@ impl From for DeviceCapability { } } } + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2Gen2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!( + UsbHighestSpeed::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod vconn_power { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VconnPower); 7] = [ + (0b000, VconnPower::OneW), + (0b001, VconnPower::OnePointFiveW), + (0b010, VconnPower::TwoW), + (0b011, VconnPower::ThreeW), + (0b100, VconnPower::FourW), + (0b101, VconnPower::FiveW), + (0b110, VconnPower::SixW), + ]; + for (raw, expected) in cases { + assert_eq!(VconnPower::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 7..=255u8 { + assert!(VconnPower::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/vpd_vdo.rs b/src/vdm/structured/command/discover_identity/vpd_vdo.rs index 1d01042..e07fa83 100644 --- a/src/vdm/structured/command/discover_identity/vpd_vdo.rs +++ b/src/vdm/structured/command/discover_identity/vpd_vdo.rs @@ -174,3 +174,57 @@ impl TryFrom for MaximumVbusVoltage { } } } + +#[cfg(test)] +mod tests { + use super::*; + + mod charge_through_current_support { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ChargeThroughCurrentSupport); 2] = [ + (0, ChargeThroughCurrentSupport::ThreeAmps), + (1, ChargeThroughCurrentSupport::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!(ChargeThroughCurrentSupport::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + ChargeThroughCurrentSupport::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyVolts), + (0b01, MaximumVbusVoltage::ThirtyVolts), + (0b10, MaximumVbusVoltage::FortyVolts), + (0b11, MaximumVbusVoltage::FiftyVolts), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index eb1e663..5482f5f 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -123,3 +123,68 @@ impl TryFrom<[u8; 4]> for Header { u32::from_le_bytes(bytes).try_into() } } + +#[cfg(test)] +mod tests { + use super::*; + + mod command { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, Command); 7] = [ + (1, Command::DiscoverIdentity), + (2, Command::DiscoverSvids), + (3, Command::DiscoverModes), + (4, Command::EnterMode), + (5, Command::ExitMode), + (6, Command::Attention), + (16, Command::SvidSpecific(16)), + ]; + for (raw, expected) in cases { + assert_eq!(Command::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn svid_specific_upper_range() { + for v in [16u8, 17, 31, 127, 255] { + assert_eq!(Command::try_from(v), Ok(Command::SvidSpecific(v))); + } + } + + #[test] + fn invalid_values() { + // 0 and 7..=15 are invalid + assert!(Command::try_from(0u8).is_err()); + for v in 7..=15u8 { + assert!(Command::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod command_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CommandType); 4] = [ + (0, CommandType::Request), + (1, CommandType::Ack), + (2, CommandType::Nak), + (3, CommandType::Busy), + ]; + for (raw, expected) in cases { + assert_eq!(CommandType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(CommandType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} From e8ba3ed0892867e3f39b68b8420e805a20b5af74 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 10:33:47 -0700 Subject: [PATCH 05/26] Make header module public --- src/vdm/structured/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vdm/structured/mod.rs b/src/vdm/structured/mod.rs index 082aabf..42e06a1 100644 --- a/src/vdm/structured/mod.rs +++ b/src/vdm/structured/mod.rs @@ -3,7 +3,7 @@ //! See PD spec 6.4.4.2 Structured VDM. pub mod command; -mod header; +pub mod header; mod svid; pub use header::Header; From d7409fdd3d62526c74964869e744d311e853e843 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 10:41:44 -0700 Subject: [PATCH 06/26] Make CommandType deserialization infallible --- src/vdm/structured/header.rs | 51 +++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index 5482f5f..c330d4e 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -48,21 +48,21 @@ impl TryFrom for Command { #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum CommandType { - Request, - Ack, - Nak, - Busy, + Request = 0b00, + Ack = 0b01, + Nak = 0b10, + Busy = 0b11, } -impl TryFrom for CommandType { - type Error = (); - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Request), - 1 => Ok(Self::Ack), - 2 => Ok(Self::Nak), - 3 => Ok(Self::Busy), - _ => Err(()), +impl From for CommandType { + fn from(value: u8) -> Self { + match value & 0b11 { + 0b00 => Self::Request, + 0b01 => Self::Ack, + 0b10 => Self::Nak, + // technically >0b11 is unreachable since we masked with 0b11, but we'll + // treat any cosmic rays as a Busy response to make this From, not TryFrom + 0b11 | _ => Self::Busy, } } } @@ -91,7 +91,6 @@ bitfield::bitfield! { pub enum ParseError { InvalidCommand, - InvalidCommandType, } impl TryFrom for Header { @@ -99,10 +98,7 @@ impl TryFrom for Header { fn try_from(raw: Raw) -> Result { Ok(Self { command: raw.command().try_into().map_err(|()| ParseError::InvalidCommand)?, - command_type: raw - .command_type() - .try_into() - .map_err(|()| ParseError::InvalidCommandType)?, + command_type: raw.command_type().into(), object_position: ObjectPosition(raw.object_position()), structured_vdm_version: StructuredVdmVersion(raw.structured_vdm_version()), svid: Svid(raw.svid()), @@ -176,14 +172,27 @@ mod tests { (3, CommandType::Busy), ]; for (raw, expected) in cases { - assert_eq!(CommandType::try_from(raw), Ok(expected), "raw={raw}"); + assert_eq!(CommandType::from(raw), expected, "raw={raw}"); } } #[test] fn invalid_values() { - for v in 4..=255u8 { - assert!(CommandType::try_from(v).is_err(), "raw={v} should be invalid"); + // since the value is masked with 0b11, all values >0b11 are technically invalid but actually unreachable + // assert that we loop in order rather than panic + let expected = [ + CommandType::Request, + CommandType::Ack, + CommandType::Nak, + CommandType::Busy, + ]; + + for (raw, expected) in (4..=255u8).zip(expected.iter().cycle().copied()) { + assert_eq!( + CommandType::from(raw), + expected, + "raw={raw} should parse as {expected:?}" + ); } } } From ca56e4e448daddf71762ec613366ba2f622e8554 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 10:55:26 -0700 Subject: [PATCH 07/26] Add sop::IdHeaderVdo parsing --- .../command/discover_identity/mod.rs | 12 + .../command/discover_identity/sop.rs | 139 ---------- .../discover_identity/sop/id_header_vdo.rs | 256 ++++++++++++++++++ .../command/discover_identity/sop/mod.rs | 36 +++ 4 files changed, 304 insertions(+), 139 deletions(-) delete mode 100644 src/vdm/structured/command/discover_identity/sop.rs create mode 100644 src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/sop/mod.rs diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs index 85c6526..5d48477 100644 --- a/src/vdm/structured/command/discover_identity/mod.rs +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -29,6 +29,18 @@ pub enum ConnectorType { Plug, } +impl TryFrom for ConnectorType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::Receptacle), + 0b11 => Ok(Self::Plug), + _ => Err(()), + } + } +} + /// Contains the XID assigned by USB-IF to the product before certification in binary format. /// /// See PD spec 6.4.4.3.1.2 Cert Stat VDO, table 6.37 Cert Stat VDO. diff --git a/src/vdm/structured/command/discover_identity/sop.rs b/src/vdm/structured/command/discover_identity/sop.rs deleted file mode 100644 index d699f05..0000000 --- a/src/vdm/structured/command/discover_identity/sop.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! [`Response`] contains the response to a Discover Identity Command targetting SOP. - -use super::{CertStatVdo, ConnectorType, ProductVdo}; -use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; - -/// The response to a Discover Identity Command using SOP. -/// -/// Each response contains up to 7 VDOs. -/// -/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - /// The header for this Structured VDM Message. - pub header: Header, - - /// Information corresponding to the Product. - pub id: IdHeaderVdo, - - /// The XID assigned by the USB-IF to the product. - pub cert_stat: Option, - - /// Identity information relating to the product. - pub product: Option, - - /// The Product-specific VDOs. - /// - /// The types of these VDOs are determined by fields in the [`Self::id`] field. - pub product_type_vdos: [ProductTypeVdo; 3], -} - -/// The ID Header VDO contains information corresponding to the Power Delivery Product. -/// -/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct IdHeaderVdo { - /// The USB Vendor ID as assigned by the USB-IF. - pub usb_vendor_id: u16, - - /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. - pub connector_type: ConnectorType, - - /// Indicates the type of Product when in DFP Data Role, whether a VDO will be - /// returned, and if so, the type of VDO to be returned. - /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. - pub product_type_dfp: ProductTypeDfp, - - /// Indicates whether or not the Product (either a Cable Plug or a device that - /// can operate in the UFP role) is capable of supporting Modes. - pub modal_operation_supported: bool, - - /// Indicates the type of Product when in UFP Data Role, whether a VDO will be - /// returned, and if so, the type of VDO to be returned. - /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. - pub product_type_ufp: ProductTypeUfp, - - /// Whether or not the Port has a USB Device Capability. - pub usb_communication_capable_as_usb_device: bool, - - /// Whether or not the Port has a USB Host Capability. - pub usb_communication_capable_as_usb_host: bool, -} - -/// The [`IdHeaderVdo::product_type_dfp`] field indicates the type of Product when -/// in DFP Data Role, whether a VDO will be returned, and if so, the type of VDO -/// to be returned. -/// -/// See PD spec 6.4.4.3.1.1.6 Product Type (DFP), table 6.36 Product Types (DFP). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ProductTypeDfp { - /// This is not a DFP. - /// - /// [`Response::product_type_vdos`] is empty. - NotADfp, - - /// The product is a PDUSB Hub. - /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. - /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), - /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. - Hub, - - /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate - /// Modes as an AMC. - /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. - /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), - /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. - Host, - - /// The product is a charger / power brick. - /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. - /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), - /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. - Charger, -} - -/// The [`IdHeaderVdo::product_type_ufp`] field indicates the type of Product when -/// in the UFP Data Role, whether a VDO will be returned, and if so, the type of -/// VDO to be returned. -/// -/// See PD spec 6.4.4.3.1.1.3 Product Type (UFP), table 6.34 Product Types (UFP). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ProductTypeUfp { - /// This is not a UFP. - /// - /// [`Response::product_type_vdos`] is empty. - NotAUfp, - - /// The product is a PDUSB Hub. - /// - /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. - Hub, - - /// The product is a PDUSB Device other than a Hub. - /// - /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. - Peripheral, - - /// The product is a PSD, e.g., power bank. - /// - /// [`Response::product_type_vdos`] is empty. - Psd, -} diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs new file mode 100644 index 0000000..67208e9 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -0,0 +1,256 @@ +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates the type of Product when in DFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type_dfp: ProductTypeDfp, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when in UFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type_ufp: ProductTypeUfp, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +bitfield::bitfield! { + /// The raw value of an [`IdHeaderVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + pub u8, connector_type, set_connector_type: 22, 21; + pub u8, product_type_dfp, set_product_type_dfp: 25, 23; + pub bool, modal_operation_supported, set_modal_operation_supported: 26; + pub u8, product_type_ufp, set_product_type_ufp: 29, 27; + pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; +} + +/// Errors that can occur when parsing an [`IdHeaderVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseIdHeaderVdoError { + InvalidConnectorType, + InvalidProductTypeDfp, + InvalidProductTypeUfp, +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_vendor_id: raw.usb_vendor_id(), + connector_type: raw + .connector_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidConnectorType)?, + product_type_dfp: raw + .product_type_dfp() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductTypeDfp)?, + modal_operation_supported: raw.modal_operation_supported(), + product_type_ufp: raw + .product_type_ufp() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductTypeUfp)?, + usb_communication_capable_as_usb_device: raw.usb_communication_capable_as_usb_device(), + usb_communication_capable_as_usb_host: raw.usb_communication_capable_as_usb_host(), + }) + } +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// The [`IdHeaderVdo::product_type_dfp`] field indicates the type of Product when +/// in DFP Data Role, whether a VDO will be returned, and if so, the type of VDO +/// to be returned. +/// +/// See PD spec 6.4.4.3.1.1.6 Product Type (DFP), table 6.36 Product Types (DFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeDfp { + /// This is not a DFP. + /// + /// [`Response::product_type_vdos`] is empty. + NotADfp, + + /// The product is a PDUSB Hub. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Hub, + + /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate + /// Modes as an AMC. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Host, + + /// The product is a charger / power brick. + /// + /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Charger, +} + +impl TryFrom for ProductTypeDfp { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotADfp), + 0b001 => Ok(Self::Hub), + 0b010 => Ok(Self::Host), + 0b011 => Ok(Self::Charger), + _ => Err(()), + } + } +} + +/// The [`IdHeaderVdo::product_type_ufp`] field indicates the type of Product when +/// in the UFP Data Role, whether a VDO will be returned, and if so, the type of +/// VDO to be returned. +/// +/// See PD spec 6.4.4.3.1.1.3 Product Type (UFP), table 6.34 Product Types (UFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeUfp { + /// This is not a UFP. + /// + /// [`Response::product_type_vdos`] is empty. + NotAUfp, + + /// The product is a PDUSB Hub. + /// + /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + Hub, + + /// The product is a PDUSB Device other than a Hub. + /// + /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + Peripheral, + + /// The product is a PSD, e.g., power bank. + /// + /// [`Response::product_type_vdos`] is empty. + Psd, +} + +impl TryFrom for ProductTypeUfp { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotAUfp), + 0b001 => Ok(Self::Hub), + 0b010 => Ok(Self::Peripheral), + 0b011 => Ok(Self::Psd), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod product_type_dfp { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductTypeDfp); 4] = [ + (0b000, ProductTypeDfp::NotADfp), + (0b001, ProductTypeDfp::Hub), + (0b010, ProductTypeDfp::Host), + (0b011, ProductTypeDfp::Charger), + ]; + for (raw, expected) in cases { + assert_eq!(ProductTypeDfp::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b100, 0b101, 0b110, 0b111] { + assert!(ProductTypeDfp::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod product_type_ufp { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductTypeUfp); 4] = [ + (0b000, ProductTypeUfp::NotAUfp), + (0b001, ProductTypeUfp::Hub), + (0b010, ProductTypeUfp::Peripheral), + (0b011, ProductTypeUfp::Psd), + ]; + for (raw, expected) in cases { + assert_eq!(ProductTypeUfp::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b100, 0b101, 0b110, 0b111] { + assert!(ProductTypeUfp::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs new file mode 100644 index 0000000..e2d4ccf --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -0,0 +1,36 @@ +//! [`Response`] contains the response to a Discover Identity Command targetting SOP. + +use crate::vdm::structured::{ + command::discover_identity::{CertStatVdo, ProductTypeVdo, ProductVdo}, + Header, +}; + +pub mod id_header_vdo; + +pub use id_header_vdo::IdHeaderVdo; + +/// The response to a Discover Identity Command using SOP. +/// +/// Each response contains up to 7 VDOs. +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Response { + /// The header for this Structured VDM Message. + pub header: Header, + + /// Information corresponding to the Product. + pub id: IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: Option, + + /// Identity information relating to the product. + pub product: Option, + + /// The Product-specific VDOs. + /// + /// The types of these VDOs are determined by fields in the [`Self::id`] field. + pub product_type_vdos: [ProductTypeVdo; 3], +} From ef35b78190eadbdf4e0fbcd4a575faa067787449 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:01:36 -0700 Subject: [PATCH 08/26] add sop_prime::IdHeaderVdo parsing --- .../command/discover_identity/sop_prime.rs | 91 ---------- .../sop_prime/id_header_vdo.rs | 164 ++++++++++++++++++ .../discover_identity/sop_prime/mod.rs | 34 ++++ 3 files changed, 198 insertions(+), 91 deletions(-) delete mode 100644 src/vdm/structured/command/discover_identity/sop_prime.rs create mode 100644 src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs create mode 100644 src/vdm/structured/command/discover_identity/sop_prime/mod.rs diff --git a/src/vdm/structured/command/discover_identity/sop_prime.rs b/src/vdm/structured/command/discover_identity/sop_prime.rs deleted file mode 100644 index 7c1c71b..0000000 --- a/src/vdm/structured/command/discover_identity/sop_prime.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! [`Response`] contains the response to a Discover Identity Command targetting SOP'. - -use super::{CertStatVdo, ConnectorType, ProductVdo}; -use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; - -/// The response to a Discover Identity Command using `SOP'`. -/// -/// Each response contains up to 7 VDOs. The first three -/// -/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - /// The header for this Structured VDM Message. - pub header: Header, - - /// Information corresponding to the Product. - pub id: IdHeaderVdo, - - /// The XID assigned by the USB-IF to the product. - pub cert_stat: Option, - - /// Identity information relating to the product. - pub product: Option, - - /// The Product-specific VDOs. - /// - /// The types of these VDOs are determined by fields in the [`Self::id`] field. - pub product_type_vdos: [ProductTypeVdo; 3], -} - -/// The ID Header VDO contains information corresponding to the Power Delivery Product. -/// -/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct IdHeaderVdo { - /// The USB Vendor ID as assigned by the USB-IF. - pub usb_vendor_id: u16, - - /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. - pub connector_type: ConnectorType, - - /// Indicates whether or not the Product (either a Cable Plug or a device that - /// can operate in the UFP role) is capable of supporting Modes. - pub modal_operation_supported: bool, - - /// Indicates the type of Product when the Product is a Cable Plug or VPD, whether - /// a VDO will be returned, and if so, the type of VDO to be returned. - /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. - pub product_type: ProductType, - - /// Whether or not the Port has a USB Device Capability. - pub usb_communication_capable_as_usb_device: bool, - - /// Whether or not the Port has a USB Host Capability. - pub usb_communication_capable_as_usb_host: bool, -} - -/// The `SOP'` Product Type (Cable Plug/VPD) field indicates the type of Product -/// when the Product is a Cable Plug or VPD, whether a VDO will be returned, and -/// if so, the type of VDO to be returned. -/// -/// The type changes how [`Response::product_type_vdos`] is interpreted. -/// -/// See PD spec 6.4.4.3.1.1.4 Product Type (Cable Plug), table 6.35 Product Types (Cable Plug/VPD). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ProductType { - /// No other Product Type is appropriate. - /// - /// [`Response::product_type_vdos`] is empty. - NotACablePlugVpd, - - /// The Product is a cable that does not incorporate signal conditioning circuits. - /// - /// The first item in [`Response::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. - PassiveCable, - - /// The Product is a cable that incorporates signal conditioning circuits. - /// - /// The first item in [`Response::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. - /// The second item is a [`ActiveCableVdo2`][`super::ActiveCableVdo2`]. - ActiveCable, - - /// The Product is a `VCONN`-powered USB device. - /// - /// The first item in [`Response::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. - Vpd, -} diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs new file mode 100644 index 0000000..eafb64f --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -0,0 +1,164 @@ +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when the Product is a Cable Plug or VPD, whether + /// a VDO will be returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + pub product_type: ProductType, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +bitfield::bitfield! { + /// The raw value of an [`IdHeaderVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + pub u8, connector_type, set_connector_type: 22, 21; + pub bool, modal_operation_supported, set_modal_operation_supported: 26; + pub u8, product_type, set_product_type: 29, 27; + pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; +} + +/// Errors that can occur when parsing an [`IdHeaderVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseIdHeaderVdoError { + InvalidConnectorType, + InvalidProductType, +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_vendor_id: raw.usb_vendor_id(), + connector_type: raw + .connector_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidConnectorType)?, + modal_operation_supported: raw.modal_operation_supported(), + product_type: raw + .product_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductType)?, + usb_communication_capable_as_usb_device: raw.usb_communication_capable_as_usb_device(), + usb_communication_capable_as_usb_host: raw.usb_communication_capable_as_usb_host(), + }) + } +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// The `SOP'` Product Type (Cable Plug/VPD) field indicates the type of Product +/// when the Product is a Cable Plug or VPD, whether a VDO will be returned, and +/// if so, the type of VDO to be returned. +/// +/// The type changes how [`Response::product_type_vdos`] is interpreted. +/// +/// See PD spec 6.4.4.3.1.1.4 Product Type (Cable Plug), table 6.35 Product Types (Cable Plug/VPD). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductType { + /// No other Product Type is appropriate. + /// + /// [`Response::product_type_vdos`] is empty. + NotACablePlugVpd, + + /// The Product is a cable that does not incorporate signal conditioning circuits. + /// + /// The first item in [`Response::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. + PassiveCable, + + /// The Product is a cable that incorporates signal conditioning circuits. + /// + /// The first item in [`Response::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. + /// The second item is a [`ActiveCableVdo2`][`super::ActiveCableVdo2`]. + ActiveCable, + + /// The Product is a `VCONN`-powered USB device. + /// + /// The first item in [`Response::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. + Vpd, +} + +impl TryFrom for ProductType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotACablePlugVpd), + 0b001 => Ok(Self::PassiveCable), + 0b010 => Ok(Self::ActiveCable), + 0b011 => Ok(Self::Vpd), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod product_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductType); 4] = [ + (0b000, ProductType::NotACablePlugVpd), + (0b001, ProductType::PassiveCable), + (0b010, ProductType::ActiveCable), + (0b011, ProductType::Vpd), + ]; + for (raw, expected) in cases { + assert_eq!(ProductType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b100, 0b101, 0b110, 0b111] { + assert!(ProductType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs new file mode 100644 index 0000000..610b2a9 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -0,0 +1,34 @@ +//! [`Response`] contains the response to a Discover Identity Command targetting SOP'. + +use super::{CertStatVdo, ProductVdo}; +use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; + +pub mod id_header_vdo; + +pub use id_header_vdo::IdHeaderVdo; + +/// The response to a Discover Identity Command using `SOP'`. +/// +/// Each response contains up to 7 VDOs. The first three +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Response { + /// The header for this Structured VDM Message. + pub header: Header, + + /// Information corresponding to the Product. + pub id: IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: Option, + + /// Identity information relating to the product. + pub product: Option, + + /// The Product-specific VDOs. + /// + /// The types of these VDOs are determined by fields in the [`Self::id`] field. + pub product_type_vdos: [ProductTypeVdo; 3], +} From febbd19eb91d20037d804e5c34f997d5e31a6753 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:08:40 -0700 Subject: [PATCH 09/26] Add parsing for ProductVdo --- .../command/discover_identity/mod.rs | 16 +----- .../command/discover_identity/product_vdo.rs | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 src/vdm/structured/command/discover_identity/product_vdo.rs diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs index 5d48477..d36c1f1 100644 --- a/src/vdm/structured/command/discover_identity/mod.rs +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -5,6 +5,7 @@ pub mod active_cable_vdo; pub mod dfp_vdo; pub mod passive_cable_vdo; +pub mod product_vdo; pub mod sop; pub mod sop_prime; pub mod ufp_vdo; @@ -13,6 +14,7 @@ pub mod vpd_vdo; pub use active_cable_vdo::{ActiveCableVdo1, ActiveCableVdo2}; pub use dfp_vdo::DfpVdo; pub use passive_cable_vdo::PassiveCableVdo; +pub use product_vdo::ProductVdo; pub use ufp_vdo::UfpVdo; pub use vpd_vdo::VpdVdo; @@ -48,20 +50,6 @@ impl TryFrom for ConnectorType { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct CertStatVdo(pub u32); -/// The Product VDO contains identity information relating to the product. -/// -/// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ProductVdo { - /// The USB Product ID, as defined by the USB 2.0 / USB 3.2 specifications. - pub usb_product_id: u16, - - /// The USB Device Release Number, as defined by the USB 2.0 / USB 3.2 specifications. - pub bcd_device: u16, -} - /// An unspecified Product Type VDO in the Product Type VDO(s) of the Discover /// Identity Command response. /// diff --git a/src/vdm/structured/command/discover_identity/product_vdo.rs b/src/vdm/structured/command/discover_identity/product_vdo.rs new file mode 100644 index 0000000..e9bc0d7 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/product_vdo.rs @@ -0,0 +1,51 @@ +//! [`ProductVdo`] contains identity information relating to the product. +//! +//! See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. + +/// The Product VDO contains identity information relating to the product. +/// +/// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductVdo { + /// The USB Product ID, as defined by the USB 2.0 / USB 3.2 specifications. + pub usb_product_id: u16, + + /// The USB Device Release Number, as defined by the USB 2.0 / USB 3.2 specifications. + pub bcd_device: u16, +} + +bitfield::bitfield! { + /// The Raw value of a [`ProductVdo`], before parsing bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`ProductVdo::usb_product_id`]. + pub u16, usb_product_id, set_usb_product_id: 15, 0; + + /// See [`ProductVdo::bcd_device`]. + pub u16, bcd_device, set_bcd_device: 31, 16 +} + +impl From for ProductVdo { + fn from(raw: Raw) -> Self { + Self { + usb_product_id: raw.usb_product_id(), + bcd_device: raw.bcd_device(), + } + } +} + +impl From for ProductVdo { + fn from(value: u32) -> Self { + Raw(value).into() + } +} + +impl From<[u8; 4]> for ProductVdo { + fn from(bytes: [u8; 4]) -> Self { + u32::from_le_bytes(bytes).into() + } +} From f99f971553713c328e9dc0ff49522f80e788b864 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 11:09:35 -0700 Subject: [PATCH 10/26] Add docs to Raw methods --- .../command/discover_identity/sop/id_header_vdo.rs | 14 ++++++++++++++ .../discover_identity/sop_prime/id_header_vdo.rs | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs index 67208e9..ef823e8 100644 --- a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -41,12 +41,26 @@ bitfield::bitfield! { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Raw(u32); impl Debug; + + /// See [`IdHeaderVdo::usb_vendor_id`]. pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + + /// See [`IdHeaderVdo::connector_type`]. pub u8, connector_type, set_connector_type: 22, 21; + + /// See [`IdHeaderVdo::product_type_dfp`]. pub u8, product_type_dfp, set_product_type_dfp: 25, 23; + + /// See [`IdHeaderVdo::modal_operation_supported`]. pub bool, modal_operation_supported, set_modal_operation_supported: 26; + + /// See [`IdHeaderVdo::product_type_ufp`]. pub u8, product_type_ufp, set_product_type_ufp: 29, 27; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_device`]. pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_host`]. pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; } diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index eafb64f..600acdb 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -35,11 +35,23 @@ bitfield::bitfield! { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Raw(u32); impl Debug; + + /// See [`IdHeaderVdo::usb_vendor_id`]. pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + + /// See [`IdHeaderVdo::connector_type`]. pub u8, connector_type, set_connector_type: 22, 21; + + /// See [`IdHeaderVdo::modal_operation_supported`]. pub bool, modal_operation_supported, set_modal_operation_supported: 26; + + /// See [`IdHeaderVdo::product_type`]. pub u8, product_type, set_product_type: 29, 27; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_device`]. pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_host`]. pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; } From b8e18c3522a5aeaa49688a69b1f604c08b2ae94b Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 9 Apr 2026 14:27:20 -0700 Subject: [PATCH 11/26] Remove Header from response --- .../command/discover_identity/sop/mod.rs | 16 ++++------------ .../command/discover_identity/sop_prime/mod.rs | 13 ++++--------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs index e2d4ccf..bf2bfca 100644 --- a/src/vdm/structured/command/discover_identity/sop/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -1,25 +1,17 @@ -//! [`Response`] contains the response to a Discover Identity Command targetting SOP. +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP. -use crate::vdm::structured::{ - command::discover_identity::{CertStatVdo, ProductTypeVdo, ProductVdo}, - Header, -}; +use crate::vdm::structured::command::discover_identity::{CertStatVdo, ProductTypeVdo, ProductVdo}; pub mod id_header_vdo; pub use id_header_vdo::IdHeaderVdo; -/// The response to a Discover Identity Command using SOP. -/// -/// Each response contains up to 7 VDOs. +/// The response VDOs to a Discover Identity Command using SOP. /// /// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - /// The header for this Structured VDM Message. - pub header: Header, - +pub struct ResponseVdos { /// Information corresponding to the Product. pub id: IdHeaderVdo, diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs index 610b2a9..0f1ec59 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -1,23 +1,18 @@ -//! [`Response`] contains the response to a Discover Identity Command targetting SOP'. +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP'. use super::{CertStatVdo, ProductVdo}; -use crate::vdm::structured::{command::discover_identity::ProductTypeVdo, Header}; +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; pub mod id_header_vdo; pub use id_header_vdo::IdHeaderVdo; -/// The response to a Discover Identity Command using `SOP'`. -/// -/// Each response contains up to 7 VDOs. The first three +/// The response VDOs to a Discover Identity Command using `SOP'`. /// /// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - /// The header for this Structured VDM Message. - pub header: Header, - +pub struct ResponseVdos { /// Information corresponding to the Product. pub id: IdHeaderVdo, From 8476d6430808df5af9593387ed0c96e2932dbf02 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 13:33:05 -0700 Subject: [PATCH 12/26] Fix incorrect parsing for SOP' product type Co-authored-by: Copilot --- .../discover_identity/sop_prime/id_header_vdo.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index 600acdb..29dd3a5 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -138,9 +138,9 @@ impl TryFrom for ProductType { fn try_from(value: u8) -> Result { match value { 0b000 => Ok(Self::NotACablePlugVpd), - 0b001 => Ok(Self::PassiveCable), - 0b010 => Ok(Self::ActiveCable), - 0b011 => Ok(Self::Vpd), + 0b011 => Ok(Self::PassiveCable), + 0b100 => Ok(Self::ActiveCable), + 0b110 => Ok(Self::Vpd), _ => Err(()), } } @@ -157,9 +157,9 @@ mod tests { fn all_valid_variants() { let cases: [(u8, ProductType); 4] = [ (0b000, ProductType::NotACablePlugVpd), - (0b001, ProductType::PassiveCable), - (0b010, ProductType::ActiveCable), - (0b011, ProductType::Vpd), + (0b011, ProductType::PassiveCable), + (0b100, ProductType::ActiveCable), + (0b110, ProductType::Vpd), ]; for (raw, expected) in cases { assert_eq!(ProductType::try_from(raw), Ok(expected), "raw={raw}"); @@ -168,7 +168,7 @@ mod tests { #[test] fn invalid_values() { - for v in [0b100, 0b101, 0b110, 0b111] { + for v in [0b001, 0b010, 0b101, 0b111] { assert!(ProductType::try_from(v).is_err(), "raw={v} should be invalid"); } } From e7bbc2912bc5175f1fe5eb94b44352107b891291 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 16:04:27 -0700 Subject: [PATCH 13/26] Change ResponseVdos to be fully parsed Callers can now use the Product Type VDO enums to fully inspect the type without having to perform additional parsing --- .../discover_identity/id_header_vdo.rs | 42 +++++++++++++ .../command/discover_identity/mod.rs | 2 + .../discover_identity/sop/id_header_vdo.rs | 55 ++++++++++++----- .../command/discover_identity/sop/mod.rs | 59 ++++++++++++++++--- .../sop_prime/id_header_vdo.rs | 24 +++++++- .../discover_identity/sop_prime/mod.rs | 35 ++++++++--- 6 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 src/vdm/structured/command/discover_identity/id_header_vdo.rs diff --git a/src/vdm/structured/command/discover_identity/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/id_header_vdo.rs new file mode 100644 index 0000000..b425ada --- /dev/null +++ b/src/vdm/structured/command/discover_identity/id_header_vdo.rs @@ -0,0 +1,42 @@ +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// This type differs from [`sop::IdHeaderVdo`] and [`sop_prime::IdHeaderVdo`] in +/// that it does not contain the product type fields. These fields are encoded into +/// the [`sop::ResponseVdos`] and [`sop_prime::ResponseVdos`] instead, in their +/// [`sop::ResponseVdos::dfp_product_type_vdos`], [`sop::ResponseVdos::ufp_product_type_vdos`], +/// and [`sop_prime::ResponseVdos::product_type_vdos`] fields, which provide a more +/// ergonomic API for accessing the Product Type VDOs. +/// +/// This type is not meant to be parsed directly; use the [`From`] implementations +/// on [`sop::IdHeaderVdo`] and [`sop_prime::IdHeaderVdo`] instead. +/// +/// [`sop::IdHeaderVdo`]: crate::vdm::structured::command::discover_identity::sop::IdHeaderVdo +/// [`sop_prime::IdHeaderVdo`]: crate::vdm::structured::command::discover_identity::sop_prime::IdHeaderVdo +/// [`sop::ResponseVdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos +/// [`sop_prime::ResponseVdos`]: crate::vdm::structured::command::discover_identity::sop_prime::ResponseVdos +/// [`sop::ResponseVdos::dfp_product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos::dfp_product_type_vdos +/// [`sop::ResponseVdos::ufp_product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos::ufp_product_type_vdos +/// [`sop_prime::ResponseVdos::product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop_prime::ResponseVdos::product_type_vdos +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs index d36c1f1..31fc6b5 100644 --- a/src/vdm/structured/command/discover_identity/mod.rs +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -4,6 +4,7 @@ pub mod active_cable_vdo; pub mod dfp_vdo; +pub mod id_header_vdo; pub mod passive_cable_vdo; pub mod product_vdo; pub mod sop; @@ -13,6 +14,7 @@ pub mod vpd_vdo; pub use active_cable_vdo::{ActiveCableVdo1, ActiveCableVdo2}; pub use dfp_vdo::DfpVdo; +pub use id_header_vdo::IdHeaderVdo; pub use passive_cable_vdo::PassiveCableVdo; pub use product_vdo::ProductVdo; pub use ufp_vdo::UfpVdo; diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs index ef823e8..7bd64f4 100644 --- a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -2,7 +2,14 @@ use crate::vdm::structured::command::discover_identity::ConnectorType; /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// +/// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] +/// in that it contains the product type fields, which are encoded into the [`ResponseVdos::dfp_product_type_vdos`] +/// and [`ResponseVdos::ufp_product_type_vdos`] fields. This type is meant to be parsed directly from the raw VDO. +/// /// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// [`ResponseVdos::dfp_product_type_vdos`]: super::ResponseVdos::dfp_product_type_vdos +/// [`ResponseVdos::ufp_product_type_vdos`]: super::ResponseVdos::ufp_product_type_vdos #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct IdHeaderVdo { @@ -15,7 +22,9 @@ pub struct IdHeaderVdo { /// Indicates the type of Product when in DFP Data Role, whether a VDO will be /// returned, and if so, the type of VDO to be returned. /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + /// The value of this type changes how [`ResponseVdos::dfp_product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::dfp_product_type_vdos`]: super::ResponseVdos::dfp_product_type_vdos pub product_type_dfp: ProductTypeDfp, /// Indicates whether or not the Product (either a Cable Plug or a device that @@ -25,7 +34,9 @@ pub struct IdHeaderVdo { /// Indicates the type of Product when in UFP Data Role, whether a VDO will be /// returned, and if so, the type of VDO to be returned. /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + /// The value of this type changes how [`ResponseVdos::ufp_product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::ufp_product_type_vdos`]: super::ResponseVdos::ufp_product_type_vdos pub product_type_ufp: ProductTypeUfp, /// Whether or not the Port has a USB Device Capability. @@ -114,6 +125,18 @@ impl TryFrom<[u8; 4]> for IdHeaderVdo { } } +impl From for crate::vdm::structured::command::discover_identity::IdHeaderVdo { + fn from(value: IdHeaderVdo) -> Self { + Self { + usb_vendor_id: value.usb_vendor_id, + connector_type: value.connector_type, + modal_operation_supported: value.modal_operation_supported, + usb_communication_capable_as_usb_device: value.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: value.usb_communication_capable_as_usb_host, + } + } +} + /// The [`IdHeaderVdo::product_type_dfp`] field indicates the type of Product when /// in DFP Data Role, whether a VDO will be returned, and if so, the type of VDO /// to be returned. @@ -124,15 +147,15 @@ impl TryFrom<[u8; 4]> for IdHeaderVdo { pub enum ProductTypeDfp { /// This is not a DFP. /// - /// [`Response::product_type_vdos`] is empty. + /// The Product Type VDOs is empty. NotADfp, /// The product is a PDUSB Hub. /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. Hub, @@ -140,20 +163,20 @@ pub enum ProductTypeDfp { /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate /// Modes as an AMC. /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. Host, /// The product is a charger / power brick. /// - /// If the device is not a Dual-Role Device, the first item in [`Response::product_type_vdos`] - /// is a [`DfpVdo`][`super::DfpVdo`]. + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. /// - /// If the device is a Dual-Role Device, the first item in [`Response::product_type_vdos`] + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. Charger, @@ -183,22 +206,22 @@ impl TryFrom for ProductTypeDfp { pub enum ProductTypeUfp { /// This is not a UFP. /// - /// [`Response::product_type_vdos`] is empty. + /// The Product Type VDOs is empty. NotAUfp, /// The product is a PDUSB Hub. /// - /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + /// The first item in the Product Type VDOs is a [`UfpVdo`][`super::UfpVdo`]. Hub, /// The product is a PDUSB Device other than a Hub. /// - /// The first item in [`Response::product_type_vdos`] is a [`UfpVdo`][`super::UfpVdo`]. + /// The first item in the Product Type VDOs is a [`UfpVdo`][`super::UfpVdo`]. Peripheral, /// The product is a PSD, e.g., power bank. /// - /// [`Response::product_type_vdos`] is empty. + /// The Product Type VDOs is empty. Psd, } diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs index bf2bfca..a46005e 100644 --- a/src/vdm/structured/command/discover_identity/sop/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -1,6 +1,6 @@ //! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP. -use crate::vdm::structured::command::discover_identity::{CertStatVdo, ProductTypeVdo, ProductVdo}; +use crate::vdm::structured::command::discover_identity::{CertStatVdo, DfpVdo, ProductVdo, UfpVdo}; pub mod id_header_vdo; @@ -13,16 +13,61 @@ pub use id_header_vdo::IdHeaderVdo; #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ResponseVdos { /// Information corresponding to the Product. - pub id: IdHeaderVdo, + /// + /// To get an SOP-specific ID Header VDO, use the [`Into`] implementations on + /// this field. + pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, /// The XID assigned by the USB-IF to the product. - pub cert_stat: Option, + pub cert_stat: CertStatVdo, /// Identity information relating to the product. - pub product: Option, + pub product: ProductVdo, + + /// The Product-specific DFP VDOs. + /// + /// These are determined by the [`IdHeaderVdo::product_type_dfp`] field during + /// parsing. + pub dfp_product_type_vdos: DfpProductTypeVdos, - /// The Product-specific VDOs. + /// The Product-specific UFP VDOs. /// - /// The types of these VDOs are determined by fields in the [`Self::id`] field. - pub product_type_vdos: [ProductTypeVdo; 3], + /// These are determined by the [`IdHeaderVdo::product_type_ufp`] field during + /// parsing. + pub ufp_product_type_vdos: UfpProductTypeVdos, +} + +/// The Product Type DFP VDOs, parsed based on [`IdHeaderVdo::product_type_dfp`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DfpProductTypeVdos { + /// This is not a DFP. + NotADfp, + + /// The product is a PDUSB Hub. + Hub(DfpVdo), + + /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate + /// Modes as an AMC. + Host(DfpVdo), + + /// The product is a charger / power brick. + Charger(DfpVdo), +} + +/// The Product Type UFP VDOs, parsed based on [`IdHeaderVdo::product_type_ufp`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UfpProductTypeVdos { + /// This is not a UFP. + NotAUfp, + + /// The product is a PDUSB Hub. + Hub(UfpVdo), + + /// The product is a PDUSB Device other than a Hub. + Peripheral(UfpVdo), + + /// The product is a PSD, e.g., power bank. + Psd, } diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index 29dd3a5..1f7037d 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -2,7 +2,13 @@ use crate::vdm::structured::command::discover_identity::ConnectorType; /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// +/// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] +/// in that it contains the product type fields, which are encoded into the [`ResponseVdos::product_type_vdos`] +/// field. This type is meant to be parsed directly from the raw VDO. +/// /// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct IdHeaderVdo { @@ -19,7 +25,9 @@ pub struct IdHeaderVdo { /// Indicates the type of Product when the Product is a Cable Plug or VPD, whether /// a VDO will be returned, and if so, the type of VDO to be returned. /// - /// The value of this type changes how [`Response::product_type_vdos`] is interpreted. + /// The value of this type changes how [`ResponseVdos::product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos pub product_type: ProductType, /// Whether or not the Port has a USB Device Capability. @@ -100,12 +108,22 @@ impl TryFrom<[u8; 4]> for IdHeaderVdo { } } +impl From for crate::vdm::structured::command::discover_identity::IdHeaderVdo { + fn from(id_header_vdo: IdHeaderVdo) -> Self { + Self { + usb_vendor_id: id_header_vdo.usb_vendor_id, + connector_type: id_header_vdo.connector_type, + modal_operation_supported: id_header_vdo.modal_operation_supported, + usb_communication_capable_as_usb_device: id_header_vdo.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: id_header_vdo.usb_communication_capable_as_usb_host, + } + } +} + /// The `SOP'` Product Type (Cable Plug/VPD) field indicates the type of Product /// when the Product is a Cable Plug or VPD, whether a VDO will be returned, and /// if so, the type of VDO to be returned. /// -/// The type changes how [`Response::product_type_vdos`] is interpreted. -/// /// See PD spec 6.4.4.3.1.1.4 Product Type (Cable Plug), table 6.35 Product Types (Cable Plug/VPD). #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs index 0f1ec59..df27d81 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -1,7 +1,8 @@ //! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP'. -use super::{CertStatVdo, ProductVdo}; -use crate::vdm::structured::command::discover_identity::ProductTypeVdo; +use crate::vdm::structured::command::discover_identity::{ + ActiveCableVdo1, ActiveCableVdo2, CertStatVdo, PassiveCableVdo, ProductVdo, VpdVdo, +}; pub mod id_header_vdo; @@ -14,16 +15,36 @@ pub use id_header_vdo::IdHeaderVdo; #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ResponseVdos { /// Information corresponding to the Product. - pub id: IdHeaderVdo, + /// + /// To get an SOP'-specific ID Header VDO, use the [`Into`] implementations + /// on this field. + pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, /// The XID assigned by the USB-IF to the product. - pub cert_stat: Option, + pub cert_stat: CertStatVdo, /// Identity information relating to the product. - pub product: Option, + pub product: ProductVdo, /// The Product-specific VDOs. /// - /// The types of these VDOs are determined by fields in the [`Self::id`] field. - pub product_type_vdos: [ProductTypeVdo; 3], + /// These are determined by [`IdHeaderVdo::product_type`] during parsing. + pub product_type_vdos: ProductTypeVdos, +} + +/// The Product Type VDOs, parsed based on [`IdHeaderVdo::product_type`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeVdos { + /// No other Product Type is appropriate. + NotACablePlugVpd, + + /// The Product is a cable that does not incorporate signal conditioning circuits. + PassiveCable(PassiveCableVdo), + + /// The Product is a cable that incorporates signal conditioning circuits. + ActiveCable(ActiveCableVdo1, ActiveCableVdo2), + + /// The Product is a `VCONN`-powered USB device. + Vpd(VpdVdo), } From 5a4efa4832c2d774142c8535ac76f0494781406c Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Thu, 23 Apr 2026 16:04:34 -0700 Subject: [PATCH 14/26] Fix clippy lint --- src/vdm/structured/header.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index c330d4e..99908dd 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -62,7 +62,8 @@ impl From for CommandType { 0b10 => Self::Nak, // technically >0b11 is unreachable since we masked with 0b11, but we'll // treat any cosmic rays as a Busy response to make this From, not TryFrom - 0b11 | _ => Self::Busy, + // 0b11 | _ => Self::Busy, // 0b11 is explicitly Busy but this bothers clippy::wildcard_in_or_patterns + _ => Self::Busy, } } } From aa7515b17474d8197cd86b9e14d8ba2919c92960 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:00:29 -0700 Subject: [PATCH 15/26] Fix typo --- src/vdm/structured/command/discover_identity/mod.rs | 2 +- src/vdm/structured/command/discover_identity/sop/mod.rs | 2 +- src/vdm/structured/command/discover_identity/sop_prime/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs index 31fc6b5..8461ab0 100644 --- a/src/vdm/structured/command/discover_identity/mod.rs +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -55,7 +55,7 @@ pub struct CertStatVdo(pub u32); /// An unspecified Product Type VDO in the Product Type VDO(s) of the Discover /// Identity Command response. /// -/// The type of this VDO is determined by the ID Header VDO and whether targetting +/// The type of this VDO is determined by the ID Header VDO and whether targeting /// SOP or SOP'. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs index a46005e..2838531 100644 --- a/src/vdm/structured/command/discover_identity/sop/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -1,4 +1,4 @@ -//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP. +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targeting SOP. use crate::vdm::structured::command::discover_identity::{CertStatVdo, DfpVdo, ProductVdo, UfpVdo}; diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs index df27d81..fcf5d65 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -1,4 +1,4 @@ -//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targetting SOP'. +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targeting SOP'. use crate::vdm::structured::command::discover_identity::{ ActiveCableVdo1, ActiveCableVdo2, CertStatVdo, PassiveCableVdo, ProductVdo, VpdVdo, From d4b121fb6d4e0347178ecc6a2a8601f3caa75acc Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:00:37 -0700 Subject: [PATCH 16/26] Fix broken rustdoc links --- .../discover_identity/sop_prime/id_header_vdo.rs | 16 ++++++++++++---- src/vdm/structured/header.rs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index 1f7037d..e106890 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -130,23 +130,31 @@ impl From for crate::vdm::structured::command::discover_identity::I pub enum ProductType { /// No other Product Type is appropriate. /// - /// [`Response::product_type_vdos`] is empty. + /// [`ResponseVdos::product_type_vdos`] is empty. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos NotACablePlugVpd, /// The Product is a cable that does not incorporate signal conditioning circuits. /// - /// The first item in [`Response::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos PassiveCable, /// The Product is a cable that incorporates signal conditioning circuits. /// - /// The first item in [`Response::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. /// The second item is a [`ActiveCableVdo2`][`super::ActiveCableVdo2`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos ActiveCable, /// The Product is a `VCONN`-powered USB device. /// - /// The first item in [`Response::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos Vpd, } diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index 99908dd..496ab01 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -1,4 +1,4 @@ -//! [`Header]` defines the VDM Header for a Structured VDM Message. +//! [`Header`] defines the VDM Header for a Structured VDM Message. use crate::vdm::structured::Svid; From b20ac803dff8cf2758807b2949ffe637cc3c20b7 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:02:28 -0700 Subject: [PATCH 17/26] Add comment about clippy lint --- src/vdm/structured/header.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index 496ab01..4d32874 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -62,7 +62,7 @@ impl From for CommandType { 0b10 => Self::Nak, // technically >0b11 is unreachable since we masked with 0b11, but we'll // treat any cosmic rays as a Busy response to make this From, not TryFrom - // 0b11 | _ => Self::Busy, // 0b11 is explicitly Busy but this bothers clippy::wildcard_in_or_patterns + // 0b11 | _ bothers clippy::wildcard_in_or_patterns, so we'll just use a wildcard arm _ => Self::Busy, } } From 74b1b1611bb2835fa89ff0d7365e346eb8a70113 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:03:30 -0700 Subject: [PATCH 18/26] Fix off-by-one in header::Raw --- src/vdm/structured/header.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index 4d32874..541db2f 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -87,7 +87,7 @@ bitfield::bitfield! { pub u8, command_type, set_command_type: 7, 6; pub u8, object_position, set_object_position: 10, 8; pub u8, structured_vdm_version, set_structured_vdm_version: 14, 11; - pub u16, svid, set_svid: 30, 16; + pub u16, svid, set_svid: 31, 16; } pub enum ParseError { From 9b2d287adbe7b14ca71e5f0c7e30c37be5baf2d1 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:13:42 -0700 Subject: [PATCH 19/26] Added BCD and ProductId newtypes --- src/lib.rs | 1 + src/usb.rs | 26 +++++++++++++++++++ .../command/discover_identity/product_vdo.rs | 10 ++++--- 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/usb.rs diff --git a/src/lib.rs b/src/lib.rs index ed8ff45..d85ef85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod pdinfo; pub mod pdo; pub mod type_c; pub mod ucsi; +pub mod usb; pub mod vdm; use core::hash::Hash; diff --git a/src/usb.rs b/src/usb.rs new file mode 100644 index 0000000..67196d7 --- /dev/null +++ b/src/usb.rs @@ -0,0 +1,26 @@ +//! Universal Serial Bus (USB) related types and traits from the USB 2.0 and 3.2 +//! specifications. + +/// A Binary-Coded Decimal (BCD) format as defined by the USB 2.0 specification. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Bcd(pub u16); + +impl Bcd { + /// Parse the BCD value into its major, minor, and subminor components in the + /// format `jj.m.n` where + /// - `jj` is the major version (2 digits) + /// - `m` is the minor version (1 digit) + /// - `n` is the subminor version (1 digit) + pub const fn jjmn(&self) -> (u8, u8, u8) { + let jj = (self.0 >> 12) as u8; + let m = ((self.0 >> 8) & 0xF) as u8; + let n = ((self.0 >> 4) & 0xF) as u8; + (jj, m, n) + } +} + +/// The USB Product ID as assigned by the USB-IF. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductId(pub u16); diff --git a/src/vdm/structured/command/discover_identity/product_vdo.rs b/src/vdm/structured/command/discover_identity/product_vdo.rs index e9bc0d7..f955d6d 100644 --- a/src/vdm/structured/command/discover_identity/product_vdo.rs +++ b/src/vdm/structured/command/discover_identity/product_vdo.rs @@ -2,6 +2,8 @@ //! //! See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. +use crate::usb::{Bcd, ProductId}; + /// The Product VDO contains identity information relating to the product. /// /// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. @@ -9,10 +11,10 @@ #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ProductVdo { /// The USB Product ID, as defined by the USB 2.0 / USB 3.2 specifications. - pub usb_product_id: u16, + pub usb_product_id: ProductId, /// The USB Device Release Number, as defined by the USB 2.0 / USB 3.2 specifications. - pub bcd_device: u16, + pub bcd_device: Bcd, } bitfield::bitfield! { @@ -32,8 +34,8 @@ bitfield::bitfield! { impl From for ProductVdo { fn from(raw: Raw) -> Self { Self { - usb_product_id: raw.usb_product_id(), - bcd_device: raw.bcd_device(), + usb_product_id: ProductId(raw.usb_product_id()), + bcd_device: Bcd(raw.bcd_device()), } } } From 2424c93715481adc210cc51f1f909f0a92ad7eaf Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:23:15 -0700 Subject: [PATCH 20/26] Add method to get response-specific ID Header VDO objects --- .../discover_identity/sop/id_header_vdo.rs | 24 +++++++++++++++++++ .../command/discover_identity/sop/mod.rs | 18 ++++++++++++-- .../sop_prime/id_header_vdo.rs | 13 ++++++++++ .../discover_identity/sop_prime/mod.rs | 17 +++++++++++-- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs index 7bd64f4..503c164 100644 --- a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -1,5 +1,7 @@ use crate::vdm::structured::command::discover_identity::ConnectorType; +use super::{DfpProductTypeVdos, UfpProductTypeVdos}; + /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// /// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] @@ -196,6 +198,17 @@ impl TryFrom for ProductTypeDfp { } } +impl From for ProductTypeUfp { + fn from(ufp_product_type_vdos: UfpProductTypeVdos) -> Self { + match ufp_product_type_vdos { + UfpProductTypeVdos::NotAUfp => Self::NotAUfp, + UfpProductTypeVdos::Hub(_) => Self::Hub, + UfpProductTypeVdos::Peripheral(_) => Self::Peripheral, + UfpProductTypeVdos::Psd => Self::Psd, + } + } +} + /// The [`IdHeaderVdo::product_type_ufp`] field indicates the type of Product when /// in the UFP Data Role, whether a VDO will be returned, and if so, the type of /// VDO to be returned. @@ -239,6 +252,17 @@ impl TryFrom for ProductTypeUfp { } } +impl From for ProductTypeDfp { + fn from(dfp_product_type_vdos: DfpProductTypeVdos) -> Self { + match dfp_product_type_vdos { + DfpProductTypeVdos::NotADfp => Self::NotADfp, + DfpProductTypeVdos::Hub(_) => Self::Hub, + DfpProductTypeVdos::Host(_) => Self::Host, + DfpProductTypeVdos::Charger(_) => Self::Charger, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs index 2838531..dd5de9f 100644 --- a/src/vdm/structured/command/discover_identity/sop/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -14,8 +14,7 @@ pub use id_header_vdo::IdHeaderVdo; pub struct ResponseVdos { /// Information corresponding to the Product. /// - /// To get an SOP-specific ID Header VDO, use the [`Into`] implementations on - /// this field. + /// To get an SOP-specific ID Header VDO, use [`Self::id()`]. pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, /// The XID assigned by the USB-IF to the product. @@ -37,6 +36,21 @@ pub struct ResponseVdos { pub ufp_product_type_vdos: UfpProductTypeVdos, } +impl ResponseVdos { + /// Gets the SOP-specific ID Header VDO from this response. + pub fn id(&self) -> IdHeaderVdo { + IdHeaderVdo { + usb_vendor_id: self.id.usb_vendor_id, + connector_type: self.id.connector_type, + product_type_dfp: self.dfp_product_type_vdos.into(), + modal_operation_supported: self.id.modal_operation_supported, + product_type_ufp: self.ufp_product_type_vdos.into(), + usb_communication_capable_as_usb_device: self.id.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: self.id.usb_communication_capable_as_usb_host, + } + } +} + /// The Product Type DFP VDOs, parsed based on [`IdHeaderVdo::product_type_dfp`]. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index e106890..faf41b8 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -1,5 +1,7 @@ use crate::vdm::structured::command::discover_identity::ConnectorType; +use super::ProductTypeVdos; + /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// /// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] @@ -172,6 +174,17 @@ impl TryFrom for ProductType { } } +impl From for ProductType { + fn from(product_type_vdos: ProductTypeVdos) -> Self { + match product_type_vdos { + ProductTypeVdos::NotACablePlugVpd => Self::NotACablePlugVpd, + ProductTypeVdos::PassiveCable(_) => Self::PassiveCable, + ProductTypeVdos::ActiveCable(_, _) => Self::ActiveCable, + ProductTypeVdos::Vpd(_) => Self::Vpd, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs index fcf5d65..9ede491 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -16,8 +16,7 @@ pub use id_header_vdo::IdHeaderVdo; pub struct ResponseVdos { /// Information corresponding to the Product. /// - /// To get an SOP'-specific ID Header VDO, use the [`Into`] implementations - /// on this field. + /// To get an SOP'-specific ID Header VDO, use [`Self::id()`]. pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, /// The XID assigned by the USB-IF to the product. @@ -32,6 +31,20 @@ pub struct ResponseVdos { pub product_type_vdos: ProductTypeVdos, } +impl ResponseVdos { + /// Gets the SOP'-specific ID Header VDO from this response. + pub fn id(&self) -> IdHeaderVdo { + IdHeaderVdo { + usb_vendor_id: self.id.usb_vendor_id, + connector_type: self.id.connector_type, + modal_operation_supported: self.id.modal_operation_supported, + product_type: self.product_type_vdos.into(), + usb_communication_capable_as_usb_device: self.id.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: self.id.usb_communication_capable_as_usb_host, + } + } +} + /// The Product Type VDOs, parsed based on [`IdHeaderVdo::product_type`]. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] From 18a2e39329506989ff4df60092de3df1cfb87189 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:23:31 -0700 Subject: [PATCH 21/26] cargo fmt --- .../discover_identity/active_cable_vdo.rs | 83 ++++--------------- .../command/discover_identity/ufp_vdo.rs | 5 +- 2 files changed, 19 insertions(+), 69 deletions(-) diff --git a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs index d9c4bdc..668f53d 100644 --- a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs +++ b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs @@ -806,10 +806,7 @@ mod tests { #[test] fn invalid_values() { for v in 5..=255u8 { - assert!( - UsbHighestSpeed::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -824,11 +821,7 @@ mod tests { (0b10, VbusCurrentHandlingCapability::FiveAmps), ]; for (raw, expected) in cases { - assert_eq!( - VbusCurrentHandlingCapability::try_from(raw), - Ok(expected), - "raw={raw}" - ); + assert_eq!(VbusCurrentHandlingCapability::try_from(raw), Ok(expected), "raw={raw}"); } } @@ -881,10 +874,7 @@ mod tests { #[test] fn invalid_values() { for v in 4..=255u8 { - assert!( - MaximumVbusVoltage::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -899,21 +889,14 @@ mod tests { (0b11, CableTerminationType::BothEndsActive), ]; for (raw, expected) in cases { - assert_eq!( - CableTerminationType::try_from(raw), - Ok(expected), - "raw={raw}" - ); + assert_eq!(CableTerminationType::try_from(raw), Ok(expected), "raw={raw}"); } } #[test] fn invalid_values() { for v in [0u8, 1] { - assert!( - CableTerminationType::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(CableTerminationType::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -944,10 +927,7 @@ mod tests { fn invalid_values() { assert!(CableLatency::try_from(0u8).is_err()); for v in 11..=255u8 { - assert!( - CableLatency::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(CableLatency::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -957,10 +937,8 @@ mod tests { #[test] fn all_valid_variants() { - let cases: [(u8, UsbTypeCOrCaptive); 2] = [ - (0b10, UsbTypeCOrCaptive::UsbTypeC), - (0b11, UsbTypeCOrCaptive::Captive), - ]; + let cases: [(u8, UsbTypeCOrCaptive); 2] = + [(0b10, UsbTypeCOrCaptive::UsbTypeC), (0b11, UsbTypeCOrCaptive::Captive)]; for (raw, expected) in cases { assert_eq!(UsbTypeCOrCaptive::try_from(raw), Ok(expected), "raw={raw}"); } @@ -969,10 +947,7 @@ mod tests { #[test] fn invalid_values() { for v in [0u8, 1] { - assert!( - UsbTypeCOrCaptive::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(UsbTypeCOrCaptive::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -1011,10 +986,7 @@ mod tests { #[test] fn invalid_values() { for v in 2..=255u8 { - assert!( - UsbLanesSupported::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(UsbLanesSupported::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -1024,8 +996,7 @@ mod tests { #[test] fn all_valid_variants() { - let cases: [(u8, ActiveElement); 2] = - [(0, ActiveElement::Redriver), (1, ActiveElement::Retimer)]; + let cases: [(u8, ActiveElement); 2] = [(0, ActiveElement::Redriver), (1, ActiveElement::Retimer)]; for (raw, expected) in cases { assert_eq!(ActiveElement::try_from(raw), Ok(expected), "raw={raw}"); } @@ -1034,10 +1005,7 @@ mod tests { #[test] fn invalid_values() { for v in 2..=255u8 { - assert!( - ActiveElement::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(ActiveElement::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -1057,10 +1025,7 @@ mod tests { #[test] fn invalid_values() { for v in 2..=255u8 { - assert!( - PhysicalConnection::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(PhysicalConnection::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -1070,26 +1035,17 @@ mod tests { #[test] fn all_valid_variants() { - let cases: [(u8, U3ToU0TransitionMode); 2] = [ - (0, U3ToU0TransitionMode::Direct), - (1, U3ToU0TransitionMode::ThroughU3S), - ]; + let cases: [(u8, U3ToU0TransitionMode); 2] = + [(0, U3ToU0TransitionMode::Direct), (1, U3ToU0TransitionMode::ThroughU3S)]; for (raw, expected) in cases { - assert_eq!( - U3ToU0TransitionMode::try_from(raw), - Ok(expected), - "raw={raw}" - ); + assert_eq!(U3ToU0TransitionMode::try_from(raw), Ok(expected), "raw={raw}"); } } #[test] fn invalid_values() { for v in 2..=255u8 { - assert!( - U3ToU0TransitionMode::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(U3ToU0TransitionMode::try_from(v).is_err(), "raw={v} should be invalid"); } } } @@ -1116,10 +1072,7 @@ mod tests { #[test] fn invalid_values() { for v in 7..=255u8 { - assert!( - U3CldPower::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(U3CldPower::try_from(v).is_err(), "raw={v} should be invalid"); } } } diff --git a/src/vdm/structured/command/discover_identity/ufp_vdo.rs b/src/vdm/structured/command/discover_identity/ufp_vdo.rs index 20be4ac..98be917 100644 --- a/src/vdm/structured/command/discover_identity/ufp_vdo.rs +++ b/src/vdm/structured/command/discover_identity/ufp_vdo.rs @@ -271,10 +271,7 @@ mod tests { #[test] fn invalid_values() { for v in 5..=255u8 { - assert!( - UsbHighestSpeed::try_from(v).is_err(), - "raw={v} should be invalid" - ); + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); } } } From 2397f7449f4dcd72a7304427ce156b079dae7ea1 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Fri, 24 Apr 2026 12:28:02 -0700 Subject: [PATCH 22/26] cargo +nightly fmt --- src/ucsi/lpm/get_alternate_modes.rs | 3 ++- .../structured/command/discover_identity/sop/id_header_vdo.rs | 3 +-- .../command/discover_identity/sop_prime/id_header_vdo.rs | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ucsi/lpm/get_alternate_modes.rs b/src/ucsi/lpm/get_alternate_modes.rs index 2debf94..0f115a2 100644 --- a/src/ucsi/lpm/get_alternate_modes.rs +++ b/src/ucsi/lpm/get_alternate_modes.rs @@ -9,7 +9,8 @@ use bitfield::bitfield; use super::Recipient; use crate::ucsi::lpm::InvalidRecipient; use crate::ucsi::{CommandHeaderRaw, COMMAND_LEN}; -use crate::vdm::{structured::Svid, AltModeId}; +use crate::vdm::structured::Svid; +use crate::vdm::AltModeId; /// Data length for the GET_ALTERNATE_MODES command response pub const RESPONSE_DATA_LEN: usize = 12; diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs index 503c164..5352257 100644 --- a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -1,6 +1,5 @@ -use crate::vdm::structured::command::discover_identity::ConnectorType; - use super::{DfpProductTypeVdos, UfpProductTypeVdos}; +use crate::vdm::structured::command::discover_identity::ConnectorType; /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs index faf41b8..4ccf699 100644 --- a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -1,6 +1,5 @@ -use crate::vdm::structured::command::discover_identity::ConnectorType; - use super::ProductTypeVdos; +use crate::vdm::structured::command::discover_identity::ConnectorType; /// The ID Header VDO contains information corresponding to the Power Delivery Product. /// From 9202ebb3f29c8fd52830dbf8800ea6cbf7789ef7 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 27 Apr 2026 10:33:34 -0700 Subject: [PATCH 23/26] Fix comment --- src/usb.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/usb.rs b/src/usb.rs index 67196d7..162b905 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -9,9 +9,9 @@ pub struct Bcd(pub u16); impl Bcd { /// Parse the BCD value into its major, minor, and subminor components in the /// format `jj.m.n` where - /// - `jj` is the major version (2 digits) - /// - `m` is the minor version (1 digit) - /// - `n` is the subminor version (1 digit) + /// - `jj` is the major version (2 nibbles) + /// - `m` is the minor version (1 nibble) + /// - `n` is the subminor version (1 nibble) pub const fn jjmn(&self) -> (u8, u8, u8) { let jj = (self.0 >> 12) as u8; let m = ((self.0 >> 8) & 0xF) as u8; From d5be1851f93284b6ce4c01a53b3b771cd7de2ac9 Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 27 Apr 2026 10:34:32 -0700 Subject: [PATCH 24/26] Add doc comment and derives for error type --- src/vdm/structured/header.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs index 541db2f..eda335b 100644 --- a/src/vdm/structured/header.rs +++ b/src/vdm/structured/header.rs @@ -90,6 +90,9 @@ bitfield::bitfield! { pub u16, svid, set_svid: 31, 16; } +/// Errors that can occur when parsing a [`Header`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ParseError { InvalidCommand, } From 8aad60a2669d9c387d713b3ae4313f7f8040c85c Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 27 Apr 2026 10:37:17 -0700 Subject: [PATCH 25/26] Fix jjmn implementation and add tests --- src/usb.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/usb.rs b/src/usb.rs index 162b905..01dbd8d 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -13,9 +13,9 @@ impl Bcd { /// - `m` is the minor version (1 nibble) /// - `n` is the subminor version (1 nibble) pub const fn jjmn(&self) -> (u8, u8, u8) { - let jj = (self.0 >> 12) as u8; - let m = ((self.0 >> 8) & 0xF) as u8; - let n = ((self.0 >> 4) & 0xF) as u8; + let jj = (self.0 >> 8) as u8; + let m = ((self.0 >> 4) & 0xF) as u8; + let n = (self.0 & 0xF) as u8; (jj, m, n) } } @@ -24,3 +24,14 @@ impl Bcd { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ProductId(pub u16); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bcd_jjmn() { + assert_eq!(Bcd(0x1234).jjmn(), (0x12, 0x3, 0x4)); + assert_eq!(Bcd(0xFEDC).jjmn(), (0xFE, 0xD, 0xC)); + } +} From 66387a81690ebf1370fd94e9a70be7d7b5c9631c Mon Sep 17 00:00:00 2001 From: Adam Sasine Date: Mon, 27 Apr 2026 11:02:29 -0700 Subject: [PATCH 26/26] Add doc comment to field --- src/vdm/structured/command/discover_identity/vpd_vdo.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vdm/structured/command/discover_identity/vpd_vdo.rs b/src/vdm/structured/command/discover_identity/vpd_vdo.rs index e07fa83..36ceafd 100644 --- a/src/vdm/structured/command/discover_identity/vpd_vdo.rs +++ b/src/vdm/structured/command/discover_identity/vpd_vdo.rs @@ -20,6 +20,8 @@ pub struct VpdVdo { /// The impedance through `VBUS` the VPD adds in series between the Source and Sink. pub vbus_impedance: VbusImpedance, + + /// The level of current the VPD can pass through when Charge Through is active. pub charge_through_current_support: ChargeThroughCurrentSupport, /// The maximum voltage that a Sink shall negotiate through the VPD Charge Through