use std::f64::consts::PI;
use igs_iec_tase2_model::generated::iso802::clause_6_2_1::DataReal;
use igs_iec_tase2_model::mms::MmsValue;
use igs_iso9506_mms_codec::generated::types::Data;
use crate::config::{PointKind, Simulation, SimulationKind};
const SINE_PERIODS: u64 = 60;
#[derive(Debug, Clone)]
pub struct Stepper {
simulation: Simulation,
count: u64,
ramp: f64,
flag: bool,
}
impl Stepper {
pub fn new(simulation: Simulation) -> Self {
let ramp = simulation.min;
Self {
simulation,
count: 0,
ramp,
flag: false,
}
}
pub fn next(&mut self) -> Data {
self.count = self.count.saturating_add(1);
match self.simulation.kind {
SimulationKind::Toggle => {
self.flag = !self.flag;
Data::Boolean(self.flag)
}
SimulationKind::Ramp => {
let next = self.ramp + self.simulation.step;
self.ramp = if next > self.simulation.max {
self.simulation.min
} else {
next
};
self.value_of(self.ramp)
}
SimulationKind::Sine => {
let phase = 2.0 * PI * (self.count % SINE_PERIODS) as f64 / SINE_PERIODS as f64;
let level = self.simulation.min
+ (self.simulation.max - self.simulation.min) * (1.0 + phase.sin()) / 2.0;
self.value_of(level)
}
}
}
fn value_of(&self, level: f64) -> Data {
match self.simulation.point {
PointKind::Integer => Data::Integer(level.round() as i128),
PointKind::Unsigned => Data::Unsigned(level.round().max(0.0) as u128),
PointKind::Float => DataReal(level).to_mms(),
PointKind::Boolean => Data::Boolean(level > 0.0),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use std::time::Duration;
use igs_iec_tase2_link::bilateral::{ObjectName, Scope};
use super::*;
fn simulation(
kind: SimulationKind,
point: PointKind,
min: f64,
max: f64,
step: f64,
) -> Simulation {
Simulation {
name: ObjectName {
scope: Scope::Domain("ICC1".to_string()),
name: "Point".to_string(),
},
kind,
period: Duration::from_secs(1),
min,
max,
step,
point,
}
}
#[test]
fn a_ramp_climbs_by_its_step_and_starts_again_after_its_maximum() {
let mut stepper = Stepper::new(simulation(
SimulationKind::Ramp,
PointKind::Integer,
0.0,
3.0,
1.0,
));
let values: Vec<Data> = (0..5).map(|_| stepper.next()).collect();
assert_eq!(
values,
vec![
Data::Integer(1),
Data::Integer(2),
Data::Integer(3),
Data::Integer(0),
Data::Integer(1),
]
);
}
#[test]
fn a_toggle_changes_at_each_period() {
let mut stepper = Stepper::new(simulation(
SimulationKind::Toggle,
PointKind::Boolean,
0.0,
0.0,
0.0,
));
assert_eq!(stepper.next(), Data::Boolean(true));
assert_eq!(stepper.next(), Data::Boolean(false));
}
#[test]
fn a_sine_stays_between_its_bounds_and_is_a_real() {
let mut stepper = Stepper::new(simulation(
SimulationKind::Sine,
PointKind::Float,
-1.0,
1.0,
0.0,
));
for _ in 0..SINE_PERIODS {
let Data::FloatingPoint(float) = stepper.next() else {
panic!("a real is expected");
};
let value = DataReal::from_mms(&Data::FloatingPoint(float)).unwrap().0;
assert!((-1.0..=1.0).contains(&value), "{value}");
}
}
#[test]
fn an_unsigned_point_is_never_negative() {
let mut stepper = Stepper::new(simulation(
SimulationKind::Sine,
PointKind::Unsigned,
-5.0,
5.0,
0.0,
));
for _ in 0..SINE_PERIODS {
let Data::Unsigned(_) = stepper.next() else {
panic!("an unsigned is expected");
};
}
}
}