diff --git a/Cargo.lock b/Cargo.lock index 9d0bc5a65..fbfa491d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,7 +828,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.0#0061a1e94a25c8db33ac1e8a0bb5a6b638fe2cd7" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1#b9c9ca3b87fb5c0a505071ecd26e0e63b0cddb51" dependencies = [ "aquamarine", "bincode", diff --git a/Cargo.toml b/Cargo.toml index bad506d0f..b20132d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,7 @@ embedded-hal = "1.0" embedded-hal-async = "1.0" embedded-services = { path = "./embedded-service" } embedded-storage-async = "0.4.1" -embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd", tag = "v0.1.0", default-features = false } +embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd", tag = "v0.1.1", default-features = false } fw-update-interface = { path = "./fw-update-interface" } fw-update-interface-test-mocks = { path = "./fw-update-interface-test-mocks" } generic-array = "1.4.1" diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index e33d0f4b2..f6de9a8c1 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -706,6 +706,18 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "embedded-usb-pd" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1#b9c9ca3b87fb5c0a505071ecd26e0e63b0cddb51" +dependencies = [ + "aquamarine", + "bincode", + "bitfield 0.19.4", + "defmt 0.3.100", + "embedded-hal-async", +] + [[package]] name = "espi-device" version = "0.1.0" @@ -1227,7 +1239,7 @@ dependencies = [ "embedded-cfu-protocol", "embedded-mcu-hal", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.0)", "hidi2c-target-service", "mimxrt600-fcb 0.1.0", "num_enum", @@ -1481,7 +1493,7 @@ dependencies = [ "embedded-hal-async", "embedded-io-async 0.6.1", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.0)", "fw-update-interface", "heapless", "itertools 0.14.0", @@ -1558,7 +1570,7 @@ dependencies = [ "bitfield 0.17.0", "defmt 0.3.100", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1)", "heapless", "power-policy-interface", ] @@ -1572,7 +1584,7 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1)", "fw-update-interface", "heapless", "power-policy-interface", diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index 7977e9f91..58d4c3c44 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -638,6 +638,17 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "embedded-usb-pd" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1#b9c9ca3b87fb5c0a505071ecd26e0e63b0cddb51" +dependencies = [ + "aquamarine", + "bincode", + "bitfield 0.19.4", + "embedded-hal-async", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -1267,7 +1278,7 @@ dependencies = [ "embedded-batteries-async", "embedded-cfu-protocol", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.0)", "env_logger", "log", "odp-service-common", @@ -1430,7 +1441,7 @@ version = "0.1.0" dependencies = [ "bitfield 0.17.0", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1)", "heapless", "log", "power-policy-interface", @@ -1444,7 +1455,7 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-services", - "embedded-usb-pd", + "embedded-usb-pd 0.1.0 (git+https://github.com/OpenDevicePartnership/embedded-usb-pd?tag=v0.1.1)", "fw-update-interface", "heapless", "log", diff --git a/examples/std/src/lib/type_c/mock_controller.rs b/examples/std/src/lib/type_c/mock_controller.rs index 09fe3698a..da3a8b8fb 100644 --- a/examples/std/src/lib/type_c/mock_controller.rs +++ b/examples/std/src/lib/type_c/mock_controller.rs @@ -5,14 +5,16 @@ use embedded_services::GlobalRawMutex; use embedded_services::named::Named; use embedded_usb_pd::ado::Ado; use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; -use embedded_usb_pd::{LocalPortId, PdError}; +use embedded_usb_pd::{LocalPortId, PdError, pdo}; use embedded_usb_pd::{PowerRole, type_c::Current}; use embedded_usb_pd::{type_c::ConnectionState, ucsi::lpm}; use log::{debug, info}; use power_policy_interface::capability::PowerCapability; use type_c_interface::control::dp::{DpConfig, DpPinConfig, DpStatus}; -use type_c_interface::control::pd::{PdStateMachineConfig, PortStatus}; +use type_c_interface::control::pd::{ + PdSinkInfo, PdSourceInfo, PdStateMachineConfig, PortStatus, SinkContract, SourceContract, +}; use type_c_interface::control::power::SystemPowerState; use type_c_interface::control::retimer::RetimerFwUpdateState; use type_c_interface::control::svid::DiscoveredSvids; @@ -55,13 +57,45 @@ impl ControllerState { let mut events = PortEventBitfield::none(); match role { PowerRole::Source => { - status.available_source_contract = Some(capability); - status.unconstrained_power = unconstrained; + let pdo = pdo::source::Pdo::Fixed(pdo::source::FixedData { + voltage_mv: capability.voltage_mv, + current_ma: capability.current_ma, + ..Default::default() + }); + status.available_source_contract = Some(SourceContract { + capability, + pd: pdo::Rdo::for_pdo(0, pdo).map(|rdo| PdSourceInfo { + rx_fixed_5v_data: Some(pdo::sink::FixedData { + voltage_mv: capability.voltage_mv, + operational_current_ma: capability.current_ma, + unconstrained_power: unconstrained, + ..Default::default() + }), + pdo, + rdo, + }), + }); events.status.set_new_power_contract_as_provider(true); } PowerRole::Sink => { - status.available_sink_contract = Some(capability); - status.unconstrained_power = unconstrained; + let pdo = pdo::sink::Pdo::Fixed(pdo::sink::FixedData { + voltage_mv: capability.voltage_mv, + operational_current_ma: capability.current_ma, + ..Default::default() + }); + status.available_sink_contract = Some(SinkContract { + capability, + pd: pdo::Rdo::for_pdo(0, pdo).map(|rdo| PdSinkInfo { + rx_fixed_5v_data: pdo::source::FixedData { + voltage_mv: capability.voltage_mv, + current_ma: capability.current_ma, + unconstrained_power: unconstrained, + ..Default::default() + }, + pdo, + rdo, + }), + }); events.status.set_new_power_contract_as_consumer(true); events.status.set_sink_ready(true); } diff --git a/type-c-interface-mocks/src/port/mod.rs b/type-c-interface-mocks/src/port/mod.rs index a8e5139f6..b2fc95550 100644 --- a/type-c-interface-mocks/src/port/mod.rs +++ b/type-c-interface-mocks/src/port/mod.rs @@ -7,14 +7,14 @@ use embedded_services::error; use embedded_services::event::NonBlockingSender; use embedded_services::named::Named; use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; -use embedded_usb_pd::{PdError, PlugOrientation, PowerRole, ado::Ado, type_c::ConnectionState}; +use embedded_usb_pd::{PdError, PlugOrientation, PowerRole, ado::Ado, pdo, type_c::ConnectionState}; use power_policy_interface::capability::{ ConsumerFlags, ConsumerPowerCapability, PowerCapability, ProviderFlags, ProviderPowerCapability, PsuType, }; use power_policy_interface::psu::{Error as PsuError, Psu, State}; use type_c_interface::control::{ dp::{DpConfig, DpStatus}, - pd::PortStatus, + pd::{PdSinkInfo, PdSourceInfo, PortStatus, SinkContract, SourceContract}, svid::DiscoveredSvids, tbt::TbtConfig, usb::UsbControlConfig, @@ -100,7 +100,6 @@ where ) -> Result<(), PsuError> { let mut status = PortStatus::new(); status.connection_state = Some(ConnectionState::Attached); - status.dual_power = config.dual_power; status.plug_orientation = config.plug_orientation; status.power_role = role; self.psu_state.attach()?; @@ -121,7 +120,24 @@ where PowerRole::Sink => { status_event.set_new_power_contract_as_consumer(true); status_event.set_sink_ready(true); - status.available_sink_contract = Some(capability); + let pdo = pdo::sink::Pdo::Fixed(pdo::sink::FixedData { + voltage_mv: capability.voltage_mv, + operational_current_ma: capability.current_ma, + ..Default::default() + }); + status.available_sink_contract = Some(SinkContract { + capability, + pd: pdo::Rdo::for_pdo(0, pdo).map(|rdo| PdSinkInfo { + rx_fixed_5v_data: pdo::source::FixedData { + dual_role_power: config.dual_power, + voltage_mv: capability.voltage_mv, + current_ma: capability.current_ma, + ..Default::default() + }, + pdo, + rdo, + }), + }); self.psu_state .update_consumer_power_capability(Some(ConsumerPowerCapability { capability, @@ -133,7 +149,24 @@ where } PowerRole::Source => { status_event.set_new_power_contract_as_provider(true); - status.available_source_contract = Some(capability); + let pdo = pdo::source::Pdo::Fixed(pdo::source::FixedData { + voltage_mv: capability.voltage_mv, + current_ma: capability.current_ma, + ..Default::default() + }); + status.available_source_contract = Some(SourceContract { + capability, + pd: pdo::Rdo::for_pdo(0, pdo).map(|rdo| PdSourceInfo { + rx_fixed_5v_data: Some(pdo::sink::FixedData { + dual_role_power: config.dual_power, + voltage_mv: capability.voltage_mv, + operational_current_ma: capability.current_ma, + ..Default::default() + }), + pdo, + rdo, + }), + }); self.psu_state .update_requested_provider_power_capability(Some(ProviderPowerCapability { capability, @@ -240,7 +273,9 @@ where } fn reports_unconstrained_power(&self) -> bool { - self.status.available_sink_contract.is_some() && self.status.unconstrained_power + self.status + .available_sink_contract + .is_some_and(|contract| contract.unconstrained_power()) } async fn get_other_vdm(&mut self) -> Result { diff --git a/type-c-interface-mocks/tests/connect_disconnect.rs b/type-c-interface-mocks/tests/connect_disconnect.rs index cb388851e..4289aea12 100644 --- a/type-c-interface-mocks/tests/connect_disconnect.rs +++ b/type-c-interface-mocks/tests/connect_disconnect.rs @@ -49,9 +49,18 @@ async fn test_plug_sink_broadcasts_events() { assert!(!data.previous_status.is_connected()); assert!(data.current_status.is_connected()); assert_eq!(data.current_status.connection_state, Some(ConnectionState::Attached)); - assert_eq!(data.current_status.available_sink_contract, Some(TEST_CAPABILITY)); + assert_eq!( + data.current_status + .available_sink_contract + .map(|contract| contract.capability), + Some(TEST_CAPABILITY) + ); assert_eq!(data.current_status.power_role, PowerRole::Sink); - assert!(data.current_status.dual_power); + assert!( + data.current_status + .available_sink_contract + .is_some_and(|contract| contract.dual_role_power()) + ); assert_eq!(data.current_status.plug_orientation, PlugOrientation::CC2); assert!(type_c_channel.try_receive().is_err()); @@ -90,7 +99,12 @@ async fn test_plug_source_broadcasts_events() { }; assert!(data.status_event.plug_inserted_or_removed()); assert!(data.status_event.new_power_contract_as_provider()); - assert_eq!(data.current_status.available_source_contract, Some(TEST_CAPABILITY)); + assert_eq!( + data.current_status + .available_source_contract + .map(|contract| contract.capability), + Some(TEST_CAPABILITY) + ); assert_eq!(data.current_status.power_role, PowerRole::Source); // State should be Idle since power policy hasn't directed us to connect yet diff --git a/type-c-interface/src/control/pd.rs b/type-c-interface/src/control/pd.rs index db34072cd..56cabb25f 100644 --- a/type-c-interface/src/control/pd.rs +++ b/type-c-interface/src/control/pd.rs @@ -3,21 +3,165 @@ use embedded_usb_pd::{ DataRole, PlugOrientation, PowerRole, pdinfo::{AltMode, PowerPathStatus}, + pdo::{self, sink::FrsRequiredCurrent}, type_c::ConnectionState, }; +use power_policy_interface::capability::PowerCapability; + +/// Information about the negotiated source contract from PD +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PdSourceInfo { + /// Received 5V sink PDO data from port partner + /// + /// Contains various flags that aren't present in other PDOs + pub rx_fixed_5v_data: Option, + /// PDO associated with this contract + pub pdo: pdo::source::Pdo, + /// RDO associated with this contract + pub rdo: pdo::Rdo, +} + +/// Source contract +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SourceContract { + /// Power capability + pub capability: PowerCapability, + /// PD contract information, if any + pub pd: Option, +} + +impl SourceContract { + /// Create a source contract without PD-specific information + pub const fn from_capability(capability: PowerCapability) -> Self { + Self { capability, pd: None } + } + + /// Returns true if the port partner has dual role power capability + pub fn dual_role_power(&self) -> bool { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.dual_role_power)) + .unwrap_or(false) + } + + /// Returns true if the port partner has higher capability + pub fn higher_capability(&self) -> bool { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.higher_capability)) + .unwrap_or(false) + } + + /// Returns true if the port partner has unconstrained power + pub fn unconstrained_power(&self) -> bool { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.unconstrained_power)) + .unwrap_or(false) + } + + /// Returns true if the port partner is USB comms capable + pub fn usb_comms_capable(&self) -> bool { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.usb_comms_capable)) + .unwrap_or(false) + } + + /// Returns true if the port partner has dual role data capability + pub fn dual_role_data(&self) -> bool { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.dual_role_data)) + .unwrap_or(false) + } + + /// Returns required FRS current for the port partner, if supported + pub fn frs_required_current(&self) -> Option { + self.pd + .and_then(|pd| pd.rx_fixed_5v_data.map(|data| data.frs_required_current)) + } +} + +/// Information about the negotiated sink contract from PD +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PdSinkInfo { + /// Received 5V source PDO data from port partner + /// + /// Contains various flags that aren't present in other PDOs + pub rx_fixed_5v_data: pdo::source::FixedData, + /// PDO associated with this contract + pub pdo: pdo::sink::Pdo, + /// RDO associated with this contract + pub rdo: pdo::Rdo, +} + +/// Sink contract +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SinkContract { + /// Power capability + pub capability: PowerCapability, + /// PD contract information, if any + pub pd: Option, +} + +impl SinkContract { + /// Create a sink contract without PD-specific information + pub const fn from_capability(capability: PowerCapability) -> Self { + Self { capability, pd: None } + } + + /// Returns true if the port partner has dual role power capability + pub fn dual_role_power(&self) -> bool { + self.pd.map(|pd| pd.rx_fixed_5v_data.dual_role_power).unwrap_or(false) + } + + /// Returns true if the port partner has USB suspend supported + pub fn usb_suspend_supported(&self) -> bool { + self.pd + .map(|pd| pd.rx_fixed_5v_data.usb_suspend_supported) + .unwrap_or(false) + } + + /// Returns true if the port partner has unconstrained power + pub fn unconstrained_power(&self) -> bool { + self.pd + .map(|pd| pd.rx_fixed_5v_data.unconstrained_power) + .unwrap_or(false) + } + + /// Returns true if the port partner is USB comms capable + pub fn usb_comms_capable(&self) -> bool { + self.pd.map(|pd| pd.rx_fixed_5v_data.usb_comms_capable).unwrap_or(false) + } + + /// Returns true if the port partner has dual role data capability + pub fn dual_role_data(&self) -> bool { + self.pd.map(|pd| pd.rx_fixed_5v_data.dual_role_data).unwrap_or(false) + } + + /// Returns true if the port partner has unchunked extended messages support + pub fn unchunked_extended_messages_support(&self) -> bool { + self.pd + .map(|pd| pd.rx_fixed_5v_data.unchunked_extended_messages_support) + .unwrap_or(false) + } + + /// Returns true if the port partner is EPR capable + pub fn epr_capable(&self) -> bool { + self.pd.map(|pd| pd.rx_fixed_5v_data.epr_capable).unwrap_or(false) + } +} /// Port status #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PortStatus { /// Current available source contract - pub available_source_contract: Option, + pub available_source_contract: Option, /// Current available sink contract - pub available_sink_contract: Option, + pub available_sink_contract: Option, /// Current connection state pub connection_state: Option, - /// Port partner supports dual-power roles - pub dual_power: bool, /// plug orientation pub plug_orientation: PlugOrientation, /// power role @@ -28,10 +172,6 @@ pub struct PortStatus { pub alt_mode: AltMode, /// Power path status pub power_path: PowerPathStatus, - /// EPR mode active - pub epr: bool, - /// Port partner is unconstrained - pub unconstrained_power: bool, } impl PortStatus { @@ -42,14 +182,11 @@ impl PortStatus { available_source_contract: None, available_sink_contract: None, connection_state: None, - dual_power: false, plug_orientation: PlugOrientation::CC1, power_role: PowerRole::Sink, data_role: DataRole::Dfp, alt_mode: AltMode::none(), power_path: PowerPathStatus::none(), - epr: false, - unconstrained_power: false, } } diff --git a/type-c-interface/src/port/pd.rs b/type-c-interface/src/port/pd.rs index bf5902c22..7f51953a1 100644 --- a/type-c-interface/src/port/pd.rs +++ b/type-c-interface/src/port/pd.rs @@ -31,7 +31,7 @@ pub trait Pd: Named { /// Returns whether this port reports unconstrained power to the system. /// /// This is the port's own determination and can differ from - /// [`PortStatus::unconstrained_power`], which is what the partner reports in its PDO. + /// [`PortStatus`], which is what the partner reports in its PDO. fn reports_unconstrained_power(&self) -> bool; /// Get the Rx Other VDM data for this port diff --git a/type-c-service/src/controller/max_sink_voltage.rs b/type-c-service/src/controller/max_sink_voltage.rs index c2a280dfe..ba13f2113 100644 --- a/type-c-service/src/controller/max_sink_voltage.rs +++ b/type-c-service/src/controller/max_sink_voltage.rs @@ -43,8 +43,12 @@ impl< { let mut shared_state = self.shared_state.lock().await; if shared_state.sink_ready_deadline.is_none() { + let epr_capable = self + .status + .available_sink_contract + .is_some_and(|contract| contract.epr_capable()); shared_state.sink_ready_deadline = - Some(Instant::now() + Self::check_sink_ready_timeout_duration(self.status.epr)); + Some(Instant::now() + Self::check_sink_ready_timeout_duration(epr_capable)); } if self diff --git a/type-c-service/src/controller/power.rs b/type-c-service/src/controller/power.rs index 8e7811f62..11c501c5e 100644 --- a/type-c-service/src/controller/power.rs +++ b/type-c-service/src/controller/power.rs @@ -32,8 +32,8 @@ impl< }; match self.config.unconstrained_sink { - UnconstrainedSink::Auto => status.unconstrained_power, - UnconstrainedSink::PowerThresholdMilliwatts(threshold) => contract.max_power_mw() >= threshold, + UnconstrainedSink::Auto => contract.unconstrained_power(), + UnconstrainedSink::PowerThresholdMilliwatts(threshold) => contract.capability.max_power_mw() >= threshold, UnconstrainedSink::Never => false, } } @@ -42,8 +42,8 @@ impl< pub(super) async fn process_new_consumer_contract(&mut self, new_status: &PortStatus) -> Result<(), PdError> { info!("Process new consumer contract"); let unconstrained = self.is_unconstrained_sink(new_status); - let available_sink_contract = new_status.available_sink_contract.map(|c| { - let mut c: ConsumerPowerCapability = c.into(); + let available_sink_contract = new_status.available_sink_contract.map(|contract| { + let mut c: ConsumerPowerCapability = contract.capability.into(); c.flags.unconstrained_power = unconstrained; c.flags.psu_type = Some(PsuType::TypeC); c @@ -70,8 +70,8 @@ impl< /// Handle a new contract as provider pub(super) async fn process_new_provider_contract(&mut self, new_status: &PortStatus) -> Result<(), PdError> { info!("Process New provider contract"); - let capability = new_status.available_source_contract.map(|caps| { - let mut caps = ProviderPowerCapability::from(caps); + let capability = new_status.available_source_contract.map(|contract| { + let mut caps = ProviderPowerCapability::from(contract.capability); caps.flags.psu_type = Some(PsuType::TypeC); caps }); @@ -178,9 +178,9 @@ impl< } /// Returns the timeout duration for the sink ready check. - pub(super) fn check_sink_ready_timeout_duration(is_epr: bool) -> Duration { + pub(super) fn check_sink_ready_timeout_duration(epr_capable: bool) -> Duration { Duration::from_millis( - (if is_epr { + (if epr_capable { T_PS_TRANSITION_EPR_MS } else { T_PS_TRANSITION_SPR_MS @@ -215,9 +215,13 @@ impl< if new_contract && !sink_ready && contract_changed { // Start the timeout // Double the spec maximum transition time to provide a safety margin for hardware/controller delays or out-of-spec controllers. - let timeout = Self::check_sink_ready_timeout_duration(new_status.epr); + let timeout = Self::check_sink_ready_timeout_duration( + new_status + .available_sink_contract + .is_some_and(|contract| contract.epr_capable()), + ); - debug!("({}): Sink ready timeout started for {}ms", self.name, timeout); + debug!("({}): Sink ready timeout started for {:?}", self.name, timeout); *deadline = Some(Instant::now() + timeout); } else if deadline.is_some() && (!new_status.is_connected() || new_status.available_sink_contract.is_none() || sink_ready) diff --git a/type-c-service/src/service/ucsi.rs b/type-c-service/src/service/ucsi.rs index 6083aeb2f..c8f2022d9 100644 --- a/type-c-service/src/service/ucsi.rs +++ b/type-c-service/src/service/ucsi.rs @@ -90,7 +90,7 @@ impl<'port, Reg: Registration<'port>> Service<'port, Reg> { // when new type-C PSUs are attached let power_mw = port_status .available_sink_contract - .map(|contract| contract.max_power_mw()) + .map(|contract| contract.capability.max_power_mw()) .unwrap_or(0); Some(self.config.ucsi_battery_charging_config.status_of(power_mw)) diff --git a/type-c-service/tests/debug_accessory.rs b/type-c-service/tests/debug_accessory.rs index 1a012fcde..a51f9d8de 100644 --- a/type-c-service/tests/debug_accessory.rs +++ b/type-c-service/tests/debug_accessory.rs @@ -13,7 +13,7 @@ use power_policy_interface::{ service::event::Event as PowerPolicyEvent, }; use type_c_interface::{ - control::pd::PortStatus, + control::pd::{PortStatus, SourceContract}, port::event::{PortEventBitfield, PortStatusEventBitfield}, service::event::{DebugAccessoryData, EventData as TypeCEventData}, util::POWER_CAPABILITY_USB_DEFAULT_USB2, @@ -81,7 +81,7 @@ async fn simulate_interrupt(port: &mut TestPort<'_, '_>, status: PortStatus, sta /// Port status of a debug accessory sourcing USB default current. const DEBUG_ACCESSORY_SOURCE_STATUS: PortStatus = PortStatus { - available_source_contract: Some(POWER_CAPABILITY_USB_DEFAULT_USB2), + available_source_contract: Some(SourceContract::from_capability(POWER_CAPABILITY_USB_DEFAULT_USB2)), connection_state: Some(ConnectionState::DebugAccessory), power_role: PowerRole::Source, ..PortStatus::new() diff --git a/type-c-service/tests/power.rs b/type-c-service/tests/power.rs index ba6a90bbe..30470688c 100644 --- a/type-c-service/tests/power.rs +++ b/type-c-service/tests/power.rs @@ -8,6 +8,7 @@ use embedded_services::info; use embedded_usb_pd::{ PowerRole, constants::{T_PS_TRANSITION_EPR_MS, T_PS_TRANSITION_SPR_MS}, + pdo, type_c::ConnectionState, }; use power_policy_interface::{ @@ -19,9 +20,11 @@ use power_policy_interface::{ service::event::Event as PowerPolicyEvent, }; use type_c_interface::{ - control::pd::PortStatus, - port::event::{PortEvent, PortEventBitfield, PortStatusEventBitfield}, - port::max_sink_voltage::MaxSinkVoltage, + control::pd::{PdSinkInfo, PortStatus, SinkContract, SourceContract}, + port::{ + event::{PortEvent, PortEventBitfield, PortStatusEventBitfield}, + max_sink_voltage::MaxSinkVoltage, + }, util::POWER_CAPABILITY_5V_1A5, }; use type_c_interface_test_mocks::controller::{ @@ -52,7 +55,7 @@ impl Test for TestBasicConsumerFlow { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -177,7 +180,7 @@ impl Test for TestBasicProviderFlow { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_source_contract: Some(POWER_CAPABILITY_5V_1A5), + available_source_contract: Some(SourceContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Source, ..Default::default() @@ -307,14 +310,14 @@ impl Test for TestConsumerFlowTimerSinkReady { let mut mock0 = mock.lock().await; // Plug: report a connected sink so the port begins the consumer-attach flow. mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() })); // Timer-driven sink-ready poll: still a connected sink, which completes the contract. mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -362,7 +365,9 @@ impl Test for TestConsumerFlowTimerSinkReady { // The connect must have waited for the sink-ready timer to elapse, proving it was // timer-driven rather than an immediate hardware sink-ready event. assert!( - elapsed >= Duration::from_millis(T_PS_TRANSITION_SPR_MS.maximum.0 as u64), + elapsed >= Duration::from_millis(T_PS_TRANSITION_SPR_MS.maximum.0 as u64) + // Sink ready timeout uses 2x the maximum to allow for plenty of time + && elapsed < Duration::from_millis(2 * T_PS_TRANSITION_EPR_MS.maximum.0 as u64), "consumer connected before the sink-ready timer could elapse: {}ms", elapsed.as_millis() ); @@ -425,6 +430,115 @@ impl Test for TestConsumerFlowTimerSinkReady { } } +/// Test that the sink ready timeout is correctly longer for EPR (Extended Power Range) sinks. +struct TestConsumerFlowTimerSinkReadyEPR; + +impl Test for TestConsumerFlowTimerSinkReadyEPR { + async fn run<'port, 'ch>( + &mut self, + _type_c_receiver: TypeCServiceReceiver<'port, 'ch>, + power_policy_receiver: PowerPolicyServiceReceiver<'port, 'ch>, + port0: TestPort<'port, 'ch>, + _port1: TestPort<'port, 'ch>, + _port2: TestPort<'port, 'ch>, + ) { + let TestPort { + port, + mock, + shared_state, + interrupt_sender, + mut event_receiver, + } = port0; + + let available_sink_contract = Some(SinkContract { + capability: POWER_CAPABILITY_5V_1A5, + pd: Some(PdSinkInfo { + rx_fixed_5v_data: pdo::source::FixedData { + current_ma: 1500, + voltage_mv: 5000, + epr_capable: true, + ..Default::default() + }, + pdo: pdo::sink::Pdo::Fixed(pdo::sink::FixedData { + operational_current_ma: 1500, + voltage_mv: 5000, + ..Default::default() + }), + rdo: pdo::Rdo::Fixed(pdo::rdo::FixedVarData { + operating_current_ma: 1500, + ..Default::default() + }), + }), + }); + + { + // Queue the controller's status responses in call order. No hardware sink-ready event + // is ever raised, so the sink-ready poll below is driven entirely by the software timer. + let mut mock0 = mock.lock().await; + // Plug: report a connected sink so the port begins the consumer-attach flow. + mock0.next_result_get_port_status.push_back(Ok(PortStatus { + available_sink_contract, + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Sink, + ..Default::default() + })); + // Timer-driven sink-ready poll: still a connected sink, which completes the contract. + mock0.next_result_get_port_status.push_back(Ok(PortStatus { + available_sink_contract, + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Sink, + ..Default::default() + })); + // Unplug: report a detached/default status so the consumer disconnects. + mock0.next_result_get_port_status.push_back(Ok(Default::default())); + // Sink path is enabled when the power policy connects the consumer. + mock0.next_result_enable_sink_path.push_back(Ok(())); + } + + info!("Starting test: consumer flow with software sink-ready timeout"); + // Initially detached with no pending sink-ready timeout. + assert_eq!(port.lock().await.state().psu_state, PsuState::Detached); + assert!(shared_state.lock().await.sink_ready_deadline().is_none()); + info!("Starting test: consumer flow with software sink-ready timeout"); + + let start = Instant::now(); + + // Plug in with a new consumer contract but WITHOUT a hardware sink-ready event. + let mut interrupt = PortEventBitfield::none(); + interrupt.status.set_plug_inserted_or_removed(true); + interrupt.status.set_new_power_contract_as_consumer(true); + info!("Sending plug interrupt to port"); + interrupt_sender.send(interrupt).await; + + // Drive the receiver manually so the intermediate state is observable before the timer + // fires. This first event is the plug interrupt that was just sent. + info!("Waiting for first event from event receiver"); + let event = event_receiver.wait_event().await; + info!("Received first event from event receiver: {:?}", event); + port.lock().await.process_event(event).await.unwrap(); + + // The port is attached but not consuming yet, the sink-ready timeout is armed, and no + // consumer connection has been broadcast to the power policy. + assert_eq!(port.lock().await.state().psu_state, PsuState::Idle); + assert!(shared_state.lock().await.sink_ready_deadline().is_some()); + assert!(power_policy_receiver.try_receive().is_err()); + + // The next event is synthesized *inside* `wait_event` by a real timer; nothing in this test + // injects a sink-ready event. This call blocks until that timer elapses. + let event = event_receiver.wait_event().await; + let elapsed = start.elapsed(); + port.lock().await.process_event(event).await.unwrap(); + + // The connect must have waited for the sink-ready timer to elapse, proving it was + // timer-driven rather than an immediate hardware sink-ready event. + assert!( + elapsed >= Duration::from_millis(T_PS_TRANSITION_EPR_MS.maximum.0 as u64), + "consumer connected before the sink-ready timer could elapse: {}ms", + elapsed.as_millis() + ); + } +} + /// Test that changing the max sink voltage while a consumer is connected disables the sink path and /// notifies the power policy, which broadcasts a `ConsumerDisconnected` event with the manual /// renegotiation reason. Setting the same voltage should do neither. @@ -443,7 +557,7 @@ impl Test for TestSinkDisableOnVoltageChange { { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -574,7 +688,7 @@ impl Test for TestSetMaxVoltageSinkReadyDeadlineInvalidation { let mut mock = mock.lock().await; mock.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -666,7 +780,7 @@ impl Test for TestSetMaxSinkVoltageRecovery { let mut mock = mock.lock().await; mock.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -749,7 +863,7 @@ impl Test for TestSetMaxSinkVoltageRecovery { let mut mock = mock.lock().await; mock.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -820,7 +934,7 @@ impl Test for TestConsumerToProviderRoleSwap { { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -918,7 +1032,7 @@ impl Test for TestConsumerToProviderRoleSwap { { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_source_contract: Some(POWER_CAPABILITY_5V_1A5), + available_source_contract: Some(SourceContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Source, ..Default::default() @@ -985,7 +1099,7 @@ impl Test for TestProviderToConsumerRoleSwap { { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_source_contract: Some(POWER_CAPABILITY_5V_1A5), + available_source_contract: Some(SourceContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Source, ..Default::default() @@ -1059,7 +1173,7 @@ impl Test for TestProviderToConsumerRoleSwap { { let mut mock0 = port0.mock.lock().await; mock0.next_result_get_port_status.push_back(Ok(PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -1118,7 +1232,7 @@ impl Test for TestHardResetDisconnect { _port2: TestPort<'port, 'ch>, ) { let connected_status = PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -1191,7 +1305,7 @@ impl Test for TestHardResetSinkReady { _port2: TestPort<'port, 'ch>, ) { let connected_status = PortStatus { - available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + available_sink_contract: Some(SinkContract::from_capability(POWER_CAPABILITY_5V_1A5)), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, ..Default::default() @@ -1250,7 +1364,10 @@ impl Test for TestProviderRecontractAfterHardReset { _port2: TestPort<'port, 'ch>, ) { let connected_status = PortStatus { - available_source_contract: Some(POWER_CAPABILITY_5V_1A5), + available_source_contract: Some(SourceContract { + capability: POWER_CAPABILITY_5V_1A5, + pd: None, + }), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Source, ..Default::default() @@ -1348,6 +1465,17 @@ async fn test_consumer_flow_timer_sink_ready() { .await; } +#[tokio::test] +async fn test_consumer_flow_timer_sink_ready_erp() { + common::run_test( + Duration::from_secs(10), + Default::default(), + Default::default(), + TestConsumerFlowTimerSinkReadyEPR, + ) + .await; +} + #[tokio::test] async fn test_sink_disable_on_voltage_change() { common::run_test( diff --git a/type-c-service/tests/unconstrained.rs b/type-c-service/tests/unconstrained.rs index e029a7131..20f1fe6e4 100644 --- a/type-c-service/tests/unconstrained.rs +++ b/type-c-service/tests/unconstrained.rs @@ -6,14 +6,14 @@ use embassy_time::{Duration, Timer, with_timeout}; use embedded_services::named::Named; -use embedded_usb_pd::{PowerRole, type_c::ConnectionState}; +use embedded_usb_pd::{PowerRole, pdo, type_c::ConnectionState}; use log::info; use power_policy_interface::{ capability::PowerCapability, service::{UnconstrainedState, event::Event as PowerPolicyEvent}, }; use type_c_interface::{ - control::pd::PortStatus, + control::pd::{PdSinkInfo, PortStatus, SinkContract}, port::event::{PortEvent, PortStatusEventBitfield}, }; use type_c_interface_test_mocks::controller::{FnCall as ControllerFnCall, pd::FnCall as PdFnCall}; @@ -53,11 +53,27 @@ const DETACHED: PortStatus = PortStatus::new(); /// Port status of an attached sink offering [`CAPABILITY`]. fn sink_status(unconstrained: bool) -> PortStatus { + let pdo = pdo::sink::Pdo::Fixed(pdo::sink::FixedData { + voltage_mv: CAPABILITY.voltage_mv, + operational_current_ma: CAPABILITY.current_ma, + ..Default::default() + }); PortStatus { - available_sink_contract: Some(CAPABILITY), + available_sink_contract: Some(SinkContract { + capability: CAPABILITY, + pd: pdo::Rdo::for_pdo(0, pdo).map(|rdo| PdSinkInfo { + rx_fixed_5v_data: pdo::source::FixedData { + voltage_mv: CAPABILITY.voltage_mv, + current_ma: CAPABILITY.current_ma, + unconstrained_power: unconstrained, + ..Default::default() + }, + pdo, + rdo, + }), + }), connection_state: Some(ConnectionState::Attached), power_role: PowerRole::Sink, - unconstrained_power: unconstrained, ..PortStatus::new() } }