Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 21 additions & 1 deletion docs/src/design/orchestrator/orchestrator-overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Orchestrator State Machine
# Orchestrator

The orchestrator is the eRoT's boot-sequence controller. It walks the platform
trust chain — verifying each component's firmware and releasing it from reset in
Expand Down Expand Up @@ -28,6 +28,10 @@ avoid the two drifting apart.
- [**Platform Architecture**](./orchestrator-platform.md): The platform half around
the core — surrounding services, capability contracts, the board device table,
and the fail-safe rules at the responsibility boundary.
- [**Runtime**](./orchestrator-runtime.md): How the runtime loop
gathers hardware interrupts, IPC channel messages, and watchdog deadlines into
the core's event stream, and carries the resulting effects and decisions back
out.

## Design Principles

Expand All @@ -48,6 +52,22 @@ hiding them as implicit state changes. See the
The platform supplies the trust chain (component ids, kinds, and required/optional
policy) and the recovery-retry cap at startup.

## Board composition

*Board composition* (or *system composition*) is the per-target choice — described
declaratively in a [`system.json5`](../../architecture.md) file, assembled by
Pigweed at build time — of how the platform's functions are split across
processes and which resources each process owns: hardware register blocks and the
kernel [interrupt objects and IPC channels](../pw-kernel-ipc.md) built on top of
them. It is separate from the
board-supplied *policy* above: policy is *what* to verify (the trust chain);
composition is *how* the surrounding services are wired. The orchestrator core
and its platform-agnostic crates are the same across every composition — a driver
may own a GPIO bank and forward boot-progress over a channel in one image, while
the orchestrator holds the pins directly in another. That choice changes which
inbound sources the [runtime](./orchestrator-runtime.md) sees and who owns each
device, but never the core's states or the loop that serves them.

## Relationship to CSA Architecture

The state machine is a direct implementation of the boot sequence described in
Expand Down
111 changes: 111 additions & 0 deletions docs/src/design/orchestrator/orchestrator-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Runtime

The **runtime** is the orchestrator's event loop: the single task that sits
between two collaborators — the pure [state machine](./orchestrator-machine.md)
(the *core*) that decides, and the `PlatformDriver` that executes — turning
outside happenings (interrupts, timeouts, IPC messages) into the `Event`s the
core consumes, and handing the `Effect`s it emits to the driver. It holds no
policy of its own — every decision stays in the core; the runtime only moves
information across the process boundary.

It gathers three possible inbound sources — **hardware interrupts**
(boot-progress lines the orchestrator owns directly), **IPC channels**, and
Comment on lines +11 to +12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the boot monitoring really steered by hardware interrupts? Our trait based implementation as of now is naturally agnostic, but when we discussed this we talked about polling the GPIO lines, instead of waiting for real hardware interrupts handled by the hardware NVIC and ISRs.

I understand what you want to say by this, but if we are not relying on hardware inerrupts we should adapt the wording here.

**watchdog deadlines** (timeouts) — into the one event stream the core
supervises, and carries the core's decision back out. Which sources are present
is a [board-composition](./orchestrator-overview.md#board-composition) choice:
boot-progress is a hardware interrupt only when the orchestrator owns the pins;
otherwise a monitor forwards it as an IPC message.

The unifying idea is the kernel **wait group**: interrupts and IPC are not
separate mechanisms but interchangeable *members* of one group, and a watchdog
deadline is that same wait's *timeout* — so all three collapse into the return
of one `object_wait(handle, signal_mask, deadline)`. A *signal* here is just a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we link the source defintiion/link to pigweed kernel upstream sources here?

named bit on a waitable object, not a source in its own right: a latched IRQ bit
on an interrupt object, or `READABLE` / `USER` on a channel (a service raises
`Signals::USER` on a client channel to notify without a reply). The loop is
written once against "a member that signaled, or the deadline that lapsed,"
never against a specific source; what differs between sources is only the
*decoder* that turns each into an `Event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does this decoder look like and where is it defined?


Two neighboring pages carry the supporting detail:
[Platform Architecture](./orchestrator-platform.md) names the services and the
responsibility boundary, and [pw_kernel IPC](../pw-kernel-ipc.md) gives the
concrete channel syscalls. The worked, compiling reference is the QEMU
integration test at `target/ast10x0/tests/orchestrator/runtime/main.rs`.

## The single wait point

The runtime is a single-threaded loop parked in one place: a kernel
`object_wait` over a **wait group**. Every inbound source is registered once as a
*member* of that group (`wait_group_add`), and the wait returns whichever member
signaled. Each member resolves to at most one `Event`:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So what is the use case for a registered member to resolve to no Event?
If this is not the case, we should be precise and say

Suggested change
signaled. Each member resolves to at most one `Event`:
signaled. Each member maps to exactly one `Event`:


- **Boot-progress signals** — a component reaching a checkpoint raises a
boot-progress signal. Depending on the board's composition it arrives as an
interrupt object the orchestrator holds directly or as a message a monitor
forwards over a channel; either way the loop maps it to
[`Event::ComponentReady`] (an `Active` component's iRoT-verified readiness)
or [`Event::Booted`] (a `Passive` component's liveness).
- **Watchdog deadlines** — the timer is not a separate task. `BootWatchdogs`
(`services/orchestrator/server`) folds all armed boot windows and the commit
window into a *single deadline* passed straight to `object_wait`. When the
wait returns `DeadlineExceeded`, `poll_expired()` yields the mapped
[`Event::Timeout`] / [`Event::CommitTimeout`].
- **IPC channel messages** — an update agent, management path, peer service, or
a boot-progress-forwarding monitor holds a channel *initiator*; the runtime
holds the *handler*. A readable channel is another object the loop waits on;
its message decodes to an `Event` — an [`Event::UpdateRequest`] to answer, or
a forwarded [`Event::Booted`] / [`Event::ComponentReady`] notification.

The three share one `object_wait`, so a slow image hash on one path cannot
delay a boot window on another — this is the "never block" rule of the
[Platform Architecture](./orchestrator-platform.md#responsibility-scope) made
concrete: the loop only ever blocks at the wait, and only until the *nearest*
of any signal, any channel, or the nearest deadline.

**Members are uniform; only the decode differs.** A wait-group member is an
object watched for a signal bit — a latched IRQ bit on an *interrupt object*, or
`READABLE` / `USER` on a *channel* — and the loop treats them identically: wait,
see which member signaled, run that member's decode, dispatch the resulting
`Event`. Nothing above the decode step knows which kind a member is. That
uniformity pushes two questions *below* the runtime layer, where they belong:

- **Who owns the underlying hardware** — does the orchestrator own the GPIO bank
and hold the boot-progress interrupt object itself, or does a monitor/GPIO
server own the pins and forward boot-progress over a channel? — is a
[board-composition](./orchestrator-overview.md#board-composition) choice made
in `system.json5`. Either shape is just one member of the group; the loop is
byte-for-byte the same.
- **What a member means** is the per-member decode.

```mermaid
flowchart LR
subgraph SRC["Inbound sources (SIG/CH are wait-group members; TMR is the wait's timeout; boot-progress rides CH if a monitor owns the pins)"]
SIG["Boot-progress signal<br/>(directly-owned interrupt object, latched)"]
TMR["Watchdog deadline<br/>(BootWatchdogs → one Instant)"]
CH["IPC channel<br/>(handler endpoint, READABLE)"]
end

WAIT["object_wait(signals, deadline)<br/>the single park point"]

subgraph MAP["Inbound adapters (source → Event)"]
SMAP["signal → ComponentReady / Booted"]
TMAP["poll_expired → Timeout / CommitTimeout"]
CMAP["channel_read → decode → UpdateRequest / ..."]
end

CORE["Orchestrator::dispatch<br/>(pure reducer)"]

OUT["Effects → PlatformDriver<br/>+ response → channel_respond"]

SIG --> WAIT --> SMAP --> CORE
TMR --> WAIT --> TMAP --> CORE
CH --> WAIT --> CMAP --> CORE
CORE --> OUT
```

[`Event::ComponentReady`]: ./orchestrator-machine.md
[`Event::Booted`]: ./orchestrator-machine.md
[`Event::Timeout`]: ./orchestrator-machine.md
[`Event::CommitTimeout`]: ./orchestrator-machine.md
[`Event::UpdateRequest`]: ./orchestrator-machine.md