rigless 0.3.0

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, and a CPU core of its own that translates the firmware's instructions to host code
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's on the board: `mcu`, the crystals on
//! its oscillator pins, the rail its regulator sees. It implements [`Board`] for that struct.
//! Reaching any of it is a field path: `board.mcu.clocks`, `board.crystal`. Nothing is looked up by
//! name, because the board holds the things themselves. The one thing it doesn't hold is a chip on
//! an interface's wires, which it asks the interface for as the type it hung there.
//!
//! **A board puts things together. It doesn't choose what a part has.** Which of a part's own
//! peripherals are modeled is the part's business, decided once in the part's own module. What a
//! board adds is 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 where the board hands itself to the runtime: one [`Target`] per MCU, each
//! borrowing that MCU as a [`Mcu`]. The session then steps its target through [`Board::mcu`], which
//! it reaches on every access. The board keeps ownership throughout, so every model on it stays a
//! field path, and a test reaches one through
//! [`Session::board_mut`](crate::session::Session::board_mut) while a session holds the board.
//!
//! The board's other edge is the world's, [`crate::edge`]. A board that drives or senses something
//! implements [`Edge`](crate::edge::Edge) with its own outputs and signals types: it reads what the
//! firmware drives off its pins and hands each chip its share of the signals, turned the way the chip is
//! mounted. Both are the board's because both are wiring: which pin is which output, and which way each
//! package sits, are lines on the schematic. [`Board::edge`] reaches that edge as bytes, for whatever
//! steps a plant between rounds or prints the outputs. A board with no edge leaves it 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.

use crate::chip::uart;
use crate::edge::Coupled;
use crate::emulator::CoreSpec;
use crate::mcu::Mcu;
use crate::snapshot::Store;
use crate::soc::Error;

pub mod armv7m;
pub mod flight;
#[cfg(feature = "matekh743")]
pub mod matekh743;
#[cfg(feature = "pixracer")]
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 = "pixracer")]
        (pixracer::NAME, pixracer::boxed),
        #[cfg(feature = "matekh743")]
        (matekh743::NAME, matekh743::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);
}

/// 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 a session runs one of them, picked by its index in
/// [`Board::targets`]. That order is the board's to choose and is the same on every run.
///
/// A board is [`Store`]: a snapshot saves the MCU it carries and everything it keeps of its own. One
/// that doesn't say how answers that it can't, and its runs start from reset.
pub trait Board: Store + 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 edge, reached as bytes, or nothing for a board with nothing to drive or sense. A board
    /// that implements [`Edge`](crate::edge::Edge) answers itself.
    fn edge(&mut self) -> Option<&mut dyn Coupled> {
        None
    }

    /// The wires a host may sit on, in the order the board's description lists them, or 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
    }

    /// What hangs on the far end of the wire called `name`, or nothing where the board has no such wire
    /// or nothing hangs there.
    fn host_mut(&mut self, _name: &str) -> Option<&mut dyn uart::Peer> {
        None
    }
}

/// One MCU of a board as the runtime sees it: the MCU itself, and how its emulator is started. How
/// its interrupts reach the core is the MCU's own, in its bus's description.
///
/// It borrows rather than owns, because the board owns. The runtime asks for the targets when a
/// session opens and when the emulator starts, and keeps none of them.
#[non_exhaustive]
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,
    /// The description of this target's core, which the run makes it from.
    pub core: CoreSpec,
}

impl<'a> Target<'a> {
    /// The target called `name`, borrowing `mcu`, whose core `core` describes.
    #[must_use]
    pub fn new(name: &'static str, mcu: &'a mut dyn Mcu, core: CoreSpec) -> Self {
        Self { name, mcu, core }
    }
}

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("core", &self.core)
            .finish_non_exhaustive()
    }
}