use crate::chip::nmea::Position;
use crate::edge::{Field, Pulse};
use crate::link::{Wire, WireError};
use crate::time::VirtualNanos;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Curve {
pub zero: VirtualNanos,
pub full: VirtualNanos,
}
pub const FULL: u32 = 1000;
impl Curve {
#[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")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Actuator {
pub name: &'static str,
pub pulse: Option<Pulse>,
pub command: Option<u32>,
}
impl Actuator {
#[must_use]
pub fn on(name: &'static str, pulse: Option<Pulse>, curve: Curve) -> Self {
Self {
name,
pulse,
command: pulse.map(|pulse| curve.command(pulse)),
}
}
const fn idle(name: &'static str) -> Self {
Self {
name,
pulse: None,
command: None,
}
}
}
pub trait Actuate {
fn actuators(&self) -> &[Actuator];
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct Signals {
pub angular_rate: [i32; 3],
pub specific_force: [i32; 3],
pub magnetic_field: [i32; 3],
pub pressure: u32,
pub temperature: i32,
pub battery_voltage: u32,
pub battery_current: i32,
pub position: Position,
pub velocity: [i32; 3],
}
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],
};
crate::wire!(Signals {
angular_rate,
specific_force,
magnetic_field,
pressure,
temperature,
battery_voltage,
battery_current,
position,
velocity
});
pub const SIGNAL_FIELDS: &[Field] = &[
Field::new("angular_rate.x", "mdeg/s", 4, true),
Field::new("angular_rate.y", "mdeg/s", 4, true),
Field::new("angular_rate.z", "mdeg/s", 4, true),
Field::new("specific_force.x", "1e-6 g", 4, true),
Field::new("specific_force.y", "1e-6 g", 4, true),
Field::new("specific_force.z", "1e-6 g", 4, true),
Field::new("magnetic_field.x", "nT", 4, true),
Field::new("magnetic_field.y", "nT", 4, true),
Field::new("magnetic_field.z", "nT", 4, true),
Field::new("pressure", "Pa", 4, false),
Field::new("temperature", "0.01 degC", 4, true),
Field::new("battery_voltage", "mV", 4, false),
Field::new("battery_current", "mA", 4, true),
Field::new("position.latitude", "1e-7 deg", 4, true),
Field::new("position.longitude", "1e-7 deg", 4, true),
Field::new("position.altitude", "mm", 4, true),
Field::new("velocity.north", "mm/s", 4, true),
Field::new("velocity.east", "mm/s", 4, true),
Field::new("velocity.down", "mm/s", 4, true),
];
const MOST_OUTPUTS: usize = 13;
const FIELDS_PER_OUTPUT: usize = 3;
const NAMES: [&str; MOST_OUTPUTS] = [
"PWM1", "PWM2", "PWM3", "PWM4", "PWM5", "PWM6", "PWM7", "PWM8", "PWM9", "PWM10", "PWM11",
"PWM12", "PWM13",
];
static OUTPUT_FIELDS: [Field; FIELDS_PER_OUTPUT * MOST_OUTPUTS] = [
Field::new("PWM1.width", "ns", 8, false),
Field::new("PWM1.period", "ns", 8, false),
Field::new("PWM1.command", "1e-3", 4, false),
Field::new("PWM2.width", "ns", 8, false),
Field::new("PWM2.period", "ns", 8, false),
Field::new("PWM2.command", "1e-3", 4, false),
Field::new("PWM3.width", "ns", 8, false),
Field::new("PWM3.period", "ns", 8, false),
Field::new("PWM3.command", "1e-3", 4, false),
Field::new("PWM4.width", "ns", 8, false),
Field::new("PWM4.period", "ns", 8, false),
Field::new("PWM4.command", "1e-3", 4, false),
Field::new("PWM5.width", "ns", 8, false),
Field::new("PWM5.period", "ns", 8, false),
Field::new("PWM5.command", "1e-3", 4, false),
Field::new("PWM6.width", "ns", 8, false),
Field::new("PWM6.period", "ns", 8, false),
Field::new("PWM6.command", "1e-3", 4, false),
Field::new("PWM7.width", "ns", 8, false),
Field::new("PWM7.period", "ns", 8, false),
Field::new("PWM7.command", "1e-3", 4, false),
Field::new("PWM8.width", "ns", 8, false),
Field::new("PWM8.period", "ns", 8, false),
Field::new("PWM8.command", "1e-3", 4, false),
Field::new("PWM9.width", "ns", 8, false),
Field::new("PWM9.period", "ns", 8, false),
Field::new("PWM9.command", "1e-3", 4, false),
Field::new("PWM10.width", "ns", 8, false),
Field::new("PWM10.period", "ns", 8, false),
Field::new("PWM10.command", "1e-3", 4, false),
Field::new("PWM11.width", "ns", 8, false),
Field::new("PWM11.period", "ns", 8, false),
Field::new("PWM11.command", "1e-3", 4, false),
Field::new("PWM12.width", "ns", 8, false),
Field::new("PWM12.period", "ns", 8, false),
Field::new("PWM12.command", "1e-3", 4, false),
Field::new("PWM13.width", "ns", 8, false),
Field::new("PWM13.period", "ns", 8, false),
Field::new("PWM13.command", "1e-3", 4, false),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Outputs<const N: usize>(pub [Actuator; N]);
impl<const N: usize> Outputs<N> {
pub const IDLE: Self = {
let mut actuators = [Actuator::idle(""); N];
let mut index = 0;
while index < N {
actuators[index] = Actuator::idle(NAMES[index]);
index += 1;
}
Self(actuators)
};
pub const FIELDS: &'static [Field] = OUTPUT_FIELDS.split_at(FIELDS_PER_OUTPUT * N).0;
}
impl<const N: usize> Actuate for Outputs<N> {
fn actuators(&self) -> &[Actuator] {
&self.0
}
}
impl<const N: usize> Wire for Outputs<N> {
fn encode(&self, out: &mut Vec<u8>) {
for actuator in &self.0 {
let (width, period) = actuator
.pulse
.map_or((0, 0), |pulse| (pulse.width.0, pulse.period.0));
width.encode(out);
period.encode(out);
actuator.command.unwrap_or(0).encode(out);
}
}
fn decode(bytes: &mut &[u8]) -> Result<Self, WireError> {
let mut outputs = Self::IDLE;
for actuator in &mut outputs.0 {
let width: VirtualNanos = Wire::decode(bytes)?;
let period: VirtualNanos = Wire::decode(bytes)?;
let command: u32 = Wire::decode(bytes)?;
actuator.pulse = (period.0 != 0).then_some(Pulse { width, period });
actuator.command = actuator.pulse.map(|_| command);
}
Ok(outputs)
}
}
#[cfg(test)]
pub mod conformance {
use super::{AT_REST, Outputs, Signals};
use crate::chip::nmea::Position;
use crate::chip::uart::Peer;
use crate::edge::{Condition, Coupled, Edge};
use crate::link::{Wire, values_of};
use crate::time::VirtualNanos;
pub fn wrong(stuck: bool, bias: i32) -> Condition<3> {
Condition {
stuck,
bias: [bias, bias - 1, bias - 2],
noise: [1, 2, 3].map(|axis| bias.unsigned_abs() * 1_000 + axis),
}
}
pub fn the_declared_fields_walk_the_encoded_signals_one_by_one<B>()
where
B: Edge<Signals = Signals>,
{
let signals = Signals {
angular_rate: [-1, -2, -3],
specific_force: [-4, -5, -6],
magnetic_field: [-7, -8, -9],
pressure: 3_000_000_000,
temperature: -11,
battery_voltage: 3_000_000_001,
battery_current: -13,
position: Position {
latitude: -14,
longitude: -15,
altitude: -16,
},
velocity: [-17, -18, -19],
};
let mut bytes = Vec::new();
signals.encode(&mut bytes);
assert_eq!(
values_of(B::FIELDS, &bytes).unwrap(),
[
-1,
-2,
-3,
-4,
-5,
-6,
-7,
-8,
-9,
3_000_000_000,
-11,
3_000_000_001,
-13,
-14,
-15,
-16,
-17,
-18,
-19
],
"every signed field negative and every unsigned one past an i32, so a wrong sign shows"
);
}
pub fn the_declared_conditions_walk_the_encoded_conditions_one_by_one_and_rest_at_zero<B>(
conditions: &B::Conditions,
expected: &[i64],
) where
B: Edge,
B::Conditions: Wire + Default,
{
let mut bytes = Vec::new();
B::Conditions::default().encode(&mut bytes);
assert!(
values_of(B::CONDITIONS, &bytes)
.unwrap()
.iter()
.all(|&value| value == 0),
"the board as built crosses as zeros"
);
let mut bytes = Vec::new();
conditions.encode(&mut bytes);
assert_eq!(
values_of(B::CONDITIONS, &bytes).unwrap(),
expected,
"every flag, bias, noise and rate where its field says, each bias negative and one rate \
past an i32 so a wrong sign shows"
);
}
pub fn the_edge_passes_the_links_own_check<B: Coupled>(board: &mut B) {
assert_eq!(board.check(), Ok(()));
}
pub fn the_board_is_built_at_rest<B, const N: usize>(board: &mut B)
where
B: Edge<Signals = Signals, Outputs = Outputs<N>>,
{
assert_eq!(B::REST, AT_REST);
assert_eq!(board.drive(), Outputs::IDLE, "nothing drives a pin yet");
}
pub fn each_chip_listed_has_a_silence_and_a_place<B: Edge>(board: &mut B) {
for (index, chip) in B::CHIPS.iter().enumerate() {
let silence = format!("{chip}.silent");
assert!(
B::CONDITIONS
.iter()
.any(|condition| condition.name == silence),
"{chip}"
);
assert!(<B as Edge>::place(board, index).is_some(), "{chip}");
}
assert!(<B as Edge>::place(board, B::CHIPS.len()).is_none());
}
pub fn the_receiver_on_the_gps_port_reports_where_the_board_is<B>(
board: &mut B,
receiver: impl FnOnce(&mut B) -> &mut dyn Peer,
) where
B: Edge<Signals = Signals>,
{
board.sense(&Signals {
position: Position {
latitude: 515_074_000,
longitude: -1_278_000,
altitude: 11_000,
},
velocity: [10_000, 0, 0],
..AT_REST
});
let receiver = receiver(board);
let epoch: Vec<u8> = std::iter::from_fn(|| receiver.transmit(VirtualNanos(0)))
.map(|word| u8::try_from(word).expect("a byte"))
.collect();
let epoch = String::from_utf8(epoch).unwrap();
assert!(
epoch.starts_with("$GPGGA,000000.00,5130.44400,N,00007.66800,W,1,10,0.90,11.000,M,"),
"{epoch}"
);
assert!(
epoch.contains("$GPRMC,000000.00,A,5130.44400,N,00007.66800,W,19.43,0.00,"),
"ten meters a second north is 19.43 knots on course zero: {epoch}"
);
}
}