-
Notifications
You must be signed in to change notification settings - Fork 44
Power policy direct async call #553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v0.2.0
Are you sure you want to change the base?
Power policy direct async call #553
Conversation
1588510 to
aa3e2e0
Compare
Cargo Vet Audit Failed
If the unvetted dependencies are not neededPlease modify Cargo.toml file to avoid including the dependencies. If the unvetted dependencies are neededPost a new comment with the questionnaire below to the PR to help the auditors vet the dependencies. Copy and paste the questionnaire as a new comment and provide your answers:1. What crates (with version) need to be audited? 2. How many of the crates are version updates vs new dependencies? 3. To confirm none of the already included crates serve your needs, please provide a brief description of the purpose of the new crates. 4. Any extra notes to the auditors to help with their audits. |
cda7d9a to
3467d89
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the power policy service architecture to use direct async calls instead of deferred IPC patterns. It introduces a proxy pattern for power device communication, extracts service message types into dedicated crates for better modularity, and adds comprehensive test coverage for the power policy service.
Changes:
- Refactors power policy to use direct async communication with devices via a proxy pattern
- Extracts thermal, debug, and battery service message types into separate reusable crates
- Implements MCTP relay infrastructure in embedded-services for message routing
- Adds test coverage for power policy consumer flows
Reviewed changes
Copilot reviewed 26 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| type-c-service/src/wrapper/proxy.rs | New proxy pattern for power device communication channels |
| power-policy-service/src/*.rs | Refactored to use direct device trait calls instead of action state machine |
| thermal-service-messages/ | New crate containing thermal service request/response types |
| debug-service-messages/ | New crate containing debug service request/response types |
| battery-service-messages/ | New crate containing battery service request/response types |
| embedded-service/src/relay/ | New MCTP relay infrastructure for message serialization |
| embedded-service/src/event.rs | New event sender/receiver traits |
| embedded-service/src/power/policy/device.rs | Refactored device with InternalState and DeviceTrait |
| espi-service/src/mctp.rs | MCTP relay implementation using new infrastructure |
| power-policy-service/tests/ | New test files for consumer flows |
| examples/std/src/bin/*.rs | Updated examples for new architecture |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| state: RefCell::new(InternalState::new( | ||
| intermediate.storage, | ||
| // Safe because both have N elements | ||
| power_senders | ||
| .into_array() | ||
| .unwrap_or_else(|_| panic!("Failed to create power events")), | ||
| )), |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code calls InternalState::new() but the implementation shows InternalState::try_new() which returns Option<Self>. This will cause a compilation error. The method should be try_new and the result should be unwrapped or handled properly.
| state: RefCell::new(InternalState::new( | |
| intermediate.storage, | |
| // Safe because both have N elements | |
| power_senders | |
| .into_array() | |
| .unwrap_or_else(|_| panic!("Failed to create power events")), | |
| )), | |
| state: RefCell::new( | |
| InternalState::try_new( | |
| intermediate.storage, | |
| // Safe because both have N elements | |
| power_senders | |
| .into_array() | |
| .unwrap_or_else(|_| panic!("Failed to create power events")), | |
| ) | |
| .unwrap_or_else(|| panic!("Failed to create internal state")), | |
| ), |
| /// Common event sender trait | ||
| pub trait Sender<E> { | ||
| /// Attempt to send an event | ||
| /// | ||
| /// Return none if the event cannot currently be sent | ||
| fn try_send(&mut self, event: E) -> Option<()>; | ||
| /// Send an event | ||
| fn send(&mut self, event: E) -> impl Future<Output = ()>; | ||
| } | ||
|
|
||
| /// Common event receiver trait | ||
| pub trait Receiver<E> { | ||
| /// Attempt to receive an event | ||
| /// | ||
| /// Return none if there are no pending events | ||
| fn try_next(&mut self) -> Option<E>; | ||
| /// Receiver an event |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Sender and Receiver traits lack documentation. Consider adding doc comments explaining their purpose, when they should be used, and any invariants. For example, document whether try_send returning None means the channel is full or the receiver is closed.
| /// Common event sender trait | |
| pub trait Sender<E> { | |
| /// Attempt to send an event | |
| /// | |
| /// Return none if the event cannot currently be sent | |
| fn try_send(&mut self, event: E) -> Option<()>; | |
| /// Send an event | |
| fn send(&mut self, event: E) -> impl Future<Output = ()>; | |
| } | |
| /// Common event receiver trait | |
| pub trait Receiver<E> { | |
| /// Attempt to receive an event | |
| /// | |
| /// Return none if there are no pending events | |
| fn try_next(&mut self) -> Option<E>; | |
| /// Receiver an event | |
| /// Abstraction over components that can send events of type `E`. | |
| /// | |
| /// This trait provides a common interface for event senders backed by | |
| /// channel-like primitives (for example, [`DynamicSender`]). It allows code | |
| /// to be written against this trait instead of a concrete channel type. | |
| /// | |
| /// # Semantics | |
| /// | |
| /// - [`Sender::try_send`] is **non-blocking**: it attempts to enqueue an | |
| /// event immediately and returns: | |
| /// - `Some(())` if the event was successfully enqueued. | |
| /// - `None` if the event cannot currently be sent. In this crate's | |
| /// implementations, this covers both the channel being full and the | |
| /// receiver side being closed. | |
| /// - [`Sender::send`] returns a future that will **asynchronously** send | |
| /// the event according to the underlying implementation's semantics. | |
| pub trait Sender<E> { | |
| /// Attempt to send an event without blocking. | |
| /// | |
| /// Returns `Some(())` on success. | |
| /// | |
| /// Returns `None` if the event cannot currently be sent. For the | |
| /// provided `DynamicSender` implementation, this occurs when the | |
| /// underlying channel is full or the receiver side has been closed. | |
| fn try_send(&mut self, event: E) -> Option<()>; | |
| /// Send an event, waiting asynchronously until it can be enqueued. | |
| /// | |
| /// The returned future completes once the event has been sent according | |
| /// to the semantics of the underlying sender implementation. | |
| fn send(&mut self, event: E) -> impl Future<Output = ()>; | |
| } | |
| /// Abstraction over components that can receive events of type `E`. | |
| /// | |
| /// This trait provides a common interface for event receivers backed by | |
| /// channel-like primitives (for example, [`DynamicReceiver`]). It allows | |
| /// consumers to be written against this trait instead of a concrete channel | |
| /// type. | |
| /// | |
| /// # Semantics | |
| /// | |
| /// - [`Receiver::try_next`] is **non-blocking**: it attempts to receive an | |
| /// event immediately and returns: | |
| /// - `Some(event)` if an event is available. | |
| /// - `None` if no event can currently be received. In this crate's | |
| /// implementations, this covers both the channel being empty and the | |
| /// sender side being closed. | |
| /// - [`Receiver::wait_next`] returns a future that will **asynchronously** | |
| /// wait for and yield the next event. | |
| pub trait Receiver<E> { | |
| /// Attempt to receive the next event without blocking. | |
| /// | |
| /// Returns `Some(event)` if an event is immediately available. | |
| /// | |
| /// Returns `None` if there are no pending events. For the provided | |
| /// `DynamicReceiver` implementation, this occurs when the underlying | |
| /// channel is empty or the sender side has been closed. | |
| fn try_next(&mut self) -> Option<E>; | |
| /// Receive the next event, waiting asynchronously until one is available. |
4be5bff to
a838d81
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 28 out of 38 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let e @ Err(_) = device.state.lock().await.connect_provider(target_power) { | ||
| let state = device.state.lock().await.state(); |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state is read twice (lines 88-89 and 89) which could lead to a race condition if the state changes between the two locks. Consider reading the state once and storing it in a variable.
| @@ -0,0 +1,43 @@ | |||
| //! Common traits for event senders and receivers | |||
| use embassy_sync::channel::{DynamicReceiver, DynamicSender}; | |||
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for Future. The trait methods return impl Future but there's no use core::future::Future; statement in this file.
| use embassy_sync::channel::{DynamicReceiver, DynamicSender}; | |
| use embassy_sync::channel::{DynamicReceiver, DynamicSender}; | |
| use core::future::Future; |
| /// | ||
| /// Return none if there are no pending events | ||
| fn try_next(&mut self) -> Option<E>; | ||
| /// Receiver an event |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in documentation comment: "Receiver an event" should be "Receive an event".
| /// Receiver an event | |
| /// Receive an event |
| let mut futures = heapless::Vec::<_, 16>::new(); | ||
| for device in self.devices().iter_only::<device::Device<'static, D, R>>() { | ||
| // TODO: check this at compile time | ||
| let _ = futures.push(async { device.receiver.lock().await.wait_next().await }); | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded maximum of 16 futures could be a limitation if more than 16 devices need to be supported. Consider making this configurable or documenting this limitation. The comment "TODO: check this at compile time" suggests this is a known issue, but the error handling (ignoring the push result with let _) means devices beyond 16 will silently not be monitored for events.
| if futures | ||
| .push(async { | ||
| let mut lock = receiver.lock().await; | ||
| lock.receive().await | ||
| }) | ||
| .is_err() | ||
| { | ||
| error!("Futures vec overflow"); | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a futures vec overflow occurs (line 113), the error is logged but execution continues. This means that if there are more power_proxy_receivers than MAX_SUPPORTED_PORTS, some receivers won't be monitored for commands, which could lead to unresponsive ports. Consider returning an error or panicking during initialization if this condition occurs.
| fn disconnect(&mut self) -> impl Future<Output = Result<(), Error>>; | ||
| /// Connect this device to provide power to an external connection | ||
| fn connect_provider(&mut self, capability: ProviderPowerCapability) -> impl Future<Output = Result<(), Error>>; | ||
| /// Connect this device to consume power from an external connection | ||
| fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> impl Future<Output = Result<(), Error>>; |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing import for Future. The trait methods in DeviceTrait return impl Future but there's no use core::future::Future; statement imported in this file.
| let device_state = device.state.lock().await.state(); | ||
|
|
||
| if let e @ Err(_) = device | ||
| .state | ||
| .lock() | ||
| .await | ||
| .connect_consumer(new_consumer.consumer_power_capability) |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar race condition issue: the device state is read on line 234, but then the state is read again on lines 236-240. Between these operations, another task could modify the state, making the error message show an incorrect state. Consider holding the lock or reading the state once and reusing it.
| if matches!(consumer_device.state.lock().await.state(), State::ConnectedConsumer(_)) { | ||
| // Disconnect the current consumer if needed | ||
| info!("Device{}: Disconnecting current consumer", current_consumer.device_id.0); | ||
| // disconnect current consumer and set idle | ||
| consumer.disconnect().await?; | ||
| consumer_device.device.lock().await.disconnect().await?; | ||
| if let Err(e) = consumer_device.state.lock().await.disconnect(false) { |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition between checking the state on line 204 and calling disconnect on line 209. Another task could change the state between these operations. The check and disconnect operations should be performed while holding a single lock to ensure atomicity.
| result | ||
| } | ||
|
|
||
| /// Updated the requested provider capability |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in function documentation: "Updated" should be "Update" to match the imperative mood used in other function documentation.
| /// Updated the requested provider capability | |
| /// Update the requested provider capability |
| //! [(GlobalPortId(0), power::policy::DeviceId(0)), (GlobalPortId(1), power::policy::DeviceId(1))], | ||
| //! )); | ||
| //! static INTERMEDIATE: StaticCell<IntermediateStorage<NUM_PORTS, NoopRawMutex>> = StaticCell::new(); | ||
| //! let intermediate = INTERMEDIATE.init(storage.create_intermediate()); | ||
| //! static REFERENCED: StaticCell<ReferencedStorage<NUM_PORTS, NoopRawMutex>> = StaticCell::new(); | ||
| //! let referenced = REFERENCED.init(storage.create_referenced().unwrap()); | ||
| //! let referenced = REFERENCED.init(intermediate.create_referenced()); | ||
| //! let _backing = referenced.create_backing().unwrap(); | ||
| //! } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example code in the documentation is outdated. Line 35 shows the old Storage::new signature that included power::policy::DeviceId in a tuple with GlobalPortId, but the new signature (line 232 in the actual code) only takes an array of GlobalPortId. The documentation example also doesn't show the correct usage of try_create_referenced which now requires policy_args parameter.
d3aa53a to
57c10a6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 28 out of 40 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Attempt to connect the requester as a provider | ||
| pub(super) async fn connect_provider(&self, requester_id: DeviceId) { | ||
| pub(super) async fn connect_provider(&self, requester_id: DeviceId) -> Result<(), Error> { | ||
| trace!("Device{}: Attempting to connect as provider", requester_id.0); | ||
| let requester = match self.context.get_device(requester_id) { | ||
| Ok(device) => device, | ||
| Err(_) => { | ||
| error!("Device{}: Invalid device", requester_id.0); | ||
| return; | ||
| } | ||
| }; | ||
| let requester = self.context.get_device(requester_id)?; | ||
| let requested_power_capability = match requester.requested_provider_capability().await { |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new generic connect_provider implementation introduces non-trivial provider-side policy behavior (including updated state tracking via InternalState and new error paths), but there are no automated tests exercising these provider flows, while consumer behavior is now covered by tests/consumer.rs. To keep regressions from slipping in, consider adding tests that cover successful provider connections, upgrade attempts, and failure cases for this path.
|
|
||
| for node in self.context.devices() { | ||
| let device = node.data::<Device>().ok_or(Error::InvalidDevice)?; | ||
| let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?; |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Device now has a lifetime parameter (defined as pub struct Device<'a, D: Lockable, R: Receiver<RequestData>>), but here it is being used as Device<D, R> without specifying that lifetime. This will fail to compile with a "generic arguments" error; you likely want to use Device<'_, D, R> (or a concrete lifetime) to match the struct definition.
| let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?; | |
| let device = node.data::<Device<'_, D, R>>().ok_or(Error::InvalidDevice)?; |
| let mut unconstrained_new = UnconstrainedState::default(); | ||
| for node in self.context.devices() { | ||
| let device = node.data::<Device>().ok_or(Error::InvalidDevice)?; | ||
| let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?; |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, Device requires a lifetime argument but is referenced as Device<D, R> here, which will not compile. Update this to include the lifetime parameter (for example Device<'_, D, R>) so it matches the Device type definition.
| let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?; | |
| let device = node.data::<Device<'_, D, R>>().ok_or(Error::InvalidDevice)?; |
|
|
||
| // Determine total requested power draw | ||
| for device in self.context.devices().iter_only::<device::Device>() { | ||
| for device in self.context.devices().iter_only::<device::Device<D, R>>() { |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
device::Device has been updated to take a lifetime parameter (Device<'a, D, R>), but this iter_only::<device::Device<D, R>>() invocation only supplies the D and R generics. This will fail to compile due to the missing lifetime; it should be updated to include the lifetime argument (for example device::Device<'static, D, R> or device::Device<'_, D, R> depending on the intended lifetime).
| for device in self.context.devices().iter_only::<device::Device<D, R>>() { | |
| for device in self | |
| .context | |
| .devices() | |
| .iter_only::<device::Device<'_, D, R>>() | |
| { |
| let mut futures = heapless::Vec::<_, 16>::new(); | ||
| for device in self.devices().iter_only::<device::Device<'static, D, R>>() { | ||
| // TODO: check this at compile time | ||
| let _ = futures.push(async { device.receiver.lock().await.wait_next().await }); |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait_request builds a heapless::Vec with a fixed capacity of 16 futures and silently ignores any additional devices by discarding the push result. If more than 16 power-policy devices are registered, events from the extra devices will never be observed, which can lead to starvation or lost events; consider either sizing this vector based on the maximum number of devices, enforcing a compile-time limit, or returning an error/log when the capacity is exceeded instead of dropping those devices.
| let _ = futures.push(async { device.receiver.lock().await.wait_next().await }); | |
| if futures | |
| .push(async { device.receiver.lock().await.wait_next().await }) | |
| .is_err() | |
| { | |
| panic!("too many power-policy devices registered; maximum supported is 16"); | |
| } |
57c10a6 to
fbe2f13
Compare
fbe2f13 to
fd95083
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 28 out of 40 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Common event sender trait | ||
| pub trait Sender<E> { | ||
| /// Attempt to send an event | ||
| /// | ||
| /// Return none if the event cannot currently be sent | ||
| fn try_send(&mut self, event: E) -> Option<()>; | ||
| /// Send an event | ||
| fn send(&mut self, event: E) -> impl Future<Output = ()>; | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file defines Sender::send and Receiver::wait_next returning impl Future, but Future is never imported, so this will fail to compile. Add use core::future::Future; (or fully qualify core::future::Future) at the top of the file so the trait bounds resolve.
| /// Trait for devices that can be controlled by a power policy implementation | ||
| pub trait DeviceTrait { | ||
| /// Disconnect power from this device | ||
| fn disconnect(&mut self) -> impl Future<Output = Result<(), Error>>; | ||
| /// Connect this device to provide power to an external connection | ||
| fn connect_provider(&mut self, capability: ProviderPowerCapability) -> impl Future<Output = Result<(), Error>>; | ||
| /// Connect this device to consume power from an external connection | ||
| fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> impl Future<Output = Result<(), Error>>; |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DeviceTrait uses impl Future<Output = Result<..., Error>> in its method signatures but Future is not in scope in this module, which will cause a compile error. Import core::future::Future (or fully qualify it) near the top of this file before using it in the trait definition.
| /// | ||
| /// Return none if there are no pending events | ||
| fn try_next(&mut self) -> Option<E>; | ||
| /// Receiver an event |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment /// Receiver an event has a grammatical error ("Receiver" vs "Receive"). Consider updating it to /// Receive an event for clarity and consistency with surrounding documentation.
| /// Receiver an event | |
| /// Receive an event |
No description provided.