Skip to content

Conversation

@RobertZ2011
Copy link
Contributor

No description provided.

@RobertZ2011 RobertZ2011 self-assigned this Nov 3, 2025
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from 1588510 to aa3e2e0 Compare November 4, 2025 23:15
@github-actions
Copy link

github-actions bot commented Nov 4, 2025

Cargo Vet Audit Failed

cargo vet has failed in this PR. Please run cargo vet --locked locally to check for new or updated unvetted dependencies.
Details about the vetting process can be found in supply-chain/README.md

If the unvetted dependencies are not needed

Please modify Cargo.toml file to avoid including the dependencies.

If the unvetted dependencies are needed

Post a new comment with the questionnaire below to the PR to help the auditors vet the dependencies.
After the auditors have vetted the dependencies, the PR will need to be rebased to pick up the new audits and pass this check.

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.

@github-actions github-actions bot added the cargo vet PRs pending auditor review label Nov 4, 2025
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from cda7d9a to 3467d89 Compare January 13, 2026 18:07
Copilot AI review requested due to automatic review settings January 13, 2026 18:07
@RobertZ2011 RobertZ2011 changed the base branch from main to v0.2.0 January 13, 2026 18:07
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 317 to 323
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")),
)),
Copy link

Copilot AI Jan 13, 2026

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.

Suggested change
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")),
),

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +21
/// 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
Copy link

Copilot AI Jan 13, 2026

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.

Suggested change
/// 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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 22, 2026 19:47
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from 4be5bff to a838d81 Compare January 22, 2026 19:47
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +88 to +89
if let e @ Err(_) = device.state.lock().await.connect_provider(target_power) {
let state = device.state.lock().await.state();
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,43 @@
//! Common traits for event senders and receivers
use embassy_sync::channel::{DynamicReceiver, DynamicSender};
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
use embassy_sync::channel::{DynamicReceiver, DynamicSender};
use embassy_sync::channel::{DynamicReceiver, DynamicSender};
use core::future::Future;

Copilot uses AI. Check for mistakes.
///
/// Return none if there are no pending events
fn try_next(&mut self) -> Option<E>;
/// Receiver an event
Copy link

Copilot AI Jan 22, 2026

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".

Suggested change
/// Receiver an event
/// Receive an event

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +268
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 });
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to 114
if futures
.push(async {
let mut lock = receiver.lock().await;
lock.receive().await
})
.is_err()
{
error!("Futures vec overflow");
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +259 to +263
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>>;
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +240
let device_state = device.state.lock().await.state();

if let e @ Err(_) = device
.state
.lock()
.await
.connect_consumer(new_consumer.consumer_power_capability)
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +209
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) {
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
result
}

/// Updated the requested provider capability
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
/// Updated the requested provider capability
/// Update the requested provider capability

Copilot uses AI. Check for mistakes.
Comment on lines 35 to 42
//! [(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();
//! }
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 22, 2026 21:54
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from d3aa53a to 57c10a6 Compare January 22, 2026 21:54
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 32 to 36
/// 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 {
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.

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)?;
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?;
let device = node.data::<Device<'_, D, R>>().ok_or(Error::InvalidDevice)?;

Copilot uses AI. Check for mistakes.
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)?;
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
let device = node.data::<Device<D, R>>().ok_or(Error::InvalidDevice)?;
let device = node.data::<Device<'_, D, R>>().ok_or(Error::InvalidDevice)?;

Copilot uses AI. Check for mistakes.

// 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>>() {
Copy link

Copilot AI Jan 22, 2026

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).

Suggested change
for device in self.context.devices().iter_only::<device::Device<D, R>>() {
for device in self
.context
.devices()
.iter_only::<device::Device<'_, D, R>>()
{

Copilot uses AI. Check for mistakes.
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 });
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
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");
}

Copilot uses AI. Check for mistakes.
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from 57c10a6 to fbe2f13 Compare January 22, 2026 22:42
Copilot AI review requested due to automatic review settings January 22, 2026 22:45
@RobertZ2011 RobertZ2011 force-pushed the power-policy-direct-async-call branch from fbe2f13 to fd95083 Compare January 22, 2026 22:45
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +5 to +13
/// 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 = ()>;
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +263
/// 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>>;
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
///
/// Return none if there are no pending events
fn try_next(&mut self) -> Option<E>;
/// Receiver an event
Copy link

Copilot AI Jan 22, 2026

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.

Suggested change
/// Receiver an event
/// Receive an event

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cargo vet PRs pending auditor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant