Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions services/fwmanager/api/BUILD.bazel
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",
)
311 changes: 311 additions & 0 deletions services/fwmanager/api/src/boot_control.rs
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);
Comment thread
embediver marked this conversation as resolved.
Outdated

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);
Comment thread
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)
);
}
}
15 changes: 15 additions & 0 deletions services/fwmanager/api/src/lib.rs
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};