diff --git a/embedded-service/src/type_c/comms.rs b/embedded-service/src/type_c/comms.rs index 9a998cd31..f23200d76 100644 --- a/embedded-service/src/type_c/comms.rs +++ b/embedded-service/src/type_c/comms.rs @@ -2,8 +2,8 @@ use embedded_usb_pd::GlobalPortId; -/// Message generated when a debug acessory is connected or disconnected -#[derive(Copy, Clone, Debug)] +/// Message generated when a debug accessory is connected or disconnected +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DebugAccessoryMessage { /// Port @@ -13,7 +13,7 @@ pub struct DebugAccessoryMessage { } /// UCSI connector change message -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct UsciChangeIndicator { /// Port @@ -23,7 +23,7 @@ pub struct UsciChangeIndicator { } /// Top-level comms message -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum CommsMessage { /// Debug accessory message diff --git a/embedded-service/src/type_c/external.rs b/embedded-service/src/type_c/external.rs index b86650d1f..6f05a6ac2 100644 --- a/embedded-service/src/type_c/external.rs +++ b/embedded-service/src/type_c/external.rs @@ -159,7 +159,7 @@ pub enum Command { } /// UCSI command response -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct UcsiResponse { /// Notify the OPM, the function call diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index 0f8a7130b..f99d51f29 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -71,10 +71,6 @@ path = "src/bin/type_c/service.rs" name = "type-c-unconstrained" path = "src/bin/type_c/unconstrained.rs" -[[bin]] -name = "type-c-ucsi" -path = "src/bin/type_c/ucsi.rs" - # Needed otherwise cargo will pull from git [patch."https://github.com/OpenDevicePartnership/embedded-services"] embedded-services = { path = "../../embedded-service" } diff --git a/examples/std/src/bin/type_c/ucsi.rs b/examples/std/src/bin/type_c/ucsi.rs deleted file mode 100644 index 251d78797..000000000 --- a/examples/std/src/bin/type_c/ucsi.rs +++ /dev/null @@ -1,261 +0,0 @@ -use embassy_executor::{Executor, Spawner}; -use embassy_sync::mutex::Mutex; -use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::{self, PowerCapability}; -use embedded_services::type_c::ControllerId; -use embedded_services::type_c::external::{UcsiResponseResult, execute_ucsi_command}; -use embedded_usb_pd::GlobalPortId; -use embedded_usb_pd::ucsi::lpm::get_connector_capability::OperationModeFlags; -use embedded_usb_pd::ucsi::ppm::ack_cc_ci::Ack; -use embedded_usb_pd::ucsi::ppm::get_capability::ResponseData as UcsiCapabilities; -use embedded_usb_pd::ucsi::ppm::set_notification_enable::NotificationEnable; -use embedded_usb_pd::ucsi::{Command, lpm, ppm}; -use log::*; -use static_cell::StaticCell; -use std_examples::type_c::mock_controller; -use type_c_service::service::config::Config; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; - -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const CONTROLLER1_ID: ControllerId = ControllerId(1); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: policy::DeviceId = policy::DeviceId(0); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const POWER1_ID: policy::DeviceId = policy::DeviceId(1); -const CFU0_ID: u8 = 0x00; -const CFU1_ID: u8 = 0x01; - -#[embassy_executor::task] -async fn opm_task(spawner: Spawner) { - static STORAGE0: StaticCell> = StaticCell::new(); - let storage0 = STORAGE0.init(Storage::new(CONTROLLER0_ID, CFU0_ID, [(PORT0_ID, POWER0_ID)])); - static REFERENCED0: StaticCell> = StaticCell::new(); - let referenced0 = REFERENCED0.init( - storage0 - .create_referenced() - .expect("Failed to create referenced storage"), - ); - - static STATE0: StaticCell = StaticCell::new(); - let state0 = STATE0.init(mock_controller::ControllerState::new()); - static CONTROLLER0: StaticCell> = StaticCell::new(); - let controller0 = CONTROLLER0.init(Mutex::new(mock_controller::Controller::new(state0))); - static WRAPPER0: StaticCell = StaticCell::new(); - let wrapper0 = WRAPPER0.init( - mock_controller::Wrapper::try_new(controller0, Default::default(), referenced0, mock_controller::Validator) - .expect("Failed to create wrapper"), - ); - spawner.spawn(wrapper_task(wrapper0).unwrap()); - - static STORAGE1: StaticCell> = StaticCell::new(); - let storage1 = STORAGE1.init(Storage::new(CONTROLLER1_ID, CFU1_ID, [(PORT1_ID, POWER1_ID)])); - static REFERENCED1: StaticCell> = StaticCell::new(); - let referenced1 = REFERENCED1.init( - storage1 - .create_referenced() - .expect("Failed to create referenced storage"), - ); - - static STATE1: StaticCell = StaticCell::new(); - let state1 = STATE1.init(mock_controller::ControllerState::new()); - static CONTROLLER1: StaticCell> = StaticCell::new(); - let controller1 = CONTROLLER1.init(Mutex::new(mock_controller::Controller::new(state1))); - static WRAPPER1: StaticCell = StaticCell::new(); - let wrapper1 = WRAPPER1.init( - mock_controller::Wrapper::try_new(controller1, Default::default(), referenced1, mock_controller::Validator) - .expect("Failed to create wrapper"), - ); - spawner.spawn(wrapper_task(wrapper1).unwrap()); - - const CAPABILITY: PowerCapability = PowerCapability { - voltage_mv: 20000, - current_ma: 5000, - }; - - info!("Resetting PPM..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::PpmCommand(ppm::Command::PpmReset)) - .await - .into(); - let response = response.unwrap(); - if !response.cci.reset_complete() || response.cci.error() { - error!("PPM reset failed: {:?}", response.cci); - } else { - info!("PPM reset successful"); - } - - info!("Set Notification enable..."); - let mut notifications = NotificationEnable::default(); - notifications.set_cmd_complete(true); - notifications.set_connect_change(true); - let response: UcsiResponseResult = execute_ucsi_command(Command::PpmCommand(ppm::Command::SetNotificationEnable( - ppm::set_notification_enable::Args { - notification_enable: notifications, - }, - ))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.cmd_complete() || response.cci.error() { - error!("Set Notification enable failed: {:?}", response.cci); - } else { - info!("Set Notification enable successful"); - } - - info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { - ack: *Ack::default().set_command_complete(true), - }))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.ack_command() || response.cci.error() { - error!("Sending command complete ack failed: {:?}", response.cci); - } else { - info!("Sending command complete ack successful"); - } - - info!("Connecting sinks on both ports"); - state0.connect_sink(CAPABILITY, false).await; - state1.connect_sink(CAPABILITY, false).await; - - // Ensure connect flow has time to complete - embassy_time::Timer::after_millis(1000).await; - - info!("Port 0: Get connector status..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( - GlobalPortId(0), - lpm::CommandData::GetConnectorStatus, - ))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.cmd_complete() || response.cci.error() { - error!("Get connector status failed: {:?}", response.cci); - } else { - info!( - "Get connector status successful, connector change: {:?}", - response.cci.connector_change() - ); - } - - info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { - ack: *Ack::default().set_command_complete(true).set_connector_change(true), - }))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.ack_command() || response.cci.error() { - error!("Sending command complete ack failed: {:?}", response.cci); - } else { - info!( - "Sending command complete ack successful, connector change: {:?}", - response.cci.connector_change() - ); - } - - info!("Port 1: Get connector status..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( - GlobalPortId(1), - lpm::CommandData::GetConnectorStatus, - ))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.cmd_complete() || response.cci.error() { - error!("Get connector status failed: {:?}", response.cci); - } else { - info!( - "Get connector status successful, connector change: {:?}", - response.cci.connector_change() - ); - } - - info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { - ack: *Ack::default().set_command_complete(true).set_connector_change(true), - }))) - .await - .into(); - let response = response.unwrap(); - if !response.cci.ack_command() || response.cci.error() { - error!("Sending command complete ack failed: {:?}", response.cci); - } else { - info!( - "Sending command complete ack successful, connector change: {:?}", - response.cci.connector_change() - ); - } -} - -#[embassy_executor::task(pool_size = 2)] -async fn wrapper_task(wrapper: &'static mock_controller::Wrapper<'static>) { - wrapper.register().await.unwrap(); - - loop { - if let Err(e) = wrapper.process_next_event().await { - error!("Error processing wrapper: {e:#?}"); - } - } -} - -#[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Config { - ucsi_capabilities: UcsiCapabilities { - num_connectors: 2, - bcd_usb_pd_spec: 0x0300, - bcd_type_c_spec: 0x0200, - bcd_battery_charging_spec: 0x0120, - ..Default::default() - }, - ucsi_port_capabilities: Some( - *lpm::get_connector_capability::ResponseData::default() - .set_operation_mode( - *OperationModeFlags::default() - .set_drp(true) - .set_usb2(true) - .set_usb3(true), - ) - .set_consumer(true) - .set_provider(true) - .set_swap_to_dfp(true) - .set_swap_to_snk(true) - .set_swap_to_src(true), - ), - ..Default::default() - }) - .await; - unreachable!() -} - -#[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); -} - -#[embassy_executor::task] -async fn task(spawner: Spawner) { - info!("Starting main task"); - - embedded_services::init().await; - - spawner.spawn(power_policy_service_task().unwrap()); - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(opm_task(spawner).unwrap()); -} - -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(task(spawner).unwrap()); - }); -} diff --git a/type-c-service/Cargo.toml b/type-c-service/Cargo.toml index 10e03ffab..eb9a331dc 100644 --- a/type-c-service/Cargo.toml +++ b/type-c-service/Cargo.toml @@ -59,4 +59,5 @@ log = [ "embassy-time/log", "embassy-sync/log", "tps6699x/log", + "power-policy-service/log", ] diff --git a/type-c-service/tests/common/mod.rs b/type-c-service/tests/common/mod.rs index f1d687d24..c58f3620b 100644 --- a/type-c-service/tests/common/mod.rs +++ b/type-c-service/tests/common/mod.rs @@ -23,7 +23,7 @@ use paste::paste; use static_cell::StaticCell; pub mod mock; -pub const DEFAULT_TEST_DURATION: Duration = Duration::from_secs(5); +pub const DEFAULT_TEST_DURATION: Duration = Duration::from_secs(15); pub const DEFAULT_PER_CALL_TIMEOUT: Duration = Duration::from_secs(1); diff --git a/type-c-service/tests/ucsi.rs b/type-c-service/tests/ucsi.rs new file mode 100644 index 000000000..bddc14499 --- /dev/null +++ b/type-c-service/tests/ucsi.rs @@ -0,0 +1,337 @@ +//! Integration test for UCSI + +use crate::common::{DEFAULT_PER_CALL_TIMEOUT, DEFAULT_TEST_DURATION, Test, mock}; + +use embassy_sync::mutex::Mutex; +use embassy_sync::pubsub::{DynSubscriber, WaitResult}; +use embassy_time::with_timeout; +use embedded_services::power::policy::PowerCapability; +use embedded_services::type_c::comms::UsciChangeIndicator; +use embedded_services::type_c::external::UcsiResponse; +use embedded_services::{GlobalRawMutex, type_c}; +use embedded_services::{info, power}; +use embedded_usb_pd::GlobalPortId; +use embedded_usb_pd::ucsi::cci::GlobalCci; +use embedded_usb_pd::ucsi::lpm; +use embedded_usb_pd::ucsi::lpm::ResponseData as LpmResponseData; +use embedded_usb_pd::ucsi::lpm::get_connector_capability::{ + OperationModeFlags, ResponseData as UcsiConnectorCapability, +}; +use embedded_usb_pd::ucsi::lpm::get_connector_status::{ + BatteryChargingCapabilityStatus, ConnectedStatus, ConnectorStatusChange, +}; +use embedded_usb_pd::ucsi::ppm::ack_cc_ci::Ack; +use embedded_usb_pd::ucsi::ppm::get_capability::ResponseData as PpmCapabilities; +use embedded_usb_pd::ucsi::{Command, ResponseData as UcsiResponseData, ppm}; + +mod common; + +const CAPABILITY: PowerCapability = PowerCapability { + voltage_mv: 20000, + current_ma: 5000, +}; + +/// Test LPM commands for a single port: connect, GetConnectorStatus, AckCcCi. +async fn test_lpm( + port: &'static Mutex>, + port_id: GlobalPortId, + type_c_receiver: &mut DynSubscriber<'static, type_c::comms::CommsMessage>, +) { + info!("Testing LPM commands for port {:?}", port_id); + + info!("Testing GetConnectorCapability"); + let expected_response = LpmResponseData::GetConnectorCapability( + *UcsiConnectorCapability::default() + .set_operation_mode( + *OperationModeFlags::default() + .set_drp(true) + .set_usb2(true) + .set_usb3(true), + ) + .set_consumer(true) + .set_provider(true) + .set_swap_to_dfp(true) + .set_swap_to_snk(true) + .set_swap_to_src(true), + ); + // Don't need to push a response because the PPM overrides the LPM response. + + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( + port_id, + lpm::CommandData::GetConnectorCapability, + ))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_cmd_complete(true), + data: Ok(Some(UcsiResponseData::Lpm(expected_response))), + }) + ); + + // Acknowledge the CCI + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + ack: *Ack::default().set_command_complete(true), + }))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_ack_command(true), + data: Ok(None), + }) + ); + + // Connect the port, verify UCSI event, read connector status, and acknowledge the CCI. + port.lock().await.next_result_enable_sink_path.push_back(Ok(())); + port.lock().await.connect_sink(CAPABILITY, false).await; + + // Give some time for the connect to be processed + let message = with_timeout(DEFAULT_PER_CALL_TIMEOUT, type_c_receiver.next_message()).await; + assert_eq!( + message, + Ok(WaitResult::Message(type_c::comms::CommsMessage::UcsiCci( + UsciChangeIndicator { + port: port_id, + notify_opm: true, + } + ))) + ); + + info!("Testing GetConnectorStatus"); + let mut status_change = ConnectorStatusChange::default(); + status_change.set_connect_change(true); + status_change.set_battery_charging_status_change(true); + + let connected_status = ConnectedStatus { + battery_charging_status: Some(BatteryChargingCapabilityStatus::Nominal), + ..Default::default() + }; + + let expected_response = LpmResponseData::GetConnectorStatus(lpm::get_connector_status::ResponseData { + status_change, + connect_status: true, + status: Some(connected_status), + }); + + port.lock() + .await + .next_result_execute_ucsi_command + .push_back(Ok(Some(expected_response))); + + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( + port_id, + lpm::CommandData::GetConnectorStatus, + ))), + ) + .await; + + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default() + .set_cmd_complete(true) + // + 1 to convert between 0-based and 1-based port IDs + .set_connector_change(GlobalPortId(port_id.0 + 1)), + data: Ok(Some(UcsiResponseData::Lpm(expected_response))), + }) + ); + + // Acknowledge the CCI + info!("Acknowledging CCI for port {}", port_id.0); + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + ack: *Ack::default().set_command_complete(true).set_connector_change(true), + }))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_ack_command(true), + data: Ok(None), + }) + ); + + // Disconnect to prepare for the next test + info!("Disconnecting port {}", port_id.0); + port.lock().await.disconnect().await; + + // Give some time for the disconnect to be processed + let message = with_timeout(DEFAULT_PER_CALL_TIMEOUT, type_c_receiver.next_message()).await; + assert_eq!( + message, + Ok(WaitResult::Message(type_c::comms::CommsMessage::UcsiCci( + UsciChangeIndicator { + port: port_id, + notify_opm: true, + } + ))) + ); + + // Get disconnected port status + info!("Getting disconnected port status for port {}", port_id.0); + let expected_response = LpmResponseData::GetConnectorStatus(lpm::get_connector_status::ResponseData::default()); + port.lock() + .await + .next_result_execute_ucsi_command + .push_back(Ok(Some(expected_response))); + + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( + port_id, + lpm::CommandData::GetConnectorStatus, + ))), + ) + .await; + + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default() + .set_cmd_complete(true) + // + 1 to convert between 0-based and 1-based port IDs + .set_connector_change(GlobalPortId(port_id.0 + 1)), + data: Ok(Some(UcsiResponseData::Lpm(expected_response))), + }) + ); + + info!("Acknowledging CCI for port {}", port_id.0); + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + ack: *Ack::default().set_command_complete(true).set_connector_change(true), + }))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_ack_command(true), + data: Ok(None), + }) + ); +} + +struct TestUcsi; + +impl Test for TestUcsi { + async fn run( + &mut self, + mut type_c_receiver: DynSubscriber<'static, type_c::comms::CommsMessage>, + _power_policy_event_receiver: DynSubscriber<'static, power::policy::CommsMessage>, + port0: &'static Mutex>, + port1: &'static Mutex>, + port2: &'static Mutex>, + ) { + // Reset the PPM + info!("PPM Reset"); + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::PpmReset)), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + // OPM is supposed to poll for the reset complete flag + notify_opm: false, + cci: *GlobalCci::default().set_reset_complete(true), + data: Ok(None), + }) + ); + + // Enable notifications + info!("Enabling notifications"); + let mut notifications = embedded_usb_pd::ucsi::ppm::set_notification_enable::NotificationEnable::default(); + notifications.set_cmd_complete(true); + notifications.set_connect_change(true); + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::SetNotificationEnable( + ppm::set_notification_enable::Args { + notification_enable: notifications, + }, + ))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_cmd_complete(true), + data: Ok(None), + }) + ); + + let response = with_timeout( + DEFAULT_PER_CALL_TIMEOUT, + type_c::external::execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + ack: *Ack::default().set_command_complete(true), + }))), + ) + .await; + assert_eq!( + response, + Ok(UcsiResponse { + notify_opm: true, + cci: *GlobalCci::default().set_ack_command(true), + data: Ok(None), + }) + ); + + test_lpm(port0, GlobalPortId(0), &mut type_c_receiver).await; + test_lpm(port1, GlobalPortId(1), &mut type_c_receiver).await; + test_lpm(port2, GlobalPortId(2), &mut type_c_receiver).await; + } +} + +#[tokio::test] +async fn ucsi() { + common::run_test( + DEFAULT_TEST_DURATION, + type_c_service::service::config::Config { + ucsi_capabilities: PpmCapabilities { + num_connectors: 3, + bcd_usb_pd_spec: 0x0300, + bcd_type_c_spec: 0x0200, + bcd_battery_charging_spec: 0x0120, + ..Default::default() + }, + ucsi_port_capabilities: Some( + *UcsiConnectorCapability::default() + .set_operation_mode( + *OperationModeFlags::default() + .set_drp(true) + .set_usb2(true) + .set_usb3(true), + ) + .set_consumer(true) + .set_provider(true) + .set_swap_to_dfp(true) + .set_swap_to_snk(true) + .set_swap_to_src(true), + ), + ..Default::default() + }, + power_policy_service::config::Config::default(), + TestUcsi, + ) + .await; +}