diff --git a/Cargo.lock b/Cargo.lock index c404b4f5..59dcc20d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#7b618f5689cef191171d81d33a2fa6b5af46d33f" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#ad569e652481df8111c14c4263f20287e79e12ef" dependencies = [ "aquamarine", "bincode", diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index a29fc76f..7eae469f 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -748,7 +748,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#7b618f5689cef191171d81d33a2fa6b5af46d33f" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#ad569e652481df8111c14c4263f20287e79e12ef" dependencies = [ "aquamarine", "bincode", diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index 421afb73..62fe0a60 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#7b618f5689cef191171d81d33a2fa6b5af46d33f" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#ad569e652481df8111c14c4263f20287e79e12ef" dependencies = [ "aquamarine", "bincode", diff --git a/examples/std/src/bin/type_c/ucsi.rs b/examples/std/src/bin/type_c/ucsi.rs index 7dae5719..77f43ee9 100644 --- a/examples/std/src/bin/type_c/ucsi.rs +++ b/examples/std/src/bin/type_c/ucsi.rs @@ -226,6 +226,7 @@ async fn type_c_service_task() -> ! { .set_swap_to_snk(true) .set_swap_to_src(true), ), + ..Default::default() }) .await; unreachable!() diff --git a/power-policy-service/src/config.rs b/power-policy-service/src/config.rs index bb249fb3..4ee3fce9 100644 --- a/power-policy-service/src/config.rs +++ b/power-policy-service/src/config.rs @@ -10,6 +10,10 @@ pub struct Config { pub provider_unlimited: PowerCapability, /// Power capability of every provider in limited power mode pub provider_limited: PowerCapability, + /// Minimum power threshold to consume power from. + /// + /// If [`None`], the service will consume from providers, regardless of how much power they provide. + pub min_consumer_threshold_mw: Option, } impl Default for Config { @@ -27,6 +31,8 @@ impl Default for Config { voltage_mv: 5000, current_ma: 1500, }, + // No minimum threshold + min_consumer_threshold_mw: None, } } } diff --git a/power-policy-service/src/consumer.rs b/power-policy-service/src/consumer.rs index 2b632298..0600193f 100644 --- a/power-policy-service/src/consumer.rs +++ b/power-policy-service/src/consumer.rs @@ -40,8 +40,21 @@ impl PowerPolicy { for node in self.context.devices() { let device = node.data::().ok_or(Error::InvalidDevice)?; + let consumer_capability = device.consumer_capability().await; + // Don't consider consumers below minimum threshold + if consumer_capability + .zip(self.config.min_consumer_threshold_mw) + .is_some_and(|(cap, min)| cap.capability.max_power_mw() < min) + { + info!( + "Device{}: Not considering consumer, power capability is too low", + device.id().0, + ); + continue; + } + // Update the best available consumer - best_consumer = match (best_consumer, device.consumer_capability().await) { + best_consumer = match (best_consumer, consumer_capability) { // Nothing available (None, None) => None, // No existing consumer diff --git a/type-c-service/Cargo.toml b/type-c-service/Cargo.toml index 76f16e49..f8686f29 100644 --- a/type-c-service/Cargo.toml +++ b/type-c-service/Cargo.toml @@ -29,9 +29,9 @@ static_cell = { workspace = true } tps6699x = { workspace = true, features = ["embassy"] } [dev-dependencies] +embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } embassy-sync = { workspace = true, features = ["std"] } critical-section = { workspace = true, features = ["std"] } -embassy-time = { workspace = true, features = ["std"] } embassy-time-driver = { workspace = true } embassy-futures.workspace = true tokio = { workspace = true, features = ["rt", "macros", "time"] } diff --git a/type-c-service/src/driver/tps6699x.rs b/type-c-service/src/driver/tps6699x.rs index 23846024..0f8c5c75 100644 --- a/type-c-service/src/driver/tps6699x.rs +++ b/type-c-service/src/driver/tps6699x.rs @@ -284,13 +284,13 @@ impl Controller for Tps6699x<'_, M, B> { /// Returns the current status of the port async fn get_port_status(&mut self, port: LocalPortId) -> Result> { let status = self.tps6699x.get_port_status(port).await?; - trace!("Port{} status: {:#?}", port.0, status); + debug!("Port{} status: {:#?}", port.0, status); let pd_status = self.tps6699x.get_pd_status(port).await?; - trace!("Port{} PD status: {:#?}", port.0, pd_status); + debug!("Port{} PD status: {:#?}", port.0, pd_status); let port_control = self.tps6699x.get_port_control(port).await?; - trace!("Port{} control: {:#?}", port.0, port_control); + debug!("Port{} control: {:#?}", port.0, port_control); let mut port_status = PortStatus::default(); diff --git a/type-c-service/src/service/config.rs b/type-c-service/src/service/config.rs index 4a8e9a9e..7545a481 100644 --- a/type-c-service/src/service/config.rs +++ b/type-c-service/src/service/config.rs @@ -1,4 +1,102 @@ -use embedded_usb_pd::ucsi; +use embedded_usb_pd::ucsi::{self, lpm::get_connector_status::BatteryChargingCapabilityStatus}; + +/// UCSI battery charging capability status configuration. +/// +/// This struct holds the power thresholds for determining the battery charging capability status as reported by the +/// UCSI `GET_CONNECTOR_STATUS` command. +/// +/// See [`try_new`][`Self::try_new`] for details on creating a valid configuration, and [`status_of`][`Self::status_of`] +/// for determining the status based on a power level. +/// +/// The [`Default`][`Self::default`] implementation creates a configuration where the status of all power levels is +/// considered [`Nominal`][BatteryChargingCapabilityStatus::Nominal]. +#[derive(Debug, Clone, Copy, Default)] +pub struct UcsiBatteryChargingThresholdConfig { + /// Power threshold (in milliwatts) to be considered not charging. + /// + /// Below this level, `GET_CONNECTOR_STATUS` will report [`NotCharging`][BatteryChargingCapabilityStatus::NotCharging]. + not_charging_mw: Option, + + /// Power threshold (in milliwatts) to be considered very slow charging. + /// + /// Below this level, `GET_CONNECTOR_STATUS` will report [`VerySlow`][BatteryChargingCapabilityStatus::VerySlow]. + very_slow_mw: Option, + + /// Power threshold (in milliwatts) to be considered slow charging. + /// + /// Below this level, `GET_CONNECTOR_STATUS` will report [`Slow`][BatteryChargingCapabilityStatus::Slow]. + slow_mw: Option, +} + +impl UcsiBatteryChargingThresholdConfig { + /// Create a new [`UcsiBatteryChargingThresholdConfig`], ensuring the exclusive thresholds are in the correct order. + /// + /// The thresholds must satisfy: + /// + /// ```text + /// not_charging_mw < very_slow_charging_mw < slow_charging_mw + /// ``` + /// + /// Any of the thresholds can be [`None`], which ignores that threshold in the ordering checks and subsequently when + /// determining the status in [`status_of`][`Self::status_of`]. + /// + /// Returns [`None`] if the thresholds are misordered. + pub const fn try_new( + not_charging_mw: Option, + very_slow_charging_mw: Option, + slow_charging_mw: Option, + ) -> Option { + if let (Some(not), Some(very_slow)) = (not_charging_mw, very_slow_charging_mw) + && not >= very_slow + { + return None; + }; + + if let (Some(very_slow), Some(slow)) = (very_slow_charging_mw, slow_charging_mw) + && very_slow >= slow + { + return None; + }; + + if let (Some(not), Some(slow)) = (not_charging_mw, slow_charging_mw) + && not >= slow + { + return None; + }; + + Some(Self { + not_charging_mw, + very_slow_mw: very_slow_charging_mw, + slow_mw: slow_charging_mw, + }) + } + + /// Compare a power level (in milliwatts) against the exclusive thresholds and return the corresponding status. + /// + /// If below a threshold, that status is returned. If a threshold is [`None`], it is ignored and its status won't be + /// returned. The order of checks is from lowest to highest threshold: + /// 1. `not_charging_mw` -> [`NotCharging`][BatteryChargingCapabilityStatus::NotCharging] + /// 1. `very_slow_charging_mw` -> [`VerySlow`][BatteryChargingCapabilityStatus::VerySlow] + /// 1. `slow_charging_mw` -> [`Slow`][BatteryChargingCapabilityStatus::Slow] + /// 1. Above all thresholds -> [`Nominal`][BatteryChargingCapabilityStatus::Nominal] + pub const fn status_of(&self, power_mw: u32) -> BatteryChargingCapabilityStatus { + if let Some(threshold) = self.not_charging_mw + && power_mw < threshold + { + BatteryChargingCapabilityStatus::NotCharging + } else if let Some(threshold) = self.very_slow_mw + && power_mw < threshold + { + BatteryChargingCapabilityStatus::VerySlow + } else if let Some(threshold) = self.slow_mw + && power_mw < threshold + { + BatteryChargingCapabilityStatus::Slow + } else { + BatteryChargingCapabilityStatus::Nominal + } + } +} /// Type-c service configuration #[derive(Debug, Clone, Copy, Default)] @@ -7,4 +105,133 @@ pub struct Config { pub ucsi_capabilities: ucsi::ppm::get_capability::ResponseData, /// Optional override for UCSI port capabilities pub ucsi_port_capabilities: Option, + /// UCSI battery charging configuration + pub ucsi_battery_charging_config: UcsiBatteryChargingThresholdConfig, +} + +#[cfg(test)] +mod tests { + use super::*; + + mod ucsi_battery_charging_threshold_config { + //! Tests for [`UcsiBatteryChargingThresholdConfig`] + + use super::*; + + mod try_new { + //! Tests for [`UcsiBatteryChargingThresholdConfig::try_new`] + + use super::*; + + #[test] + fn valid() { + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(3000)).is_some()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), Some(3000)).is_some()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, Some(3000)).is_some()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), None).is_some()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(None, None, None).is_some()); + } + + #[test] + fn invalid() { + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(3000), Some(2000), Some(1000)).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(2000), Some(2000), Some(3000)).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(1000), Some(3000)).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(2000)).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(3000), None, Some(1000)).is_none()); + } + + #[test] + fn equal_is_invalid() { + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(1000), None).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), Some(2000)).is_none()); + assert!(UcsiBatteryChargingThresholdConfig::try_new(Some(3000), None, Some(3000)).is_none()); + } + } + + /// Test that the default config permits any power level to be nominal. + #[test] + fn default() { + let config = UcsiBatteryChargingThresholdConfig::default(); + assert_eq!(config.status_of(0), BatteryChargingCapabilityStatus::Nominal); + } + + mod status_of { + //! Tests for [`UcsiBatteryChargingThresholdConfig::status_of`] + + use super::*; + + #[test] + fn not_charging() { + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(999), BatteryChargingCapabilityStatus::NotCharging); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, None).unwrap(); + assert_eq!(config.status_of(999), BatteryChargingCapabilityStatus::NotCharging); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), None).unwrap(); + assert_eq!(config.status_of(999), BatteryChargingCapabilityStatus::NotCharging); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, Some(3000)).unwrap(); + assert_eq!(config.status_of(999), BatteryChargingCapabilityStatus::NotCharging); + } + + #[test] + fn very_slow() { + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(1999), BatteryChargingCapabilityStatus::VerySlow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), None).unwrap(); + assert_eq!(config.status_of(1999), BatteryChargingCapabilityStatus::VerySlow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), None).unwrap(); + assert_eq!(config.status_of(1999), BatteryChargingCapabilityStatus::VerySlow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(1999), BatteryChargingCapabilityStatus::VerySlow); + } + + #[test] + fn slow() { + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(2999), BatteryChargingCapabilityStatus::Slow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, None, Some(3000)).unwrap(); + assert_eq!(config.status_of(2999), BatteryChargingCapabilityStatus::Slow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(2999), BatteryChargingCapabilityStatus::Slow); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, Some(3000)).unwrap(); + assert_eq!(config.status_of(2999), BatteryChargingCapabilityStatus::Slow); + } + + #[test] + fn nominal() { + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(3001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, None, None).unwrap(); + assert_eq!(config.status_of(0), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, None).unwrap(); + assert_eq!(config.status_of(1001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), None).unwrap(); + assert_eq!(config.status_of(2001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, None, Some(3000)).unwrap(); + assert_eq!(config.status_of(3001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), Some(2000), None).unwrap(); + assert_eq!(config.status_of(2001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(None, Some(2000), Some(3000)).unwrap(); + assert_eq!(config.status_of(3001), BatteryChargingCapabilityStatus::Nominal); + + let config = UcsiBatteryChargingThresholdConfig::try_new(Some(1000), None, Some(3000)).unwrap(); + assert_eq!(config.status_of(3001), BatteryChargingCapabilityStatus::Nominal); + } + } + } } diff --git a/type-c-service/src/service/mod.rs b/type-c-service/src/service/mod.rs index ee2c06d5..753fc5ba 100644 --- a/type-c-service/src/service/mod.rs +++ b/type-c-service/src/service/mod.rs @@ -72,6 +72,10 @@ pub struct Service<'a> { pub enum PowerPolicyEvent { /// Unconstrained state changed Unconstrained(power_policy::UnconstrainedState), + /// Consumer disconnected + ConsumerDisconnected, + /// Consumer connected + ConsumerConnected, } /// Type-C service events @@ -149,7 +153,7 @@ impl<'a> Service<'a> { } self.set_cached_port_status(port_id, status).await?; - self.generate_ucsi_event(port_id, event).await; + self.handle_ucsi_port_event(port_id, event, &status).await; Ok(()) } diff --git a/type-c-service/src/service/power.rs b/type-c-service/src/service/power.rs index f42b1898..c6bb628d 100644 --- a/type-c-service/src/service/power.rs +++ b/type-c-service/src/service/power.rs @@ -16,6 +16,12 @@ impl<'a> Service<'a> { power_policy::CommsData::Unconstrained(state) => { return Event::PowerPolicy(PowerPolicyEvent::Unconstrained(state)); } + power_policy::CommsData::ConsumerDisconnected(_) => { + return Event::PowerPolicy(PowerPolicyEvent::ConsumerDisconnected); + } + power_policy::CommsData::ConsumerConnected(_, _) => { + return Event::PowerPolicy(PowerPolicyEvent::ConsumerConnected); + } _ => { // No other events currently implemented } @@ -91,6 +97,20 @@ impl<'a> Service<'a> { pub(super) async fn process_power_policy_event(&self, message: &PowerPolicyEvent) -> Result<(), Error> { match message { PowerPolicyEvent::Unconstrained(state) => self.process_unconstrained_state_change(state).await, + PowerPolicyEvent::ConsumerDisconnected => { + let mut state = self.state.lock().await; + state.ucsi.psu_connected = false; + // Notify OPM because this can affect battery charging capability status + self.pend_ucsi_connected_ports(&mut state).await; + Ok(()) + } + PowerPolicyEvent::ConsumerConnected => { + let mut state = self.state.lock().await; + state.ucsi.psu_connected = true; + // Notify OPM because this can affect battery charging capability status + self.pend_ucsi_connected_ports(&mut state).await; + Ok(()) + } } } } diff --git a/type-c-service/src/service/ucsi.rs b/type-c-service/src/service/ucsi.rs index 98af9c01..e11a1602 100644 --- a/type-c-service/src/service/ucsi.rs +++ b/type-c-service/src/service/ucsi.rs @@ -1,12 +1,12 @@ use embedded_services::warn; -use embedded_usb_pd::PdError; use embedded_usb_pd::ucsi::cci::{Cci, GlobalCci}; -use embedded_usb_pd::ucsi::lpm::get_connector_status::ConnectorStatusChange; +use embedded_usb_pd::ucsi::lpm::get_connector_status::{BatteryChargingCapabilityStatus, ConnectorStatusChange}; use embedded_usb_pd::ucsi::ppm::set_notification_enable::NotificationEnable; use embedded_usb_pd::ucsi::ppm::state_machine::{ GlobalInput as PpmInput, GlobalOutput as PpmOutput, GlobalStateMachine as StateMachine, InvalidTransition, }; use embedded_usb_pd::ucsi::{GlobalCommand, ResponseData, lpm, ppm}; +use embedded_usb_pd::{PdError, PowerRole}; use super::*; @@ -19,6 +19,13 @@ pub(super) struct State { notifications_enabled: NotificationEnable, /// Queued pending port notifications pending_ports: heapless::Deque, + /// Ports that have a valid battery charging status capability + /// + /// We provide a battery charging status only after the port has negotiated power. + /// This prevents the port from temporarily reporting slow or no charging before the contract has finalized. + valid_battery_charging_capability: heapless::FnvIndexSet, + /// PSU connected + pub(super) psu_connected: bool, } impl<'a> Service<'a> { @@ -27,6 +34,7 @@ impl<'a> Service<'a> { debug!("Resetting PPM"); state.notifications_enabled = NotificationEnable::default(); state.pending_ports.clear(); + state.valid_battery_charging_capability.clear(); } /// Set notification enable implementation @@ -58,20 +66,70 @@ impl<'a> Service<'a> { } } + /// Determine the battery charging capability status for the given port + fn determine_battery_charging_capability_status( + &self, + state: &mut State, + port_id: GlobalPortId, + port_status: &PortStatus, + ) -> Option { + if port_status.power_role == PowerRole::Sink { + if state.valid_battery_charging_capability.contains(&port_id) && !state.psu_connected { + // Only run this logic when no PSU is attached to prevent excessive notifications + // when new type-C PSUs are attached + let power_mw = port_status + .available_sink_contract + .map(|contract| contract.max_power_mw()) + .unwrap_or(0); + + Some(self.config.ucsi_battery_charging_config.status_of(power_mw)) + } else { + // Report normal charging until something changes + Some(BatteryChargingCapabilityStatus::Nominal) + } + } else { + // This field only applies to sinks + None + } + } + async fn process_lpm_command( &self, + state: &mut super::State, command: &ucsi::lpm::GlobalCommand, ) -> Result, PdError> { debug!("Processing LPM command: {:?}", command); - if matches!(command.operation(), lpm::CommandData::GetConnectorCapability) { - // Override the capabilities if present in the config - if let Some(capabilities) = &self.config.ucsi_port_capabilities { - Ok(Some(lpm::ResponseData::GetConnectorCapability(*capabilities))) - } else { - self.context.execute_ucsi_command(*command).await + match command.operation() { + lpm::CommandData::GetConnectorCapability => { + // Override the capabilities if present in the config + if let Some(capabilities) = &self.config.ucsi_port_capabilities { + Ok(Some(lpm::ResponseData::GetConnectorCapability(*capabilities))) + } else { + self.context.execute_ucsi_command(*command).await + } } - } else { - self.context.execute_ucsi_command(*command).await + lpm::CommandData::GetConnectorStatus => { + let mut response = self.context.execute_ucsi_command(*command).await; + if let Ok(Some(lpm::ResponseData::GetConnectorStatus(lpm::get_connector_status::ResponseData { + status_change: ref mut states_change, + status: + Some(lpm::get_connector_status::ConnectedStatus { + ref mut battery_charging_status, + .. + }), + .. + }))) = response + { + let raw_port = command.port().0 as usize; + let port_status = state.port_status.get(raw_port).ok_or(PdError::InvalidPort)?; + *battery_charging_status = + self.determine_battery_charging_capability_status(&mut state.ucsi, command.port(), port_status); + states_change.set_battery_charging_status_change(battery_charging_status.is_some()); + } + + response + } + _ => self.context.execute_ucsi_command(*command).await, } } @@ -111,7 +169,7 @@ impl<'a> Service<'a> { /// Process an external UCSI command pub(super) async fn process_ucsi_command(&self, command: &GlobalCommand) -> external::UcsiResponse { - let state = &mut self.state.lock().await.ucsi; + let state = &mut self.state.lock().await; let mut next_input = Some(PpmInput::Command(command)); let mut response: external::UcsiResponse = external::UcsiResponse { notify_opm: false, @@ -124,7 +182,7 @@ impl<'a> Service<'a> { // Using a loop allows all logic to be centralized loop { let output = if let Some(next_input) = next_input.take() { - state.ppm_state_machine.consume(next_input) + state.ucsi.ppm_state_machine.consume(next_input) } else { error!("Unexpected end of state machine processing"); return external::UcsiResponse { @@ -154,12 +212,12 @@ impl<'a> Service<'a> { match command { ucsi::GlobalCommand::PpmCommand(ppm_command) => { response.data = self - .process_ppm_command(state, ppm_command) + .process_ppm_command(&mut state.ucsi, ppm_command) .map(|inner| inner.map(ResponseData::Ppm)); } ucsi::GlobalCommand::LpmCommand(lpm_command) => { response.data = self - .process_lpm_command(lpm_command) + .process_lpm_command(state, lpm_command) .await .map(|inner| inner.map(ResponseData::Lpm)); } @@ -168,20 +226,20 @@ impl<'a> Service<'a> { // Don't return yet, need to inform state machine that command is complete } PpmOutput::OpmNotifyCommandComplete => { - response.notify_opm = state.notifications_enabled.cmd_complete(); + response.notify_opm = state.ucsi.notifications_enabled.cmd_complete(); response.cci.set_cmd_complete(true); response.cci.set_error(response.data.is_err()); - self.set_cci_connector_change(state, &mut response.cci); + self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); return response; } PpmOutput::AckComplete(ack) => { - response.notify_opm = state.notifications_enabled.cmd_complete(); + response.notify_opm = state.ucsi.notifications_enabled.cmd_complete(); if ack.command_complete() { response.cci.set_ack_command(true); } if ack.connector_change() { - self.ack_connector_change(state, &mut response.cci).await; + self.ack_connector_change(&mut state.ucsi, &mut response.cci).await; } return response; @@ -189,18 +247,18 @@ impl<'a> Service<'a> { PpmOutput::ResetComplete => { // Resets don't follow the normal command execution flow // So do any reset processing here - self.process_ppm_reset(state); + self.process_ppm_reset(&mut state.ucsi); // Don't notify OPM because it'll poll response.notify_opm = false; response.cci = Cci::new_reset_complete(); - self.set_cci_connector_change(state, &mut response.cci); + self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); return response; } PpmOutput::OpmNotifyBusy => { // Notify if notifications are enabled in general - response.notify_opm = !state.notifications_enabled.is_empty(); + response.notify_opm = !state.ucsi.notifications_enabled.is_empty(); response.cci.set_busy(true); - self.set_cci_connector_change(state, &mut response.cci); + self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); return response; } }, @@ -209,15 +267,20 @@ impl<'a> Service<'a> { response.notify_opm = false; response.cci = Cci::default(); response.data = Ok(None); - self.set_cci_connector_change(state, &mut response.cci); + self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); return response; } } } } - /// Convert from general PD events into UCSI-specific events - pub(super) async fn generate_ucsi_event(&self, port_id: GlobalPortId, port_event: PortStatusChanged) { + /// Handle PD port events, update UCSI state, and generate corresponding UCSI notifications + pub(super) async fn handle_ucsi_port_event( + &self, + port_id: GlobalPortId, + port_event: PortStatusChanged, + port_status: &PortStatus, + ) { let state = &mut self.state.lock().await.ucsi; let mut ucsi_event = ConnectorStatusChange::default(); @@ -229,12 +292,25 @@ impl<'a> Service<'a> { ucsi_event.set_connector_partner_changed(true); } - if port_event.new_power_contract_as_consumer() || port_event.new_power_contract_as_provider() { + if port_event.new_power_contract_as_consumer() + || port_event.new_power_contract_as_provider() + || port_event.sink_ready() + { ucsi_event.set_negotiated_power_level_change(true); ucsi_event.set_power_op_mode_change(true); ucsi_event.set_external_supply_change(true); ucsi_event.set_power_direction_changed(true); ucsi_event.set_battery_charging_status_change(true); + + // Power negotiation completed, battery charging capability status is now valid + if state.valid_battery_charging_capability.insert(port_id).is_err() { + error!("Valid battery charging capability overflow for port {:?}", port_id); + } + } + + if !port_status.is_connected() { + // Reset battery charging capability status when disconnected + let _ = state.valid_battery_charging_capability.remove(&port_id); } if ucsi_event.filter_enabled(state.notifications_enabled).is_empty() { @@ -242,6 +318,20 @@ impl<'a> Service<'a> { return; } + self.pend_ucsi_port(state, port_id).await; + } + + /// Pend UCSI events for all connected ports + pub(super) async fn pend_ucsi_connected_ports(&self, state: &mut super::State) { + for (port_id, port_status) in state.port_status.iter().enumerate() { + if port_status.is_connected() { + self.pend_ucsi_port(&mut state.ucsi, GlobalPortId(port_id as u8)).await; + } + } + } + + /// Pend a UCSI event for the given port + async fn pend_ucsi_port(&self, state: &mut State, port_id: GlobalPortId) { if state.pending_ports.iter().any(|pending| *pending == port_id) { // Already have a pending event for this port, don't need to process it twice return;