pub type HilResult<T> = Result<T, HilError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HilError {
ChannelOutOfRange,
Timeout,
NotConnected,
Overflow,
}
#[derive(Debug, Clone, Copy)]
pub struct AnalogIn {
pub channel: u8,
pub range_v: f32,
pub resolution_bits: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct AnalogOut {
pub channel: u8,
pub range_v: f32,
pub resolution_bits: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigitalDir {
Input,
Output,
}
#[derive(Debug, Clone, Copy)]
pub struct DigitalChannel {
pub channel: u8,
pub dir: DigitalDir,
pub active_high: bool,
}
pub trait HilDevice {
fn read_analog(&self, channel: u8) -> HilResult<f32>;
fn write_analog(&mut self, channel: u8, value: f32) -> HilResult<()>;
fn read_digital(&self, channel: u8) -> HilResult<bool>;
fn write_digital(&mut self, channel: u8, state: bool) -> HilResult<()>;
fn step_time(&mut self, dt_us: u32);
fn time_us(&self) -> u64;
fn reset(&mut self);
}
#[derive(Debug, Clone, Copy)]
pub enum Waveform {
Constant(f32),
Step {
value_before: f32,
value_after: f32,
t_start_us: u64,
},
Ramp {
start: f32,
end: f32,
t_start_us: u64,
duration_us: u64,
},
Sine {
amplitude: f32,
frequency_hz: f32,
phase_rad: f32,
offset: f32,
},
}
impl Waveform {
pub fn evaluate(&self, t_us: u64) -> f32 {
match *self {
Self::Constant(v) => v,
Self::Step {
value_before,
value_after,
t_start_us,
} => {
if t_us >= t_start_us {
value_after
} else {
value_before
}
}
Self::Ramp {
start,
end,
t_start_us,
duration_us,
} => {
if t_us <= t_start_us {
start
} else if t_us >= t_start_us + duration_us {
end
} else {
let frac = (t_us - t_start_us) as f32 / duration_us as f32;
start + frac * (end - start)
}
}
Self::Sine {
amplitude,
frequency_hz,
phase_rad,
offset,
} => {
let t_s = t_us as f32 * 1e-6;
let arg = 2.0 * core::f32::consts::PI * frequency_hz * t_s + phase_rad;
offset + amplitude * arg.sin()
}
}
}
}
pub struct SimHil<const N_AI: usize, const N_AO: usize, const N_DIO: usize> {
analog_in: [f32; N_AI],
analog_out: [f32; N_AO],
digital_io: [bool; N_DIO],
time_us: u64,
waveforms: [Option<Waveform>; N_AI],
}
impl<const N_AI: usize, const N_AO: usize, const N_DIO: usize> SimHil<N_AI, N_AO, N_DIO> {
pub fn new() -> Self {
Self {
analog_in: [0.0; N_AI],
analog_out: [0.0; N_AO],
digital_io: [false; N_DIO],
time_us: 0,
waveforms: core::array::from_fn(|_| None),
}
}
pub fn set_waveform(&mut self, channel: usize, waveform: Waveform) {
if channel < N_AI {
self.waveforms[channel] = Some(waveform);
}
}
pub fn inject_analog(&mut self, channel: usize, value: f32) {
if channel < N_AI {
self.analog_in[channel] = value;
}
}
}
impl<const N_AI: usize, const N_AO: usize, const N_DIO: usize> Default
for SimHil<N_AI, N_AO, N_DIO>
{
fn default() -> Self {
Self::new()
}
}
impl<const N_AI: usize, const N_AO: usize, const N_DIO: usize> HilDevice
for SimHil<N_AI, N_AO, N_DIO>
{
fn read_analog(&self, channel: u8) -> HilResult<f32> {
let ch = channel as usize;
if ch >= N_AI {
return Err(HilError::ChannelOutOfRange);
}
if let Some(wf) = self.waveforms[ch] {
Ok(wf.evaluate(self.time_us))
} else {
Ok(self.analog_in[ch])
}
}
fn write_analog(&mut self, channel: u8, value: f32) -> HilResult<()> {
let ch = channel as usize;
if ch >= N_AO {
return Err(HilError::ChannelOutOfRange);
}
self.analog_out[ch] = value;
Ok(())
}
fn read_digital(&self, channel: u8) -> HilResult<bool> {
let ch = channel as usize;
if ch >= N_DIO {
return Err(HilError::ChannelOutOfRange);
}
Ok(self.digital_io[ch])
}
fn write_digital(&mut self, channel: u8, state: bool) -> HilResult<()> {
let ch = channel as usize;
if ch >= N_DIO {
return Err(HilError::ChannelOutOfRange);
}
self.digital_io[ch] = state;
Ok(())
}
fn step_time(&mut self, dt_us: u32) {
self.time_us += dt_us as u64;
}
fn time_us(&self) -> u64 {
self.time_us
}
fn reset(&mut self) {
self.analog_in = [0.0; N_AI];
self.analog_out = [0.0; N_AO];
self.digital_io = [false; N_DIO];
self.time_us = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn waveform_step() {
let wf = Waveform::Step {
value_before: 0.0,
value_after: 5.0,
t_start_us: 1000,
};
assert_eq!(wf.evaluate(0), 0.0);
assert_eq!(wf.evaluate(999), 0.0);
assert_eq!(wf.evaluate(1000), 5.0);
assert_eq!(wf.evaluate(2000), 5.0);
}
#[test]
fn waveform_ramp() {
let wf = Waveform::Ramp {
start: 0.0,
end: 10.0,
t_start_us: 0,
duration_us: 1000,
};
assert_eq!(wf.evaluate(0), 0.0);
assert!((wf.evaluate(500) - 5.0).abs() < 0.01);
assert_eq!(wf.evaluate(1000), 10.0);
assert_eq!(wf.evaluate(2000), 10.0);
}
#[test]
fn waveform_sine() {
let wf = Waveform::Sine {
amplitude: 1.0,
frequency_hz: 1000.0,
phase_rad: 0.0,
offset: 0.0,
};
assert!((wf.evaluate(0)).abs() < 0.01);
let quarter_us = 250u64; assert!(
(wf.evaluate(quarter_us) - 1.0).abs() < 0.01,
"val={:.4}",
wf.evaluate(quarter_us)
);
}
#[test]
fn sim_hil_analog_io() {
let mut hil = SimHil::<4, 4, 8>::new();
hil.write_analog(0, 3.3).unwrap();
assert_eq!(hil.analog_out[0], 3.3);
hil.inject_analog(1, 2.5);
assert_eq!(hil.read_analog(1).unwrap(), 2.5);
}
#[test]
fn sim_hil_digital_io() {
let mut hil = SimHil::<2, 2, 4>::new();
hil.write_digital(2, true).unwrap();
assert!(hil.read_digital(2).unwrap());
hil.write_digital(2, false).unwrap();
assert!(!hil.read_digital(2).unwrap());
}
#[test]
fn sim_hil_waveform() {
let mut hil = SimHil::<2, 2, 2>::new();
hil.set_waveform(0, Waveform::Constant(5.0));
assert_eq!(hil.read_analog(0).unwrap(), 5.0);
hil.step_time(1000);
assert_eq!(hil.time_us(), 1000);
}
#[test]
fn sim_hil_channel_out_of_range() {
let hil = SimHil::<2, 2, 2>::new();
assert_eq!(hil.read_analog(5), Err(HilError::ChannelOutOfRange));
}
#[test]
fn sim_hil_reset() {
let mut hil = SimHil::<2, 2, 2>::new();
hil.write_analog(0, 3.3).unwrap();
hil.step_time(5000);
hil.reset();
assert_eq!(hil.time_us(), 0);
assert_eq!(hil.analog_out[0], 0.0);
}
}