Skip to content

Commit d79b0cf

Browse files
chryshleongross
authored andcommitted
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: 9elements#2 Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christina Quast <christina.quast@9elements.com>
1 parent 1a10fcd commit d79b0cf

2 files changed

Lines changed: 235 additions & 0 deletions

File tree

services/fwmanager/api/BUILD.bazel

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Licensed under the Apache-2.0 license
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
5+
6+
rust_library(
7+
name = "fwmanager_api",
8+
srcs = [
9+
"src/lib.rs",
10+
],
11+
edition = "2024",
12+
visibility = ["//visibility:public"],
13+
deps = [
14+
"//hal/blocking",
15+
],
16+
)
17+
18+
# Host tests: build on the host platform, no kernel/QEMU.
19+
rust_test(
20+
name = "fwmanager_api_test",
21+
crate = ":fwmanager_api",
22+
)

services/fwmanager/api/src/lib.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Licensed under the Apache-2.0 license
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Device-facing capability traits for the Boot Orchestrator.
5+
//!
6+
//! `BootControl` is the actuation capability: the orchestrator drives a
7+
//! single managed device's reset without knowing which controller line it
8+
//! maps to. The binding of a HAL reset controller line to a device happens
9+
//! once, in platform configuration, via [`HalBootControl`].
10+
11+
#![cfg_attr(not(test), no_std)]
12+
13+
use openprot_hal_blocking::system_control::ResetControl;
14+
15+
/// Actuation capability: hold a managed device in reset and release it.
16+
///
17+
/// Stateless pass-through by design — sequencing discipline (hold before
18+
/// release, release only after verification) belongs to the orchestrator
19+
/// flows, where it is observable behavior.
20+
///
21+
/// # How the orchestrator uses it
22+
///
23+
/// During verified release the orchestrator parks a device in
24+
/// reset, verifies its active slot while nothing is running, then releases
25+
/// it to boot the image it just checked:
26+
///
27+
/// ```ignore
28+
/// // `dev` is this device's BootControl, obtained from the registry.
29+
/// fn verified_release<D: BootControl>(dev: &mut D) -> Result<(), D::Error> {
30+
/// dev.hold_in_reset()?; // freeze the device; its flash is now safe to inspect
31+
/// verify_active_slot()?; // re-hash + signature check (a separate capability)
32+
/// dev.release()?; // run the just-verified image
33+
/// Ok(())
34+
/// }
35+
/// ```
36+
///
37+
/// In a trial boot the same hold/release pair brackets a
38+
/// watchdog-bounded window; the new slot is committed only if a good boot is
39+
/// observed, otherwise the device falls back to the previous slot:
40+
///
41+
/// ```ignore
42+
/// dev.hold_in_reset()?;
43+
/// store.set_trial(new_slot)?; // tentative boot selection — not yet committed
44+
/// dev.release()?; // boot the trial image
45+
/// match monitor.await_boot(window)? {
46+
/// Booted => store.commit(new_slot)?, // observed good => make it active
47+
/// Failed | Timeout => { /* nothing committed; previous slot still active */ }
48+
/// }
49+
/// ```
50+
pub trait BootControl {
51+
/// The error type reported by this device's boot control.
52+
type Error: core::fmt::Debug;
53+
54+
/// Holds the device in reset.
55+
fn hold_in_reset(&mut self) -> Result<(), Self::Error>;
56+
57+
/// Releases the device from reset.
58+
fn release(&mut self) -> Result<(), Self::Error>;
59+
}
60+
61+
/// Binds one reset line of a HAL reset controller to one managed device.
62+
pub struct HalBootControl<C: ResetControl> {
63+
controller: C,
64+
reset_id: C::ResetId,
65+
}
66+
67+
impl<C: ResetControl> HalBootControl<C> {
68+
/// Creates the binding of `controller`'s line `reset_id` to a device.
69+
pub fn new(controller: C, reset_id: C::ResetId) -> Self {
70+
Self {
71+
controller,
72+
reset_id,
73+
}
74+
}
75+
76+
/// Read access to the underlying controller.
77+
pub fn controller(&self) -> &C {
78+
&self.controller
79+
}
80+
}
81+
82+
impl<C: ResetControl> BootControl for HalBootControl<C> {
83+
type Error = C::Error;
84+
85+
fn hold_in_reset(&mut self) -> Result<(), Self::Error> {
86+
self.controller.reset_assert(&self.reset_id)
87+
}
88+
89+
fn release(&mut self) -> Result<(), Self::Error> {
90+
self.controller.reset_deassert(&self.reset_id)
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use super::*;
97+
use core::time::Duration;
98+
use openprot_hal_blocking::system_control::{Error, ErrorKind, ErrorType};
99+
100+
// Normally set in config.rs
101+
const BMC_LINE: u8 = 7;
102+
103+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
104+
enum Call {
105+
Assert(u8),
106+
Deassert(u8),
107+
}
108+
109+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
110+
struct MockError(ErrorKind);
111+
112+
impl Error for MockError {
113+
fn kind(&self) -> ErrorKind {
114+
self.0
115+
}
116+
}
117+
118+
/// Mock HAL reset controller: records every call it receives.
119+
struct MockResetController {
120+
calls: Vec<Call>,
121+
fail: Option<ErrorKind>,
122+
}
123+
124+
impl MockResetController {
125+
fn new() -> Self {
126+
Self {
127+
calls: Vec::new(),
128+
fail: None,
129+
}
130+
}
131+
132+
fn failing(kind: ErrorKind) -> Self {
133+
Self {
134+
calls: Vec::new(),
135+
fail: Some(kind),
136+
}
137+
}
138+
139+
fn calls(&self) -> &[Call] {
140+
&self.calls
141+
}
142+
}
143+
144+
impl ErrorType for MockResetController {
145+
type Error = MockError;
146+
}
147+
148+
impl ResetControl for MockResetController {
149+
type ResetId = u8; // Reset line is GPIO here. Real driver should use Enum
150+
151+
fn reset_assert(&mut self, reset_id: &u8) -> Result<(), MockError> {
152+
if let Some(kind) = self.fail {
153+
return Err(MockError(kind));
154+
}
155+
self.calls.push(Call::Assert(*reset_id));
156+
Ok(())
157+
}
158+
159+
fn reset_deassert(&mut self, reset_id: &u8) -> Result<(), MockError> {
160+
if let Some(kind) = self.fail {
161+
return Err(MockError(kind));
162+
}
163+
self.calls.push(Call::Deassert(*reset_id));
164+
Ok(())
165+
}
166+
167+
fn reset_pulse(&mut self, _: &u8, _: Duration) -> Result<(), MockError> {
168+
panic!(
169+
"BootControl must never pulse: hold and release are distinct orchestrator steps"
170+
);
171+
}
172+
173+
fn reset_is_asserted(&self, _: &u8) -> Result<bool, MockError> {
174+
panic!("BootControl does not query line state");
175+
}
176+
}
177+
178+
// `hold_in_reset()` must assert exactly the configured line (BMC = 7)
179+
// and nothing else.
180+
#[test]
181+
fn holding_a_device_in_reset_asserts_its_configured_line() {
182+
let mut bmc = HalBootControl::new(MockResetController::new(), BMC_LINE);
183+
184+
bmc.hold_in_reset().expect("hold_in_reset failed");
185+
186+
assert_eq!(bmc.controller().calls(), &[Call::Assert(BMC_LINE)]);
187+
}
188+
189+
#[test]
190+
fn holding_a_device_in_reset_deasserts_its_configured_line() {
191+
let mut bmc = HalBootControl::new(MockResetController::new(), BMC_LINE);
192+
193+
bmc.hold_in_reset().expect("hold_in_reset failed");
194+
bmc.release().expect("release failed");
195+
196+
assert_eq!(
197+
bmc.controller().calls(),
198+
&[Call::Assert(BMC_LINE), Call::Deassert(BMC_LINE)]
199+
);
200+
}
201+
202+
#[test]
203+
fn controller_error_propagates_through_boot_control() {
204+
let mut bmc = HalBootControl::new(
205+
MockResetController::failing(ErrorKind::InvalidResetId),
206+
BMC_LINE,
207+
);
208+
let err = bmc
209+
.hold_in_reset()
210+
.expect_err("expected the controller error to propagate");
211+
assert_eq!(err.kind(), ErrorKind::InvalidResetId);
212+
}
213+
}

0 commit comments

Comments
 (0)