Skip to content

fwmanager: Add BootMonitor capability and GpioBootMonitor adapter - #368

Open
chrysh wants to merge 3 commits into
OpenPRoT:mainfrom
9elements:add-boot-monitor
Open

fwmanager: Add BootMonitor capability and GpioBootMonitor adapter#368
chrysh wants to merge 3 commits into
OpenPRoT:mainfrom
9elements:add-boot-monitor

Conversation

@chrysh

@chrysh chrysh commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

BootMonitor reports a device's boot liveness as BootStatus in the api leaf crate; GpioBootMonitor implements it over one HAL GpioPort input line. MonitorError meets the core::error::Error bound and keeps the concrete HAL error reachable through source(). new() rejects empty pin masks; boot latches are cleared by the reset path, never the observer.

Closes 9elements#6.

(Replaces 9elements#20, which was opened against the fork's main by mistake.)

/// path, since a hung device reports nothing — a stuck boot is caught by the
/// orchestrator's timeout, not by this enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootStatus {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets make this #[non_exhaustive].

Also, do we need an Unknown variant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non_exhaustive: Don't we want every consumer to get a compile error when a value is added, so that they must consciously decide how to handle the new state. non_exhaustive forces consumers to write a _ arm, and since every BootStatus variant is decision-relevant for the orchestrator, a future variant silently falling into that arm seems worse than a breaking change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning for Unknown: a hung device reports nothing, so "unknown" is already covered by the timeout path (Booting until the orchestrator gives up). It would hide a bad state.

Do you have strong opinions for either?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinions.
I'm fine with unknown always represented by the error path.
Maybe AMD folks have an opinion whether it should be non_exhaustive or not.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non_exhaustive: Don't we want every consumer to get a compile error when a value is added, so that they must consciously decide how to handle the new state. non_exhaustive forces consumers to write a _ arm, and since every BootStatus variant is decision-relevant for the orchestrator, a future variant silently falling into that arm seems worse than a breaking change.

I thought for a bit and exhaustive is right call here. Adding another variant should be a build error. The change should be one atomic commit that touches not only the enum but every consumer the compiler flags, reviewed together in a single PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

ActivePolarity::ActiveHigh => high,
ActivePolarity::ActiveLow => !high,
};
Ok(if booted {

@rusty1968 rusty1968 Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BootStatus has four values: InReset, Booting,
Booted, and Failed. But this monitor only looks at a single wire and asks
one question — "is it on or off?" — so it can only ever answer Booting or
Booted:

Ok(if booted { BootStatus::Booted } else { BootStatus::Booting })

It never returns InReset or Failed. A device that's still held in reset looks exactly the same on that one wire as a device that's been let go but hasn't finished starting up — both read as Booting. And there's no way for a device to say "I tried to boot and failed" over a single ready wire, so Failed never comes up either. A consumer would assume from the return type that this API returns the wide enum.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not wrong. How would you solve this issue best? Make it return a bool?

@rusty1968 rusty1968 Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be solved with the rustdoc so the reader knows the intent. It is not a bug it is just for clarity.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootStatus {
/// Held in reset by the orchestrator; not yet released.
InReset,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BootStatus is the little list of values a boot monitor can report about a device: InReset, Booting, Booted, Failed. That said InReset doesn't belong on that list. It's not something a monitor can see — it's something the orchestrator already did. The list should just be Booting, Booted, Failed.

type Error: core::error::Error;

/// Returns the current liveness of the device.
///

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document this only returns a subset.

/// Any given monitor may only ever produce a *subset* of [`BootStatus`],
/// depending on the signals it can access: a single ready pin yields only
/// `Booting`/`Booted`, while a fault-channel backend can also report
/// `Failed`. This is a capability difference between backends, not an
/// incomplete implementation. Consumers must still handle the full set —
/// they cannot know statically which backend they hold.

/// in every sample, so it would report every device `Booted` from the
/// moment reset is released — a misbinding that must fail at
/// construction, in platform bring-up, not stay silent in the field.
pub fn new(port: P, ready_pin: P::Mask, active: ActivePolarity) -> Self {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you build a GpioBootMonitor, it takes the GPIO port and keeps it for itself — and that "port" is a whole bank of pins, not the single pin the
monitor actually watches. So the first monitor you build swallows the entire bank, and there's nothing left for a second one.

That's a problem whenever two devices put their boot-complete signals on different pins of the same bank, which is a normal way to wire a board. You'd
naturally want one monitor per device, but you can't: after the first GpioBootMonitor::new, the bank is gone.

It's not hypothetical. Our immediate target, the AST1060 SGPIOM, is a serial GPIO expander whose P is SgpiomBankPort — a whole 32-line register
bank, not a single pin. Because 32 unrelated lines are packed into each fixed bank, different devices' ready lines routinely land in the same bank by
construction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Fixed.

/// reads the line, it cannot re-arm it.
///
/// [`HalBootControl`]: crate::HalBootControl
pub struct GpioBootMonitor<P: GpioPort> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider borrowing the port instead of owning it.

Make the monitor hold a shared reference:

pub struct GpioBootMonitor<'a, P: GpioPort> {
    port: &'a P,
    ready_pin: P::Mask,
    active: ActivePolarity,
}

Because read_input(&self) already needs only &self, nothing else in the monitor has to change. Several monitors can now borrow the same bank at once:

let mon_a = GpioBootMonitor::new(&bank, pin3, ActiveHigh);
let mon_b = GpioBootMonitor::new(&bank, pin5, ActiveHigh);
let mon_c = GpioBootMonitor::new(&bank, pin7, ActiveHigh);

This directly matches the read-only nature of the adapter; removes the single-owner constraint with the smallest possible change.

The issue is it Introduces a lifetime parameter on the type, so the platform must keep the bank alive somewhere for at least as long as the monitors.

What is your take?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good point. Shared &P only works because monitors read. If the same bank also carries output lines it will need &mut P. In this case the &mut of the controller and the & the monitor borrows conflict. In case this happens, we will have to do what SplitPort is doing.

chrysh added a commit to 9elements/openprot that referenced this pull request Jul 29, 2026
InReset is orchestrator state, not something a monitor can observe —
a device held in reset reads exactly like one still booting, so the
variant could never be reported. Remove it from BootStatus.

Document on boot_status() that any given backend may only ever produce
a subset of BootStatus depending on the signals it can access (a single
ready pin yields only Booting/Booted); this is a capability difference
between backends, and consumers must still handle the full set.

Addresses review feedback on PR OpenPRoT#368.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
chrysh added a commit to 9elements/openprot that referenced this pull request Jul 29, 2026
Owning the port swallowed a whole GPIO bank per monitor, making a
second monitor on the same bank impossible. On the AST1060 SGPIOM a
bank packs 32 unrelated lines, so distinct devices' ready signals
routinely share one bank by construction.

The monitor only reads (read_input takes &self), so hold &'a P instead:
several monitors can now watch different lines of the same bank, with
platform configuration keeping the bank alive for as long as its
monitors. Also note in the adapter docs that a single ready line only
ever yields the Booting/Booted subset of BootStatus.

Addresses review feedback on PR OpenPRoT#368.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
chrysh added 3 commits July 29, 2026 23:12
BootMonitor reports a device's boot liveness as BootStatus in the api
leaf crate; GpioBootMonitor implements it over one HAL GpioPort input
line. MonitorError meets the core::error::Error bound and keeps the
concrete HAL error reachable through source(). new() rejects empty pin
masks; boot latches are cleared by the reset path, never the observer.

Closes: #6
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
InReset is orchestrator state, not something a monitor can observe —
a device held in reset reads exactly like one still booting, so the
variant could never be reported. Remove it from BootStatus.

Document on boot_status() that any given backend may only ever produce
a subset of BootStatus depending on the signals it can access (a single
ready pin yields only Booting/Booted); this is a capability difference
between backends, and consumers must still handle the full set.

Addresses review feedback on PR OpenPRoT#368.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
Owning the port swallowed a whole GPIO bank per monitor, making a
second monitor on the same bank impossible. On the AST1060 SGPIOM a
bank packs 32 unrelated lines, so distinct devices' ready signals
routinely share one bank by construction.

The monitor only reads (read_input takes &self), so hold &'a P instead:
several monitors can now watch different lines of the same bank, with
platform configuration keeping the bank alive for as long as its
monitors. Also note in the adapter docs that a single ready line only
ever yields the Booting/Booted subset of BootStatus.

Addresses review feedback on PR OpenPRoT#368.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
@chrysh
chrysh force-pushed the add-boot-monitor branch from 44d7caf to 66f76f8 Compare July 29, 2026 21:13
@rusty1968
rusty1968 requested a review from JesseMelon July 29, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Downstream device boot monitoring

3 participants