rigless 0.0.4

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model, and unit of virtual time in Rust; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: Apache-2.0
//! A machine: the board, which **has** its MCUs and the parts wired to them.
//!
//! A board module declares a struct whose fields are what is on the board — `mcu`, the crystals on
//! its oscillator pins, the rail its regulator sees — and implements [`Board`]. Reaching any of it
//! is a field path: `pixracer.mcu.rcc`, `pixracer.hse`. Nothing is looked up by name and nothing is
//! downcast, because the board holds the things themselves.
//!
//! **A board composes; it does not choose.** Which of a part's own peripherals are modelled is the
//! part's business, decided once in the part's own module. What a board adds is what a board adds:
//! crystals, a rail, and the integrated circuits on its buses. A board that fits no crystal is a
//! board with no crystal, not a part missing a clock tree.
//!
//! [`Board::targets`] is the one place the board hands itself to the runtime: one [`Target`] per MCU,
//! each borrowing that MCU as a [`Mcu`] so the session can step it. The board keeps ownership
//! throughout, so a test that holds a board can reach every model on it by field while the session
//! is between rounds.

use crate::mcu::Mcu;
use crate::sidecar::{InterruptLine, SidecarCommand};
use crate::soc::Error;

pub mod armv7m;
#[cfg(feature = "stm32f427")]
pub mod pixracer;
#[cfg(feature = "stm32f427")]
pub mod stm32f427_bench;

/// A board's constructor: the board assembled, or the error in its own description.
pub type Constructor = fn() -> Result<Box<dyn Board>, Error>;

/// Every machine this build carries, by the name `rigless run --machine` matches, with its constructor.
#[must_use]
pub fn all() -> &'static [(&'static str, Constructor)] {
    const MACHINES: &[(&str, Constructor)] = &[
        #[cfg(feature = "stm32f427")]
        (pixracer::NAME, pixracer::boxed),
        #[cfg(feature = "stm32f427")]
        (stm32f427_bench::NAME, stm32f427_bench::boxed),
    ];
    MACHINES
}

/// A quartz crystal on a board, as the oscillator it drives sees it: a frequency, or nothing fitted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Crystal(pub u64);

impl Crystal {
    /// No crystal on those pins.
    pub const NONE: Self = Self(0);
}

/// The regulated rail a board runs its MCU from, in millivolts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Supply(pub u32);

/// What a session drives: a board, and the MCUs on it.
///
/// A board with two MCUs is two targets, and the session runs an engine for each. The session steps
/// them in the order [`Board::targets`] lists them, so the order is the board's to choose and is the
/// same on every run.
pub trait Board: Send + std::fmt::Debug {
    /// The name `rigless run --machine` matches.
    fn name(&self) -> &'static str;

    /// One target per MCU on the board, in the board's order, each borrowing the MCU it is.
    fn targets(&mut self) -> Vec<Target<'_>>;

    /// The names of the MCUs on the board, in the same order as [`Board::targets`].
    fn target_names(&self) -> Vec<&'static str>;

    /// The index of the target called `name`.
    fn target(&self, name: &str) -> Option<usize> {
        self.target_names().iter().position(|&had| had == name)
    }
}

/// One MCU of a board as the runtime sees it: the MCU itself, how its engine is started, and how its
/// interrupts route.
///
/// It borrows rather than owns, because the board owns. A target lives for one round of the session.
pub struct Target<'a> {
    /// The name the command line matches and the output labels this target with.
    pub name: &'static str,
    /// The MCU, whole, borrowed from the board that has it.
    pub mcu: &'a mut dyn Mcu,
    /// What this target's engine is started as.
    pub sidecar: SidecarCommand,
    /// The engine's line for one of the MCU's interrupt numbers, or `None` when it has no line for it.
    pub interrupt_line: fn(u16) -> Option<InterruptLine>,
}

impl std::fmt::Debug for Target<'_> {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("Target")
            .field("name", &self.name)
            .field("sidecar", &self.sidecar)
            .finish_non_exhaustive()
    }
}