use std::time::Instant;
use epics_base_rs::error::{CaError, CaResult};
use epics_base_rs::server::record::{
FieldDesc, LinkType, ProcessAction, ProcessOutcome, Record, link_field_type,
};
use epics_base_rs::types::{DbFieldType, EpicsValue};
pub struct ThrottleRecord {
pub val: f64,
pub oval: f64,
pub sent: f64,
pub osent: f64,
pub wait: i16,
pub hopr: f64,
pub lopr: f64,
pub drvlh: f64,
pub drvll: f64,
pub drvls: i16,
pub drvlc: i16,
pub ver: String,
pub sts: i16,
pub prec: i16,
pub dprec: i16,
pub dly: f64,
pub out: String,
pub ov: i16,
pub sinp: String,
pub siv: i16,
pub sync: i16,
limit_flag: bool,
delay_active: bool,
last_send_time: Option<Instant>,
pending_value: Option<f64>,
out_written: bool,
}
impl Default for ThrottleRecord {
fn default() -> Self {
Self {
val: 0.0,
oval: 0.0,
sent: 0.0,
osent: 0.0,
wait: 0,
hopr: 0.0,
lopr: 0.0,
drvlh: 0.0,
drvll: 0.0,
drvls: 0, drvlc: 0, ver: "0-2-1".to_string(),
sts: 0, prec: 0,
dprec: 0,
dly: 0.0,
out: String::new(),
ov: 3, sinp: String::new(),
siv: 3, sync: 0, limit_flag: false,
delay_active: false,
last_send_time: None,
pending_value: None,
out_written: false,
}
}
}
const MAX_DLY: f64 = 86_400.0;
fn validate_dly(v: f64) -> CaResult<()> {
if !v.is_finite() {
return Err(CaError::InvalidValue(format!(
"throttle DLY must be finite, got {v}"
)));
}
if v > MAX_DLY {
return Err(CaError::InvalidValue(format!(
"throttle DLY must not exceed {MAX_DLY} seconds, got {v}"
)));
}
Ok(())
}
impl ThrottleRecord {
fn check_limits(&mut self, val: f64) -> Result<f64, ()> {
if !self.limit_flag {
self.drvls = 0; return Ok(val);
}
if val < self.drvll {
self.drvls = 1; if self.drvlc == 1 {
return Ok(self.drvll);
}
return Err(());
}
if val > self.drvlh {
self.drvls = 2; if self.drvlc == 1 {
return Ok(self.drvlh);
}
return Err(());
}
self.drvls = 0; Ok(val)
}
fn send_value(&mut self, value: f64) -> bool {
if link_field_type(&self.out) == LinkType::Constant
|| link_field_type(&self.out) == LinkType::Empty
{
self.sts = 1; self.out_written = false;
return false;
}
self.osent = self.sent;
self.sent = value;
self.last_send_time = Some(Instant::now());
self.sts = 2; self.out_written = true;
true
}
fn delay_elapsed(&self) -> bool {
if self.dly <= 0.0 {
return true;
}
match self.last_send_time {
Some(t) => t.elapsed().as_secs_f64() >= self.dly,
None => true, }
}
}
static FIELDS: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "OVAL",
dbf_type: DbFieldType::Double,
read_only: true,
},
FieldDesc {
name: "SENT",
dbf_type: DbFieldType::Double,
read_only: true,
},
FieldDesc {
name: "OSENT",
dbf_type: DbFieldType::Double,
read_only: true,
},
FieldDesc {
name: "WAIT",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "HOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "LOPR",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "DRVLH",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "DRVLL",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "DRVLS",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "DRVLC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "VER",
dbf_type: DbFieldType::String,
read_only: true,
},
FieldDesc {
name: "STS",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "PREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "DPREC",
dbf_type: DbFieldType::Short,
read_only: false,
},
FieldDesc {
name: "DLY",
dbf_type: DbFieldType::Double,
read_only: false,
},
FieldDesc {
name: "OUT",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "OV",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "SINP",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "SIV",
dbf_type: DbFieldType::Short,
read_only: true,
},
FieldDesc {
name: "SYNC",
dbf_type: DbFieldType::Short,
read_only: false,
},
];
impl Record for ThrottleRecord {
fn record_type(&self) -> &'static str {
"throttle"
}
fn pre_process_actions(&mut self) -> Vec<ProcessAction> {
if self.sync == 1 {
self.sync = 0;
return vec![ProcessAction::ReadDbLink {
link_field: "SINP",
target_field: "VAL",
}];
}
Vec::new()
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
let mut actions = Vec::new();
self.out_written = false;
if self.delay_active && self.delay_elapsed() {
self.delay_active = false;
self.wait = 0;
match self.pending_value.take() {
Some(pv) => {
if self.send_value(pv) {
actions.push(ProcessAction::WriteDbLink {
link_field: "OUT",
value: EpicsValue::Double(self.sent),
});
}
if self.dly > 0.0 {
self.delay_active = true;
self.wait = 1;
let delay = std::time::Duration::from_secs_f64(self.dly);
actions.push(ProcessAction::ReprocessAfter(delay));
}
return Ok(ProcessOutcome::complete_with(actions));
}
None => {
return Ok(ProcessOutcome::complete_with(actions));
}
}
}
let proc_flag = match self.check_limits(self.val) {
Ok(clamped) => {
self.val = clamped;
true
}
Err(()) => {
self.val = self.oval;
false
}
};
if !proc_flag {
return Ok(ProcessOutcome::complete_with(actions));
}
self.oval = self.val;
if self.delay_active {
self.pending_value = Some(self.val);
self.wait = 1;
let remaining = self.dly
- self
.last_send_time
.map(|t| t.elapsed().as_secs_f64())
.unwrap_or(0.0);
let delay = std::time::Duration::from_secs_f64(remaining.max(0.001));
actions.push(ProcessAction::ReprocessAfter(delay));
return Ok(ProcessOutcome::complete_with(actions));
}
if self.send_value(self.val) {
actions.push(ProcessAction::WriteDbLink {
link_field: "OUT",
value: EpicsValue::Double(self.sent),
});
}
if self.dly > 0.0 {
self.delay_active = true;
self.wait = 1;
let delay = std::time::Duration::from_secs_f64(self.dly);
actions.push(ProcessAction::ReprocessAfter(delay));
return Ok(ProcessOutcome::complete_with(actions));
}
self.delay_active = false;
self.wait = 0;
Ok(ProcessOutcome::complete_with(actions))
}
fn can_device_write(&self) -> bool {
true
}
fn special(&mut self, field: &str, after: bool) -> CaResult<()> {
if !after {
return Ok(());
}
match field {
"DLY" => {
if self.dly < 0.0 {
self.dly = 0.0;
} else if validate_dly(self.dly).is_err() {
self.dly = MAX_DLY;
}
}
"DRVLH" | "DRVLL" => {
self.limit_flag = self.drvlh > self.drvll;
if !self.limit_flag {
self.drvls = 0; } else if self.val < self.drvll {
self.drvls = 1; } else if self.val > self.drvlh {
self.drvls = 2; } else {
self.drvls = 0; }
}
_ => {}
}
Ok(())
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => Some(EpicsValue::Double(self.val)),
"OVAL" => Some(EpicsValue::Double(self.oval)),
"SENT" => Some(EpicsValue::Double(self.sent)),
"OSENT" => Some(EpicsValue::Double(self.osent)),
"WAIT" => Some(EpicsValue::Short(self.wait)),
"HOPR" => Some(EpicsValue::Double(self.hopr)),
"LOPR" => Some(EpicsValue::Double(self.lopr)),
"DRVLH" => Some(EpicsValue::Double(self.drvlh)),
"DRVLL" => Some(EpicsValue::Double(self.drvll)),
"DRVLS" => Some(EpicsValue::Short(self.drvls)),
"DRVLC" => Some(EpicsValue::Short(self.drvlc)),
"VER" => Some(EpicsValue::String(self.ver.clone())),
"STS" => Some(EpicsValue::Short(self.sts)),
"PREC" => Some(EpicsValue::Short(self.prec)),
"DPREC" => Some(EpicsValue::Short(self.dprec)),
"DLY" => Some(EpicsValue::Double(self.dly)),
"OUT" => Some(EpicsValue::String(self.out.clone())),
"OV" => Some(EpicsValue::Short(self.ov)),
"SINP" => Some(EpicsValue::String(self.sinp.clone())),
"SIV" => Some(EpicsValue::Short(self.siv)),
"SYNC" => Some(EpicsValue::Short(self.sync)),
_ => None,
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"VAL" => match value {
EpicsValue::Double(v) => {
self.val = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"HOPR" => match value {
EpicsValue::Double(v) => {
self.hopr = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"LOPR" => match value {
EpicsValue::Double(v) => {
self.lopr = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"DRVLH" => match value {
EpicsValue::Double(v) => {
self.drvlh = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"DRVLL" => match value {
EpicsValue::Double(v) => {
self.drvll = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"DRVLC" => match value {
EpicsValue::Short(v) => {
self.drvlc = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"PREC" => match value {
EpicsValue::Short(v) => {
self.prec = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"DPREC" => match value {
EpicsValue::Short(v) => {
self.dprec = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"DLY" => match value {
EpicsValue::Double(v) => {
validate_dly(v)?;
self.dly = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"OUT" => match value {
EpicsValue::String(v) => {
self.out = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"SINP" => match value {
EpicsValue::String(v) => {
self.sinp = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"SYNC" => match value {
EpicsValue::Short(v) => {
self.sync = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"OVAL" | "SENT" | "OSENT" | "WAIT" | "DRVLS" | "VER" | "STS" | "OV" | "SIV" => {
Err(CaError::ReadOnlyField(name.into()))
}
_ => Err(CaError::FieldNotFound(name.into())),
}
}
fn field_list(&self) -> &'static [FieldDesc] {
FIELDS
}
fn should_fire_forward_link(&self) -> bool {
self.out_written
}
fn init_record(&mut self, pass: u8) -> CaResult<()> {
if pass == 1 {
self.sts = 0; self.val = 0.0;
self.limit_flag = self.drvlh > self.drvll;
self.delay_active = false;
self.last_send_time = None;
self.pending_value = None;
self.out_written = false;
}
Ok(())
}
}