rigless 0.0.5

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; a fork of QEMU is built in and executes the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR 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: `board.mcu.rcc`, `board.hse`. Nothing is looked up by name, because the board holds
//! the things themselves; the one thing it does not hold is a chip on an interface's wires, which it
//! asks the interface for as the type it hung there.
//!
//! **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.
//!
//! The board's other edge is the world's, [`crate::edge`]. [`Board::actuators`] reads the outputs the
//! firmware is driving, each a pulse off a pin and what that pulse commands, and a board that measures
//! something implements [`Edge`](crate::edge::Edge) with its own signals type, handing each chip its
//! share turned the way the chip is mounted. Both are the board's because both are wiring: which timer
//! channel is which output, and which way each package sits, are lines on the schematic. Whatever steps
//! a plant calls the two between rounds; a board that drives nothing leaves `actuators` at its default.
//!
//! A board may also have wires a host sits on: a serial line a ground station or a person talks to.
//! [`Board::hosts`] names them and the port each listens on, and [`Board::attach_host`] hangs whatever
//! the caller puts there, a socket from `rigless run` or a test's stand-in; ADR-0027.

use crate::chip::uart;
use crate::edge::Actuator;
use crate::emulator::{EmulatorCommand, InterruptLine};
use crate::mcu::Mcu;
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>;

/// Builds a board with an edge, as [`coupled`] lists them.
pub type CoupledConstructor = fn() -> Result<Box<dyn crate::edge::Coupled>, 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
}

/// Every machine with an edge a plant can be coupled to, by name: what `rigless run --link` builds.
#[must_use]
pub fn coupled() -> &'static [(&'static str, CoupledConstructor)] {
    const COUPLED: &[(&str, CoupledConstructor)] = &[
        #[cfg(feature = "stm32f427")]
        (pixracer::NAME, pixracer::coupled),
    ];
    COUPLED
}

/// 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);
}

/// A wire of a board a host sits on: the name the board's description gives it, and the port `rigless
/// run` listens on for it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostWire {
    /// The wire, named as the board's description names it.
    pub name: &'static str,
    /// The TCP port a host connects to.
    pub port: u16,
}

/// 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 MCU of the target at `target`, or `None` past the last: the session reaches it here on
    /// every access, so nothing is built on the way.
    fn mcu(&mut self, target: usize) -> Option<&mut dyn Mcu>;

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

    /// The board's outputs as they stand, in the order its description lists them; none, for a board
    /// that drives nothing.
    fn actuators(&mut self) -> Vec<Actuator> {
        Vec::new()
    }

    /// The wires a host may sit on, in the order the board's description lists them; none, for a board
    /// with nothing a host talks to.
    fn hosts(&self) -> Vec<HostWire> {
        Vec::new()
    }

    /// Hangs `peer` on the far end of the wire called `name`, and says whether the board has one.
    fn attach_host(&mut self, _name: &str, _peer: Box<dyn uart::Peer>) -> bool {
        false
    }
}

/// 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 emulator is started as.
    pub emulator: EmulatorCommand,
    /// 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("emulator", &self.emulator)
            .finish_non_exhaustive()
    }
}