-
Notifications
You must be signed in to change notification settings - Fork 25
fwmanager: Add BootControl trait and HAL adapter #355
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6ec92a8
fwmanager: Add BootControl trait and HAL adapter
chrysh daa9aa7
Update services/fwmanager/api/src/boot_control.rs
chrysh 5fb6c51
fwmanager: Strengthen BootControl error contract
chrysh 19942a9
fwmanager: Move HalBootControl into hal-adapters crate
chrysh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Licensed under the Apache-2.0 license | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") | ||
|
|
||
| rust_library( | ||
| name = "fwmanager_api", | ||
| srcs = [ | ||
| "src/boot_control.rs", | ||
| "src/lib.rs", | ||
| ], | ||
| edition = "2024", | ||
| visibility = ["//visibility:public"], | ||
| deps = [ | ||
| "//hal/blocking", | ||
| ], | ||
| ) | ||
|
|
||
| # Host tests: build on the host platform, no kernel/QEMU. | ||
| rust_test( | ||
| name = "fwmanager_api_test", | ||
| crate = ":fwmanager_api", | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,311 @@ | ||
| // Licensed under the Apache-2.0 license | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //! Actuation capability: hold a managed device in reset and release it. | ||
|
|
||
| use openprot_hal_blocking::system_control::{Error as HalError, ErrorKind, ResetControl}; | ||
|
|
||
| /// Actuation capability: hold a managed device in reset and release it. | ||
| /// | ||
| /// Stateless pass-through by design — sequencing discipline (hold before | ||
| /// release, release only after verification) belongs to the orchestrator | ||
| /// flows, where it is observable behavior. | ||
| /// | ||
| /// # How the orchestrator uses it | ||
| /// | ||
| /// During verified release the orchestrator parks a device in | ||
| /// reset, verifies its active slot while nothing is running, then releases | ||
| /// it to boot the image it just checked: | ||
| /// | ||
| /// ```ignore | ||
| /// // `dev` is this device's BootControl, obtained from the registry. | ||
| /// fn verified_release<D: BootControl>(dev: &mut D) -> Result<(), D::Error> { | ||
| /// dev.hold_in_reset()?; // freeze the device; its flash is now safe to inspect | ||
| /// verify_active_slot()?; // re-hash + signature check (a separate capability) | ||
| /// dev.release()?; // run the just-verified image | ||
| /// Ok(()) | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// In a trial boot the same hold/release pair brackets a | ||
| /// watchdog-bounded window; the new slot is committed only if a good boot is | ||
| /// observed, otherwise the device falls back to the previous slot: | ||
| /// | ||
| /// ```ignore | ||
| /// dev.hold_in_reset()?; | ||
| /// store.set_trial(new_slot)?; // tentative boot selection — not yet committed | ||
| /// dev.release()?; // boot the trial image | ||
| /// match monitor.await_boot(window)? { | ||
| /// Booted => store.commit(new_slot)?, // observed good => make it active | ||
| /// Failed | Timeout => { /* nothing committed; previous slot still active */ } | ||
| /// } | ||
| /// ``` | ||
| pub trait BootControl { | ||
| /// The error type reported by this device's boot control. | ||
| /// | ||
| /// Requires [`core::error::Error`] (in `core` since Rust 1.81) so the | ||
| /// orchestrator gets `Display` and a `source()` cause chain, not just the | ||
| /// `Debug` dump, *and* the HAL [`Error`](HalError) trait so generic | ||
| /// consumers can categorize failures via `kind()` without knowing the | ||
| /// concrete error type. HAL-backed implementations satisfy both through | ||
| /// [`BootError`]. | ||
| type Error: core::error::Error + HalError; | ||
|
|
||
| /// Holds the device in reset. | ||
| fn hold_in_reset(&mut self) -> Result<(), Self::Error>; | ||
|
|
||
| /// Releases the device from reset. | ||
| fn release(&mut self) -> Result<(), Self::Error>; | ||
| } | ||
|
|
||
| /// Adapts any HAL system-control error into a [`core::error::Error`]. | ||
| /// | ||
| /// Reset controllers keep implementing the HAL `Error`/`kind()` pattern | ||
| /// unchanged; this wrapper supplies the `Display` and `core::error::Error` | ||
| /// machinery [`BootControl::Error`] requires, so no per-implementation work is | ||
| /// needed. The underlying category stays reachable via [`BootError::kind`]. | ||
| #[derive(Debug)] | ||
| pub struct BootError<E>(pub E); | ||
|
|
||
| impl<E: HalError> core::fmt::Display for BootError<E> { | ||
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| write!(f, "boot control error: {:?}", self.0.kind()) | ||
| } | ||
| } | ||
|
|
||
| impl<E: HalError> core::error::Error for BootError<E> {} | ||
|
|
||
| impl<E: HalError> HalError for BootError<E> { | ||
| /// The wrapped error's category, so a generic `BootControl` consumer can | ||
| /// branch on it (retry `Busy`, escalate `HardwareFailure`, ...) without | ||
| /// knowing the concrete error type. | ||
| fn kind(&self) -> ErrorKind { | ||
| self.0.kind() | ||
| } | ||
| } | ||
|
|
||
| /// Binds one reset line of a HAL reset controller to one managed device. | ||
| pub struct HalBootControl<C: ResetControl> { | ||
| controller: C, | ||
| reset_id: C::ResetId, | ||
| } | ||
|
|
||
| impl<C: ResetControl> HalBootControl<C> { | ||
| /// Creates the binding of `controller`'s line `reset_id` to a device. | ||
| pub fn new(controller: C, reset_id: C::ResetId) -> Self { | ||
| Self { | ||
| controller, | ||
| reset_id, | ||
| } | ||
| } | ||
|
|
||
| /// Read access to the underlying controller. | ||
| pub fn controller(&self) -> &C { | ||
| &self.controller | ||
| } | ||
| } | ||
|
|
||
| impl<C: ResetControl> BootControl for HalBootControl<C> { | ||
| type Error = BootError<C::Error>; | ||
|
|
||
| fn hold_in_reset(&mut self) -> Result<(), Self::Error> { | ||
| self.controller | ||
| .reset_assert(&self.reset_id) | ||
| .map_err(BootError) | ||
| } | ||
|
|
||
| fn release(&mut self) -> Result<(), Self::Error> { | ||
| self.controller | ||
| .reset_deassert(&self.reset_id) | ||
| .map_err(BootError) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use core::time::Duration; | ||
| use openprot_hal_blocking::system_control::{Error as HalError, ErrorKind, ErrorType}; | ||
|
|
||
| // Normally set in config.rs | ||
| const BMC_LINE: u8 = 7; | ||
|
|
||
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] | ||
| enum Call { | ||
| Assert(u8), | ||
| Deassert(u8), | ||
| } | ||
|
|
||
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] | ||
| struct MockError(ErrorKind); | ||
|
|
||
| impl HalError for MockError { | ||
| fn kind(&self) -> ErrorKind { | ||
| self.0 | ||
| } | ||
| } | ||
|
|
||
| /// Mock HAL reset controller: records every call it receives. | ||
| struct MockResetController { | ||
| calls: Vec<Call>, | ||
| fail: Option<ErrorKind>, | ||
| } | ||
|
|
||
| impl MockResetController { | ||
| fn new() -> Self { | ||
| Self { | ||
| calls: Vec::new(), | ||
| fail: None, | ||
| } | ||
| } | ||
|
|
||
| fn failing(kind: ErrorKind) -> Self { | ||
| Self { | ||
| calls: Vec::new(), | ||
| fail: Some(kind), | ||
| } | ||
| } | ||
|
|
||
| fn calls(&self) -> &[Call] { | ||
| &self.calls | ||
| } | ||
| } | ||
|
|
||
| impl ErrorType for MockResetController { | ||
| type Error = MockError; | ||
| } | ||
|
|
||
| impl ResetControl for MockResetController { | ||
| type ResetId = u8; // Reset line is GPIO here. Real driver should use Enum | ||
|
|
||
| fn reset_assert(&mut self, reset_id: &u8) -> Result<(), MockError> { | ||
| if let Some(kind) = self.fail { | ||
| return Err(MockError(kind)); | ||
| } | ||
| self.calls.push(Call::Assert(*reset_id)); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn reset_deassert(&mut self, reset_id: &u8) -> Result<(), MockError> { | ||
| if let Some(kind) = self.fail { | ||
| return Err(MockError(kind)); | ||
| } | ||
| self.calls.push(Call::Deassert(*reset_id)); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn reset_pulse(&mut self, _: &u8, _: Duration) -> Result<(), MockError> { | ||
| panic!( | ||
| "BootControl must never pulse: hold and release are distinct orchestrator steps" | ||
| ); | ||
| } | ||
|
|
||
| fn reset_is_asserted(&self, _: &u8) -> Result<bool, MockError> { | ||
| panic!("BootControl does not query line state"); | ||
| } | ||
| } | ||
|
|
||
| // `hold_in_reset()` must assert exactly the configured line (BMC = 7) | ||
| // and nothing else. | ||
| #[test] | ||
| fn holding_a_device_in_reset_asserts_its_configured_line() { | ||
| let mut bmc = HalBootControl::new(MockResetController::new(), BMC_LINE); | ||
|
|
||
| bmc.hold_in_reset().expect("hold_in_reset failed"); | ||
|
|
||
| assert_eq!(bmc.controller().calls(), &[Call::Assert(BMC_LINE)]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn releasing_a_device_from_reset_deasserts_its_configured_line() { | ||
| let mut bmc = HalBootControl::new(MockResetController::new(), BMC_LINE); | ||
|
|
||
| bmc.hold_in_reset().expect("hold_in_reset failed"); | ||
| bmc.release().expect("release failed"); | ||
|
|
||
| assert_eq!( | ||
| bmc.controller().calls(), | ||
| &[Call::Assert(BMC_LINE), Call::Deassert(BMC_LINE)] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn controller_error_propagates_through_boot_control() { | ||
| let mut bmc = HalBootControl::new( | ||
| MockResetController::failing(ErrorKind::InvalidResetId), | ||
| BMC_LINE, | ||
| ); | ||
| let err = bmc | ||
| .hold_in_reset() | ||
| .expect_err("expected the controller error to propagate"); | ||
| assert_eq!(err.kind(), ErrorKind::InvalidResetId); | ||
|
embediver marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // ---- Error contract the orchestrator depends on ------------------------ | ||
| // | ||
| // `BootControl::Error` requires the modern `core::error::Error` (in `core` | ||
| // since Rust 1.81) instead of the old `Debug`-only bound. `HalBootControl` | ||
| // satisfies it through the `BootError` adapter, which supplies `Display` + | ||
| // the `core::error::Error` marker over any HAL error while passing `kind()` | ||
| // through for categorization. | ||
|
|
||
| /// Compile-time fence: the error must satisfy `core::error::Error` | ||
| /// (Display + source) *and* the HAL `Error` (with `kind()`), so that | ||
| /// generic `BootControl` consumers get both. Fails to compile if either is | ||
| /// dropped. | ||
| fn _assert_error_contract<E: core::error::Error + HalError>() {} | ||
|
|
||
| #[test] | ||
| fn boot_error_satisfies_the_full_contract() { | ||
| _assert_error_contract::<BootError<MockError>>(); | ||
| } | ||
|
|
||
| #[test] | ||
| fn boot_error_renders_a_human_readable_message() { | ||
| let err = BootError(MockError(ErrorKind::HardwareFailure)); | ||
|
|
||
| // `Display`, not the `Debug` dump — this is what `core::error::Error` | ||
| // buys over the old `Debug`-only bound. | ||
| assert_eq!(err.to_string(), "boot control error: HardwareFailure"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn boot_error_is_a_leaf_with_no_source() { | ||
| let err = BootError(MockError(ErrorKind::Timeout)); | ||
|
|
||
| // HAL errors carry no nested `core::error::Error` cause. | ||
| assert!(core::error::Error::source(&err).is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn dyn_error_downcasts_back_to_the_concrete_type() { | ||
| let err = BootError(MockError(ErrorKind::PermissionDenied)); | ||
|
|
||
| let dyn_err: &dyn core::error::Error = &err; | ||
| let recovered = dyn_err | ||
| .downcast_ref::<BootError<MockError>>() | ||
| .expect("expected to recover the concrete error type"); | ||
| assert_eq!(recovered.kind(), ErrorKind::PermissionDenied); | ||
| } | ||
|
|
||
| /// Categorize a failure from *any* `BootControl` — the whole point of the | ||
| /// `kind()` bound is that this compiles for a generic `D`, not just a | ||
| /// concrete error type. | ||
| fn categorize_hold_failure<D: BootControl>(dev: &mut D) -> Option<ErrorKind> { | ||
| dev.hold_in_reset().err().map(|e| e.kind()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn error_kind_is_reachable_through_generic_boot_control() { | ||
| let mut bmc = HalBootControl::new( | ||
| MockResetController::failing(ErrorKind::HardwareFailure), | ||
| BMC_LINE, | ||
| ); | ||
|
|
||
| // No concrete error type in sight — `kind()` comes from the trait bound. | ||
| assert_eq!( | ||
| categorize_hold_failure(&mut bmc), | ||
| Some(ErrorKind::HardwareFailure) | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Licensed under the Apache-2.0 license | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //! Device-facing capability traits for the Boot Orchestrator. | ||
| //! | ||
| //! `BootControl` is the actuation capability: the orchestrator drives a | ||
| //! single managed device's reset without knowing which controller line it | ||
| //! maps to. The binding of a HAL reset controller line to a device happens | ||
| //! once, in platform configuration, via [`HalBootControl`]. | ||
|
|
||
| #![cfg_attr(not(test), no_std)] | ||
|
|
||
| mod boot_control; | ||
|
|
||
| pub use boot_control::{BootControl, HalBootControl}; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.