diff --git a/Cargo.lock b/Cargo.lock index 5beff2f4b..9ca47f76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2055,13 +2055,15 @@ dependencies = [ "embassy-futures", "embassy-sync", "embassy-time", - "embassy-time-driver", "embedded-cfu-protocol", "embedded-hal-async", "embedded-services", "embedded-usb-pd", + "env_logger", "heapless 0.9.2", "log", + "paste", + "power-policy-service", "static_cell", "tokio", "tps6699x", diff --git a/Cargo.toml b/Cargo.toml index 17aa95545..1b1a4dadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,10 +65,11 @@ embedded-hal = "1.0" embedded-hal-async = "1.0" embedded-services = { path = "./embedded-service" } embedded-storage-async = "0.4.1" -embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd", tag = "v0.1.0" , default-features = false } +embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd", tag = "v0.1.0", default-features = false } mctp-rs = { git = "https://github.com/OpenDevicePartnership/mctp-rs", tag = "v0.1.0" } num_enum = { version = "0.7.5", default-features = false } portable-atomic = { version = "1.11", default-features = false } +power-policy-service = { path = "./power-policy-service" } heapless = "0.9.2" log = "0.4" proc-macro2 = "1.0" diff --git a/embedded-service/src/type_c/controller.rs b/embedded-service/src/type_c/controller.rs index 9de50e2db..62b79e8f3 100644 --- a/embedded-service/src/type_c/controller.rs +++ b/embedded-service/src/type_c/controller.rs @@ -27,7 +27,7 @@ use crate::{GlobalRawMutex, IntrusiveNode, broadcaster::immediate as broadcaster pub const MAX_NUM_DATA_OBJECTS: usize = 7; // 7 VDOs of 4 bytes each /// Port status -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PortStatus { /// Current available source contract @@ -144,7 +144,7 @@ pub struct DpPinConfig { } /// DisplayPort status data -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DpStatus { /// DP alt-mode entered @@ -154,7 +154,7 @@ pub struct DpStatus { } /// DisplayPort configuration data -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DpConfig { /// DP alt-mode enabled @@ -214,7 +214,7 @@ impl Default for SendVdm { /// USB control configuration #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UsbControlConfig { /// Enable USB2 data path pub usb2_enabled: bool, @@ -236,7 +236,7 @@ impl Default for UsbControlConfig { /// Thunderbolt control configuration #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Default, Copy, PartialEq)] +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] pub struct TbtConfig { /// Enable Thunderbolt pub tbt_enabled: bool, @@ -244,7 +244,7 @@ pub struct TbtConfig { /// PD state-machine configuration #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Default, Copy, PartialEq)] +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] pub struct PdStateMachineConfig { /// Enable or disable the PD state-machine pub enabled: bool, @@ -523,7 +523,7 @@ pub enum Response<'a> { } /// Controller status -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ControllerStatus<'a> { /// Current controller mode diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index 13fcb65a8..0f8a7130b 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -67,10 +67,6 @@ path = "src/bin/type_c/basic.rs" name = "type-c-service" path = "src/bin/type_c/service.rs" -[[bin]] -name = "type-c-external" -path = "src/bin/type_c/external.rs" - [[bin]] name = "type-c-unconstrained" path = "src/bin/type_c/unconstrained.rs" diff --git a/examples/std/src/bin/type_c/external.rs b/examples/std/src/bin/type_c/external.rs deleted file mode 100644 index 13c24752f..000000000 --- a/examples/std/src/bin/type_c/external.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Low-level example of external messaging with a simple type-C service -use embassy_executor::{Executor, Spawner}; -use embassy_sync::mutex::Mutex; -use embassy_time::Timer; -use embedded_services::{ - GlobalRawMutex, power, - type_c::{Cached, ControllerId, external}, -}; -use embedded_usb_pd::GlobalPortId; -use log::*; -use static_cell::StaticCell; -use std_examples::type_c::mock_controller; -use type_c_service::wrapper::backing::Storage; - -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); - -#[embassy_executor::task] -async fn controller_task() { - static STATE: StaticCell = StaticCell::new(); - let state = STATE.init(mock_controller::ControllerState::new()); - - static STORAGE: StaticCell> = StaticCell::new(); - let backing_storage = STORAGE.init(Storage::new( - CONTROLLER0_ID, - 0, // CFU component ID (unused) - [(PORT0_ID, POWER0_ID)], - )); - static REFERENCED: StaticCell> = - StaticCell::new(); - let referenced = REFERENCED.init( - backing_storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - - static CONTROLLER: StaticCell> = StaticCell::new(); - let controller = CONTROLLER.init(Mutex::new(mock_controller::Controller::new(state))); - - static WRAPPER: StaticCell = StaticCell::new(); - let wrapper = WRAPPER.init( - mock_controller::Wrapper::try_new( - controller, - Default::default(), - referenced, - crate::mock_controller::Validator, - ) - .expect("Failed to create wrapper"), - ); - - wrapper.register().await.unwrap(); - loop { - if let Err(e) = wrapper.process_next_event().await { - error!("Error processing wrapper: {e:#?}"); - } - } -} - -#[embassy_executor::task] -async fn task(_spawner: Spawner) { - info!("Starting main task"); - embedded_services::init().await; - - // Allow the controller to initialize and register itself - Timer::after_secs(1).await; - info!("Getting controller status"); - let controller_status = external::get_controller_status(ControllerId(0)).await.unwrap(); - info!("Controller status: {controller_status:?}"); - - info!("Getting port status"); - let port_status = external::get_port_status(GlobalPortId(0), Cached(true)).await.unwrap(); - info!("Port status: {port_status:?}"); - - info!("Getting retimer fw update status"); - let rt_fw_update_status = external::port_get_rt_fw_update_status(GlobalPortId(0)).await.unwrap(); - info!("Get retimer fw update status: {rt_fw_update_status:?}"); - - info!("Setting retimer fw update state"); - external::port_set_rt_fw_update_state(GlobalPortId(0)).await.unwrap(); - - info!("Clearing retimer fw update state"); - external::port_clear_rt_fw_update_state(GlobalPortId(0)).await.unwrap(); - - info!("Setting retimer compliance"); - external::port_set_rt_compliance(GlobalPortId(0)).await.unwrap(); - - info!("Setting max sink voltage"); - external::set_max_sink_voltage(GlobalPortId(0), Some(5000)) - .await - .unwrap(); - - info!("Clearing dead battery flag"); - external::clear_dead_battery_flag(GlobalPortId(0)).await.unwrap(); - - info!("Reconfiguring retimer"); - external::reconfigure_retimer(GlobalPortId(0)).await.unwrap(); -} - -#[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() -} - -fn main() { - env_logger::builder().filter_level(log::LevelFilter::Trace).init(); - - static EXECUTOR: StaticCell = StaticCell::new(); - let executor = EXECUTOR.init(Executor::new()); - executor.run(|spawner| { - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(task(spawner).unwrap()); - spawner.spawn(controller_task().unwrap()); - }); -} diff --git a/type-c-service/Cargo.toml b/type-c-service/Cargo.toml index a1a89affd..10e03ffab 100644 --- a/type-c-service/Cargo.toml +++ b/type-c-service/Cargo.toml @@ -30,12 +30,18 @@ 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"] } +embassy-time = { workspace = true, features = [ + "log", + "std", + "generic-queue-8", +] } critical-section = { workspace = true, features = ["std"] } -embassy-time-driver = { workspace = true } -embassy-futures.workspace = true tokio = { workspace = true, features = ["rt", "macros", "time"] } +env_logger = "0.11.8" +paste = "1" +static_cell.workspace = true +log.workspace = true +power-policy-service.workspace = true [features] default = [] diff --git a/type-c-service/tests/common/mock.rs b/type-c-service/tests/common/mock.rs new file mode 100644 index 000000000..511b5b8b5 --- /dev/null +++ b/type-c-service/tests/common/mock.rs @@ -0,0 +1,755 @@ +#![allow(dead_code)] +#![allow(clippy::panic)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +use core::num::NonZeroU8; +use embassy_sync::{mutex::Mutex, signal::Signal}; +use embedded_cfu_protocol::protocol_definitions::{FwUpdateOfferResponse, HostToken}; +use embedded_services::{ + GlobalRawMutex, + power::policy::PowerCapability, + type_c::{ + controller::{ + AttnVdm, ControllerStatus, DiscoveredSvids, DpConfig, DpStatus, OtherVdm, PdStateMachineConfig, PortStatus, + RetimerFwUpdateState, SendVdm, SystemPowerState, TbtConfig, TypeCStateMachineState, UsbControlConfig, + }, + event::PortEvent, + }, +}; +use embedded_usb_pd::{Error, ado::Ado}; +use embedded_usb_pd::{LocalPortId, PdError}; +use embedded_usb_pd::{PowerRole, type_c::Current}; +use embedded_usb_pd::{type_c::ConnectionState, ucsi::lpm}; +use log::{debug, info}; +use std::collections::VecDeque; + +/// Enum containing all possible function calls +#[derive(Debug, PartialEq, Eq)] +pub enum FnCall { + WaitPortEvent, + ClearPortEvents(LocalPortId), + GetPortStatus(LocalPortId), + EnableSinkPath(LocalPortId, bool), + GetControllerStatus, + ResetController, + GetRtFwUpdateStatus(LocalPortId), + SetRtFwUpdateState(LocalPortId), + ClearRtFwUpdateState(LocalPortId), + SetRtCompliance(LocalPortId), + GetPdAlert(LocalPortId), + SetUnconstrainedPower(LocalPortId, bool), + GetActiveFwVersion, + StartFwUpdate, + AbortFwUpdate, + FinalizeFwUpdate, + WriteFwContents(usize, Vec), + SetMaxSinkVoltage(LocalPortId, Option), + ReconfigureRetimer(LocalPortId), + ClearDeadBatteryFlag(LocalPortId), + GetOtherVdm(LocalPortId), + GetAttnVdm(LocalPortId), + SendVdm(LocalPortId, SendVdm), + SetUsbControl(LocalPortId, UsbControlConfig), + GetDpStatus(LocalPortId), + SetDpConfig(LocalPortId, DpConfig), + ExecuteDrst(LocalPortId), + SetTbtConfig(LocalPortId, TbtConfig), + SetPdStateMachineConfig(LocalPortId, PdStateMachineConfig), + SetTypeCStateMachineConfig(LocalPortId, TypeCStateMachineState), + ExecuteUcsiCommand(lpm::LocalCommand), + ExecuteElectricalDisconnect(LocalPortId, Option), + SetPowerState(LocalPortId, SystemPowerState), + GetDiscoveredSvids(LocalPortId), + HardReset(LocalPortId), + GetDiscoverIdentitySopResponse(LocalPortId), + GetDiscoverIdentitySopPrimeResponse(LocalPortId), +} + +pub struct ControllerState<'a> { + /// The function calls that have been made + pub fn_calls: VecDeque, + /// Next result to return for [`Controller::wait_port_event`] + pub next_result_wait_port_event: VecDeque>, + /// Next result to return for [`Controller::clear_port_events`] + pub next_result_clear_port_events: VecDeque>, + /// Next result to return for [`Controller::get_port_status`] + pub next_result_get_port_status: VecDeque>, + /// Next result to return for [`Controller::enable_sink_path`] + pub next_result_enable_sink_path: VecDeque>, + /// Next result to return for [`Controller::get_controller_status`] + pub next_result_get_controller_status: VecDeque, PdError>>, + /// Next result to return for [`Controller::reset_controller`] + pub next_result_reset_controller: VecDeque>, + /// Next result to return for [`Controller::get_rt_fw_update_status`] + pub next_result_get_rt_fw_update_status: VecDeque>, + /// Next result to return for [`Controller::set_rt_fw_update_state`] + pub next_result_set_rt_fw_update_state: VecDeque>, + /// Next result to return for [`Controller::clear_rt_fw_update_state`] + pub next_result_clear_rt_fw_update_state: VecDeque>, + /// Next result to return for [`Controller::set_rt_compliance`] + pub next_result_set_rt_compliance: VecDeque>, + /// Next result to return for [`Controller::get_pd_alert`] + pub next_result_get_pd_alert: VecDeque, PdError>>, + /// Next result to return for [`Controller::set_unconstrained_power`] + pub next_result_set_unconstrained_power: VecDeque>, + /// Next result to return for [`Controller::get_active_fw_version`] + pub next_result_get_active_fw_version: VecDeque>, + /// Next result to return for [`Controller::start_fw_update`] + pub next_result_start_fw_update: VecDeque>, + /// Next result to return for [`Controller::abort_fw_update`] + pub next_result_abort_fw_update: VecDeque>, + /// Next result to return for [`Controller::finalize_fw_update`] + pub next_result_finalize_fw_update: VecDeque>, + /// Next result to return for [`Controller::write_fw_contents`] + pub next_result_write_fw_contents: VecDeque>, + /// Next result to return for [`Controller::set_max_sink_voltage`] + pub next_result_set_max_sink_voltage: VecDeque>, + /// Next result to return for [`Controller::reconfigure_retimer`] + pub next_result_reconfigure_retimer: VecDeque>, + /// Next result to return for [`Controller::clear_dead_battery_flag`] + pub next_result_clear_dead_battery_flag: VecDeque>, + /// Next result to return for [`Controller::get_other_vdm`] + pub next_result_get_other_vdm: VecDeque>, + /// Next result to return for [`Controller::get_attn_vdm`] + pub next_result_get_attn_vdm: VecDeque>, + /// Next result to return for [`Controller::send_vdm`] + pub next_result_send_vdm: VecDeque>, + /// Next result to return for [`Controller::set_usb_control`] + pub next_result_set_usb_control: VecDeque>, + /// Next result to return for [`Controller::get_dp_status`] + pub next_result_get_dp_status: VecDeque>, + /// Next result to return for [`Controller::set_dp_config`] + pub next_result_set_dp_config: VecDeque>, + /// Next result to return for [`Controller::execute_drst`] + pub next_result_execute_drst: VecDeque>, + /// Next result to return for [`Controller::set_tbt_config`] + pub next_result_set_tbt_config: VecDeque>, + /// Next result to return for [`Controller::set_pd_state_machine_config`] + pub next_result_set_pd_state_machine_config: VecDeque>, + /// Next result to return for [`Controller::set_type_c_state_machine_config`] + pub next_result_set_type_c_state_machine_config: VecDeque>, + /// Next result to return for [`Controller::execute_ucsi_command`] + pub next_result_execute_ucsi_command: VecDeque, PdError>>, + /// Next result to return for [`Controller::execute_electrical_disconnect`] + pub next_result_execute_electrical_disconnect: VecDeque>, + /// Next result to return for [`Controller::set_power_state`] + pub next_result_set_power_state: VecDeque>, + /// Next result to return for [`Controller::get_discovered_svids`] + pub next_result_get_discovered_svids: VecDeque>, + /// Next result to return for [`Controller::hard_reset`] + pub next_result_hard_reset: VecDeque>, + /// Next result to return for [`Controller::get_discover_identity_sop_response`] + pub next_result_get_discover_identity_sop_response: + VecDeque>, + /// Next result to return for [`Controller::get_discover_identity_sop_prime_response`] + pub next_result_get_discover_identity_sop_prime_response: VecDeque< + Result, + >, + interrupt: &'a Signal, +} + +impl<'a> ControllerState<'a> { + pub fn new(interrupt: &'a Signal) -> Self { + Self { + fn_calls: VecDeque::new(), + next_result_wait_port_event: VecDeque::new(), + next_result_clear_port_events: VecDeque::new(), + next_result_get_port_status: VecDeque::new(), + next_result_enable_sink_path: VecDeque::new(), + next_result_get_controller_status: VecDeque::new(), + next_result_reset_controller: VecDeque::new(), + next_result_get_rt_fw_update_status: VecDeque::new(), + next_result_set_rt_fw_update_state: VecDeque::new(), + next_result_clear_rt_fw_update_state: VecDeque::new(), + next_result_set_rt_compliance: VecDeque::new(), + next_result_get_pd_alert: VecDeque::new(), + next_result_set_unconstrained_power: VecDeque::new(), + next_result_get_active_fw_version: VecDeque::new(), + next_result_start_fw_update: VecDeque::new(), + next_result_abort_fw_update: VecDeque::new(), + next_result_finalize_fw_update: VecDeque::new(), + next_result_write_fw_contents: VecDeque::new(), + next_result_set_max_sink_voltage: VecDeque::new(), + next_result_reconfigure_retimer: VecDeque::new(), + next_result_clear_dead_battery_flag: VecDeque::new(), + next_result_get_other_vdm: VecDeque::new(), + next_result_get_attn_vdm: VecDeque::new(), + next_result_send_vdm: VecDeque::new(), + next_result_set_usb_control: VecDeque::new(), + next_result_get_dp_status: VecDeque::new(), + next_result_set_dp_config: VecDeque::new(), + next_result_execute_drst: VecDeque::new(), + next_result_set_tbt_config: VecDeque::new(), + next_result_set_pd_state_machine_config: VecDeque::new(), + next_result_set_type_c_state_machine_config: VecDeque::new(), + next_result_execute_ucsi_command: VecDeque::new(), + next_result_execute_electrical_disconnect: VecDeque::new(), + next_result_set_power_state: VecDeque::new(), + next_result_get_discovered_svids: VecDeque::new(), + next_result_hard_reset: VecDeque::new(), + next_result_get_discover_identity_sop_response: VecDeque::new(), + next_result_get_discover_identity_sop_prime_response: VecDeque::new(), + interrupt, + } + } + + /// Simulate a connection + pub async fn connect(&mut self, role: PowerRole, capability: PowerCapability, debug: bool, unconstrained: bool) { + let mut status = PortStatus::new(); + status.connection_state = Some(if debug { + ConnectionState::DebugAccessory + } else { + ConnectionState::Attached + }); + match role { + PowerRole::Source => { + status.available_source_contract = Some(capability); + status.unconstrained_power = unconstrained; + } + PowerRole::Sink => { + status.available_sink_contract = Some(capability); + status.unconstrained_power = unconstrained; + } + } + self.next_result_get_port_status.push_back(Ok(status)); + + let mut events = PortEvent::none(); + events.status.set_plug_inserted_or_removed(true); + events.status.set_new_power_contract_as_consumer(true); + events.status.set_sink_ready(true); + self.next_result_clear_port_events.push_back(Ok(events)); + self.next_result_wait_port_event.push_back(Ok(())); + self.interrupt.signal(()); + } + + /// Simulate a sink connecting + pub async fn connect_sink(&mut self, capability: PowerCapability, unconstrained: bool) { + self.connect(PowerRole::Sink, capability, false, unconstrained).await; + } + + /// Simulate a disconnection + pub async fn disconnect(&mut self) { + self.next_result_get_port_status.push_back(Ok(PortStatus::default())); + + let mut events = PortEvent::none(); + events.status.set_plug_inserted_or_removed(true); + self.next_result_clear_port_events.push_back(Ok(events)); + self.next_result_wait_port_event.push_back(Ok(())); + self.interrupt.signal(()); + } + + /// Simulate a debug accessory source connecting + pub async fn connect_debug_accessory_source(&mut self, current: Current) { + self.connect(PowerRole::Source, current.into(), true, false).await; + } + + /// Simulate a PD alert + pub async fn send_pd_alert(&mut self, ado: Ado) { + self.next_result_get_pd_alert.push_back(Ok(Some(ado))); + + let mut events = PortEvent::none(); + events.notification.set_alert(true); + self.next_result_clear_port_events.push_back(Ok(events)); + self.next_result_wait_port_event.push_back(Ok(())); + self.interrupt.signal(()); + } +} + +pub struct Controller<'a> { + state: &'a Mutex>, + interrupt: &'a Signal, +} + +impl<'a> Controller<'a> { + pub fn new( + state: &'a Mutex>, + interrupt: &'a Signal, + ) -> Self { + Self { state, interrupt } + } + + /// Function to demonstrate calling functions directly on the controller + pub fn custom_function(&self) { + info!("Custom function called on controller"); + } +} + +impl embedded_services::type_c::controller::Controller for Controller<'_> { + type BusError = (); + + async fn wait_port_event(&mut self) -> Result<(), Error> { + self.interrupt.wait().await; + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::WaitPortEvent); + state + .next_result_wait_port_event + .pop_front() + .expect("next_result_wait_port_event not set") + .map_err(Error::Pd) + } + + async fn clear_port_events(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ClearPortEvents(port)); + let result = state + .next_result_clear_port_events + .pop_front() + .expect("next_result_clear_port_events not set"); + debug!("Clear port events: {result:#?}"); + result.map_err(Error::Pd) + } + + async fn get_port_status(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetPortStatus(port)); + let result = state + .next_result_get_port_status + .pop_front() + .expect("next_result_get_port_status not set"); + debug!("Get port status: {result:#?}"); + result.map_err(Error::Pd) + } + + async fn enable_sink_path(&mut self, port: LocalPortId, enable: bool) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::EnableSinkPath(port, enable)); + debug!("Enable sink path: {enable}"); + state + .next_result_enable_sink_path + .pop_front() + .expect("next_result_enable_sink_path not set") + .map_err(Error::Pd) + } + + async fn get_controller_status(&mut self) -> Result, Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetControllerStatus); + debug!("Get controller status"); + state + .next_result_get_controller_status + .pop_front() + .expect("next_result_get_controller_status not set") + .map_err(Error::Pd) + } + + async fn reset_controller(&mut self) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ResetController); + debug!("Reset controller"); + state + .next_result_reset_controller + .pop_front() + .expect("next_result_reset_controller not set") + .map_err(Error::Pd) + } + + async fn get_rt_fw_update_status( + &mut self, + port: LocalPortId, + ) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetRtFwUpdateStatus(port)); + debug!("Get retimer fw update status"); + state + .next_result_get_rt_fw_update_status + .pop_front() + .expect("next_result_get_rt_fw_update_status not set") + .map_err(Error::Pd) + } + + async fn set_rt_fw_update_state(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::SetRtFwUpdateState(port)); + debug!("Set retimer fw update state"); + state + .next_result_set_rt_fw_update_state + .pop_front() + .expect("next_result_set_rt_fw_update_state not set") + .map_err(Error::Pd) + } + + async fn clear_rt_fw_update_state(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ClearRtFwUpdateState(port)); + debug!("Clear retimer fw update state"); + state + .next_result_clear_rt_fw_update_state + .pop_front() + .expect("next_result_clear_rt_fw_update_state not set") + .map_err(Error::Pd) + } + + async fn set_rt_compliance(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::SetRtCompliance(port)); + debug!("Set retimer compliance"); + state + .next_result_set_rt_compliance + .pop_front() + .expect("next_result_set_rt_compliance not set") + .map_err(Error::Pd) + } + + async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetPdAlert(port)); + let result = state + .next_result_get_pd_alert + .pop_front() + .expect("next_result_get_pd_alert not set"); + if let Ok(Some(ado)) = &result { + debug!("Port{}: Get PD alert: {ado:#?}", port.0); + } else { + debug!("Port{}: No PD alert", port.0); + } + result.map_err(Error::Pd) + } + + async fn set_unconstrained_power( + &mut self, + port: LocalPortId, + unconstrained: bool, + ) -> Result<(), Error> { + let mut state = self.state.lock().await; + state + .fn_calls + .push_back(FnCall::SetUnconstrainedPower(port, unconstrained)); + debug!("Set unconstrained power: {unconstrained}"); + state + .next_result_set_unconstrained_power + .pop_front() + .expect("next_result_set_unconstrained_power not set") + .map_err(Error::Pd) + } + + async fn get_active_fw_version(&mut self) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetActiveFwVersion); + state + .next_result_get_active_fw_version + .pop_front() + .expect("next_result_get_active_fw_version not set") + .map_err(Error::Pd) + } + + async fn start_fw_update(&mut self) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::StartFwUpdate); + state + .next_result_start_fw_update + .pop_front() + .expect("next_result_start_fw_update not set") + .map_err(Error::Pd) + } + + async fn abort_fw_update(&mut self) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::AbortFwUpdate); + state + .next_result_abort_fw_update + .pop_front() + .expect("next_result_abort_fw_update not set") + .map_err(Error::Pd) + } + + async fn finalize_fw_update(&mut self) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::FinalizeFwUpdate); + state + .next_result_finalize_fw_update + .pop_front() + .expect("next_result_finalize_fw_update not set") + .map_err(Error::Pd) + } + + async fn write_fw_contents(&mut self, offset: usize, data: &[u8]) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::WriteFwContents(offset, data.to_vec())); + state + .next_result_write_fw_contents + .pop_front() + .expect("next_result_write_fw_contents not set") + .map_err(Error::Pd) + } + + async fn set_max_sink_voltage( + &mut self, + port: LocalPortId, + voltage_mv: Option, + ) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::SetMaxSinkVoltage(port, voltage_mv)); + debug!("Set max sink voltage for port {}: {:?}", port.0, voltage_mv); + state + .next_result_set_max_sink_voltage + .pop_front() + .expect("next_result_set_max_sink_voltage not set") + .map_err(Error::Pd) + } + + async fn reconfigure_retimer(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ReconfigureRetimer(port)); + debug!("reconfigure_retimer(port: {port:?})"); + state + .next_result_reconfigure_retimer + .pop_front() + .expect("next_result_reconfigure_retimer not set") + .map_err(Error::Pd) + } + + async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ClearDeadBatteryFlag(port)); + debug!("clear_dead_battery_flag(port: {port:?})"); + state + .next_result_clear_dead_battery_flag + .pop_front() + .expect("next_result_clear_dead_battery_flag not set") + .map_err(Error::Pd) + } + + async fn get_other_vdm(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetOtherVdm(port)); + debug!("Get other VDM for port {port:?}"); + state + .next_result_get_other_vdm + .pop_front() + .expect("next_result_get_other_vdm not set") + .map_err(Error::Pd) + } + + async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetAttnVdm(port)); + debug!("Get attention VDM for port {port:?}"); + state + .next_result_get_attn_vdm + .pop_front() + .expect("next_result_get_attn_vdm not set") + .map_err(Error::Pd) + } + + async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), Error> { + let mut state = self.state.lock().await; + debug!("Send VDM for port {port:?}: {tx_vdm:?}"); + state.fn_calls.push_back(FnCall::SendVdm(port, tx_vdm)); + state + .next_result_send_vdm + .pop_front() + .expect("next_result_send_vdm not set") + .map_err(Error::Pd) + } + + async fn set_usb_control( + &mut self, + port: LocalPortId, + config: UsbControlConfig, + ) -> Result<(), Error> { + let mut state = self.state.lock().await; + debug!( + "set_usb_control(port: {port:?}, usb2: {}, usb3: {}, usb4: {})", + config.usb2_enabled, config.usb3_enabled, config.usb4_enabled + ); + state.fn_calls.push_back(FnCall::SetUsbControl(port, config)); + state + .next_result_set_usb_control + .pop_front() + .expect("next_result_set_usb_control not set") + .map_err(Error::Pd) + } + + async fn get_dp_status(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetDpStatus(port)); + debug!("Get DisplayPort status for port {port:?}"); + state + .next_result_get_dp_status + .pop_front() + .expect("next_result_get_dp_status not set") + .map_err(Error::Pd) + } + + async fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> Result<(), Error> { + let mut state = self.state.lock().await; + debug!( + "Set DisplayPort config for port {port:?}: enable={}, pin_cfg={:?}", + config.enable, config.dfp_d_pin_cfg + ); + state.fn_calls.push_back(FnCall::SetDpConfig(port, config)); + state + .next_result_set_dp_config + .pop_front() + .expect("next_result_set_dp_config not set") + .map_err(Error::Pd) + } + + async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::ExecuteDrst(port)); + debug!("Execute PD Data Reset for port {port:?}"); + state + .next_result_execute_drst + .pop_front() + .expect("next_result_execute_drst not set") + .map_err(Error::Pd) + } + + async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), Error> { + let mut state = self.state.lock().await; + debug!("Set Thunderbolt config for port {port:?}: {config:?}"); + state.fn_calls.push_back(FnCall::SetTbtConfig(port, config)); + state + .next_result_set_tbt_config + .pop_front() + .expect("next_result_set_tbt_config not set") + .map_err(Error::Pd) + } + + async fn set_pd_state_machine_config( + &mut self, + port: LocalPortId, + config: PdStateMachineConfig, + ) -> Result<(), Error> { + let mut state = self.state.lock().await; + debug!("Set PD State Machine config for port {port:?}: {config:?}"); + state.fn_calls.push_back(FnCall::SetPdStateMachineConfig(port, config)); + state + .next_result_set_pd_state_machine_config + .pop_front() + .expect("next_result_set_pd_state_machine_config not set") + .map_err(Error::Pd) + } + + async fn set_type_c_state_machine_config( + &mut self, + port: LocalPortId, + state: TypeCStateMachineState, + ) -> Result<(), Error> { + let mut lock = self.state.lock().await; + debug!("Set Type-C State Machine state for port {port:?}: {state:?}"); + lock.fn_calls.push_back(FnCall::SetTypeCStateMachineConfig(port, state)); + lock.next_result_set_type_c_state_machine_config + .pop_front() + .expect("next_result_set_type_c_state_machine_config not set") + .map_err(Error::Pd) + } + + async fn execute_ucsi_command( + &mut self, + command: lpm::LocalCommand, + ) -> Result, Error> { + let mut state = self.state.lock().await; + debug!("Execute UCSI command for port {:?}: {command:?}", command.port()); + state.fn_calls.push_back(FnCall::ExecuteUcsiCommand(command)); + state + .next_result_execute_ucsi_command + .pop_front() + .map(|r| r.map_err(Error::Pd)) + .expect("next_result_execute_ucsi_command not set") + } + + async fn execute_electrical_disconnect( + &mut self, + port: LocalPortId, + reconnect_time_s: Option, + ) -> Result<(), Error> { + let mut state = self.state.lock().await; + state + .fn_calls + .push_back(FnCall::ExecuteElectricalDisconnect(port, reconnect_time_s)); + debug!("Execute electrical disconnect for port {port:?} with reconnect time {reconnect_time_s:?}"); + state + .next_result_execute_electrical_disconnect + .pop_front() + .expect("next_result_execute_electrical_disconnect not set") + .map_err(Error::Pd) + } + + async fn set_power_state( + &mut self, + port: LocalPortId, + state: SystemPowerState, + ) -> Result<(), Error> { + let mut lock = self.state.lock().await; + debug!("Set power state for port {port:?}: {state:?}"); + lock.fn_calls.push_back(FnCall::SetPowerState(port, state)); + lock.next_result_set_power_state + .pop_front() + .expect("next_result_set_power_state not set") + .map_err(Error::Pd) + } + + async fn get_discovered_svids(&mut self, port: LocalPortId) -> Result> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetDiscoveredSvids(port)); + debug!("Get discovered SVIDs for port {port:?}"); + state + .next_result_get_discovered_svids + .pop_front() + .expect("next_result_get_discovered_svids not set") + .map_err(Error::Pd) + } + + async fn hard_reset(&mut self, port: LocalPortId) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::HardReset(port)); + debug!("Hard reset for port {port:?}"); + state + .next_result_hard_reset + .pop_front() + .expect("next_result_hard_reset not set") + .map_err(Error::Pd) + } + + async fn get_discover_identity_sop_response( + &mut self, + port: LocalPortId, + ) -> Result> + { + let mut state = self.state.lock().await; + state.fn_calls.push_back(FnCall::GetDiscoverIdentitySopResponse(port)); + debug!("Get Discover Identity SOP response for port {port:?}"); + state + .next_result_get_discover_identity_sop_response + .pop_front() + .expect("next_result_get_discover_identity_sop_response not set") + .map_err(Error::Pd) + } + + async fn get_discover_identity_sop_prime_response( + &mut self, + port: LocalPortId, + ) -> Result< + embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos, + Error, + > { + let mut state = self.state.lock().await; + state + .fn_calls + .push_back(FnCall::GetDiscoverIdentitySopPrimeResponse(port)); + debug!("Get Discover Identity SOP' response for port {port:?}"); + state + .next_result_get_discover_identity_sop_prime_response + .pop_front() + .expect("next_result_get_discover_identity_sop_prime_response not set") + .map_err(Error::Pd) + } +} + +pub struct Validator; + +impl type_c_service::wrapper::FwOfferValidator for Validator { + fn validate( + &self, + _current: embedded_cfu_protocol::protocol_definitions::FwVersion, + _offer: &embedded_cfu_protocol::protocol_definitions::FwUpdateOffer, + ) -> embedded_cfu_protocol::protocol_definitions::FwUpdateOfferResponse { + // For this example, we always accept the new version + FwUpdateOfferResponse::new_accept(HostToken::Driver) + } +} + +pub type Wrapper<'a> = + type_c_service::wrapper::ControllerWrapper<'a, GlobalRawMutex, Mutex>, Validator>; diff --git a/type-c-service/tests/common/mod.rs b/type-c-service/tests/common/mod.rs new file mode 100644 index 000000000..f1d687d24 --- /dev/null +++ b/type-c-service/tests/common/mod.rs @@ -0,0 +1,245 @@ +#![allow(dead_code)] +#![allow(clippy::panic)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +use embassy_futures::{ + join::{join, join3}, + select::{Either, select}, +}; +use embassy_sync::{ + mutex::Mutex, + once_lock::OnceLock, + pubsub::{DynSubscriber, PubSubChannel}, + watch::{DynReceiver, Watch}, +}; +use embassy_time::{Duration, with_timeout}; +use embedded_services::{ + GlobalRawMutex, broadcaster, info, + power::{self, policy}, + type_c::{self, ControllerId}, +}; +use embedded_usb_pd::GlobalPortId; +use paste::paste; +use static_cell::StaticCell; + +pub mod mock; +pub const DEFAULT_TEST_DURATION: Duration = Duration::from_secs(5); + +pub const DEFAULT_PER_CALL_TIMEOUT: Duration = Duration::from_secs(1); + +const CONTROLLER0_ID: ControllerId = ControllerId(0); +const PORT0_ID: GlobalPortId = GlobalPortId(0); +const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); +const CFU0_ID: u8 = 0x00; + +const CONTROLLER1_ID: ControllerId = ControllerId(1); +const PORT1_ID: GlobalPortId = GlobalPortId(1); +const POWER1_ID: power::policy::DeviceId = power::policy::DeviceId(1); +const CFU1_ID: u8 = 0x01; + +const CONTROLLER2_ID: ControllerId = ControllerId(2); +const PORT2_ID: GlobalPortId = GlobalPortId(2); +const POWER2_ID: power::policy::DeviceId = power::policy::DeviceId(2); +const CFU2_ID: u8 = 0x02; + +/// Integration test trait +/// +/// Directly taking async closures is messy and requires an intermediate trait anyway +pub trait Test { + /// Run the test + fn run( + &mut self, + type_c_receiver: DynSubscriber<'static, type_c::comms::CommsMessage>, + power_policy_event_receiver: DynSubscriber<'static, policy::CommsMessage>, + port0: &'static Mutex>, + port1: &'static Mutex>, + port2: &'static Mutex>, + ) -> impl Future; +} + +async fn controller_task(wrapper: &'static mock::Wrapper<'static>, mut completion_signal: DynReceiver<'static, ()>) { + while let Either::First(result) = select(wrapper.process_next_event(), completion_signal.get()).await { + result.unwrap(); + } +} + +async fn power_policy_task( + config: power_policy_service::config::Config, + mut completion_signal: DynReceiver<'static, ()>, +) { + if let Either::First(result) = select(power_policy_service::task::task(config), completion_signal.get()).await { + panic!("Power policy task completed before test end: {result:?}"); + } +} + +async fn type_c_service_task( + config: type_c_service::service::config::Config, + mut completion_signal: DynReceiver<'static, ()>, +) { + if let Either::First(result) = select(type_c_service::task::task(config), completion_signal.get()).await { + panic!("Type-C service task completed before test end: {result:?}"); + } +} + +pub struct PortComponents<'a> { + pub state: &'a Mutex>, + pub wrapper: &'a mock::Wrapper<'static>, +} + +macro_rules! define_controller { + ($name:ident, $controller_id:expr, $port_id:expr, $power_id:expr, $cfu_id:expr) => { + paste! { + static [<$name _STORAGE>]: ::static_cell::StaticCell<::type_c_service::wrapper::backing::Storage<1, GlobalRawMutex>> = ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _storage>] = + [<$name _STORAGE>].init(::type_c_service::wrapper::backing::Storage::new($controller_id, $cfu_id, [($port_id, $power_id)])); + + static [<$name _REFERENCED>]: ::static_cell::StaticCell<::type_c_service::wrapper::backing::ReferencedStorage<1, GlobalRawMutex>> = + ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _referenced>] = [<$name _REFERENCED>].init( + [<$name _storage>] + .create_referenced() + .expect("Failed to create referenced storage"), + ); + + static [<$name _INTERRUPT>]: ::static_cell::StaticCell<::embassy_sync::signal::Signal<::embedded_services::GlobalRawMutex, ()>> = ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _interrupt>] = [<$name _INTERRUPT>].init(::embassy_sync::signal::Signal::new()); + + static [<$name _STATE>]: ::static_cell::StaticCell<::embassy_sync::mutex::Mutex<::embedded_services::GlobalRawMutex, mock::ControllerState<'static>>> = + ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _state>] = [<$name _STATE>] + .init(::embassy_sync::mutex::Mutex::new(mock::ControllerState::new([<$name _interrupt>]))); + + static [<$name _DEVICE>]: ::static_cell::StaticCell<::embassy_sync::mutex::Mutex<::embedded_services::GlobalRawMutex, mock::Controller<'static>>> = ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _device>] = [<$name _DEVICE>] + .init(::embassy_sync::mutex::Mutex::new(mock::Controller::new([<$name _state>], [<$name _interrupt>]))); + + static [<$name _WRAPPER>]: ::static_cell::StaticCell> = ::static_cell::StaticCell::new(); + #[allow(non_snake_case)] + let [<$name _wrapper>] = [<$name _WRAPPER>].init( + mock::Wrapper::try_new( + [<$name _device>], + Default::default(), + [<$name _referenced>], + mock::Validator, + ) + .expect("Failed to create wrapper") + ); + #[allow(non_snake_case)] + let $name = PortComponents { + state: [<$name _state>], + wrapper: [<$name _wrapper>], + }; + } + }; +} + +/// Initialize services and run an integration test +pub async fn run_test( + duration: Duration, + type_c_service_config: type_c_service::service::config::Config, + power_policy_service_config: power_policy_service::config::Config, + mut test: impl Test, +) { + // Tokio runs tests in parallel, but logging is global so we need to run tests sequentially to avoid interleaved logs. + static TEST_MUTEX: OnceLock> = OnceLock::new(); + let test_mutex = TEST_MUTEX.get_or_init(|| Mutex::new(())); + let _lock = test_mutex.lock().await; + + // Initialize logging, ignore the error if the logger was already initialized by another test. + let _ = env_logger::builder().filter_level(log::LevelFilter::Info).try_init(); + + // 5 for the three controller tasks, power policy task, and type-C service task. + static COMPLETION_SIGNAL: StaticCell> = StaticCell::new(); + let completion_signal = COMPLETION_SIGNAL.init(Watch::new()); + + embedded_services::init().await; + + info!("Creating power policy service event channel"); + static POWER_POLICY_SERVICE_EVENT_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + let power_policy_service_event_channel = POWER_POLICY_SERVICE_EVENT_CHANNEL.init(PubSubChannel::new()); + + let power_policy_service_event_publisher = power_policy_service_event_channel.dyn_immediate_publisher(); + let power_policy_service_event_subscriber = power_policy_service_event_channel.dyn_subscriber().unwrap(); + + static POWER_POLICY_SERVICE_EVENT_RECEIVER: StaticCell< + broadcaster::immediate::Receiver<'static, policy::CommsMessage>, + > = StaticCell::new(); + let power_policy_service_event_receiver = POWER_POLICY_SERVICE_EVENT_RECEIVER.init( + broadcaster::immediate::Receiver::new(power_policy_service_event_publisher), + ); + + policy::policy::register_message_receiver(power_policy_service_event_receiver).unwrap(); + + info!("Creating type-C service event channel"); + static TYPE_C_SERVICE_EVENT_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + let type_c_service_event_channel = TYPE_C_SERVICE_EVENT_CHANNEL.init(PubSubChannel::new()); + + let type_c_service_event_publisher = type_c_service_event_channel.dyn_immediate_publisher(); + let type_c_service_event_subscriber = type_c_service_event_channel.dyn_subscriber().unwrap(); + + static TYPE_C_SERVICE_EVENT_RECEIVER: StaticCell< + broadcaster::immediate::Receiver<'static, type_c::comms::CommsMessage>, + > = StaticCell::new(); + let type_c_service_event_receiver = + TYPE_C_SERVICE_EVENT_RECEIVER.init(broadcaster::immediate::Receiver::new(type_c_service_event_publisher)); + + type_c::controller::register_message_receiver(type_c_service_event_receiver).unwrap(); + + define_controller!(CONTROLLER0, CONTROLLER0_ID, PORT0_ID, POWER0_ID, CFU0_ID); + let PortComponents { + state: controller0_state, + wrapper: controller0_wrapper, + } = CONTROLLER0; + controller0_wrapper.register().await.unwrap(); + + define_controller!(CONTROLLER1, CONTROLLER1_ID, PORT1_ID, POWER1_ID, CFU1_ID); + let PortComponents { + state: controller1_state, + wrapper: controller1_wrapper, + } = CONTROLLER1; + controller1_wrapper.register().await.unwrap(); + + define_controller!(CONTROLLER2, CONTROLLER2_ID, PORT2_ID, POWER2_ID, CFU2_ID); + let PortComponents { + state: controller2_state, + wrapper: controller2_wrapper, + } = CONTROLLER2; + controller2_wrapper.register().await.unwrap(); + + with_timeout( + duration, + join3( + join( + power_policy_task(power_policy_service_config, completion_signal.dyn_receiver().unwrap()), + type_c_service_task(type_c_service_config, completion_signal.dyn_receiver().unwrap()), + ), + join3( + controller_task(controller0_wrapper, completion_signal.dyn_receiver().unwrap()), + controller_task(controller1_wrapper, completion_signal.dyn_receiver().unwrap()), + controller_task(controller2_wrapper, completion_signal.dyn_receiver().unwrap()), + ), + async { + test.run( + type_c_service_event_subscriber, + power_policy_service_event_subscriber, + controller0_state, + controller1_state, + controller2_state, + ) + .await; + completion_signal.sender().send(()); + }, + ), + ) + .await + .unwrap(); +} diff --git a/type-c-service/tests/external.rs b/type-c-service/tests/external.rs new file mode 100644 index 000000000..d1baf1457 --- /dev/null +++ b/type-c-service/tests/external.rs @@ -0,0 +1,577 @@ +//! Integration test for external function calls + +mod common; + +use core::num::NonZeroU8; + +use common::Test; +use embassy_futures::join::join3; +use embassy_sync::{mutex::Mutex, pubsub::DynSubscriber}; +use embassy_time::with_timeout; +use embedded_services::{ + GlobalRawMutex, info, + power::policy, + type_c::{ + self, Cached, ControllerId, + controller::{ + ControllerStatus, DiscoveredSvids, DpConfig, DpPinConfig, DpStatus, PdStateMachineConfig, PortStatus, + RetimerFwUpdateState, SendVdm, SystemPowerState, TbtConfig, TypeCStateMachineState, UsbControlConfig, + }, + external, + }, +}; +use embedded_usb_pd::{ + GlobalPortId, LocalPortId, + usb::{Bcd, ProductId}, + vdm::structured::command::discover_identity::{ + CertStatVdo, ConnectorType, IdHeaderVdo, ProductVdo, + sop::{self, DfpProductTypeVdos, UfpProductTypeVdos}, + sop_prime::{self, ProductTypeVdos}, + }, +}; + +use common::DEFAULT_TEST_DURATION; +use common::mock; + +use crate::common::DEFAULT_PER_CALL_TIMEOUT; + +struct TestExternal; + +impl TestExternal { + async fn run_tests( + &self, + controller_id: ControllerId, + port_id: GlobalPortId, + port: &'static Mutex>, + ) { + let local_port_id = LocalPortId(0); + + // get_controller_status + info!("Testing get_controller_status"); + let expected_controller_status = ControllerStatus { + mode: "Test", + valid_fw_bank: true, + fw_version0: 0xbadbeef, + fw_version1: 0xbadcafe, + }; + port.lock() + .await + .next_result_get_controller_status + .push_back(Ok(expected_controller_status)); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::get_controller_status(controller_id)).await, + Ok(Ok(expected_controller_status)) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetControllerStatus) + ); + + // reset_controller + info!("Testing reset_controller"); + port.lock().await.next_result_reset_controller.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::reset_controller(controller_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ResetController) + ); + + // sync_controller_state + // The service fetches port status as a side-effect of syncing state. + info!("Testing sync_controller_state"); + port.lock() + .await + .next_result_get_port_status + .push_back(Ok(PortStatus::default())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::sync_controller_state(controller_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetPortStatus(local_port_id)) + ); + + // get_controller_num_ports + // Each test controller is registered with one port. + info!("Testing get_controller_num_ports"); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::get_controller_num_ports(controller_id) + ) + .await, + Ok(Ok(1)) + ); + + // controller_port_to_global_id + info!("Testing controller_port_to_global_id"); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::controller_port_to_global_id(controller_id, local_port_id) + ) + .await, + Ok(Ok(port_id)), + ); + + // global_port_to_controller_port + info!("Testing global_port_to_controller_port"); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::global_port_to_controller_port(port_id) + ) + .await, + Ok(Ok((controller_id, local_port_id))), + ); + + // get_num_ports + // Three controllers are registered in the test harness. + info!("Testing get_num_ports"); + assert_eq!(external::get_num_ports(), 3); + + // get_port_status + info!("Testing get_port_status"); + let expected_port_status = PortStatus::default(); + port.lock() + .await + .next_result_get_port_status + .push_back(Ok(expected_port_status)); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::get_port_status(port_id, Cached(false)) + ) + .await, + Ok(Ok(expected_port_status)), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetPortStatus(local_port_id)) + ); + + // get_controller_port_status + info!("Testing get_controller_port_status"); + port.lock() + .await + .next_result_get_port_status + .push_back(Ok(expected_port_status)); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::get_controller_port_status(controller_id, local_port_id, Cached(false)) + ) + .await, + Ok(Ok(expected_port_status)), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetPortStatus(local_port_id)) + ); + + // port_get_rt_fw_update_status + info!("Testing port_get_rt_fw_update_status"); + port.lock() + .await + .next_result_get_rt_fw_update_status + .push_back(Ok(RetimerFwUpdateState::Active)); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::port_get_rt_fw_update_status(port_id) + ) + .await, + Ok(Ok(RetimerFwUpdateState::Active)), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetRtFwUpdateStatus(local_port_id)) + ); + + // port_set_rt_fw_update_state + info!("Testing port_set_rt_fw_update_state"); + port.lock().await.next_result_set_rt_fw_update_state.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::port_set_rt_fw_update_state(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetRtFwUpdateState(local_port_id)) + ); + + // port_clear_rt_fw_update_state + info!("Testing port_clear_rt_fw_update_state"); + port.lock().await.next_result_clear_rt_fw_update_state.push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::port_clear_rt_fw_update_state(port_id) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ClearRtFwUpdateState(local_port_id)) + ); + + // port_set_rt_compliance + info!("Testing port_set_rt_compliance"); + port.lock().await.next_result_set_rt_compliance.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::port_set_rt_compliance(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetRtCompliance(local_port_id)) + ); + + // reconfigure_retimer + info!("Testing reconfigure_retimer"); + port.lock().await.next_result_reconfigure_retimer.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::reconfigure_retimer(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ReconfigureRetimer(local_port_id)) + ); + + // set_max_sink_voltage + info!("Testing set_max_sink_voltage"); + port.lock().await.next_result_set_max_sink_voltage.push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::set_max_sink_voltage(port_id, Some(5000)) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetMaxSinkVoltage(local_port_id, Some(5000))) + ); + + // clear_dead_battery_flag + info!("Testing clear_dead_battery_flag"); + port.lock().await.next_result_clear_dead_battery_flag.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::clear_dead_battery_flag(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ClearDeadBatteryFlag(local_port_id)) + ); + + // set_power_state + info!("Testing set_power_state"); + port.lock().await.next_result_set_power_state.push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::set_power_state(port_id, SystemPowerState::S0) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetPowerState(local_port_id, SystemPowerState::S0)) + ); + + // execute_electrical_disconnect + info!("Testing execute_electrical_disconnect"); + port.lock() + .await + .next_result_execute_electrical_disconnect + .push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::execute_electrical_disconnect(port_id, NonZeroU8::new(5)) + ) + .await, + Ok(Ok(())), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ExecuteElectricalDisconnect( + local_port_id, + NonZeroU8::new(5) + )) + ); + + // send_vdm + info!("Testing send_vdm"); + port.lock().await.next_result_send_vdm.push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::send_vdm(port_id, SendVdm::default()) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SendVdm(local_port_id, SendVdm::default())) + ); + + // set_usb_control + info!("Testing set_usb_control"); + port.lock().await.next_result_set_usb_control.push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::set_usb_control(port_id, UsbControlConfig::default()) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetUsbControl(local_port_id, UsbControlConfig::default())) + ); + + // get_dp_status + info!("Testing get_dp_status"); + let expected_dp_status = DpStatus { + alt_mode_entered: true, + dfp_d_pin_cfg: DpPinConfig { + pin_c: true, + pin_d: false, + pin_e: false, + }, + }; + port.lock() + .await + .next_result_get_dp_status + .push_back(Ok(expected_dp_status)); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::get_dp_status(port_id)).await, + Ok(Ok(expected_dp_status)) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetDpStatus(local_port_id)) + ); + + // set_dp_config + info!("Testing set_dp_config"); + let dp_config = DpConfig { + enable: true, + dfp_d_pin_cfg: DpPinConfig::default(), + }; + port.lock().await.next_result_set_dp_config.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::set_dp_config(port_id, dp_config)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetDpConfig(local_port_id, dp_config)) + ); + + // execute_drst + info!("Testing execute_drst"); + port.lock().await.next_result_execute_drst.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::execute_drst(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::ExecuteDrst(local_port_id)) + ); + + // set_tbt_config + info!("Testing set_tbt_config"); + let tbt_config = TbtConfig { tbt_enabled: true }; + port.lock().await.next_result_set_tbt_config.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::set_tbt_config(port_id, tbt_config)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetTbtConfig(local_port_id, tbt_config)) + ); + + // set_pd_state_machine_config + info!("Testing set_pd_state_machine_config"); + let pd_sm_config = PdStateMachineConfig { enabled: true }; + port.lock() + .await + .next_result_set_pd_state_machine_config + .push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::set_pd_state_machine_config(port_id, pd_sm_config) + ) + .await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetPdStateMachineConfig(local_port_id, pd_sm_config)) + ); + + // set_type_c_state_machine_config + info!("Testing set_type_c_state_machine_config"); + port.lock() + .await + .next_result_set_type_c_state_machine_config + .push_back(Ok(())); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::set_type_c_state_machine_config(port_id, TypeCStateMachineState::Drp) + ) + .await, + Ok(Ok(())), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::SetTypeCStateMachineConfig( + local_port_id, + TypeCStateMachineState::Drp + )) + ); + + // get_discovered_svids + info!("Testing get_discovered_svids"); + let expected_svids = DiscoveredSvids::default(); + port.lock() + .await + .next_result_get_discovered_svids + .push_back(Ok(expected_svids)); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::get_discovered_svids(port_id)).await, + Ok(Ok(expected_svids)) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetDiscoveredSvids(local_port_id)) + ); + + // hard_reset + info!("Testing hard_reset"); + port.lock().await.next_result_hard_reset.push_back(Ok(())); + assert_eq!( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, external::hard_reset(port_id)).await, + Ok(Ok(())) + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::HardReset(local_port_id)) + ); + + // get_discover_identity_sop_response + info!("Testing get_discover_identity_sop_response"); + let expected_sop_identity = sop::ResponseVdos { + id: IdHeaderVdo { + usb_vendor_id: 0x1234, + connector_type: ConnectorType::Plug, + modal_operation_supported: false, + usb_communication_capable_as_usb_device: true, + usb_communication_capable_as_usb_host: false, + }, + cert_stat: CertStatVdo(0x12345678), + product: ProductVdo { + usb_product_id: ProductId(0x5678), + bcd_device: Bcd(0x0100), + }, + dfp_product_type_vdos: DfpProductTypeVdos::NotADfp, + ufp_product_type_vdos: UfpProductTypeVdos::Psd, + }; + port.lock() + .await + .next_result_get_discover_identity_sop_response + .push_back(Ok(expected_sop_identity)); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::get_discover_identity_sop_response(port_id) + ) + .await, + Ok(Ok(expected_sop_identity)), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetDiscoverIdentitySopResponse(local_port_id)) + ); + + // get_discover_identity_sop_prime_response + info!("Testing get_discover_identity_sop_prime_response"); + let expected_sop_prime_identity = sop_prime::ResponseVdos { + id: IdHeaderVdo { + usb_vendor_id: 0x1234, + connector_type: ConnectorType::Plug, + modal_operation_supported: false, + usb_communication_capable_as_usb_device: true, + usb_communication_capable_as_usb_host: false, + }, + cert_stat: CertStatVdo(0x12345678), + product: ProductVdo { + usb_product_id: ProductId(0x5678), + bcd_device: Bcd(0x0100), + }, + product_type_vdos: ProductTypeVdos::NotACablePlugVpd, + }; + port.lock() + .await + .next_result_get_discover_identity_sop_prime_response + .push_back(Ok(expected_sop_prime_identity)); + assert_eq!( + with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + external::get_discover_identity_sop_prime_response(port_id) + ) + .await, + Ok(Ok(expected_sop_prime_identity)), + ); + assert_eq!( + port.lock().await.fn_calls.pop_front(), + Some(mock::FnCall::GetDiscoverIdentitySopPrimeResponse(local_port_id)) + ); + } +} + +impl Test for TestExternal { + async fn run( + &mut self, + _type_c_receiver: DynSubscriber<'static, type_c::comms::CommsMessage>, + _power_policy_event_receiver: DynSubscriber<'static, policy::CommsMessage>, + port0: &'static Mutex>, + port1: &'static Mutex>, + port2: &'static Mutex>, + ) { + join3( + self.run_tests(ControllerId(0), GlobalPortId(0), port0), + self.run_tests(ControllerId(1), GlobalPortId(1), port1), + self.run_tests(ControllerId(2), GlobalPortId(2), port2), + ) + .await; + } +} + +#[tokio::test] +async fn external() { + common::run_test( + DEFAULT_TEST_DURATION, + type_c_service::service::config::Config::default(), + power_policy_service::config::Config::default(), + TestExternal, + ) + .await; +}