fwmanager: Add BootControl trait and HAL adapter Introduce the Boot Orchestrator's actuation capability: the BootControl trait (hold_in_reset / release) and HalBootControl, which binds one HAL ResetControl line to a managed device. Includes a host unit test verifying that holding a device in reset asserts exactly its configured line. Includes tests that release deasserts the device's configured line and that a controller error surfaces through BootControl unchanged. Extend the fake reset controller with opt-in failure injection to drive the error case. Closes: https://github.com/9elements/openprot/issues/2 Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christina Quast <christina.quast@9elements.com>
diff --git a/services/fwmanager/api/BUILD.bazel b/services/fwmanager/api/BUILD.bazel new file mode 100644 index 0000000..d78ebb6 --- /dev/null +++ b/services/fwmanager/api/BUILD.bazel
@@ -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", +)
diff --git a/services/fwmanager/api/src/boot_control.rs b/services/fwmanager/api/src/boot_control.rs new file mode 100644 index 0000000..6fce01d --- /dev/null +++ b/services/fwmanager/api/src/boot_control.rs
@@ -0,0 +1,206 @@ +// 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::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. + type Error: core::fmt::Debug; + + /// 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>; +} + +/// 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 = C::Error; + + fn hold_in_reset(&mut self) -> Result<(), Self::Error> { + self.controller.reset_assert(&self.reset_id) + } + + fn release(&mut self) -> Result<(), Self::Error> { + self.controller.reset_deassert(&self.reset_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::time::Duration; + use openprot_hal_blocking::system_control::{Error, 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 Error 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 holding_a_device_in_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); + } +}
diff --git a/services/fwmanager/api/src/lib.rs b/services/fwmanager/api/src/lib.rs new file mode 100644 index 0000000..8ab488a --- /dev/null +++ b/services/fwmanager/api/src/lib.rs
@@ -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};