diff --git a/Cargo.lock b/Cargo.lock index dd56ca124..9d0bc5a65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1530,14 +1530,12 @@ dependencies = [ name = "power-policy-interface" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "critical-section", "defmt 0.3.100", "embassy-sync", "embedded-batteries-async", "embedded-services", "log", - "num_enum", ] [[package]] diff --git a/examples/pico-de-gallo/Cargo.lock b/examples/pico-de-gallo/Cargo.lock index 6ebf7ab9c..abc098a61 100644 --- a/examples/pico-de-gallo/Cargo.lock +++ b/examples/pico-de-gallo/Cargo.lock @@ -990,12 +990,10 @@ dependencies = [ name = "power-policy-interface" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "embassy-sync", "embedded-batteries-async", "embedded-services", "log", - "num_enum", ] [[package]] diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 41eb619f1..e33d0f4b2 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -1126,12 +1126,10 @@ dependencies = [ name = "power-policy-interface" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "defmt 0.3.100", "embassy-sync", "embedded-batteries-async", "embedded-services", - "num_enum", ] [[package]] diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index 41b9d85ef..7977e9f91 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -1035,12 +1035,10 @@ dependencies = [ name = "power-policy-interface" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "embassy-sync", "embedded-batteries-async", "embedded-services", "log", - "num_enum", ] [[package]] diff --git a/examples/std/src/bin/power_policy.rs b/examples/std/src/bin/power_policy.rs index b847f4015..92146c036 100644 --- a/examples/std/src/bin/power_policy.rs +++ b/examples/std/src/bin/power_policy.rs @@ -308,7 +308,10 @@ async fn run(spawner: Spawner) { dev0.simulate_attach().await; dev0.simulate_update_consumer_power_capability(Some(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none().with_unconstrained_power(), + flags: ConsumerFlags { + unconstrained_power: true, + ..Default::default() + }, })) .await; } @@ -322,7 +325,10 @@ async fn run(spawner: Spawner) { charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), Some(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none().with_unconstrained_power(), + flags: ConsumerFlags { + unconstrained_power: true, + ..Default::default() + }, }), ); } diff --git a/power-policy-interface-test-mocks/src/psu.rs b/power-policy-interface-test-mocks/src/psu.rs index 4f48d6590..a1652f687 100644 --- a/power-policy-interface-test-mocks/src/psu.rs +++ b/power-policy-interface-test-mocks/src/psu.rs @@ -4,9 +4,7 @@ use std::collections::VecDeque; use embedded_services::named::Named; use power_policy_interface::{ - capability::{ - ConsumerDisconnect, ConsumerPowerCapability, PowerCapability, ProviderFlags, ProviderPowerCapability, - }, + capability::{ConsumerPowerCapability, DisconnectFlags, PowerCapability, ProviderFlags, ProviderPowerCapability}, psu::{self, Error, Psu, State}, }; @@ -76,7 +74,7 @@ impl Mock { let capability = Some(ProviderPowerCapability { capability, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }); self.state .update_requested_provider_power_capability(capability) @@ -90,7 +88,7 @@ impl Mock { pub async fn simulate_disconnect(&mut self) { self.state.disconnect(true).unwrap(); self.notifier - .notify_disconnected(ConsumerDisconnect::none()) + .notify_disconnected(DisconnectFlags::default()) .await .unwrap(); } diff --git a/power-policy-interface/Cargo.toml b/power-policy-interface/Cargo.toml index e65a83371..b288a9198 100644 --- a/power-policy-interface/Cargo.toml +++ b/power-policy-interface/Cargo.toml @@ -15,8 +15,6 @@ workspace = true defmt = { workspace = true, optional = true } embassy-sync.workspace = true embedded-services.workspace = true -num_enum.workspace = true -bitfield.workspace = true log = { workspace = true, optional = true } embedded-batteries-async.workspace = true diff --git a/power-policy-interface/src/capability.rs b/power-policy-interface/src/capability.rs index b757e8481..ea634a6ee 100644 --- a/power-policy-interface/src/capability.rs +++ b/power-policy-interface/src/capability.rs @@ -1,6 +1,4 @@ //! Power capability definitions and related flags -use bitfield::bitfield; -use num_enum::{IntoPrimitive, TryFromPrimitive}; /// Amount of power that a device can provider or consume #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -45,7 +43,7 @@ impl From for ConsumerPowerCapability { fn from(capability: PowerCapability) -> Self { Self { capability, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), } } } @@ -64,7 +62,7 @@ impl From for ProviderPowerCapability { fn from(capability: PowerCapability) -> Self { Self { capability, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), } } } @@ -80,276 +78,73 @@ pub enum PowerCapabilityFlags { } /// PSU type -#[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] -#[num_enum(error_type(name = InvalidPsuType, constructor = InvalidPsuType))] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[repr(u8)] #[non_exhaustive] pub enum PsuType { - /// Unknown/Unspecified - Unknown, /// Type-C port TypeC, /// DC barrel jack DcJack, /// Application defined type - Custom0 = 12, + Custom0, /// Application defined type - Custom1 = 13, + Custom1, /// Application defined type - Custom2 = 14, + Custom2, /// Application defined type - Custom3 = 15, - // End to fit into 4 bits + Custom3, } -/// Conversion error for [`PsuType`] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Consumer flags +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct InvalidPsuType(pub u8); - -bitfield! { - /// Raw consumer flags bit field - #[derive(Copy, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct ConsumerFlagsRaw(u32); - impl Debug; +pub struct ConsumerFlags { /// Unconstrained power, indicates that we are drawing power from something like an outlet and not a limited source like a battery - pub bool, unconstrained_power, set_unconstrained_power: 0; + pub unconstrained_power: bool, /// PSU type - pub u8, psu_type, set_psu_type: 11, 8; + pub psu_type: Option, } -/// Type safe wrapper for consumer flags -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Provider flags +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ConsumerFlags(ConsumerFlagsRaw); - -impl ConsumerFlags { - /// Create a new consumer with no flags set - pub const fn none() -> Self { - Self(ConsumerFlagsRaw(0)) - } - - /// Builder method to set the unconstrained power flag - pub fn with_unconstrained_power(mut self) -> Self { - self.0.set_unconstrained_power(true); - self - } - - /// Check if the unconstrained power flag is set - pub fn unconstrained_power(&self) -> bool { - self.0.unconstrained_power() - } - - /// Set the unconstrained power flag - pub fn set_unconstrained_power(&mut self, value: bool) { - self.0.set_unconstrained_power(value); - } - - /// Builder method to set the PSU type - pub fn with_psu_type(mut self, value: PsuType) -> Self { - self.set_psu_type(value); - self - } - - /// Return PSU type - pub fn psu_type(&self) -> PsuType { - PsuType::try_from(self.0.psu_type()).unwrap_or(PsuType::Unknown) - } - - /// Set PSU type - pub fn set_psu_type(&mut self, value: PsuType) { - self.0.set_psu_type(value as u8); - } -} - -bitfield! { - /// Raw provider flags bit field - #[derive(Copy, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct ProviderRaw(u32); - impl Debug; +pub struct ProviderFlags { /// PSU type - pub u8, psu_type, set_psu_type: 11, 8; + pub psu_type: Option, } -/// Type safe wrapper for provider flags +/// Consumer disconnect flags #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ProviderFlags(ProviderRaw); - -impl ProviderFlags { - /// Create a new provider with no flags set - pub const fn none() -> Self { - Self(ProviderRaw(0)) - } - - /// Builder method to set the PSU type - pub fn with_psu_type(mut self, value: PsuType) -> Self { - self.set_psu_type(value); - self - } - - /// Return PSU type - pub fn psu_type(&self) -> PsuType { - PsuType::try_from(self.0.psu_type()).unwrap_or(PsuType::Unknown) - } - - /// Set PSU type - pub fn set_psu_type(&mut self, value: PsuType) { - self.0.set_psu_type(value as u8); - } -} - -bitfield! { - /// Flags for disconnect events - #[derive(Copy, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct ConsumerDisconnectRaw(u32); - impl Debug; - /// Renegotiation - /// - /// When set this flag indicates that the current consumer is attempting to negotiate a new power capability. - pub bool, renegotiation, set_renegotiation: 0; - /// Switching - /// - /// When set this flag indicates that the service is switching to a different PSU. - pub bool, switching, set_switching: 1; -} - -/// Type safe wrapper for consumer disconnect flags -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum DisconnectReason { + /// The device has been physically detached + Detached, + /// Switching to a different PSU + Switching, + /// Renegotiation triggered by the device + AutoRenegotiation, + /// Renegotiation triggered by code + ManualRenegotiation, + /// The device has changed its role + RoleSwap, + /// The device experienced a reset + Reset, +} + +impl DisconnectReason { + /// Check if the reason is a renegotiation + pub fn is_renegotiation(&self) -> bool { + matches!(self, Self::AutoRenegotiation | Self::ManualRenegotiation) + } +} + +/// Disconnection flags +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ConsumerDisconnect(ConsumerDisconnectRaw); - -impl ConsumerDisconnect { - /// Create new consumer disconnect flags with no flags set - pub const fn none() -> Self { - Self(ConsumerDisconnectRaw(0)) - } - - /// Builder method to set the renegotiation flag - pub fn with_renegotiation(mut self, value: bool) -> Self { - self.set_renegotiation(value); - self - } - - /// Set the value of the renegotiation flag - pub fn set_renegotiation(&mut self, value: bool) { - self.0.set_renegotiation(value); - } - - /// Get the value of the renegotiation flag - pub fn renegotiation(&self) -> bool { - self.0.renegotiation() - } - - /// Builder method to set the switching flag - pub fn with_switching(mut self, value: bool) -> Self { - self.set_switching(value); - self - } - - /// Set the value of the switching flag - pub fn set_switching(&mut self, value: bool) { - self.0.set_switching(value); - } - - /// Get the value of the switching flag - pub fn switching(&self) -> bool { - self.0.switching() - } -} - -impl Default for ConsumerDisconnect { - fn default() -> Self { - Self::none() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_psu_type_conversion() { - // Test valid conversions - assert_eq!(PsuType::try_from(u8::from(PsuType::TypeC)), Ok(PsuType::TypeC)); - assert_eq!(PsuType::try_from(u8::from(PsuType::DcJack)), Ok(PsuType::DcJack)); - assert_eq!(PsuType::try_from(u8::from(PsuType::Custom0)), Ok(PsuType::Custom0)); - assert_eq!(PsuType::try_from(u8::from(PsuType::Custom1)), Ok(PsuType::Custom1)); - assert_eq!(PsuType::try_from(u8::from(PsuType::Custom2)), Ok(PsuType::Custom2)); - assert_eq!(PsuType::try_from(u8::from(PsuType::Custom3)), Ok(PsuType::Custom3)); - assert_eq!(PsuType::try_from(u8::from(PsuType::Unknown)), Ok(PsuType::Unknown)); - - assert_eq!(PsuType::try_from(3), Err(InvalidPsuType(3))); - assert_eq!(PsuType::try_from(4), Err(InvalidPsuType(4))); - assert_eq!(PsuType::try_from(5), Err(InvalidPsuType(5))); - assert_eq!(PsuType::try_from(6), Err(InvalidPsuType(6))); - assert_eq!(PsuType::try_from(7), Err(InvalidPsuType(7))); - assert_eq!(PsuType::try_from(8), Err(InvalidPsuType(8))); - assert_eq!(PsuType::try_from(9), Err(InvalidPsuType(9))); - assert_eq!(PsuType::try_from(10), Err(InvalidPsuType(10))); - assert_eq!(PsuType::try_from(11), Err(InvalidPsuType(11))); - - for i in 16..=255 { - assert_eq!(PsuType::try_from(i), Err(InvalidPsuType(i))); - } - } - - #[test] - fn test_consumer_flags_unconstrained() { - let mut consumer = ConsumerFlags::none().with_unconstrained_power(); - assert_eq!(consumer.0.0, 0x1); - consumer.set_unconstrained_power(false); - assert_eq!(consumer.0.0, 0x0); - } - - #[test] - fn test_consumer_flags_psu_type() { - let mut consumer = ConsumerFlags::none().with_psu_type(PsuType::TypeC); - assert_eq!(consumer.0.0, 0x100); - consumer.set_psu_type(PsuType::Unknown); - assert_eq!(consumer.0.0, 0x0); - } - - #[test] - fn test_provider_flags_psu_type() { - let mut provider = ProviderFlags::none().with_psu_type(PsuType::TypeC); - assert_eq!(provider.0.0, 0x100); - provider.set_psu_type(PsuType::Unknown); - assert_eq!(provider.0.0, 0x0); - } - - #[test] - fn test_consumer_disconnect_renegotiation() { - let mut disconnect = ConsumerDisconnect::none().with_renegotiation(true); - assert_eq!(disconnect.0.0, 0x1); - assert!(disconnect.renegotiation()); - assert!(!disconnect.switching()); - disconnect.set_renegotiation(false); - assert_eq!(disconnect.0.0, 0x0); - assert!(!disconnect.renegotiation()); - } - - #[test] - fn test_consumer_disconnect_switching() { - let mut disconnect = ConsumerDisconnect::none().with_switching(true); - assert_eq!(disconnect.0.0, 0x2); - assert!(disconnect.switching()); - assert!(!disconnect.renegotiation()); - disconnect.set_switching(false); - assert_eq!(disconnect.0.0, 0x0); - assert!(!disconnect.switching()); - } - - #[test] - fn test_consumer_disconnect_default() { - let disconnect = ConsumerDisconnect::default(); - assert_eq!(disconnect.0.0, 0x0); - assert!(!disconnect.renegotiation()); - assert!(!disconnect.switching()); - } +pub struct DisconnectFlags { + /// Reason for the disconnect, if given + pub reason: Option, } diff --git a/power-policy-interface/src/charger/tests.rs b/power-policy-interface/src/charger/tests.rs index 518c5ea75..9fa10e7e3 100644 --- a/power-policy-interface/src/charger/tests.rs +++ b/power-policy-interface/src/charger/tests.rs @@ -4,7 +4,7 @@ use crate::capability::{ConsumerFlags, PowerCapability}; fn cap(voltage_mv: u16, current_ma: u16) -> ConsumerPowerCapability { ConsumerPowerCapability { capability: PowerCapability { voltage_mv, current_ma }, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), } } diff --git a/power-policy-interface/src/psu/event.rs b/power-policy-interface/src/psu/event.rs index 66f777722..a97b45280 100644 --- a/power-policy-interface/src/psu/event.rs +++ b/power-policy-interface/src/psu/event.rs @@ -7,7 +7,7 @@ use embedded_services::{ }; use crate::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, ProviderPowerCapability}, psu, }; @@ -23,7 +23,7 @@ pub enum EventData { /// Request the given amount of power to provider RequestedProviderCapability(Option), /// Notify that a device cannot consume or provide power anymore - Disconnected(ConsumerDisconnect), + Disconnected(DisconnectFlags), /// Notify that a device has detached Detached, } @@ -79,11 +79,11 @@ impl> crate::psu::notification::Notifier for Non fn notify_disconnected( &mut self, - flags: ConsumerDisconnect, + disconnect: DisconnectFlags, ) -> impl Future> { ready( self.0 - .try_send(EventData::Disconnected(flags)) + .try_send(EventData::Disconnected(disconnect)) .ok_or(crate::psu::notification::Error::WouldBlock), ) } @@ -130,8 +130,11 @@ impl> crate::psu::notification::Notifier for SenderNotifier Ok(()) } - async fn notify_disconnected(&mut self, flags: ConsumerDisconnect) -> Result<(), crate::psu::notification::Error> { - self.0.send(EventData::Disconnected(flags)).await; + async fn notify_disconnected( + &mut self, + disconnect: DisconnectFlags, + ) -> Result<(), crate::psu::notification::Error> { + self.0.send(EventData::Disconnected(disconnect)).await; Ok(()) } diff --git a/power-policy-interface/src/psu/notification.rs b/power-policy-interface/src/psu/notification.rs index 26c10feca..0149ec2b2 100644 --- a/power-policy-interface/src/psu/notification.rs +++ b/power-policy-interface/src/psu/notification.rs @@ -2,7 +2,7 @@ use embedded_services::sync::Lockable; -use crate::capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability}; +use crate::capability::{ConsumerPowerCapability, DisconnectFlags, ProviderPowerCapability}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -27,7 +27,7 @@ pub trait Notifier { capability: Option, ) -> impl Future>; /// Notify that a PSU has disconnected - fn notify_disconnected(&mut self, flags: ConsumerDisconnect) -> impl Future>; + fn notify_disconnected(&mut self, disconnect: DisconnectFlags) -> impl Future>; /// Notify that a PSU has detached fn notify_detached(&mut self) -> impl Future>; } @@ -54,7 +54,7 @@ pub trait NotificationHandler<'device> { fn process_notify_disconnected( &mut self, psu: &'device Self::Psu, - flags: ConsumerDisconnect, + disconnect: DisconnectFlags, ) -> impl Future>; /// Handle a notification that a PSU has detached fn process_notify_detached(&mut self, psu: &'device Self::Psu) -> impl Future>; diff --git a/power-policy-interface/src/service/event.rs b/power-policy-interface/src/service/event.rs index d88de658a..659a148a0 100644 --- a/power-policy-interface/src/service/event.rs +++ b/power-policy-interface/src/service/event.rs @@ -6,7 +6,7 @@ use embedded_services::{ }; use crate::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, ProviderPowerCapability}, psu::Psu, service::UnconstrainedState, }; @@ -21,7 +21,7 @@ use crate::{ #[non_exhaustive] pub enum EventData { /// Consumer disconnected - ConsumerDisconnected(ConsumerDisconnect), + ConsumerDisconnected(DisconnectFlags), /// Consumer connected ConsumerConnected(ConsumerPowerCapability), /// Provider disconnected @@ -38,7 +38,7 @@ where { fn from(value: Event<'device, PSU>) -> Self { match value { - Event::ConsumerDisconnected(_, flags) => EventData::ConsumerDisconnected(flags), + Event::ConsumerDisconnected(_, disconnect) => EventData::ConsumerDisconnected(disconnect), Event::ConsumerConnected(_, capability) => EventData::ConsumerConnected(capability), Event::ProviderDisconnected(_) => EventData::ProviderDisconnected, Event::ProviderConnected(_, capability) => EventData::ProviderConnected(capability), @@ -56,7 +56,7 @@ where PSU::Inner: Psu, { /// Consumer disconnected - ConsumerDisconnected(&'device PSU, ConsumerDisconnect), + ConsumerDisconnected(&'device PSU, DisconnectFlags), /// Consumer connected ConsumerConnected(&'device PSU, ConsumerPowerCapability), /// Provider disconnected @@ -116,11 +116,11 @@ impl<'device, PSU: Lockable, S: NonBlockingSender impl Future> { ready( self.sender - .try_send(Event::ConsumerDisconnected(psu, flags)) + .try_send(Event::ConsumerDisconnected(psu, disconnect)) .ok_or(crate::service::notification::Error::WouldBlock), ) } @@ -209,9 +209,9 @@ impl<'device, PSU: Lockable + 'device, S: Sender async fn notify_consumer_disconnected( &mut self, psu: &'device Self::Psu, - flags: ConsumerDisconnect, + disconnect: DisconnectFlags, ) -> Result<(), crate::service::notification::Error> { - self.sender.send(Event::ConsumerDisconnected(psu, flags)).await; + self.sender.send(Event::ConsumerDisconnected(psu, disconnect)).await; Ok(()) } diff --git a/power-policy-interface/src/service/notification.rs b/power-policy-interface/src/service/notification.rs index aea6ed115..994da4e18 100644 --- a/power-policy-interface/src/service/notification.rs +++ b/power-policy-interface/src/service/notification.rs @@ -2,7 +2,7 @@ use embedded_services::sync::Lockable; use crate::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, ProviderPowerCapability}, service::UnconstrainedState, }; @@ -22,7 +22,7 @@ pub trait Notifier<'device> { fn notify_consumer_disconnected( &mut self, psu: &'device Self::Psu, - flags: ConsumerDisconnect, + disconnect: DisconnectFlags, ) -> impl Future>; /// Notify that a consumer has been connected fn notify_consumer_connected( diff --git a/power-policy-service/src/service/consumer.rs b/power-policy-service/src/service/consumer.rs index 6823c00ea..5e820147a 100644 --- a/power-policy-service/src/service/consumer.rs +++ b/power-policy-service/src/service/consumer.rs @@ -8,7 +8,7 @@ use super::*; use power_policy_interface::psu; use power_policy_interface::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, DisconnectReason}, psu::PsuState, }; @@ -116,7 +116,7 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz let mut unconstrained_new = UnconstrainedState::default(); for psu in self.registration.psus() { if let Some(capability) = psu.lock().await.state().consumer_capability - && capability.flags.unconstrained_power() + && capability.flags.unconstrained_power { unconstrained_new.available += 1; } @@ -127,7 +127,7 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz .state .current_consumer_state .as_ref() - .is_some_and(|current| current.consumer_power_capability.flags.unconstrained_power()); + .is_some_and(|current| current.consumer_power_capability.flags.unconstrained_power); if unconstrained_new != self.state.unconstrained { info!("Unconstrained state changed: {:?}", unconstrained_new); @@ -190,7 +190,11 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz } /// Connect to a new consumer - async fn connect_new_consumer(&mut self, new_consumer: AvailableConsumer<'device, Reg::Psu>) -> Result<(), Error> { + async fn connect_new_consumer( + &mut self, + new_consumer: AvailableConsumer<'device, Reg::Psu>, + disconnect: DisconnectFlags, + ) -> Result<(), Error> { // Handle our current consumer if let Some(current_consumer) = self.state.current_consumer_state { if ptr::eq(current_consumer.psu, new_consumer.psu) @@ -217,14 +221,18 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz self.disconnect_chargers().await?; // Indicate why the current consumer is being disconnected. If we are reconnecting - // the same device, it is renegotiating a new power capability. Otherwise, the service - // is switching to a different PSU. - let flags = if ptr::eq(current_consumer.psu, new_consumer.psu) { - ConsumerDisconnect::none().with_renegotiation(true) + // the same device, it is renegotiating a new power capability. If no specific reason is given, + // then the power policy implementation initiated a switch to a different device + let reason = if ptr::eq(current_consumer.psu, new_consumer.psu) { + DisconnectReason::AutoRenegotiation + } else if let Some(reason) = disconnect.reason { + reason } else { - ConsumerDisconnect::none().with_switching(true) + DisconnectReason::Switching }; - self.notify_consumer_disconnected(current_consumer.psu, flags).await; + + self.notify_consumer_disconnected(current_consumer.psu, DisconnectFlags { reason: Some(reason) }) + .await; // Don't update the unconstrained here because this is a transitional state } @@ -247,11 +255,8 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz /// Determines and connects the best external power /// - /// `disconnect_flags` describes the reason for a disconnect and is applied to the - /// [`ServiceEvent::ConsumerDisconnected`] event when the current consumer is removed and not - /// replaced by another one. When switching between consumers the flags are derived from the - /// switch itself (see [`Self::connect_new_consumer`]). - pub(super) async fn update_current_consumer(&mut self, disconnect_flags: ConsumerDisconnect) -> Result<(), Error> { + /// Disconnect reason will be propagated if it's present. + pub(super) async fn update_current_consumer(&mut self, disconnect: DisconnectFlags) -> Result<(), Error> { let current_consumer_name = if let Some(current_consumer) = self.state.current_consumer_state { current_consumer.psu.lock().await.name() } else { @@ -270,12 +275,12 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz }; info!("Best consumer: {:#?}", best_consumer_name); if let Some(best_consumer) = best_consumer { - self.connect_new_consumer(best_consumer).await?; + self.connect_new_consumer(best_consumer, disconnect).await?; } else { // Notify disconnect if recently detached consumer was previously attached. if let Some(current_consumer) = self.state.current_consumer_state { self.disconnect_chargers().await?; - self.notify_consumer_disconnected(current_consumer.psu, disconnect_flags) + self.notify_consumer_disconnected(current_consumer.psu, disconnect) .await; } // No new consumer available diff --git a/power-policy-service/src/service/mod.rs b/power-policy-service/src/service/mod.rs index eeda28881..0d5c9411f 100644 --- a/power-policy-service/src/service/mod.rs +++ b/power-policy-service/src/service/mod.rs @@ -16,7 +16,7 @@ use power_policy_interface::charger::{Charger, PsuState}; use power_policy_interface::psu::notification::NotificationHandler as _; use power_policy_interface::service::notification::Notifier; use power_policy_interface::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, DisconnectReason, ProviderPowerCapability}, charger::{Event as ChargerEvent, EventData as ChargerEventData}, psu::{ Error, Psu, @@ -128,9 +128,9 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz } } - async fn notify_consumer_disconnected(&mut self, psu: &'device Reg::Psu, flags: ConsumerDisconnect) { + async fn notify_consumer_disconnected(&mut self, psu: &'device Reg::Psu, disconnect: DisconnectFlags) { for notifier in self.registration.notifiers() { - if let Err(e) = notifier.notify_consumer_disconnected(psu, flags).await { + if let Err(e) = notifier.notify_consumer_disconnected(psu, disconnect).await { error!("Failed to notify consumer disconnected: {:#?}", e); } } @@ -220,7 +220,10 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz async fn process_notify_detached(&mut self, device: &'device Reg::Psu) -> Result<(), Error> { info!("({}): Received notify detached", device.lock().await.name()); self.post_provider_removed(device).await; - self.update_current_consumer(ConsumerDisconnect::none()).await?; + self.update_current_consumer(DisconnectFlags { + reason: Some(DisconnectReason::Detached), + }) + .await?; Ok(()) } @@ -235,7 +238,7 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz capability, ); - self.update_current_consumer(ConsumerDisconnect::none()).await + self.update_current_consumer(DisconnectFlags::default()).await } async fn process_notify_requested_provider_capability( @@ -255,11 +258,11 @@ impl<'device, Reg: Registration<'device>, Customization: customization::Customiz async fn process_notify_disconnected( &mut self, device: &'device Reg::Psu, - flags: ConsumerDisconnect, + disconnect: DisconnectFlags, ) -> Result<(), Error> { info!("({}): Received notify disconnect", device.lock().await.name()); self.post_provider_removed(device).await; - self.update_current_consumer(flags).await?; + self.update_current_consumer(disconnect).await?; Ok(()) } } diff --git a/power-policy-service/tests/common/mod.rs b/power-policy-service/tests/common/mod.rs index fc253a70c..44d34fa73 100644 --- a/power-policy-service/tests/common/mod.rs +++ b/power-policy-service/tests/common/mod.rs @@ -16,7 +16,7 @@ use embassy_time::{Duration, with_timeout}; use embedded_services::GlobalRawMutex; use power_policy_interface::psu::event::EventData; use power_policy_interface::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, PowerCapability, ProviderPowerCapability}, + capability::{ConsumerPowerCapability, DisconnectFlags, PowerCapability, ProviderPowerCapability}, service::{UnconstrainedState, event::Event as ServiceEvent}, }; use power_policy_interface_test_mocks::charger::ChargerType; @@ -168,16 +168,16 @@ pub async fn assert_consumer_disconnected<'a>( assert_eq!(device as *const _, expected_device as *const _); } -pub async fn assert_consumer_disconnected_with_flags<'a>( +pub async fn assert_consumer_disconnected_with_reason<'a>( receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, expected_device: &DeviceType<'a>, - expected_flags: ConsumerDisconnect, + expected_disconnect: DisconnectFlags, ) { - let ServiceEvent::ConsumerDisconnected(device, flags) = receiver.receive().await else { + let ServiceEvent::ConsumerDisconnected(device, disconnect) = receiver.receive().await else { panic!("Expected ConsumerDisconnected event"); }; assert_eq!(device as *const _, expected_device as *const _); - assert_eq!(flags, expected_flags); + assert_eq!(disconnect, expected_disconnect); } pub async fn assert_consumer_connected<'a>( diff --git a/power-policy-service/tests/consumer.rs b/power-policy-service/tests/consumer.rs index 8e4dd7738..e218d5a6c 100644 --- a/power-policy-service/tests/consumer.rs +++ b/power-policy-service/tests/consumer.rs @@ -4,7 +4,7 @@ use embedded_services::info; use embedded_services::sync::Lockable; use power_policy_interface::capability::ProviderFlags; use power_policy_interface::capability::ProviderPowerCapability; -use power_policy_interface::capability::{ConsumerDisconnect, ConsumerFlags, ConsumerPowerCapability}; +use power_policy_interface::capability::{ConsumerFlags, ConsumerPowerCapability, DisconnectFlags, DisconnectReason}; mod common; @@ -29,7 +29,7 @@ use crate::common::assert_provider_connected; use crate::common::assert_provider_disconnected; use crate::common::{ DEFAULT_TIMEOUT, HIGH_POWER, assert_consumer_connected, assert_consumer_disconnected, - assert_consumer_disconnected_with_flags, run_test, + assert_consumer_disconnected_with_reason, run_test, }; use power_policy_interface_test_mocks::psu::FnCall; @@ -63,7 +63,7 @@ impl Test for TestSingle { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -74,7 +74,7 @@ impl Test for TestSingle { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -87,7 +87,14 @@ impl Test for TestSingle { { device0.lock().await.simulate_detach().await; - assert_consumer_disconnected(service_receiver, device0).await; + assert_consumer_disconnected_with_reason( + service_receiver, + device0, + DisconnectFlags { + reason: Some(DisconnectReason::Detached), + }, + ) + .await; // Power policy shouldn't call any functions on detach assert!(device0.lock().await.fn_calls.is_empty()); @@ -128,7 +135,7 @@ impl Test for TestSwapHigher { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -139,7 +146,7 @@ impl Test for TestSwapHigher { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -166,7 +173,7 @@ impl Test for TestSwapHigher { device1, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -182,7 +189,7 @@ impl Test for TestSwapHigher { device1.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device1.fn_calls.is_empty()); @@ -197,14 +204,21 @@ impl Test for TestSwapHigher { device1.lock().await.simulate_detach().await; // Should receive a disconnect event from device1 first - assert_consumer_disconnected(service_receiver, device1).await; + assert_consumer_disconnected_with_reason( + service_receiver, + device1, + DisconnectFlags { + reason: Some(DisconnectReason::Detached), + }, + ) + .await; assert_consumer_connected( service_receiver, device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -218,7 +232,7 @@ impl Test for TestSwapHigher { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -260,7 +274,7 @@ impl Test for TestDisconnect { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -271,7 +285,7 @@ impl Test for TestDisconnect { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -298,7 +312,7 @@ impl Test for TestDisconnect { device1, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -314,7 +328,7 @@ impl Test for TestDisconnect { device1.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device1.fn_calls.is_empty()); @@ -337,7 +351,7 @@ impl Test for TestDisconnect { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -351,7 +365,7 @@ impl Test for TestDisconnect { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -393,7 +407,7 @@ impl Test for TestDisconnectOtherConsumer { device0, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -404,7 +418,7 @@ impl Test for TestDisconnectOtherConsumer { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -477,7 +491,7 @@ impl Test for TestDisconnectOtherProvider { device0, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -488,7 +502,7 @@ impl Test for TestDisconnectOtherProvider { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -507,7 +521,7 @@ impl Test for TestDisconnectOtherProvider { device1, ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -518,7 +532,7 @@ impl Test for TestDisconnectOtherProvider { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -610,7 +624,7 @@ impl Test for TestNoSwap { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -621,7 +635,7 @@ impl Test for TestNoSwap { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -704,7 +718,7 @@ impl Test for TestFindBestConsumerCustomization { device1, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -715,7 +729,7 @@ impl Test for TestFindBestConsumerCustomization { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -741,7 +755,7 @@ impl Test for TestFindBestConsumerCustomization { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -757,7 +771,7 @@ impl Test for TestFindBestConsumerCustomization { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -767,10 +781,10 @@ impl Test for TestFindBestConsumerCustomization { } /// Test that disconnecting the current consumer to switch to a different PSU sets the -/// `switching` flag on the [`ServiceEvent::ConsumerDisconnected`] event. -struct TestConsumerDisconnectSwitchingFlag; +/// `Switching` reason on the [`ServiceEvent::ConsumerDisconnected`] event. +struct TestConsumerDisconnectSwitchingReason; -impl Test for TestConsumerDisconnectSwitchingFlag { +impl Test for TestConsumerDisconnectSwitchingReason { type Customization = DefaultCustomization; async fn run<'a>( @@ -780,7 +794,7 @@ impl Test for TestConsumerDisconnectSwitchingFlag { device0: &DeviceType<'a>, device1: &DeviceType<'a>, ) { - info!("Running test_consumer_disconnect_switching_flag"); + info!("Running test_consumer_disconnect_switching_reason"); // Connect device0 at low power. device0.lock().await.next_result_connect_consumer.push_back(Ok(())); device0 @@ -793,7 +807,7 @@ impl Test for TestConsumerDisconnectSwitchingFlag { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -804,7 +818,7 @@ impl Test for TestConsumerDisconnectSwitchingFlag { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -819,11 +833,13 @@ impl Test for TestConsumerDisconnectSwitchingFlag { .simulate_consumer_connection(HIGH_POWER.into()) .await; - // device0 should be disconnected with the switching flag set since we're switching to device1. - assert_consumer_disconnected_with_flags( + // device0 should be disconnected with the switching reason since we're switching to device1. + assert_consumer_disconnected_with_reason( service_receiver, device0, - ConsumerDisconnect::none().with_switching(true), + DisconnectFlags { + reason: Some(DisconnectReason::Switching), + }, ) .await; assert_consumer_connected( @@ -831,7 +847,7 @@ impl Test for TestConsumerDisconnectSwitchingFlag { device1, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -847,7 +863,7 @@ impl Test for TestConsumerDisconnectSwitchingFlag { device1.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device1.fn_calls.is_empty()); @@ -858,10 +874,10 @@ impl Test for TestConsumerDisconnectSwitchingFlag { } /// Test that disconnecting the current consumer because it renegotiated a new power capability -/// sets the `renegotiation` flag on the [`ServiceEvent::ConsumerDisconnected`] event. -struct TestConsumerDisconnectRenegotiationFlag; +/// sets the `AutoRenegotiation` reason on the [`ServiceEvent::ConsumerDisconnected`] event. +struct TestConsumerDisconnectRenegotiationReason; -impl Test for TestConsumerDisconnectRenegotiationFlag { +impl Test for TestConsumerDisconnectRenegotiationReason { type Customization = DefaultCustomization; async fn run<'a>( @@ -871,7 +887,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { device0: &DeviceType<'a>, _device1: &DeviceType<'a>, ) { - info!("Running test_consumer_disconnect_renegotiation_flag"); + info!("Running test_consumer_disconnect_renegotiation_reason"); // Connect device0 at low power. device0.lock().await.next_result_connect_consumer.push_back(Ok(())); device0 @@ -884,7 +900,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -895,7 +911,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -903,7 +919,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { // The same device renegotiates a new (higher) power capability. Since the best consumer is // still the same device but with a different capability, the service disconnects and - // reconnects it. The disconnect event should carry the renegotiation flag. + // reconnects it. The disconnect event should carry the automatic renegotiation reason. device0.lock().await.next_result_disconnect.push_back(Ok(())); device0.lock().await.next_result_connect_consumer.push_back(Ok(())); device0 @@ -912,10 +928,12 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { .simulate_update_consumer_power_capability(Some(HIGH_POWER.into())) .await; - assert_consumer_disconnected_with_flags( + assert_consumer_disconnected_with_reason( service_receiver, device0, - ConsumerDisconnect::none().with_renegotiation(true), + DisconnectFlags { + reason: Some(DisconnectReason::AutoRenegotiation), + }, ) .await; assert_consumer_connected( @@ -923,7 +941,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { device0, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -935,7 +953,7 @@ impl Test for TestConsumerDisconnectRenegotiationFlag { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); @@ -1019,10 +1037,10 @@ async fn run_test_find_best_consumer_hook() { } #[tokio::test] -async fn run_test_consumer_disconnect_switching_flag() { +async fn run_test_consumer_disconnect_switching_reason() { run_test( DEFAULT_TIMEOUT, - TestConsumerDisconnectSwitchingFlag, + TestConsumerDisconnectSwitchingReason, Default::default(), DefaultCustomization, ) @@ -1030,10 +1048,10 @@ async fn run_test_consumer_disconnect_switching_flag() { } #[tokio::test] -async fn run_test_consumer_disconnect_renegotiation_flag() { +async fn run_test_consumer_disconnect_renegotiation_reason() { run_test( DEFAULT_TIMEOUT, - TestConsumerDisconnectRenegotiationFlag, + TestConsumerDisconnectRenegotiationReason, Default::default(), DefaultCustomization, ) diff --git a/power-policy-service/tests/provider.rs b/power-policy-service/tests/provider.rs index 9533c9b70..a7057de43 100644 --- a/power-policy-service/tests/provider.rs +++ b/power-policy-service/tests/provider.rs @@ -41,7 +41,7 @@ impl Test for TestSingle { device0, ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -52,7 +52,7 @@ impl Test for TestSingle { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -96,7 +96,7 @@ impl Test for TestUpgrade { device0, ProviderPowerCapability { capability: HIGH_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -107,7 +107,7 @@ impl Test for TestUpgrade { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: HIGH_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -126,7 +126,7 @@ impl Test for TestUpgrade { device1, ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -137,7 +137,7 @@ impl Test for TestUpgrade { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -161,7 +161,7 @@ impl Test for TestUpgrade { device1, ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -172,7 +172,7 @@ impl Test for TestUpgrade { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -207,7 +207,7 @@ impl Test for TestUpgrade { device1, ProviderPowerCapability { capability: HIGH_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -218,7 +218,7 @@ impl Test for TestUpgrade { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: HIGH_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -255,7 +255,7 @@ impl Test for TestDisconnect { device0, ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }, ) .await; @@ -266,7 +266,7 @@ impl Test for TestDisconnect { device.fn_calls.pop_front().unwrap(), FnCall::ConnectProvider(ProviderPowerCapability { capability: LOW_POWER, - flags: ProviderFlags::none(), + flags: ProviderFlags::default(), }) ); assert!(device.fn_calls.is_empty()); diff --git a/power-policy-service/tests/unconstrained.rs b/power-policy-service/tests/unconstrained.rs index a3e206d1f..3f04a7ec1 100644 --- a/power-policy-service/tests/unconstrained.rs +++ b/power-policy-service/tests/unconstrained.rs @@ -46,7 +46,7 @@ impl Test for TestUnconstrained { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -56,7 +56,7 @@ impl Test for TestUnconstrained { device.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device.fn_calls.is_empty()); @@ -74,7 +74,10 @@ impl Test for TestUnconstrained { .await .simulate_consumer_connection(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none().with_unconstrained_power(), + flags: ConsumerFlags { + unconstrained_power: true, + ..Default::default() + }, }) .await; @@ -86,7 +89,10 @@ impl Test for TestUnconstrained { device1, ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none().with_unconstrained_power(), + flags: ConsumerFlags { + unconstrained_power: true, + ..Default::default() + }, }, ) .await; @@ -111,7 +117,10 @@ impl Test for TestUnconstrained { device1.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: HIGH_POWER, - flags: ConsumerFlags::none().with_unconstrained_power(), + flags: ConsumerFlags { + unconstrained_power: true, + ..Default::default() + }, }) ); assert!(device1.fn_calls.is_empty()); @@ -131,7 +140,7 @@ impl Test for TestUnconstrained { device0, ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }, ) .await; @@ -153,7 +162,7 @@ impl Test for TestUnconstrained { device0.fn_calls.pop_front().unwrap(), FnCall::ConnectConsumer(ConsumerPowerCapability { capability: LOW_POWER, - flags: ConsumerFlags::none(), + flags: ConsumerFlags::default(), }) ); assert!(device0.fn_calls.is_empty()); diff --git a/type-c-interface-mocks/src/port/mod.rs b/type-c-interface-mocks/src/port/mod.rs index bab3bb16d..a8e5139f6 100644 --- a/type-c-interface-mocks/src/port/mod.rs +++ b/type-c-interface-mocks/src/port/mod.rs @@ -125,7 +125,10 @@ where self.psu_state .update_consumer_power_capability(Some(ConsumerPowerCapability { capability, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, }))?; } PowerRole::Source => { @@ -134,7 +137,9 @@ where self.psu_state .update_requested_provider_power_capability(Some(ProviderPowerCapability { capability, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, }))?; } } diff --git a/type-c-interface-mocks/tests/connect_disconnect.rs b/type-c-interface-mocks/tests/connect_disconnect.rs index 3222352ca..cb388851e 100644 --- a/type-c-interface-mocks/tests/connect_disconnect.rs +++ b/type-c-interface-mocks/tests/connect_disconnect.rs @@ -59,7 +59,10 @@ async fn test_plug_sink_broadcasts_events() { // But the consumer capability should have been recorded let expected_capability = ConsumerPowerCapability { capability: TEST_CAPABILITY, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, }; assert_eq!(mock.state().consumer_capability, Some(expected_capability)); assert_eq!(mock.state().psu_state, PsuState::Idle); @@ -94,7 +97,9 @@ async fn test_plug_source_broadcasts_events() { // But the requested provider capability should have been recorded let expected_capability = ProviderPowerCapability { capability: TEST_CAPABILITY, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, }; assert_eq!(mock.state().requested_provider_capability, Some(expected_capability)); assert_eq!(mock.state().psu_state, PsuState::Idle); diff --git a/type-c-service/src/controller/max_sink_voltage.rs b/type-c-service/src/controller/max_sink_voltage.rs index 17ea17c8d..c2a280dfe 100644 --- a/type-c-service/src/controller/max_sink_voltage.rs +++ b/type-c-service/src/controller/max_sink_voltage.rs @@ -2,7 +2,7 @@ use embassy_time::Instant; use embedded_services::{event::NonBlockingSender, sync::Lockable}; use embedded_usb_pd::PdError; -use power_policy_interface::capability::ConsumerDisconnect; +use power_policy_interface::capability::{DisconnectFlags, DisconnectReason}; use type_c_interface::controller::max_sink_voltage::MaxSinkVoltage; use super::*; @@ -61,14 +61,16 @@ impl< // Move our local state out of the consumer state and notify the power policy so it stops // tracking us as the active consumer and broadcasts a ConsumerDisconnected event. The - // renegotiation flag marks this as a temporary disconnect for a recontract. + // disconnect reason marks this as a temporary disconnect for a recontract. if let Err(e) = self.psu_state.disconnect(true) { error!("({}): Error updating PSU state on disconnect: {:?}", self.name, e); } if let Err(e) = self .power_policy_notifier - .notify_disconnected(ConsumerDisconnect::none().with_renegotiation(true)) + .notify_disconnected(DisconnectFlags { + reason: Some(DisconnectReason::ManualRenegotiation), + }) .await { error!("({}): Failed to notify power policy of disconnect: {:#?}", self.name, e); diff --git a/type-c-service/src/controller/mod.rs b/type-c-service/src/controller/mod.rs index 8afa153c3..e802d5f14 100644 --- a/type-c-service/src/controller/mod.rs +++ b/type-c-service/src/controller/mod.rs @@ -127,6 +127,10 @@ impl< self.process_plug_event(&new_status).await?; } + if status_event.pd_hard_reset() { + self.process_hard_reset().await?; + } + // Tear down the previous contract on a power role swap before establishing the new one if status_event.power_swap_completed() { self.process_power_role_swap(&new_status).await?; diff --git a/type-c-service/src/controller/power.rs b/type-c-service/src/controller/power.rs index d910a56b0..8e7811f62 100644 --- a/type-c-service/src/controller/power.rs +++ b/type-c-service/src/controller/power.rs @@ -6,7 +6,7 @@ use embedded_usb_pd::{ constants::{T_PS_TRANSITION_EPR_MS, T_PS_TRANSITION_SPR_MS}, }; use power_policy_interface::{ - capability::{ConsumerDisconnect, ConsumerPowerCapability, ProviderPowerCapability, PsuType}, + capability::{ConsumerPowerCapability, DisconnectFlags, DisconnectReason, ProviderPowerCapability, PsuType}, psu::{Error as PsuError, Psu, State}, }; use type_c_interface::controller::power::SystemPowerStateStatus; @@ -44,8 +44,8 @@ impl< 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(); - c.flags.set_unconstrained_power(unconstrained); - c.flags.set_psu_type(PsuType::TypeC); + c.flags.unconstrained_power = unconstrained; + c.flags.psu_type = Some(PsuType::TypeC); c }); @@ -72,7 +72,7 @@ impl< info!("Process New provider contract"); let capability = new_status.available_source_contract.map(|caps| { let mut caps = ProviderPowerCapability::from(caps); - caps.flags.set_psu_type(PsuType::TypeC); + caps.flags.psu_type = Some(PsuType::TypeC); caps }); if let Err(e) = self.psu_state.update_requested_provider_power_capability(capability) { @@ -129,7 +129,9 @@ impl< } if let Err(e) = self .power_policy_notifier - .notify_disconnected(ConsumerDisconnect::none()) + .notify_disconnected(DisconnectFlags { + reason: Some(DisconnectReason::RoleSwap), + }) .await { error!( @@ -141,6 +143,40 @@ impl< Ok(()) } + /// Tear down the active power contract after a PD hard reset. + pub(super) async fn process_hard_reset(&mut self) -> Result<(), PdError> { + // The PD controller will issue its own sink ready interrupt after the hard reset, so we clear the deadline here. + self.shared_state.lock().await.sink_ready_deadline = None; + self.status.available_sink_contract = None; + self.status.available_source_contract = None; + + if !matches!( + self.psu_state.psu_state, + PsuState::ConnectedConsumer(_) | PsuState::ConnectedProvider(_) + ) { + return Ok(()); + } + + info!("({}): PD hard reset, tearing down power contract", self.name); + if let Err(e) = self.psu_state.disconnect(true) { + error!("({}): Error updating PSU state after hard reset: {:?}", self.name, e); + } + if let Err(e) = self + .power_policy_notifier + .notify_disconnected(DisconnectFlags { + reason: Some(DisconnectReason::Reset), + }) + .await + { + error!( + "({}): Failed to notify power policy of hard reset disconnect: {:#?}", + self.name, e + ); + } + + Ok(()) + } + /// Returns the timeout duration for the sink ready check. pub(super) fn check_sink_ready_timeout_duration(is_epr: bool) -> Duration { Duration::from_millis( diff --git a/type-c-service/tests/debug_accessory.rs b/type-c-service/tests/debug_accessory.rs index f83c61da1..1a012fcde 100644 --- a/type-c-service/tests/debug_accessory.rs +++ b/type-c-service/tests/debug_accessory.rs @@ -126,7 +126,9 @@ impl Test for TestDebugAccessorySource { capability, ProviderPowerCapability { capability: POWER_CAPABILITY_USB_DEFAULT_USB2, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, } ); assert!(ptr::eq(psu, port0.port)); diff --git a/type-c-service/tests/power.rs b/type-c-service/tests/power.rs index e345b8d08..ba6a90bbe 100644 --- a/type-c-service/tests/power.rs +++ b/type-c-service/tests/power.rs @@ -12,7 +12,8 @@ use embedded_usb_pd::{ }; use power_policy_interface::{ capability::{ - ConsumerDisconnect, ConsumerFlags, ConsumerPowerCapability, ProviderFlags, ProviderPowerCapability, PsuType, + ConsumerFlags, ConsumerPowerCapability, DisconnectFlags, DisconnectReason, ProviderFlags, + ProviderPowerCapability, PsuType, }, psu::{Psu, PsuState}, service::event::Event as PowerPolicyEvent, @@ -89,7 +90,10 @@ impl Test for TestBasicConsumerFlow { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -132,8 +136,14 @@ impl Test for TestBasicConsumerFlow { assert_eq!(type_c_result.err(), Some(TimeoutError)); // Power policy service should broadcast a consumer disconnect event match power_policy_result { - Ok(PowerPolicyEvent::ConsumerDisconnected(psu, _)) => { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { assert!(ptr::eq(psu, port0.port)); + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::Detached), + } + ); } _ => panic!("Did not receive consumer disconnected event"), } @@ -203,7 +213,9 @@ impl Test for TestBasicProviderFlow { capability, ProviderPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -367,7 +379,10 @@ impl Test for TestConsumerFlowTimerSinkReady { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port)); @@ -392,8 +407,14 @@ impl Test for TestConsumerFlowTimerSinkReady { // The power policy should broadcast a consumer disconnect event. match with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await { - Ok(PowerPolicyEvent::ConsumerDisconnected(psu, _)) => { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { assert!(ptr::eq(psu, port)); + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::Detached), + } + ); } _ => panic!("Did not receive consumer disconnected event"), } @@ -405,8 +426,8 @@ impl Test for TestConsumerFlowTimerSinkReady { } /// 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 renegotiation -/// flag set. Setting the same voltage should do neither. +/// notifies the power policy, which broadcasts a `ConsumerDisconnected` event with the manual +/// renegotiation reason. Setting the same voltage should do neither. struct TestSinkDisableOnVoltageChange; impl Test for TestSinkDisableOnVoltageChange { @@ -490,11 +511,16 @@ impl Test for TestSinkDisableOnVoltageChange { } port0.port.lock().await.set_max_sink_voltage(Some(9000)).await.unwrap(); - // The power policy should broadcast a consumer disconnect with the renegotiation flag set. + // The power policy should broadcast a consumer disconnect with the manual renegotiation reason. match with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await { - Ok(PowerPolicyEvent::ConsumerDisconnected(psu, flags)) => { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { assert!(ptr::eq(psu, port0.port)); - assert_eq!(flags, ConsumerDisconnect::none().with_renegotiation(true)); + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::ManualRenegotiation), + } + ); } _ => panic!("Did not receive consumer disconnected event"), } @@ -581,7 +607,10 @@ impl Test for TestSetMaxVoltageSinkReadyDeadlineInvalidation { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port)); @@ -670,7 +699,10 @@ impl Test for TestSetMaxSinkVoltageRecovery { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port)); @@ -698,8 +730,13 @@ impl Test for TestSetMaxSinkVoltageRecovery { // Power policy service should broadcast a consumer disconnected event match power_policy_result { - Ok(PowerPolicyEvent::ConsumerDisconnected(psu, flags)) => { - assert_eq!(flags, ConsumerDisconnect::none().with_renegotiation(true)); + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::ManualRenegotiation), + } + ); assert!(ptr::eq(psu, port)); } _ => panic!("Did not receive consumer disconnected event"), @@ -743,7 +780,10 @@ impl Test for TestSetMaxSinkVoltageRecovery { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port)); @@ -808,7 +848,10 @@ impl Test for TestConsumerToProviderRoleSwap { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -844,8 +887,14 @@ impl Test for TestConsumerToProviderRoleSwap { // The consumer should disconnect as soon as the swap completes. match with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await { - Ok(PowerPolicyEvent::ConsumerDisconnected(psu, _)) => { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { assert!(ptr::eq(psu, port0.port)); + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::RoleSwap), + } + ); } _ => panic!("Did not receive consumer disconnected event on role swap"), } @@ -893,7 +942,9 @@ impl Test for TestConsumerToProviderRoleSwap { capability, ProviderPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -959,7 +1010,9 @@ impl Test for TestProviderToConsumerRoleSwap { capability, ProviderPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ProviderFlags::none().with_psu_type(PsuType::TypeC), + flags: ProviderFlags { + psu_type: Some(PsuType::TypeC), + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -1033,7 +1086,10 @@ impl Test for TestProviderToConsumerRoleSwap { capability, ConsumerPowerCapability { capability: POWER_CAPABILITY_5V_1A5, - flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + flags: ConsumerFlags { + psu_type: Some(PsuType::TypeC), + ..Default::default() + }, } ); assert!(ptr::eq(psu, port0.port)); @@ -1049,6 +1105,216 @@ impl Test for TestProviderToConsumerRoleSwap { } } +/// Test that a PD hard reset tears down the active contract with the reset reason. +struct TestHardResetDisconnect; + +impl Test for TestHardResetDisconnect { + 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 connected_status = PortStatus { + available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Sink, + ..Default::default() + }; + { + let mut mock0 = port0.mock.lock().await; + mock0.next_result_get_port_status.push_back(Ok(connected_status)); + mock0.next_result_enable_sink_path.push_back(Ok(())); + } + + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_plug_inserted_or_removed(true); + port_event.set_new_power_contract_as_consumer(true); + port_event.set_sink_ready(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + match with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await { + Ok(PowerPolicyEvent::ConsumerConnected(psu, _)) => assert!(ptr::eq(psu, port0.port)), + _ => panic!("Did not receive consumer connected event"), + } + + port0 + .mock + .lock() + .await + .next_result_get_port_status + .push_back(Ok(connected_status)); + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_pd_hard_reset(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + match with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu, disconnect)) => { + assert!(ptr::eq(psu, port0.port)); + assert_eq!( + disconnect, + DisconnectFlags { + reason: Some(DisconnectReason::Reset), + } + ); + } + _ => panic!("Did not receive consumer disconnected event after hard reset"), + } + assert_eq!(port0.port.lock().await.state().psu_state, PsuState::Idle); + } +} + +/// Test that a PD hard reset cancels the sink ready deadline. +struct TestHardResetSinkReady; + +impl Test for TestHardResetSinkReady { + 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 connected_status = PortStatus { + available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Sink, + ..Default::default() + }; + { + let mut mock0 = port0.mock.lock().await; + mock0.next_result_get_port_status.push_back(Ok(connected_status)); + } + + // Connect and trigger the sink ready deadline + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_plug_inserted_or_removed(true); + port_event.set_new_power_contract_as_consumer(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + // Trigger a hard reset + port0 + .mock + .lock() + .await + .next_result_get_port_status + .push_back(Ok(connected_status)); + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_pd_hard_reset(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + assert!( + port0.shared_state.lock().await.sink_ready_deadline().is_none(), + "Sink ready deadline not cleared after hard reset" + ); + } +} + +/// Test that a provider can renegotiate the same contract after a PD hard reset. +struct TestProviderRecontractAfterHardReset; + +impl Test for TestProviderRecontractAfterHardReset { + 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 connected_status = PortStatus { + available_source_contract: Some(POWER_CAPABILITY_5V_1A5), + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Source, + ..Default::default() + }; + { + let mut mock0 = port0.mock.lock().await; + // Queue the initial connection, hard-reset status, and same-capability recontract. + mock0.next_result_get_port_status.push_back(Ok(connected_status)); + mock0.next_result_get_port_status.push_back(Ok(connected_status)); + mock0.next_result_get_port_status.push_back(Ok(connected_status)); + } + + // Establish the original provider contract. + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_plug_inserted_or_removed(true); + port_event.set_new_power_contract_as_provider(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + assert!(matches!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await, + Ok(PowerPolicyEvent::ProviderConnected(_, _)) + )); + + // Tear down the provider while the controller continues to report its capability. + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_pd_hard_reset(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + assert!(matches!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await, + Ok(PowerPolicyEvent::ProviderDisconnected(_)) + )); + + // Reannounce the same capability and require it to be published as a new contract. + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_new_power_contract_as_provider(true); + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + assert!( + matches!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()).await, + Ok(PowerPolicyEvent::ProviderConnected(_, _)) + ), + "same-capability provider contract was not published after hard reset" + ); + } +} + #[tokio::test] async fn test_basic_consumer_flow() { common::run_test( @@ -1136,3 +1402,36 @@ async fn test_provider_to_consumer_role_swap() { ) .await; } + +#[tokio::test] +async fn test_hard_reset_disconnect() { + common::run_test( + DEFAULT_TEST_DURATION, + Default::default(), + Default::default(), + TestHardResetDisconnect, + ) + .await; +} + +#[tokio::test] +async fn test_hard_reset_sink_ready() { + common::run_test( + DEFAULT_TEST_DURATION, + Default::default(), + Default::default(), + TestHardResetSinkReady, + ) + .await; +} + +#[tokio::test] +async fn test_hard_reset_reconnect_provider() { + common::run_test( + DEFAULT_TEST_DURATION, + Default::default(), + Default::default(), + TestProviderRecontractAfterHardReset, + ) + .await; +}