diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d81bb1d..cd3a396 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -336,7 +336,7 @@ jobs: EC_TEST_CLI_REV: 20347787 # Tag of the prebuilt QEMU image pulled from the odp-qemu-builder GHCR package - ODP_QEMU_TAG: sha-7e461b3 + ODP_QEMU_TAG: sha-f8af056 steps: - uses: actions/checkout@v4 @@ -359,6 +359,8 @@ jobs: sudo apt-get update && sudo apt-get install -y libudev-dev libfdt1 + libjpeg-turbo8 + libpng16-16t64 - name: Install ec-test-cli run: cargo install --git https://github.com/OpenDevicePartnership/odp-platform-common --locked --rev ${{ env.EC_TEST_CLI_REV }} ec-test-cli diff --git a/platform/dev-imxrt/Cargo.lock b/platform/dev-imxrt/Cargo.lock index badb704..4d643c1 100644 --- a/platform/dev-imxrt/Cargo.lock +++ b/platform/dev-imxrt/Cargo.lock @@ -1071,6 +1071,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -1097,6 +1099,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-services", + "heapless", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/dev-mcxa/Cargo.lock b/platform/dev-mcxa/Cargo.lock index 9689230..36aa2eb 100644 --- a/platform/dev-mcxa/Cargo.lock +++ b/platform/dev-mcxa/Cargo.lock @@ -1077,6 +1077,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -1106,6 +1108,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-services", + "heapless", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/dev-mec/Cargo.lock b/platform/dev-mec/Cargo.lock index f199e6e..a819adf 100644 --- a/platform/dev-mec/Cargo.lock +++ b/platform/dev-mec/Cargo.lock @@ -996,6 +996,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -1022,6 +1024,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-services", + "heapless", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/dev-npcx/Cargo.lock b/platform/dev-npcx/Cargo.lock index b5a650b..4296a36 100644 --- a/platform/dev-npcx/Cargo.lock +++ b/platform/dev-npcx/Cargo.lock @@ -997,6 +997,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -1026,6 +1028,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time", + "embedded-services", + "heapless 0.9.3", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/dev-qemu/Cargo.lock b/platform/dev-qemu/Cargo.lock index bf1e03b..173b1ae 100644 --- a/platform/dev-qemu/Cargo.lock +++ b/platform/dev-qemu/Cargo.lock @@ -944,6 +944,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -970,6 +972,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync 0.8.0", + "embassy-time", + "embedded-services", + "heapless 0.9.3", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/dev-qemu/README.md b/platform/dev-qemu/README.md index 247874b..4cdcc68 100644 --- a/platform/dev-qemu/README.md +++ b/platform/dev-qemu/README.md @@ -9,8 +9,9 @@ default and can instead use a stable socket for QEMU co-simulation. - [Docker](https://docs.docker.com/get-docker/) — `qemu-ec.sh` pulls a prebuilt `qemu-system-riscv32` (with `ec` machine support) from the [`odp-qemu-builder`](https://github.com/OpenDevicePartnership/odp-qemu-builder) - GHCR image and caches it under `target/qemu-ec/`. To use a local QEMU instead, - set `QEMU=/path/to/qemu-system-riscv32`. + GHCR image and caches it under `target/qemu-ec//`. To use a local QEMU + instead, set `QEMU=/path/to/qemu-system-riscv32`. +- QEMU's runtime libraries (Ubuntu: `sudo apt install libfdt1 libjpeg-turbo8 libpng16-16t64`). - `defmt-print`: `cargo install defmt-print` ## Run @@ -43,32 +44,31 @@ verifies actual standby return; the EC log alone does not prove host resume. ## Emulated runtime AC/DC input -`cargo build --release --features time-alarm-power-input` includes the wake -output and uses GPIO2 (high = AC, low = DC) instead of `ODP_WAKE_SOURCE`. -The input must be explicitly initialized by the QEMU model before startup; -the firmware rejects an unset validity bit rather than assuming DC. Source -selection happens before the TimeAlarm runner starts. Socket bytes alone do -not establish validity. - -Use the GPIO2-enabled QEMU model with `input-reset-mask=4` and `input-reset=4` -for cold AC, or `input-reset=0` for cold DC, and its `ec-gpio2` control socket. -Raw bytes `01` and `00` select AC and DC at runtime. The dedicated input task -resamples after opposite-level waits on the shared GPIO interrupt. This is a -level interface, not an event counter: a transient that returns before the -task samples it is not a recorded power-source change. - -The model retains the last valid source across control-socket disconnects -and EC warm resets, and diagnoses malformed bytes. GPIO1 remains the wake -output and GPIO0 remains the HID interrupt. The default build and the explicit -construction-time `time-alarm-wake` fixture are unchanged. This emulated -external input neither detects physical power nor reports Windows power state. +Build with `--features time-alarm-power-input` to take the power source from +GPIO2 (high = AC, low = DC) instead of `ODP_WAKE_SOURCE`. `cargo run` starts it +on DC (`EC_POWER_SOURCE=ac` for AC); switch it by writing a raw byte to the +control socket: + +```sh +cargo run --release --features time-alarm-power-input +printf '\001' | nc -NU /tmp/qemu-ec-power.sock # AC +printf '\000' | nc -NU /tmp/qemu-ec-power.sock # DC +``` + +Sending AC sets TimeAlarm's source to AC and connects a mock PSU (a 20 V / 3 A +wall adapter) to the power policy, so battery 0's `_PSR` reports online about +0.8 s later. Sending DC reverses both. + +QEMU's `gpio2 source control disconnected; retaining input level N` warning is +expected at startup and after each write; `N` is the current level. ## Sockets -While `dev-qemu` is running, the `ec` machine exposes two sockets that external +While `dev-qemu` is running, the `ec` machine exposes these sockets that external programs (such as another QEMU instance) can connect to: - I2C target: `/tmp/qemu-ec-i2c.sock` - GPIO: `/tmp/qemu-ec-gpio.sock` +- GPIO2 AC/DC control (power-input builds): `/tmp/qemu-ec-power.sock` Set `EC_UART_SOCK` to replace the UART PTY with another socket, for example `EC_UART_SOCK=/tmp/qemu-ec-uart.sock cargo run --release`. @@ -76,10 +76,12 @@ Set `EC_UART_SOCK` to replace the UART PTY with another socket, for example ## Configuration `qemu-ec.sh` reads the following environment variables: -| Variable | Default | Description | -|----------------|--------------------------|-------------------------------------------------| -| `QEMU` | (pulled from GHCR) | Override the `qemu-system-riscv32` binary. | -| `ODP_QEMU_TAG` | (pinned in `qemu-ec.sh`) | Tag of the odp-qemu-builder GHCR image to pull. | -| `EC_I2C_SOCK` | `/tmp/qemu-ec-i2c.sock` | Path for the I2C-target socket. | -| `EC_GPIO_SOCK` | `/tmp/qemu-ec-gpio.sock` | Path for the GPIO socket. | -| `EC_UART_SOCK` | (unset) | UART socket path; when unset, use a PTY. | +| Variable | Default | Description | +|-------------------|---------------------------|-------------------------------------------------| +| `QEMU` | (pulled from GHCR) | Override the `qemu-system-riscv32` binary. | +| `ODP_QEMU_TAG` | (pinned in `qemu-ec.sh`) | Tag of the odp-qemu-builder GHCR image to pull. | +| `EC_I2C_SOCK` | `/tmp/qemu-ec-i2c.sock` | Path for the I2C-target socket. | +| `EC_GPIO_SOCK` | `/tmp/qemu-ec-gpio.sock` | Path for the GPIO socket. | +| `EC_UART_SOCK` | (unset) | UART socket path; when unset, use a PTY. | +| `EC_POWER_SOURCE` | `dc` (power-input builds) | Initial GPIO2 level: `ac` or `dc`. | +| `EC_POWER_SOCK` | `/tmp/qemu-ec-power.sock` | Path for the GPIO2 control socket. | diff --git a/platform/dev-qemu/build.rs b/platform/dev-qemu/build.rs index f13cfd9..8d8f353 100644 --- a/platform/dev-qemu/build.rs +++ b/platform/dev-qemu/build.rs @@ -13,4 +13,9 @@ fn main() { println!("cargo:rustc-env=RISCV_RT_BASE_ISA=rv32i"); println!("cargo:rerun-if-env-changed=RISCV_RT_BASE_ISA"); + + // qemu-ec.sh looks for this symbol to initialize GPIO2, which this feature's firmware requires. + if env::var_os("CARGO_FEATURE_TIME_ALARM_POWER_INPUT").is_some() { + println!("cargo:rustc-link-arg-bins=--defsym=__odp_qemu_ec_power_input=1"); + } } diff --git a/platform/dev-qemu/qemu-ec.sh b/platform/dev-qemu/qemu-ec.sh index 900e760..ed2e581 100755 --- a/platform/dev-qemu/qemu-ec.sh +++ b/platform/dev-qemu/qemu-ec.sh @@ -6,11 +6,14 @@ # `scripts/integration-test.sh`, and CI. It: # # 1. Resolves a `qemu-system-riscv32` binary that supports the `ec` machine -# (GPIO + I2C target sockets). The binary is pulled once from the -# `odp-qemu-builder` image published on GHCR and cached under `target/`. +# (GPIO + I2C target sockets). The binary is pulled once per image tag from +# the `odp-qemu-builder` image published on GHCR and cached under +# `target/qemu-ec//`. # 2. Launches QEMU on the `ec` machine, exposing the EC's I2C-target and GPIO # lines as UNIX-domain sockets that external programs can connect to. UART -# uses a PTY by default or a stable socket when EC_UART_SOCK is set. +# uses a PTY by default or a stable socket when EC_UART_SOCK is set. When +# GPIO2 is initialized as the AC/DC input (see EC_POWER_SOURCE), it also +# gets its own control socket. # 3. Routes the defmt log stream: in the normal (interactive) path the # semihosting output is piped straight into `defmt-print`; in the headless # path (`DEFMT_LOG=off`) QEMU is run raw with no logging. @@ -28,6 +31,13 @@ # EC_I2C_SOCK Path for the I2C-target socket (default: /tmp/qemu-ec-i2c.sock). # EC_GPIO_SOCK Path for the GPIO socket (default: /tmp/qemu-ec-gpio.sock). # EC_UART_SOCK Optional path for a UART socket (default: unset, use a PTY). +# EC_POWER_SOURCE +# Initial GPIO2 AC/DC input, `ac` or `dc`. Defaults to `dc` for +# firmware built with `time-alarm-power-input` (build.rs marks +# it); otherwise unset, leaving GPIO2 uninitialized. +# EC_POWER_SOCK Path for the GPIO2 control socket when GPIO2 is initialized +# (default: /tmp/qemu-ec-power.sock). Write the raw byte 0x01 +# for AC or 0x00 for DC. set -euo pipefail @@ -41,17 +51,37 @@ fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-7e461b3}" +ODP_QEMU_TAG="${ODP_QEMU_TAG:-sha-f8af056}" EC_I2C_SOCK="${EC_I2C_SOCK:-/tmp/qemu-ec-i2c.sock}" EC_GPIO_SOCK="${EC_GPIO_SOCK:-/tmp/qemu-ec-gpio.sock}" EC_UART_SOCK="${EC_UART_SOCK:-}" +EC_POWER_SOURCE="${EC_POWER_SOURCE:-}" +EC_POWER_SOCK="${EC_POWER_SOCK:-/tmp/qemu-ec-power.sock}" + +# Firmware built with `time-alarm-power-input` stops at startup unless GPIO2 is +# initialized. Its build.rs leaves this marker symbol, so start it on DC. +if [[ -z "$EC_POWER_SOURCE" && -n "$ELF" ]] && grep -qaF __odp_qemu_ec_power_input "$ELF"; then + EC_POWER_SOURCE=dc +fi + +# GPIO2 (bit 2) initial level: high is AC, low is DC. +case "$EC_POWER_SOURCE" in + "") ;; + ac) EC_POWER_LEVEL=0x4 ;; + dc) EC_POWER_LEVEL=0x0 ;; + *) + echo "error: EC_POWER_SOURCE must be 'ac' or 'dc', got '$EC_POWER_SOURCE'" >&2 + exit 1 + ;; +esac # GHCR image that publishes the prebuilt QEMU (with `ec` machine support). QEMU_IMAGE="ghcr.io/opendevicepartnership/odp-qemu-builder/qemu:${ODP_QEMU_TAG}" # Location of the binary inside that image. QEMU_IMAGE_BIN="/usr/local/bin/qemu-system-riscv32" -# Where we cache the extracted binary on the host. -QEMU_CACHE_DIR="${SCRIPT_DIR}/target/qemu-ec" +# Where we cache the extracted binary on the host. Caching per tag means bumping +# ODP_QEMU_TAG pulls the new binary instead of reusing an older one. +QEMU_CACHE_DIR="${SCRIPT_DIR}/target/qemu-ec/${ODP_QEMU_TAG}" QEMU_CACHE_BIN="${QEMU_CACHE_DIR}/qemu-system-riscv32" # Resolve the QEMU binary, pulling it from GHCR on first use. @@ -112,6 +142,8 @@ fi # - `-serial pty` By default, UART0 uses a PTY for terminal/ec-test-cli. # - `-chardev socket,...` The I2C-target, GPIO, and optional UART lines as UNIX # sockets that external programs can connect to. +# - `-global odp-gpio.*` With EC_POWER_SOURCE, initialize GPIO2 (the AC/DC input) +# for its control socket, which QEMU otherwise rejects. QEMU_ARGS=( -machine ec -bios none @@ -139,6 +171,17 @@ QEMU_ARGS+=( -kernel "$ELF" ) +if [[ -n "$EC_POWER_SOURCE" ]]; then + QEMU_ARGS+=( + -global odp-gpio.input-reset-mask=0x4 + -global "odp-gpio.input-reset=${EC_POWER_LEVEL}" + -chardev "socket,id=ec-gpio2,path=${EC_POWER_SOCK},server=on,wait=off" + ) + echo "GPIO2 power input starts on ${EC_POWER_SOURCE}; write 0x01 (AC) or 0x00 (DC) to ${EC_POWER_SOCK}" >&2 + echo "Expected: QEMU warns \"gpio2 source control disconnected; retaining input level N\" at startup" \ + "and after each client disconnects (N: 1 = AC, 0 = DC)." >&2 +fi + # Headless path: no defmt logging, so semihosting is left disabled and QEMU runs # raw. The "char device redirected to /dev/pts/N" line goes to stdout where # callers (integration-test.sh) grep it. diff --git a/platform/dev-qemu/src/main.rs b/platform/dev-qemu/src/main.rs index e2afb88..2274d19 100644 --- a/platform/dev-qemu/src/main.rs +++ b/platform/dev-qemu/src/main.rs @@ -51,7 +51,8 @@ async fn init_time_alarm( Some("dc") => AcpiTimerId::DcPower, _ => panic!("Build the wake fixture with ODP_WAKE_SOURCE=ac or dc"), }; - let (relay, service) = platform_common::mock::init_with_time_alarm_configure(spawner, |service| { + #[cfg_attr(not(feature = "time-alarm-power-input"), expect(unused_variables))] + let (relay, service, psu) = platform_common::mock::init_with_time_alarm_configure(spawner, |service| { #[cfg(feature = "time-alarm-power-input")] let source = { assert!( @@ -65,8 +66,9 @@ async fn init_time_alarm( .await; spawner.spawn(time_alarm_wake(service, wake_gpio).expect("Failed to spawn TimeAlarm wake task")); #[cfg(feature = "time-alarm-power-input")] - spawner - .spawn(time_alarm_power_input(service, power_input).expect("Failed to spawn TimeAlarm power input task")); + spawner.spawn( + time_alarm_power_input(service, psu, power_input).expect("Failed to spawn TimeAlarm power input task"), + ); (relay, service) }; @@ -85,9 +87,10 @@ async fn time_alarm_wake(service: mock_time_alarm::TimeAlarmService, gpio: embas #[embassy_executor::task] async fn time_alarm_power_input( service: mock_time_alarm::TimeAlarmService, + psu: platform_common::mock::power_policy::MockPsuControl, input: embassy_qemu_riscv::gpio::Input<'static, embassy_qemu_riscv::gpio::Async>, ) { - mock_time_alarm::monitor_power_input(service, input) + mock_time_alarm::monitor_power_input(service, psu, input) .await .expect("TimeAlarm power input failed"); } diff --git a/platform/platform-common/Cargo.lock b/platform/platform-common/Cargo.lock index 4f83efc..1d82816 100644 --- a/platform/platform-common/Cargo.lock +++ b/platform/platform-common/Cargo.lock @@ -1051,6 +1051,8 @@ dependencies = [ "embedded-sensors-hal-async", "embedded-services", "odp-service-common", + "power-policy-interface", + "power-policy-service", "static_cell", "thermal-service", "thermal-service-interface", @@ -1077,6 +1079,20 @@ dependencies = [ "embedded-services", ] +[[package]] +name = "power-policy-service" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/embedded-services?branch=main#695fae0a6eb505af74f72fe7c326911a4400beeb" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-services", + "heapless", + "power-policy-interface", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/platform/platform-common/Cargo.toml b/platform/platform-common/Cargo.toml index 91fb321..b5d8ff3 100644 --- a/platform/platform-common/Cargo.toml +++ b/platform/platform-common/Cargo.toml @@ -25,6 +25,12 @@ battery-service-interface = { package = "battery-service-interface", git = "http battery-service-relay = { package = "battery-service-relay", git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main", features = [ "defmt", ] } +power-policy-service = { package = "power-policy-service", git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main", features = [ + "defmt", +] } +power-policy-interface = { package = "power-policy-interface", git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main", features = [ + "defmt", +] } thermal-service = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "main", features = [ "defmt", diff --git a/platform/platform-common/src/mock/battery.rs b/platform/platform-common/src/mock/battery.rs index eebaeb7..470bfe4 100644 --- a/platform/platform-common/src/mock/battery.rs +++ b/platform/platform-common/src/mock/battery.rs @@ -1,6 +1,7 @@ use battery_service as bs; use bs::mock::MockFuelGauge; use bs::{DefaultPowerPolicyRouting, FuelGauge as _}; +use embassy_sync::channel::{Channel, DynamicReceiver, DynamicSender}; use embassy_sync::mutex::Mutex; use embassy_time::Timer; use embedded_services::{error, info, GlobalRawMutex}; @@ -12,7 +13,13 @@ const FUEL_GAUGE_COUNT: usize = if cfg!(feature = "dual-fuel-gauge") { 2 } else type Reg = bs::ArrayRegistration<'static, FuelGauge, FUEL_GAUGE_COUNT, DefaultPowerPolicyRouting>; pub type BatteryService = bs::Service<'static, Reg>; -pub async fn init(spawner: embassy_executor::Spawner) -> BatteryService { +/// Initialize the battery service, returning it with the sender for its power-policy notifications. +pub async fn init( + spawner: embassy_executor::Spawner, +) -> ( + &'static BatteryService, + DynamicSender<'static, bs::PowerPolicyEventData>, +) { info!("Initializing battery service..."); static FUEL_GAUGE_0: StaticCell = StaticCell::new(); @@ -28,10 +35,11 @@ pub async fn init(spawner: embassy_executor::Spawner) -> BatteryService { }, ]; - let service = bs::Service::new(bs::ArrayRegistration { + static SERVICE: StaticCell = StaticCell::new(); + let service: &'static BatteryService = SERVICE.init(bs::Service::new(bs::ArrayRegistration { fuel_gauges, routing: DefaultPowerPolicyRouting, - }); + })); for (battery_id, fuel_gauge) in fuel_gauges.into_iter().enumerate() { bs::mock::init_state_machine(fuel_gauge) @@ -41,7 +49,28 @@ pub async fn init(spawner: embassy_executor::Spawner) -> BatteryService { .spawn(update_data_task(battery_id as u8, fuel_gauge).expect("Failed to spawn battery update data task")); } - service + static POWER_POLICY_EVENTS: StaticCell> = StaticCell::new(); + let power_policy_events = POWER_POLICY_EVENTS.init(Channel::new()); + spawner.spawn( + power_policy_event_task(service, power_policy_events.dyn_receiver()) + .expect("Failed to spawn battery power-policy event task"), + ); + + (service, power_policy_events.dyn_sender()) +} + +/// Apply power-policy notifications to the fuel gauges' cached external power state. +#[embassy_executor::task] +async fn power_policy_event_task( + service: &'static BatteryService, + events: DynamicReceiver<'static, bs::PowerPolicyEventData>, +) -> ! { + loop { + let event = events.receive().await; + if let Err(e) = service.process_power_policy_event(&event).await { + error!("Battery power-policy event error: {:?}", e); + } + } } #[embassy_executor::task(pool_size = FUEL_GAUGE_COUNT)] diff --git a/platform/platform-common/src/mock/mod.rs b/platform/platform-common/src/mock/mod.rs index 6822cb3..cf1f4b9 100644 --- a/platform/platform-common/src/mock/mod.rs +++ b/platform/platform-common/src/mock/mod.rs @@ -3,12 +3,13 @@ //! //! This allows for easy testing of host to EC comms. pub mod battery; +pub mod power_policy; pub mod thermal; pub mod time_alarm; crate::impl_relay_handler!( MockOdpRelayHandler, - crate::mock::battery::BatteryService, + &'static crate::mock::battery::BatteryService, crate::mock::thermal::ThermalService ); @@ -24,19 +25,27 @@ pub async fn init(spawner: embassy_executor::Spawner) -> MockOdpRelayHandler { pub async fn init_with_time_alarm( spawner: embassy_executor::Spawner, ) -> (MockOdpRelayHandler, time_alarm_service::Service<'static>) { - init_with_time_alarm_configure(spawner, |_| {}).await + let (relay, tas, _psu) = init_with_time_alarm_configure(spawner, |_| {}).await; + (relay, tas) } /// Configure TimeAlarm before its runner starts and retain its board control handle. +/// +/// Also returns the control for the mock PSU, whose adapter starts unplugged. pub async fn init_with_time_alarm_configure( spawner: embassy_executor::Spawner, configure: impl FnOnce(time_alarm::TimeAlarmService), -) -> (MockOdpRelayHandler, time_alarm::TimeAlarmService) { +) -> ( + MockOdpRelayHandler, + time_alarm::TimeAlarmService, + power_policy::MockPsuControl, +) { embedded_services::info!("Initializing mock services..."); embedded_services::init().await; let thermal = thermal::init(spawner).await; - let battery = battery::init(spawner).await; + let (battery, battery_power_events) = battery::init(spawner).await; + let psu = power_policy::init(spawner, battery_power_events); let tas = time_alarm::init_with(spawner, configure).await; let relay = MockOdpRelayHandler::new( @@ -45,5 +54,5 @@ pub async fn init_with_time_alarm_configure( time_alarm_service_relay::mctp::TimeAlarmServiceRelayHandler::new(tas), ); - (relay, tas) + (relay, tas, psu) } diff --git a/platform/platform-common/src/mock/power_policy.rs b/platform/platform-common/src/mock/power_policy.rs new file mode 100644 index 0000000..f7af215 --- /dev/null +++ b/platform/platform-common/src/mock/power_policy.rs @@ -0,0 +1,168 @@ +//! Mock power policy with one PSU: a DC jack that a mock wall adapter can be plugged into. +//! +//! The policy's notifications go to the battery service, so its `_PSR` follows the adapter. + +use embassy_sync::channel::{Channel, DynamicReceiver, DynamicSender}; +use embassy_sync::mutex::Mutex; +use embedded_services::event::MapSender; +use embedded_services::named::Named; +use embedded_services::{info, GlobalRawMutex}; +use power_policy_interface::capability::{ + ConsumerFlags, ConsumerPowerCapability, PowerCapability, ProviderPowerCapability, PsuType, +}; +use power_policy_interface::charger::mock::ChargerType; +use power_policy_interface::psu::{self, event::EventData as PsuEventData, PsuState}; +use power_policy_interface::service::event::{Event, EventData as PowerPolicyEventData, SenderNotifier}; +use power_policy_service as pps; +use pps::psu::PsuEventReceivers; +use pps::service::registration::ArrayRegistration; +use static_cell::StaticCell; + +/// The mock wall adapter: 20 V at 3 A from an outlet. +const ADAPTER: ConsumerPowerCapability = ConsumerPowerCapability { + capability: PowerCapability { + voltage_mv: 20_000, + current_ma: 3_000, + }, + flags: ConsumerFlags { + unconstrained_power: true, + psu_type: Some(PsuType::DcJack), + }, +}; + +/// The mock DC jack, registered with the power policy as its only PSU. +pub struct MockPsu { + state: psu::State, +} + +impl psu::Psu for MockPsu { + async fn disconnect(&mut self) -> Result<(), psu::Error> { + self.state.disconnect(false) + } + + async fn connect_provider(&mut self, capability: ProviderPowerCapability) -> Result<(), psu::Error> { + self.state.connect_provider(capability) + } + + async fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> Result<(), psu::Error> { + self.state.connect_consumer(capability) + } + + fn state(&self) -> &psu::State { + &self.state + } + + fn state_mut(&mut self) -> &mut psu::State { + &mut self.state + } +} + +impl Named for MockPsu { + fn name(&self) -> &'static str { + "Mock PSU" + } +} + +pub type Psu = Mutex; +const PSU_COUNT: usize = 1; + +// Service events reference their PSU, but the battery service's default routing takes only the payload. +type ServiceEvent = Event<'static, Psu>; +type BatteryEventSender = MapSender< + ServiceEvent, + PowerPolicyEventData, + DynamicSender<'static, PowerPolicyEventData>, + fn(ServiceEvent) -> PowerPolicyEventData, +>; +type ServiceNotifier = SenderNotifier<'static, Psu, BatteryEventSender>; +const NOTIFIER_COUNT: usize = 1; +const CHARGER_COUNT: usize = 0; + +type Reg = ArrayRegistration<'static, Psu, PSU_COUNT, ServiceNotifier, NOTIFIER_COUNT, ChargerType, CHARGER_COUNT>; +pub type PowerPolicyService = Mutex>; + +/// Plugs the mock adapter into the mock PSU, or unplugs it. +pub struct MockPsuControl { + psu: &'static Psu, + events: DynamicSender<'static, PsuEventData>, +} + +impl MockPsuControl { + /// Plug the adapter in (`true`) or unplug it (`false`), notifying the power policy of any change. + pub async fn set_attached(&mut self, attached: bool) -> Result<(), psu::Error> { + { + let mut psu = self.psu.lock().await; + let state = &mut psu.state; + if attached == (state.psu_state != PsuState::Detached) { + return Ok(()); + } + if attached { + state.attach()?; + state.update_consumer_power_capability(Some(ADAPTER))?; + } else { + state.detach(); + } + } + + // Notify after releasing the PSU: the policy task locks it while handling events, so waiting + // for room in the event channel while holding it could deadlock. + if attached { + info!("Mock PSU: adapter plugged in"); + self.events.send(PsuEventData::Attached).await; + self.events + .send(PsuEventData::UpdatedConsumerCapability(Some(ADAPTER))) + .await; + } else { + info!("Mock PSU: adapter unplugged"); + self.events.send(PsuEventData::Detached).await; + } + Ok(()) + } +} + +/// Start the power policy with the mock PSU, sending its notifications to `battery_events`. +/// +/// The adapter starts unplugged. +pub fn init( + spawner: embassy_executor::Spawner, + battery_events: DynamicSender<'static, PowerPolicyEventData>, +) -> MockPsuControl { + info!("Initializing power policy service..."); + + static PSU_EVENTS: StaticCell> = StaticCell::new(); + let psu_events = PSU_EVENTS.init(Channel::new()); + + static PSU: StaticCell = StaticCell::new(); + let psu: &'static Psu = PSU.init(Mutex::new(MockPsu { + state: psu::State::default(), + })); + + let to_payload: fn(ServiceEvent) -> PowerPolicyEventData = PowerPolicyEventData::from; + static SERVICE: StaticCell = StaticCell::new(); + let service = SERVICE.init(Mutex::new(pps::service::Service::new( + ArrayRegistration { + psus: [psu], + service_notifiers: [SenderNotifier::new(MapSender::new(battery_events, to_payload))], + chargers: [], + }, + pps::service::config::Config::default(), + ))); + + spawner.spawn( + power_policy_task(PsuEventReceivers::new([psu], [psu_events.dyn_receiver()]), service) + .expect("Failed to spawn power policy task"), + ); + + MockPsuControl { + psu, + events: psu_events.dyn_sender(), + } +} + +#[embassy_executor::task] +async fn power_policy_task( + psu_events: PsuEventReceivers<'static, PSU_COUNT, Psu, DynamicReceiver<'static, PsuEventData>>, + service: &'static PowerPolicyService, +) -> ! { + pps::service::task::psu_task(psu_events, service).await +} diff --git a/platform/platform-common/src/mock/time_alarm.rs b/platform/platform-common/src/mock/time_alarm.rs index 74f724c..ab1d801 100644 --- a/platform/platform-common/src/mock/time_alarm.rs +++ b/platform/platform-common/src/mock/time_alarm.rs @@ -1,10 +1,12 @@ use embedded_hal::digital::{InputPin, OutputPin}; use embedded_hal_async::digital::Wait; -use embedded_services::info; +use embedded_services::{error, info}; use static_cell::StaticCell; use time_alarm_service as tas; use time_alarm_service_interface::{AcpiTimerId, AlarmTimerSeconds, TimeAlarmService as _}; +use crate::mock::power_policy::MockPsuControl; + pub type TimeAlarmService = tas::Service<'static>; pub async fn init(spawner: embassy_executor::Spawner) -> TimeAlarmService { @@ -94,11 +96,20 @@ pub fn power_source(input: &mut P) -> Result } /// Monitor AC/DC changes by resampling after opposite-level waits, returning any GPIO error. -pub async fn monitor_power_input(service: TimeAlarmService, mut input: P) -> Result<(), P::Error> { +/// +/// AC also plugs the mock adapter into `psu` and DC unplugs it, so battery `_PSR` follows the input. +pub async fn monitor_power_input( + service: TimeAlarmService, + mut psu: MockPsuControl, + mut input: P, +) -> Result<(), P::Error> { loop { let source = power_source(&mut input)?; service.set_power_source(source); info!("TimeAlarm power input: {:?}", source); + if let Err(e) = psu.set_attached(matches!(source, AcpiTimerId::AcPower)).await { + error!("Failed to update mock PSU: {:?}", e); + } match source { AcpiTimerId::AcPower => input.wait_for_low().await?, AcpiTimerId::DcPower => input.wait_for_high().await?,