rigless 0.0.6

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 fork of QEMU built in to execute the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The board's edge: what crosses it, and the trait a board with a world implements.
//!
//! A firmware reaches the world through pins and chips. On the way out it drives outputs, and on the way
//! in the world reaches its transducers. Both halves are the board's own types, because what one board
//! drives and measures is not what another does: a flight controller's outputs are pulses to speed
//! controllers and a pump controller's may be a valve and a relay. [`Edge::Outputs`] is what the firmware
//! drives, read off the pins by [`Edge::drive`], and [`Edge::Signals`] is what the world does to the
//! transducers, which [`Edge::sense`] hands to the chips. Whatever steps a plant reads the one and writes
//! the other between rounds of the session, when every target stands at one instant. The runtime steps
//! no plant and names none: the session and the command line reach a board's edge only as bytes, through
//! [`Coupled`], and only a host that holds the board as its own type reads and senses it typed.
//!
//! Every quantity a board declares is an integer in the unit its chips take, so a value crosses the seam
//! and lands in a register without a rounding of its own on the way. The chips keep only the setters
//! their datasheets name, each on the chip's own axes, and the board, which is the one thing that knows
//! how each package is mounted and what each pin drives, is what turns its signals into those calls and
//! its pins into its outputs. A timer's pin carries a [`Pulse`].

use std::borrow::Cow;

use crate::link::{Wire, WireError};
use crate::machine::Board;
use crate::time::VirtualNanos;

/// A board with a world: what its firmware drives, what its transducers sense, and how both cross the
/// link.
///
/// The types are the board's because the quantities are: a board declares what its firmware drives and
/// what its transducers measure, in the units its chips take and on the axes its description names, and a
/// plant for that board takes the one and answers the other. A board with nothing to drive or sense has
/// no edge.
pub trait Edge: Board {
    /// What the world does to this board's transducers from one step to the next.
    type Signals;

    /// What the firmware drives, as the board reads it off its pins.
    type Outputs;

    /// The signals' fields in the order their bytes cross the link, each with its unit and width, so an
    /// engine that doesn't share this crate can check what it sends against what the board takes.
    const FIELDS: &'static [Field];

    /// The outputs' fields in the order their bytes cross the link, each with its unit and width.
    const OUTPUT_FIELDS: &'static [Field];

    /// The signals the board is built sensing, before anything changes them.
    const REST: Self::Signals;

    /// What the world does to the board from now on. The board hands each chip its share on the chip's
    /// own axes.
    fn sense(&mut self, signals: &Self::Signals);

    /// What the firmware drives as it stands, read off the board's pins.
    fn drive(&mut self) -> Self::Outputs;
}

/// A board's edge reached without its types: what the command line hands to the engine's link, which
/// carries the outputs and the signals as bytes and never names their types.
///
/// Every [`Edge`] whose outputs and signals cross the wire is one, and nothing implements it by hand.
pub trait Coupled: Board {
    /// [`Edge::FIELDS`].
    fn fields(&self) -> &'static [Field];

    /// [`Edge::OUTPUT_FIELDS`].
    fn output_fields(&self) -> &'static [Field];

    /// [`Edge::REST`], as the wire carries it.
    fn rest_wire(&self) -> Vec<u8>;

    /// [`Edge::sense`], from the signals as the wire carries them.
    fn sense_wire(&mut self, bytes: &[u8]) -> Result<(), WireError>;

    /// [`Edge::drive`], as the wire carries it.
    fn drive_wire(&mut self) -> Vec<u8>;
}

impl<B: Edge> Coupled for B
where
    B::Signals: Wire,
    B::Outputs: Wire,
{
    fn fields(&self) -> &'static [Field] {
        B::FIELDS
    }

    fn output_fields(&self) -> &'static [Field] {
        B::OUTPUT_FIELDS
    }

    fn rest_wire(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        B::REST.encode(&mut bytes);
        bytes
    }

    fn sense_wire(&mut self, bytes: &[u8]) -> Result<(), WireError> {
        let signals = B::Signals::decode(&mut &*bytes)?;
        self.sense(&signals);
        Ok(())
    }

    fn drive_wire(&mut self) -> Vec<u8> {
        let mut bytes = Vec::new();
        self.drive().encode(&mut bytes);
        bytes
    }
}

/// One field of a board's outputs or signals as it crosses the link: its name, its unit, how many bytes
/// it takes and whether it's signed. Every field is a little-endian integer.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Field {
    /// The name, with a dot before an axis or a part, like `angular_rate.x`.
    pub name: Cow<'static, str>,
    /// The unit the integer counts in, like `mdeg/s`.
    pub unit: Cow<'static, str>,
    /// How many bytes it takes: 1, 2, 4 or 8.
    pub width: u8,
    /// Whether it's signed.
    pub signed: bool,
}

impl Field {
    /// A field as a board declares it.
    #[must_use]
    pub const fn new(name: &'static str, unit: &'static str, width: u8, signed: bool) -> Self {
        Self {
            name: Cow::Borrowed(name),
            unit: Cow::Borrowed(unit),
            width,
            signed,
        }
    }
}

/// A pulse a pin repeats: how long it is high, and how long until it is high again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Pulse {
    /// The high time.
    pub width: VirtualNanos,
    /// The period.
    pub period: VirtualNanos,
}