diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 84f273f84..e8f756e7f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -33,7 +33,7 @@ concurrency: name: check env: # Crates that require std and won't build on embedded-targets - STD_CRATES: "fw-update-interface-mocks" + STD_EXCLUDED_CRATES: "--exclude fw-update-interface-mocks --exclude type-c-interface-mocks" jobs: fmt: @@ -121,7 +121,7 @@ jobs: run: cargo hack $COMMON_HACK_ARGS clippy --locked --target ${{ matrix.target }} - name: cargo hack if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }} - run: cargo hack $COMMON_HACK_ARGS clippy --exclude $STD_CRATES --locked --target ${{ matrix.target }} + run: cargo hack $COMMON_HACK_ARGS clippy $STD_EXCLUDED_CRATES --locked --target ${{ matrix.target }} deny: # cargo-deny checks licenses, advisories, sources, and bans for @@ -219,8 +219,8 @@ jobs: - name: cargo +${{ matrix.msrv }} check if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }} run: | - cargo check -F log --locked --workspace --exclude $STD_CRATES --target ${{ matrix.target }} - cargo check -F defmt --locked --workspace --exclude $STD_CRATES --target ${{ matrix.target }} + cargo check -F log --locked --workspace $STD_EXCLUDED_CRATES --target ${{ matrix.target }} + cargo check -F defmt --locked --workspace $STD_EXCLUDED_CRATES --target ${{ matrix.target }} check-arm-examples: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 4f5c1a898..bc2f0f6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -862,7 +862,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#0061a1e94a25c8db33ac1e8a0bb5a6b638fe2cd7" dependencies = [ "aquamarine", "bincode", @@ -2266,12 +2266,22 @@ dependencies = [ "power-policy-interface", ] +[[package]] +name = "type-c-interface-mocks" +version = "0.1.0" +dependencies = [ + "embedded-services", + "embedded-usb-pd", + "type-c-interface", +] + [[package]] name = "type-c-service" version = "0.1.0" dependencies = [ "bitfield 0.17.0", "bitflags 2.9.4", + "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", @@ -2279,13 +2289,17 @@ dependencies = [ "embedded-hal-async", "embedded-services", "embedded-usb-pd", + "env_logger", "fw-update-interface", "heapless 0.8.0", "log", + "paste", "power-policy-interface", + "power-policy-service", "tokio", "tps6699x", "type-c-interface", + "type-c-interface-mocks", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7073ed90d..ffa41d7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "fw-update-interface", "fw-update-interface-mocks", "mctp-rs", + "type-c-interface-mocks", ] exclude = ["examples/*"] @@ -114,6 +115,7 @@ thermal-service-relay = { path = "./thermal-service-relay" } time-alarm-service-interface = { path = "./time-alarm-service-interface" } time-alarm-service-relay = { path = "./time-alarm-service-relay" } type-c-interface = { path = "./type-c-interface" } +type-c-interface-mocks = { path = "./type-c-interface-mocks" } syn = "2.0" tps6699x = { git = "https://github.com/OpenDevicePartnership/tps6699x", branch = "v0.2.0" } tokio = { version = "1.42.0" } diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 342fa1dc9..30f8359c5 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#0061a1e94a25c8db33ac1e8a0bb5a6b638fe2cd7" dependencies = [ "aquamarine", "bincode", diff --git a/examples/rt685s-evk/src/bin/type_c.rs b/examples/rt685s-evk/src/bin/type_c.rs index 9efa5a928..78263df0f 100644 --- a/examples/rt685s-evk/src/bin/type_c.rs +++ b/examples/rt685s-evk/src/bin/type_c.rs @@ -238,7 +238,7 @@ async fn main(spawner: Spawner) { ))); static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); - let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::new( Default::default(), TypeCRegistrationType { ports: [port0, port1], diff --git a/examples/rt685s-evk/src/bin/type_c_cfu.rs b/examples/rt685s-evk/src/bin/type_c_cfu.rs index 4f484753a..018894887 100644 --- a/examples/rt685s-evk/src/bin/type_c_cfu.rs +++ b/examples/rt685s-evk/src/bin/type_c_cfu.rs @@ -376,7 +376,7 @@ async fn main(spawner: Spawner) { ))); static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); - let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::new( Default::default(), TypeCRegistrationType { ports: [port0, port1], diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index 5d6227827..6ec83388f 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "device-driver" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa3d97b2acf349b9d52c75470e2ccfc7224c49597ec12c2fb0e28826e910495" +checksum = "c2e4547bd66511372d2a38ac3c1b2892c7ebf83cf0d5411c3406e496c85a1d96" dependencies = [ "embedded-io 0.6.1", "embedded-io-async 0.6.1", @@ -641,7 +641,7 @@ dependencies = [ [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#1a8e79d3a2ac0d2837a34b045087cf0863146f7d" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#0061a1e94a25c8db33ac1e8a0bb5a6b638fe2cd7" dependencies = [ "aquamarine", "bincode", @@ -1388,7 +1388,7 @@ dependencies = [ [[package]] name = "tps6699x" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/tps6699x?branch=v0.2.0#c908a50747e8fcce831d4e53026072b5b6916a7b" +source = "git+https://github.com/OpenDevicePartnership/tps6699x?branch=v0.2.0#abe5568183bfe5fb2ea81806dded6cb60f3f9b58" dependencies = [ "bincode", "bitfield 0.19.4", diff --git a/examples/std/src/bin/type_c/service.rs b/examples/std/src/bin/type_c/service.rs index 3b5bbbc26..f10bd4eb9 100644 --- a/examples/std/src/bin/type_c/service.rs +++ b/examples/std/src/bin/type_c/service.rs @@ -144,7 +144,7 @@ async fn task(spawner: Spawner) { ))); static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); - let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::new( Config::default(), type_c_service::service::registration::ArrayRegistration { ports: [port], diff --git a/examples/std/src/bin/type_c/ucsi.rs b/examples/std/src/bin/type_c/ucsi.rs index 3797ce498..c93921f0c 100644 --- a/examples/std/src/bin/type_c/ucsi.rs +++ b/examples/std/src/bin/type_c/ucsi.rs @@ -308,7 +308,7 @@ async fn task(spawner: Spawner) { // Create type-c service static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); - let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::new( Config { ucsi_capabilities: UcsiCapabilities { num_connectors: 2, diff --git a/examples/std/src/bin/type_c/unconstrained.rs b/examples/std/src/bin/type_c/unconstrained.rs index 039690745..a4e08acae 100644 --- a/examples/std/src/bin/type_c/unconstrained.rs +++ b/examples/std/src/bin/type_c/unconstrained.rs @@ -164,7 +164,7 @@ async fn task(spawner: Spawner) { // Create type-c service static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); - let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::new( Default::default(), TypeCRegistrationType { ports: [port0, port1, port2], diff --git a/type-c-interface-mocks/Cargo.toml b/type-c-interface-mocks/Cargo.toml new file mode 100644 index 000000000..254a74dd3 --- /dev/null +++ b/type-c-interface-mocks/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "type-c-interface-mocks" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +embedded-services = { workspace = true } +embedded-usb-pd = { workspace = true } +type-c-interface = { workspace = true } + +[lints] +workspace = true diff --git a/type-c-interface-mocks/src/controller/mod.rs b/type-c-interface-mocks/src/controller/mod.rs new file mode 100644 index 000000000..8102f2d43 --- /dev/null +++ b/type-c-interface-mocks/src/controller/mod.rs @@ -0,0 +1,85 @@ +//! Mock controller implementations for testing + +use std::collections::VecDeque; + +use embedded_services::named::Named; +use embedded_usb_pd::{PdError, ado::Ado}; +use type_c_interface::control::{ + dp::DpStatus, + pd::PortStatus, + vdm::{AttnVdm, OtherVdm}, +}; + +pub mod pd; +pub mod ucsi; + +/// Contains a controller function call and its arguments +pub enum FnCall { + Pd(pd::FnCall), + Ucsi(ucsi::FnCall), +} + +/// Mock PD controller for use in tests +pub struct Mock { + name: &'static str, + /// Recorded function calls + pub fn_calls: VecDeque, + /// Next results to return for [`type_c_interface::controller::pd::Pd::get_port_status`] + pub next_result_get_port_status: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::clear_dead_battery_flag`] + pub next_result_clear_dead_battery_flag: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::enable_sink_path`] + pub next_result_enable_sink_path: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::get_pd_alert`] + pub next_result_get_pd_alert: VecDeque, PdError>>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::set_unconstrained_power`] + pub next_result_set_unconstrained_power: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::get_other_vdm`] + pub next_result_get_other_vdm: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::get_attn_vdm`] + pub next_result_get_attn_vdm: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::send_vdm`] + pub next_result_send_vdm: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::execute_drst`] + pub next_result_execute_drst: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::get_dp_status`] + pub next_result_get_dp_status: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::set_dp_config`] + pub next_result_set_dp_config: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::set_tbt_config`] + pub next_result_set_tbt_config: VecDeque>, + /// Next results to return for [`type_c_interface::controller::pd::Pd::set_usb_control`] + pub next_result_set_usb_control: VecDeque>, + /// Next results to return for [`type_c_interface::ucsi::Lpm::execute_lpm_command`] + pub next_result_execute_lpm_command: VecDeque, PdError>>, +} + +impl Mock { + /// Create a new mock with the given name + pub fn new(name: &'static str) -> Self { + Self { + fn_calls: VecDeque::new(), + name, + next_result_get_port_status: VecDeque::new(), + next_result_clear_dead_battery_flag: VecDeque::new(), + next_result_enable_sink_path: VecDeque::new(), + next_result_get_pd_alert: VecDeque::new(), + next_result_set_unconstrained_power: VecDeque::new(), + next_result_get_other_vdm: VecDeque::new(), + next_result_get_attn_vdm: VecDeque::new(), + next_result_send_vdm: VecDeque::new(), + next_result_execute_drst: VecDeque::new(), + next_result_get_dp_status: VecDeque::new(), + next_result_set_dp_config: VecDeque::new(), + next_result_set_tbt_config: VecDeque::new(), + next_result_set_usb_control: VecDeque::new(), + next_result_execute_lpm_command: VecDeque::new(), + } + } +} + +impl Named for Mock { + fn name(&self) -> &'static str { + self.name + } +} diff --git a/type-c-interface-mocks/src/controller/pd.rs b/type-c-interface-mocks/src/controller/pd.rs new file mode 100644 index 000000000..329dee879 --- /dev/null +++ b/type-c-interface-mocks/src/controller/pd.rs @@ -0,0 +1,135 @@ +//! Mock implementation of [`type_c_interface::controller::pd::Pd`] + +use embedded_usb_pd::{LocalPortId, PdError, ado::Ado}; +use type_c_interface::{ + control::{ + dp::{DpConfig, DpStatus}, + pd::PortStatus, + tbt::TbtConfig, + usb::UsbControlConfig, + vdm::{AttnVdm, OtherVdm, SendVdm}, + }, + controller::pd::Pd, +}; + +use super::FnCall as ControllerFnCall; +use super::Mock; + +/// Contains a [`Pd`] function call and its arguments +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FnCall { + GetPortStatus(LocalPortId), + ClearDeadBatteryFlag(LocalPortId), + EnableSinkPath(LocalPortId, bool), + GetPdAlert(LocalPortId), + SetUnconstrainedPower(LocalPortId, bool), + GetOtherVdm(LocalPortId), + GetAttnVdm(LocalPortId), + SendVdm(LocalPortId, SendVdm), + ExecuteDrst(LocalPortId), + GetDpStatus(LocalPortId), + SetDpConfig(LocalPortId, DpConfig), + SetTbtConfig(LocalPortId, TbtConfig), + SetUsbControl(LocalPortId, UsbControlConfig), +} + +impl Pd for Mock { + async fn get_port_status(&mut self, port: LocalPortId) -> Result { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::GetPortStatus(port))); + self.next_result_get_port_status + .pop_front() + .expect("next_result_get_port_status not set") + } + + async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::ClearDeadBatteryFlag(port))); + self.next_result_clear_dead_battery_flag + .pop_front() + .expect("next_result_clear_dead_battery_flag not set") + } + + async fn enable_sink_path(&mut self, port: LocalPortId, enable: bool) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::EnableSinkPath(port, enable))); + self.next_result_enable_sink_path + .pop_front() + .expect("next_result_enable_sink_path not set") + } + + async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, PdError> { + self.fn_calls.push_back(ControllerFnCall::Pd(FnCall::GetPdAlert(port))); + self.next_result_get_pd_alert + .pop_front() + .expect("next_result_get_pd_alert not set") + } + + async fn set_unconstrained_power(&mut self, port: LocalPortId, unconstrained: bool) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::SetUnconstrainedPower(port, unconstrained))); + self.next_result_set_unconstrained_power + .pop_front() + .expect("next_result_set_unconstrained_power not set") + } + + async fn get_other_vdm(&mut self, port: LocalPortId) -> Result { + self.fn_calls.push_back(ControllerFnCall::Pd(FnCall::GetOtherVdm(port))); + self.next_result_get_other_vdm + .pop_front() + .expect("next_result_get_other_vdm not set") + } + + async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result { + self.fn_calls.push_back(ControllerFnCall::Pd(FnCall::GetAttnVdm(port))); + self.next_result_get_attn_vdm + .pop_front() + .expect("next_result_get_attn_vdm not set") + } + + async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::SendVdm(port, tx_vdm))); + self.next_result_send_vdm + .pop_front() + .expect("next_result_send_vdm not set") + } + + async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.fn_calls.push_back(ControllerFnCall::Pd(FnCall::ExecuteDrst(port))); + self.next_result_execute_drst + .pop_front() + .expect("next_result_execute_drst not set") + } + + async fn get_dp_status(&mut self, port: LocalPortId) -> Result { + self.fn_calls.push_back(ControllerFnCall::Pd(FnCall::GetDpStatus(port))); + self.next_result_get_dp_status + .pop_front() + .expect("next_result_get_dp_status not set") + } + + async fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::SetDpConfig(port, config))); + self.next_result_set_dp_config + .pop_front() + .expect("next_result_set_dp_config not set") + } + + async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::SetTbtConfig(port, config))); + self.next_result_set_tbt_config + .pop_front() + .expect("next_result_set_tbt_config not set") + } + + async fn set_usb_control(&mut self, port: LocalPortId, config: UsbControlConfig) -> Result<(), PdError> { + self.fn_calls + .push_back(ControllerFnCall::Pd(FnCall::SetUsbControl(port, config))); + self.next_result_set_usb_control + .pop_front() + .expect("next_result_set_usb_control not set") + } +} diff --git a/type-c-interface-mocks/src/controller/ucsi.rs b/type-c-interface-mocks/src/controller/ucsi.rs new file mode 100644 index 000000000..0f9d6bc3b --- /dev/null +++ b/type-c-interface-mocks/src/controller/ucsi.rs @@ -0,0 +1,21 @@ +use embedded_usb_pd::PdError; +use embedded_usb_pd::ucsi::lpm; +use type_c_interface::ucsi::Lpm as UcsiLpm; + +use super::FnCall as ControllerFnCall; +use super::Mock; + +/// Contains a [`UcsiLpm`] function call and its arguments +pub enum FnCall { + ExecuteLpm(lpm::LocalCommand), +} + +impl UcsiLpm for Mock { + async fn execute_lpm_command(&mut self, command: lpm::LocalCommand) -> Result, PdError> { + self.fn_calls + .push_back(ControllerFnCall::Ucsi(FnCall::ExecuteLpm(command))); + self.next_result_execute_lpm_command + .pop_front() + .expect("next_result_execute_lpm_command not set") + } +} diff --git a/type-c-interface-mocks/src/lib.rs b/type-c-interface-mocks/src/lib.rs new file mode 100644 index 000000000..27362229d --- /dev/null +++ b/type-c-interface-mocks/src/lib.rs @@ -0,0 +1,3 @@ +#![allow(clippy::expect_used)] + +pub mod controller; diff --git a/type-c-interface/src/control/dp.rs b/type-c-interface/src/control/dp.rs index 21c616521..49fd78dcf 100644 --- a/type-c-interface/src/control/dp.rs +++ b/type-c-interface/src/control/dp.rs @@ -12,7 +12,7 @@ pub struct DpPinConfig { } /// DisplayPort status data -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DpStatus { /// DP alt-mode entered @@ -22,7 +22,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 diff --git a/type-c-interface/src/control/pd.rs b/type-c-interface/src/control/pd.rs index a4b9c8a2a..db34072cd 100644 --- a/type-c-interface/src/control/pd.rs +++ b/type-c-interface/src/control/pd.rs @@ -7,7 +7,7 @@ use embedded_usb_pd::{ }; /// 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 diff --git a/type-c-interface/src/control/retimer.rs b/type-c-interface/src/control/retimer.rs index e3460638d..2e997568c 100644 --- a/type-c-interface/src/control/retimer.rs +++ b/type-c-interface/src/control/retimer.rs @@ -1,7 +1,7 @@ //! Retimer related control types /// Retimer update state -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum RetimerFwUpdateState { /// Retimer FW Update Inactive diff --git a/type-c-interface/src/control/tbt.rs b/type-c-interface/src/control/tbt.rs index 192ed6c23..7b9ef2aaa 100644 --- a/type-c-interface/src/control/tbt.rs +++ b/type-c-interface/src/control/tbt.rs @@ -2,7 +2,7 @@ /// 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, diff --git a/type-c-interface/src/control/usb.rs b/type-c-interface/src/control/usb.rs index 237eb947d..ed44ff7c5 100644 --- a/type-c-interface/src/control/usb.rs +++ b/type-c-interface/src/control/usb.rs @@ -2,7 +2,7 @@ /// 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, diff --git a/type-c-interface/src/service/event.rs b/type-c-interface/src/service/event.rs index e20f6bf26..d0059a2b3 100644 --- a/type-c-interface/src/service/event.rs +++ b/type-c-interface/src/service/event.rs @@ -50,7 +50,7 @@ pub struct PortEvent<'port, Port: Lockable> { } /// Message generated when a debug accessory is connected or disconnected -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DebugAccessoryData { /// Connected @@ -58,7 +58,7 @@ pub struct DebugAccessoryData { } /// UCSI connector change message -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct UsciChangeIndicatorData { /// Port @@ -68,7 +68,7 @@ pub struct UsciChangeIndicatorData { } /// Top-level comms message -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum EventData { DebugAccessory(DebugAccessoryData), diff --git a/type-c-service/Cargo.toml b/type-c-service/Cargo.toml index 0582d28aa..fb8321ffb 100644 --- a/type-c-service/Cargo.toml +++ b/type-c-service/Cargo.toml @@ -35,6 +35,12 @@ embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } embassy-sync = { workspace = true, features = ["std"] } embassy-futures.workspace = true tokio = { workspace = true, features = ["rt", "macros", "time"] } +log.workspace = true +env_logger = "0.11.8" +type-c-interface-mocks.workspace = true +power-policy-service.workspace = true +paste.workspace = true +critical-section = { workspace = true, features = ["std"] } [features] default = [] @@ -48,6 +54,7 @@ defmt = [ "power-policy-interface/defmt", "type-c-interface/defmt", "fw-update-interface/defmt", + "power-policy-service/defmt", ] log = [ "dep:log", @@ -58,4 +65,5 @@ log = [ "power-policy-interface/log", "type-c-interface/log", "fw-update-interface/log", + "power-policy-service/log", ] diff --git a/type-c-service/src/service/mod.rs b/type-c-service/src/service/mod.rs index 699a4c276..b794b3a6a 100644 --- a/type-c-service/src/service/mod.rs +++ b/type-c-service/src/service/mod.rs @@ -48,7 +48,7 @@ pub enum Event<'port, Port: Lockable> { impl<'port, Reg: Registration<'port>> Service<'port, Reg> { /// Create a new service the given configuration - pub fn create(config: config::Config, registration: Reg) -> Self { + pub fn new(config: config::Config, registration: Reg) -> Self { Self { ucsi: ucsi::State::default(), config, diff --git a/type-c-service/src/service/power.rs b/type-c-service/src/service/power.rs index 5e443713c..b585821c2 100644 --- a/type-c-service/src/service/power.rs +++ b/type-c-service/src/service/power.rs @@ -77,13 +77,17 @@ impl<'a, Reg: Registration<'a>> Service<'a, Reg> { PowerPolicyEventData::ConsumerDisconnected => { self.ucsi.psu_connected = false; // Notify OPM because this can affect battery charging capability status - self.pend_ucsi_connected_ports().await; + if self.ucsi.notifications_enabled.battery_charge_change() { + self.pend_ucsi_connected_ports().await; + } Ok(()) } PowerPolicyEventData::ConsumerConnected(_) => { self.ucsi.psu_connected = true; // Notify OPM because this can affect battery charging capability status - self.pend_ucsi_connected_ports().await; + if self.ucsi.notifications_enabled.battery_charge_change() { + self.pend_ucsi_connected_ports().await; + } Ok(()) } _ => Ok(()), // Other events don't require any action from the service diff --git a/type-c-service/src/service/ucsi.rs b/type-c-service/src/service/ucsi.rs index ce34a84d4..b247dbbf5 100644 --- a/type-c-service/src/service/ucsi.rs +++ b/type-c-service/src/service/ucsi.rs @@ -31,18 +31,18 @@ pub struct UcsiResponse { #[derive(Default)] pub(super) struct State { /// PPM state machine - ppm_state_machine: StateMachine, + pub ppm_state_machine: StateMachine, /// Currently enabled notifications - notifications_enabled: NotificationEnable, + pub notifications_enabled: NotificationEnable, /// Queued pending port notifications - pending_ports: heapless::Deque, + pub 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, + pub valid_battery_charging_capability: heapless::FnvIndexSet, /// PSU connected - pub(super) psu_connected: bool, + pub psu_connected: bool, } impl<'port, Reg: Registration<'port>> Service<'port, Reg> { diff --git a/type-c-service/tests/common/mod.rs b/type-c-service/tests/common/mod.rs new file mode 100644 index 000000000..5dbcfe27b --- /dev/null +++ b/type-c-service/tests/common/mod.rs @@ -0,0 +1,399 @@ +use std::mem::ManuallyDrop; + +use embassy_futures::{ + join::{join, join3}, + select::{Either, select}, +}; +use embassy_sync::{ + channel::{Channel, DynamicReceiver, DynamicSender}, + mutex::Mutex, + once_lock::OnceLock, + watch, +}; +use embassy_time::{Duration, with_timeout}; +use embedded_services::{GlobalRawMutex, event::Sender}; +use embedded_usb_pd::LocalPortId; +use paste::paste; +use power_policy_interface::charger::mock::NoopCharger; +use type_c_service::service::registration::PortData; + +pub const DEFAULT_TEST_DURATION: Duration = Duration::from_secs(5); + +pub const DEFAULT_PER_CALL_TIMEOUT: Duration = Duration::from_secs(1); + +/// Total number of type-C ports +pub const TYPE_C_PORT_COUNT: usize = 3; +/// Number of senders for type-c service events +pub const TYPE_C_SERVICE_SENDER_COUNT: usize = 1; +/// Number of senders for power policy events +pub const POWER_POLICY_SENDER_COUNT: usize = 1; + +/// Mutex wrapped controller mock +pub type ControllerMockMutexType = Mutex; + +/// [`type_c_service::controller::Port`] sender to type-C service +pub type PortTypeCSender<'a> = DynamicSender<'a, type_c_interface::service::event::PortEventData>; +/// Corresponding receiver for [`PortTypeCSender`] +pub type PortTypeCReceiver<'a> = DynamicReceiver<'a, type_c_interface::service::event::PortEventData>; +/// [`type_c_service::controller::Port`] sender to power policy service +pub type PortPowerSender<'a> = DynamicSender<'a, power_policy_interface::psu::event::EventData>; +/// Corresponding receiver for [`PortPowerSender`] +pub type PortPowerReceiver<'a> = DynamicReceiver<'a, power_policy_interface::psu::event::EventData>; +/// [`type_c_service::controller::Port`] sender for loopback events +pub type PortLoopbackSender<'a> = DynamicSender<'a, type_c_service::controller::event::Loopback>; +/// Shared port state type +pub type PortSharedState = Mutex; +/// Port type +pub type PortMutexType<'port, 'ch> = Mutex< + GlobalRawMutex, + type_c_service::controller::Port< + 'port, + // Underlying controller + ControllerMockMutexType, + // Shared state between the event receiver and port logic + PortSharedState, + // Sender to the type-C service + PortTypeCSender<'ch>, + // Sender to the power policy + PortPowerSender<'ch>, + // Loopback sender + PortLoopbackSender<'ch>, + >, +>; + +/// Sender for events broadcast by the power policy service +pub type PowerPolicyServiceSender<'port, 'ch> = PowerPolicyServiceEventRouter<'port, 'ch>; +/// Receiver for events broadcast by the power policy service +pub type PowerPolicyServiceReceiver<'port, 'ch> = + DynamicReceiver<'ch, power_policy_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>>; +/// Power policy registration type +pub type PowerPolicyRegistrationType<'port, 'ch> = power_policy_service::service::registration::ArrayRegistration< + 'port, + // PSU type + PortMutexType<'port, 'ch>, + // PSU count + TYPE_C_PORT_COUNT, + // Senders for events broadcast by the service + PowerPolicyServiceSender<'port, 'ch>, + // Number of registered service event senders + POWER_POLICY_SENDER_COUNT, + // Charger type + Mutex, + // Charger count + 0, +>; +/// Power policy service type +pub type PowerPolicyServiceMutexType<'port, 'ch> = + Mutex>>; + +/// Sender for events broadcast by the type-C service +pub type TypeCServiceSender<'port, 'ch> = + DynamicSender<'ch, type_c_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>>; +/// Receiver for events broadcast by the type-C service +pub type TypeCServiceReceiver<'port, 'ch> = + DynamicReceiver<'ch, type_c_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>>; +/// Type-C service registration type +pub type TypeCRegistrationType<'port, 'ch> = type_c_service::service::registration::ArrayRegistration< + 'port, + // Port type + PortMutexType<'port, 'ch>, + // Number of type-C ports + TYPE_C_PORT_COUNT, + // Senders for events broadcast by the service + TypeCServiceSender<'port, 'ch>, + // Number of registered service event senders + TYPE_C_SERVICE_SENDER_COUNT, +>; +/// Type-C service type +pub type TypeCServiceMutexType<'port, 'ch> = + Mutex>>; + +/// Default channel size to use +pub const CHANNEL_SIZE: usize = 4; + +/// Struct to pass port components to a test implementation. +pub struct TestPort<'port, 'ch> { + /// Port logic + pub port: &'port PortMutexType<'port, 'ch>, + /// Underlying controller mock + pub mock: &'port ControllerMockMutexType, +} + +/// Integration test trait +/// +/// Directly taking async closures is messy and requires an intermediate trait anyway +pub trait Test { + /// Run the test + fn run<'port, 'ch>( + &mut self, + type_c_receiver: TypeCServiceReceiver<'port, 'ch>, + power_policy_receiver: PowerPolicyServiceReceiver<'port, 'ch>, + port0: TestPort<'port, 'ch>, + port1: TestPort<'port, 'ch>, + port2: TestPort<'port, 'ch>, + ) -> impl Future; +} + +/// Used by the [`define_port`] macro to work around macro hygiene issues. +struct PortComponents<'port, 'ch> { + port: PortMutexType<'port, 'ch>, + mock: &'port ControllerMockMutexType, + type_c_receiver: PortTypeCReceiver<'ch>, + power_policy_receiver: PortPowerReceiver<'ch>, +} + +macro_rules! define_port { + ($name:ident, $mock_name:expr, $port_name:expr, $config:expr, $local_id:expr) => { + paste! { let [<$name _type_c_channel>]: Channel< + GlobalRawMutex, + type_c_interface::service::event::PortEventData, + CHANNEL_SIZE, + > = Channel::new(); } + paste! { let [<$name _type_c_sender>] = [<$name _type_c_channel>].dyn_sender(); } + paste! { let [<$name _type_c_receiver>] = [<$name _type_c_channel>].dyn_receiver(); } + + paste! { let [<$name _power_policy_channel>]: Channel< + GlobalRawMutex, + power_policy_interface::psu::event::EventData, + CHANNEL_SIZE, + > = Channel::new(); } + paste! { let [<$name _power_policy_sender>] = [<$name _power_policy_channel>].dyn_sender(); } + paste! { let [<$name _power_policy_receiver>] = [<$name _power_policy_channel>].dyn_receiver(); } + + paste! { let [<$name _loopback_channel>]: Channel< + GlobalRawMutex, + type_c_service::controller::event::Loopback, + CHANNEL_SIZE, + > = Channel::new(); } + paste! { let [<$name _loopback_sender>] = [<$name _loopback_channel>].dyn_sender(); } + + paste! { let [<$name _mock>] = Mutex::new(type_c_interface_mocks::controller::Mock::new($mock_name)); } + paste! { let [<$name _shared_state>] = + PortSharedState::new(type_c_service::controller::state::SharedState::new()); } + paste! { let $name = PortComponents { + port: Mutex::new(type_c_service::controller::Port::new( + $port_name, + $config, + $local_id, + &paste! { [<$name _mock>] }, + &paste! { [<$name _shared_state>] }, + paste! { [<$name _type_c_sender>] }, + paste! { [<$name _power_policy_sender>] }, + paste! { [<$name _loopback_sender>] }, + )), + mock: &paste! { [<$name _mock>] }, + type_c_receiver: paste! { [<$name _type_c_receiver>] }, + power_policy_receiver: paste! { [<$name _power_policy_receiver>] }, + }; + } + }; +} + +/// Router for events from the power policy service. Forwards events to the test receiver and the type-C service. +// TODO: remove this once enum_dispatch is implemented +pub struct PowerPolicyServiceEventRouter<'port, 'ch> { + /// Sender to the test receiver + test_sender: DynamicSender<'ch, power_policy_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>>, + /// Sender to the type-C service + type_c_sender: DynamicSender<'ch, power_policy_interface::service::event::EventData>, +} + +impl<'port, 'ch> Sender>> + for PowerPolicyServiceEventRouter<'port, 'ch> +{ + fn try_send( + &mut self, + event: power_policy_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>, + ) -> Option<()> { + self.test_sender.try_send(event).ok()?; + self.type_c_sender.try_send(event.into()).ok() + } + + async fn send(&mut self, event: power_policy_interface::service::event::Event<'port, PortMutexType<'port, 'ch>>) { + join(self.test_sender.send(event), self.type_c_sender.send(event.into())).await; + } +} + +/// Power policy event loop task +async fn power_policy_task<'psu, 'ch, 'service, 'completion>( + mut completion_signal: watch::DynReceiver<'completion, ()>, + power_policy: &'service PowerPolicyServiceMutexType<'psu, 'ch>, + mut event_receivers: power_policy_service::psu::PsuEventReceivers< + 'psu, + TYPE_C_PORT_COUNT, + PortMutexType<'psu, 'ch>, + DynamicReceiver<'ch, power_policy_interface::psu::event::EventData>, + >, +) { + while let Either::First(event) = select(event_receivers.wait_event(), completion_signal.get()).await { + power_policy.lock().await.process_psu_event(event).await.unwrap(); + } +} + +/// Type-C service event loop task +async fn type_c_service_task<'port, 'ch, 'service, 'completion>( + mut completion_signal: watch::DynReceiver<'completion, ()>, + service: &'service TypeCServiceMutexType<'port, 'ch>, + mut event_receiver: type_c_service::service::event_receiver::ArrayEventReceiver< + 'port, + TYPE_C_PORT_COUNT, + PortMutexType<'port, 'ch>, + DynamicReceiver<'ch, type_c_interface::service::event::PortEventData>, + DynamicReceiver<'ch, power_policy_interface::service::event::EventData>, + >, +) { + while let Either::First(event) = select(event_receiver.wait_next(), completion_signal.get()).await { + service.lock().await.process_event(event).await.unwrap(); + } +} + +/// Initialize services and run an integration test +pub async fn run_test( + duration: Duration, + type_c_service_config: type_c_service::service::config::Config, + port_config: [type_c_service::controller::config::Config; TYPE_C_PORT_COUNT], + 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(); + + define_port!(port0, "mock0", "port0", port_config[0], LocalPortId(0)); + let PortComponents { + port: port0, + type_c_receiver: port0_type_c_receiver, + power_policy_receiver: port0_power_policy_receiver, + mock: port0_mock, + } = port0; + + define_port!(port1, "mock1", "port1", port_config[1], LocalPortId(0)); + let PortComponents { + port: port1, + type_c_receiver: port1_type_c_receiver, + power_policy_receiver: port1_power_policy_receiver, + mock: port1_mock, + } = port1; + + define_port!(port2, "mock2", "port2", port_config[2], LocalPortId(0)); + let PortComponents { + port: port2, + type_c_receiver: port2_type_c_receiver, + power_policy_receiver: port2_power_policy_receiver, + mock: port2_mock, + } = port2; + + // Channel to broadcast events from the type-C service + let type_c_service_channel: ManuallyDrop< + Channel>, CHANNEL_SIZE>, + > = ManuallyDrop::new(Channel::new()); + let type_c_service_sender = type_c_service_channel.dyn_sender(); + let type_c_service_receiver = type_c_service_channel.dyn_receiver(); + + let type_c_service = Mutex::new(type_c_service::service::Service::new( + type_c_service_config, + TypeCRegistrationType { + ports: [&port0, &port1, &port2], + port_data: [ + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(0)), + }, + ], + service_senders: [type_c_service_sender], + }, + )); + + // Channel for events from the power policy service to the type-C service + let type_c_power_policy_events: ManuallyDrop< + Channel, + > = ManuallyDrop::new(Channel::new()); + let type_c_power_policy_sender = type_c_power_policy_events.dyn_sender(); + let type_c_power_policy_receiver = type_c_power_policy_events.dyn_receiver(); + + let type_c_service_event_receivers = type_c_service::service::event_receiver::ArrayEventReceiver::new( + [&port0, &port1, &port2], + [port0_type_c_receiver, port1_type_c_receiver, port2_type_c_receiver], + type_c_power_policy_receiver, + ); + + // Channel for events from the power policy service to the test + let power_policy_service_channel: ManuallyDrop< + Channel>, CHANNEL_SIZE>, + > = ManuallyDrop::new(Channel::new()); + let power_policy_service_sender = power_policy_service_channel.dyn_sender(); + let power_policy_service_receiver = power_policy_service_channel.dyn_receiver(); + + // Router for power policy service events + let power_policy_service_event_router = PowerPolicyServiceEventRouter { + test_sender: power_policy_service_sender, + type_c_sender: type_c_power_policy_sender, + }; + + let power_policy_service = Mutex::new(power_policy_service::service::Service::new( + power_policy_service::service::registration::ArrayRegistration { + psus: [&port0, &port1, &port2], + chargers: [], + service_senders: [power_policy_service_event_router], + }, + Default::default(), + )); + + let power_policy_event_receiver = power_policy_service::psu::PsuEventReceivers { + psu_devices: [&port0, &port1, &port2], + receivers: [ + port0_power_policy_receiver, + port1_power_policy_receiver, + port2_power_policy_receiver, + ], + }; + + let completion_signal: watch::Watch = watch::Watch::new(); + let completion_sender = completion_signal.dyn_sender(); + + with_timeout( + duration, + join3( + power_policy_task( + completion_signal.dyn_receiver().unwrap(), + &power_policy_service, + power_policy_event_receiver, + ), + type_c_service_task( + completion_signal.dyn_receiver().unwrap(), + &type_c_service, + type_c_service_event_receivers, + ), + async { + test.run( + type_c_service_receiver, + power_policy_service_receiver, + TestPort { + port: &port0, + mock: port0_mock, + }, + TestPort { + port: &port1, + mock: port1_mock, + }, + TestPort { + port: &port2, + mock: port2_mock, + }, + ) + .await; + completion_sender.send(()); + }, + ), + ) + .await + .unwrap(); +} diff --git a/type-c-service/tests/power.rs b/type-c-service/tests/power.rs new file mode 100644 index 000000000..019e84346 --- /dev/null +++ b/type-c-service/tests/power.rs @@ -0,0 +1,133 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::panic)] +use std::ptr; + +use embassy_futures::join::join; +use embassy_time::{TimeoutError, with_timeout}; +use embedded_usb_pd::{PowerRole, type_c::ConnectionState}; +use power_policy_interface::{ + capability::{ConsumerFlags, ConsumerPowerCapability, PsuType}, + service::event::Event as PowerPolicyEvent, +}; +use type_c_interface::{ + control::pd::PortStatus, + port::event::{PortEvent, PortStatusEventBitfield}, +}; +use type_c_service::{controller::event::Event, util::POWER_CAPABILITY_5V_1A5}; + +use crate::common::{ + DEFAULT_PER_CALL_TIMEOUT, DEFAULT_TEST_DURATION, PowerPolicyServiceReceiver, Test, TestPort, TypeCServiceReceiver, +}; + +mod common; + +/// Test basic consumer attach flow +struct TestBasicConsumerFlow; + +impl Test for TestBasicConsumerFlow { + async fn run<'port, 'ch>( + &mut self, + type_c_receiver: TypeCServiceReceiver<'port, 'ch>, + power_policy_receiver: PowerPolicyServiceReceiver<'port, 'ch>, + port0: TestPort<'port, 'ch>, + _port1: TestPort<'port, 'ch>, + _port2: TestPort<'port, 'ch>, + ) { + { + // Set up the mock to report a sink connection and allow enabling the sink path + let mut mock0 = port0.mock.lock().await; + + mock0.next_result_get_port_status.push_back(Ok(PortStatus { + available_sink_contract: Some(POWER_CAPABILITY_5V_1A5), + connection_state: Some(ConnectionState::Attached), + power_role: PowerRole::Sink, + ..Default::default() + })); + mock0.next_result_enable_sink_path.push_back(Ok(())); + } + + // Simulate a plug event and a new consumer contract + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_plug_inserted_or_removed(true); + port_event.set_new_power_contract_as_consumer(true); + port_event.set_sink_ready(true); + + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + let (type_c_result, power_policy_result) = join( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, type_c_receiver.receive()), + with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()), + ) + .await; + + // Shouldn't get any Type-C service events in this flow + assert_eq!(type_c_result.err(), Some(TimeoutError)); + + // Power policy service should broadcast a consumer connect event + match power_policy_result { + Ok(PowerPolicyEvent::ConsumerConnected(psu, capability)) => { + assert_eq!( + capability, + ConsumerPowerCapability { + capability: POWER_CAPABILITY_5V_1A5, + flags: ConsumerFlags::none().with_psu_type(PsuType::TypeC), + } + ); + assert!(ptr::eq(psu, port0.port)); + } + _ => panic!("Did not receive consumer connected event"), + } + + { + // Set up the mock to report an unplug + let mut mock0 = port0.mock.lock().await; + let port_status = Ok(Default::default()); + mock0.next_result_get_port_status.push_back(port_status); + } + + // Simulate an unplug event + let mut port_event = PortStatusEventBitfield::none(); + port_event.set_plug_inserted_or_removed(true); + + port0 + .port + .lock() + .await + .process_event(Event::PortEvent(PortEvent::StatusChanged(port_event))) + .await + .unwrap(); + + let (type_c_result, power_policy_result) = join( + with_timeout(DEFAULT_PER_CALL_TIMEOUT, type_c_receiver.receive()), + with_timeout(DEFAULT_PER_CALL_TIMEOUT, power_policy_receiver.receive()), + ) + .await; + + // Type-C service currently shouldn't broadcast any events in this flow + assert_eq!(type_c_result.err(), Some(TimeoutError)); + // Power policy service should broadcast a consumer disconnect event + match power_policy_result { + Ok(PowerPolicyEvent::ConsumerDisconnected(psu)) => { + assert!(ptr::eq(psu, port0.port)); + } + _ => panic!("Did not receive consumer disconnected event"), + } + } +} + +#[tokio::test] +async fn test_basic_consumer_flow() { + common::run_test( + DEFAULT_TEST_DURATION, + Default::default(), + Default::default(), + TestBasicConsumerFlow, + ) + .await; +}