rigless 0.2.1

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
//! What a flight controller board senses of the vehicle it flies in and what it drives on it, the
//! same on every such board, so one plant flies any of them.
//!
//! A board's [`Edge`](crate::edge::Edge) takes [`Signals`] as what the world does to its transducers,
//! and hands each chip its share on the chip's own axes. Its outputs are [`Actuator`]s, each the pulse
//! on a pin and what the pulse commands on a [`Curve`], and a board's output type says which they are
//! through [`Actuate`]. What differs between boards, which chips there are, how each is mounted and
//! what can go wrong with it, stays the board's own.

use crate::chip::nmea::Position;
use crate::edge::Pulse;
use crate::time::VirtualNanos;

/// How the thing on an output reads a pulse: the width it takes as no command, and the one it takes as
/// the whole of it. [`Curve::command`] is where between the two a pulse falls.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Curve {
    /// The width that commands nothing. Anything narrower commands the same.
    pub zero: VirtualNanos,
    /// The width that commands everything. Anything wider commands the same.
    pub full: VirtualNanos,
}

/// The whole of a command, in the thousandths [`Curve::command`] answers in.
pub const FULL: u32 = 1000;

impl Curve {
    /// What `pulse` commands on this curve, in thousandths of the whole, held between nothing and all.
    #[must_use]
    pub fn command(self, pulse: Pulse) -> u32 {
        let span = self.full.0.saturating_sub(self.zero.0).max(1);
        let above = pulse.width.0.saturating_sub(self.zero.0).min(span);
        u32::try_from(above * u64::from(FULL) / span).expect("at most the whole")
    }
}

/// One of the board's outputs as it stands: the name the board's description gives it, the pulse on its
/// pin, and what that pulse commands on the speed controllers' curve.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Actuator {
    /// The output, named as the board's description names it.
    pub name: &'static str,
    /// The pulse on the pin, or `None` while the pin is not driven or carries no pulse.
    pub pulse: Option<Pulse>,
    /// The command in thousandths of the whole, for a pulse on the curve.
    pub command: Option<u32>,
}

impl Actuator {
    /// The output `name` carrying `pulse`, read on `curve`.
    #[must_use]
    pub fn on(name: &'static str, pulse: Option<Pulse>, curve: Curve) -> Self {
        Self {
            name,
            pulse,
            command: pulse.map(|pulse| curve.command(pulse)),
        }
    }

    /// The output `name` with nothing on its pin.
    #[must_use]
    pub(crate) const fn idle(name: &'static str) -> Self {
        Self {
            name,
            pulse: None,
            command: None,
        }
    }
}

/// What a board's outputs are, in the order its description numbers them, for a plant that drives a
/// vehicle off them.
pub trait Actuate {
    /// Every output, first to last.
    fn actuators(&self) -> &[Actuator];
}

/// What the world does to a flight controller's transducers, on the body axes, `x` forward, `y` right and
/// `z` down, in the units the chips take. Where the board is and how it moves are in the world's
/// frame, north, east and down, which is the one the receiver reports in. The default is a world
/// that does nothing at all.
#[derive(
    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct Signals {
    /// The angular rate about each body axis, in thousandths of a degree per second.
    pub angular_rate: [i32; 3],
    /// The specific force along each body axis, in millionths of a gravity.
    pub specific_force: [i32; 3],
    /// The magnetic field along each body axis, in nanotesla.
    pub magnetic_field: [i32; 3],
    /// The static pressure, in pascals.
    pub pressure: u32,
    /// The temperature the board sits at, in hundredths of a degree Celsius.
    pub temperature: i32,
    /// The voltage of the pack the board is powered from, in millivolts.
    pub battery_voltage: u32,
    /// The current the pack is delivering, in milliamps, and negative while it charges.
    pub battery_current: i32,
    /// Where the board is.
    pub position: Position,
    /// How fast the board moves, north, east and down, in millimeters a second.
    pub velocity: [i32; 3],
}

/// The world a vehicle sits in before anything moves it: a gravity's reaction up the body's `z`,
/// which points down, and the field of a mid-latitude northern site, north and down. The air is sea
/// level at twenty degrees. The board is at the geographic center of the contiguous United States,
/// five hundred meters up, on a full four-cell pack.
pub const AT_REST: Signals = Signals {
    angular_rate: [0; 3],
    specific_force: [0, 0, -1_000_000],
    magnetic_field: [22_000, 0, 42_000],
    pressure: 101_325,
    temperature: 2_000,
    battery_voltage: 16_800,
    battery_current: 0,
    position: Position {
        latitude: 398_283_000,
        longitude: -985_795_000,
        altitude: 500_000,
    },
    velocity: [0; 3],
};

impl crate::link::Wire for Signals {
    fn encode(&self, out: &mut Vec<u8>) {
        self.angular_rate.encode(out);
        self.specific_force.encode(out);
        self.magnetic_field.encode(out);
        self.pressure.encode(out);
        self.temperature.encode(out);
        self.battery_voltage.encode(out);
        self.battery_current.encode(out);
        self.position.encode(out);
        self.velocity.encode(out);
    }

    fn decode(bytes: &mut &[u8]) -> Result<Self, crate::link::WireError> {
        Ok(Self {
            angular_rate: crate::link::Wire::decode(bytes)?,
            specific_force: crate::link::Wire::decode(bytes)?,
            magnetic_field: crate::link::Wire::decode(bytes)?,
            pressure: crate::link::Wire::decode(bytes)?,
            temperature: crate::link::Wire::decode(bytes)?,
            battery_voltage: crate::link::Wire::decode(bytes)?,
            battery_current: crate::link::Wire::decode(bytes)?,
            position: crate::link::Wire::decode(bytes)?,
            velocity: crate::link::Wire::decode(bytes)?,
        })
    }
}