Skip to main content

std_rs/records/
throttle.rs

1use std::time::Instant;
2
3use epics_base_rs::error::{CaError, CaResult};
4use epics_base_rs::server::record::{
5    FieldDesc, ProcessAction, ProcessOutcome, Record, RecordProcessResult,
6};
7use epics_base_rs::types::{DbFieldType, EpicsValue};
8
9/// Throttle record — rate-limits value changes to prevent device damage.
10///
11/// Ported from EPICS std module `throttleRecord.c`.
12///
13/// When VAL is written, the record checks drive limits, optionally clips
14/// the value, sets WAIT=True, then writes SENT to the OUT link only after
15/// the minimum delay (DLY) has elapsed since the last output. If a new
16/// value arrives during the delay, it queues the latest value and sends
17/// it when the delay expires.
18pub struct ThrottleRecord {
19    /// Set value (VAL)
20    pub val: f64,
21    /// Previous set value (OVAL), read-only
22    pub oval: f64,
23    /// Last sent value (SENT), read-only
24    pub sent: f64,
25    /// Previous sent value (OSENT), read-only
26    pub osent: f64,
27    /// Busy flag (WAIT): 0=False, 1=True, read-only
28    pub wait: i16,
29    /// High operating range (HOPR)
30    pub hopr: f64,
31    /// Low operating range (LOPR)
32    pub lopr: f64,
33    /// High drive limit (DRVLH)
34    pub drvlh: f64,
35    /// Low drive limit (DRVLL)
36    pub drvll: f64,
37    /// Limit status: 0=Normal, 1=Low, 2=High (DRVLS), read-only
38    pub drvls: i16,
39    /// Limit clipping: 0=Off, 1=On (DRVLC)
40    pub drvlc: i16,
41    /// Code version string (VER), read-only
42    pub ver: String,
43    /// Record status: 0=Unknown, 1=Error, 2=Success (STS), read-only
44    pub sts: i16,
45    /// Display precision (PREC)
46    pub prec: i16,
47    /// Delay display precision (DPREC)
48    pub dprec: i16,
49    /// Delay between outputs in seconds (DLY)
50    pub dly: f64,
51    /// Output link (OUT)
52    pub out: String,
53    /// Output link valid: 0=ExtNC, 1=Ext, 2=Local, 3=Constant (OV), read-only
54    pub ov: i16,
55    /// Sync input link (SINP)
56    pub sinp: String,
57    /// Sync input link valid (SIV), read-only
58    pub siv: i16,
59    /// Sync trigger: 0=Idle, 1=Process (SYNC)
60    pub sync: i16,
61
62    // --- Private runtime state ---
63    /// Whether limits are active (drvlh > drvll)
64    limit_flag: bool,
65    /// Whether a delay is currently in progress
66    delay_active: bool,
67    /// When the last output was sent (for delay enforcement)
68    last_send_time: Option<Instant>,
69    /// Value queued during delay period (sent when delay expires)
70    pending_value: Option<f64>,
71}
72
73impl Default for ThrottleRecord {
74    fn default() -> Self {
75        Self {
76            val: 0.0,
77            oval: 0.0,
78            sent: 0.0,
79            osent: 0.0,
80            wait: 0,
81            hopr: 0.0,
82            lopr: 0.0,
83            drvlh: 0.0,
84            drvll: 0.0,
85            drvls: 0, // Normal
86            drvlc: 0, // Off
87            ver: "1.0.0".to_string(),
88            sts: 0, // Unknown
89            prec: 0,
90            dprec: 0,
91            dly: 0.0,
92            out: String::new(),
93            ov: 3, // Constant
94            sinp: String::new(),
95            siv: 3,  // Constant
96            sync: 0, // Idle
97            limit_flag: false,
98            delay_active: false,
99            last_send_time: None,
100            pending_value: None,
101        }
102    }
103}
104
105impl ThrottleRecord {
106    /// Check drive limits and optionally clip the value.
107    /// Returns Ok(value) if the value is acceptable, Err if rejected.
108    fn check_limits(&mut self, val: f64) -> Result<f64, ()> {
109        if !self.limit_flag {
110            self.drvls = 0; // Normal
111            return Ok(val);
112        }
113
114        if val > self.drvlh {
115            self.drvls = 2; // High
116            if self.drvlc != 0 {
117                return Ok(self.drvlh);
118            }
119            return Err(());
120        }
121
122        if val < self.drvll {
123            self.drvls = 1; // Low
124            if self.drvlc != 0 {
125                return Ok(self.drvll);
126            }
127            return Err(());
128        }
129
130        self.drvls = 0; // Normal
131        Ok(val)
132    }
133
134    /// Send the value to the output, updating SENT/OSENT and timing.
135    fn send_value(&mut self, value: f64) {
136        self.osent = self.sent;
137        self.sent = value;
138        self.last_send_time = Some(Instant::now());
139        self.sts = 2; // Success
140    }
141
142    /// Check if the delay period has elapsed since last send.
143    fn delay_elapsed(&self) -> bool {
144        if self.dly <= 0.0 {
145            return true;
146        }
147        match self.last_send_time {
148            Some(t) => t.elapsed().as_secs_f64() >= self.dly,
149            None => true, // Never sent before
150        }
151    }
152}
153
154static FIELDS: &[FieldDesc] = &[
155    FieldDesc {
156        name: "VAL",
157        dbf_type: DbFieldType::Double,
158        read_only: false,
159    },
160    FieldDesc {
161        name: "OVAL",
162        dbf_type: DbFieldType::Double,
163        read_only: true,
164    },
165    FieldDesc {
166        name: "SENT",
167        dbf_type: DbFieldType::Double,
168        read_only: true,
169    },
170    FieldDesc {
171        name: "OSENT",
172        dbf_type: DbFieldType::Double,
173        read_only: true,
174    },
175    FieldDesc {
176        name: "WAIT",
177        dbf_type: DbFieldType::Short,
178        read_only: true,
179    },
180    FieldDesc {
181        name: "HOPR",
182        dbf_type: DbFieldType::Double,
183        read_only: false,
184    },
185    FieldDesc {
186        name: "LOPR",
187        dbf_type: DbFieldType::Double,
188        read_only: false,
189    },
190    FieldDesc {
191        name: "DRVLH",
192        dbf_type: DbFieldType::Double,
193        read_only: false,
194    },
195    FieldDesc {
196        name: "DRVLL",
197        dbf_type: DbFieldType::Double,
198        read_only: false,
199    },
200    FieldDesc {
201        name: "DRVLS",
202        dbf_type: DbFieldType::Short,
203        read_only: true,
204    },
205    FieldDesc {
206        name: "DRVLC",
207        dbf_type: DbFieldType::Short,
208        read_only: false,
209    },
210    FieldDesc {
211        name: "VER",
212        dbf_type: DbFieldType::String,
213        read_only: true,
214    },
215    FieldDesc {
216        name: "STS",
217        dbf_type: DbFieldType::Short,
218        read_only: true,
219    },
220    FieldDesc {
221        name: "PREC",
222        dbf_type: DbFieldType::Short,
223        read_only: false,
224    },
225    FieldDesc {
226        name: "DPREC",
227        dbf_type: DbFieldType::Short,
228        read_only: false,
229    },
230    FieldDesc {
231        name: "DLY",
232        dbf_type: DbFieldType::Double,
233        read_only: false,
234    },
235    FieldDesc {
236        name: "OUT",
237        dbf_type: DbFieldType::String,
238        read_only: false,
239    },
240    FieldDesc {
241        name: "OV",
242        dbf_type: DbFieldType::Short,
243        read_only: true,
244    },
245    FieldDesc {
246        name: "SINP",
247        dbf_type: DbFieldType::String,
248        read_only: false,
249    },
250    FieldDesc {
251        name: "SIV",
252        dbf_type: DbFieldType::Short,
253        read_only: true,
254    },
255    FieldDesc {
256        name: "SYNC",
257        dbf_type: DbFieldType::Short,
258        read_only: false,
259    },
260];
261
262impl Record for ThrottleRecord {
263    fn record_type(&self) -> &'static str {
264        "throttle"
265    }
266
267    fn pre_process_actions(&mut self) -> Vec<ProcessAction> {
268        // When SYNC=1, read SINP into VAL BEFORE process() runs.
269        // This matches C EPICS where dbGetLink is synchronous/immediate.
270        if self.sync == 1 {
271            self.sync = 0;
272            return vec![ProcessAction::ReadDbLink {
273                link_field: "SINP",
274                target_field: "VAL",
275            }];
276        }
277        Vec::new()
278    }
279
280    fn process(&mut self) -> CaResult<ProcessOutcome> {
281        let mut actions = Vec::new();
282
283        // If we're being called after a delay to drain a pending value
284        if self.delay_active {
285            if self.delay_elapsed() {
286                // Delay expired — send pending value if any
287                self.delay_active = false;
288                self.wait = 0;
289                if let Some(pv) = self.pending_value.take() {
290                    self.send_value(pv);
291                    actions.push(ProcessAction::WriteDbLink {
292                        link_field: "OUT",
293                        value: EpicsValue::Double(self.sent),
294                    });
295                    // More values may have queued; start a new delay cycle
296                    if self.dly > 0.0 {
297                        self.delay_active = true;
298                        self.wait = 1;
299                        let delay = std::time::Duration::from_secs_f64(self.dly);
300                        actions.push(ProcessAction::ReprocessAfter(delay));
301                        return Ok(ProcessOutcome {
302                            result: RecordProcessResult::Complete,
303                            actions,
304                            device_did_compute: false,
305                        });
306                    }
307                }
308                return Ok(ProcessOutcome::complete_with(actions));
309            } else {
310                // Still waiting — queue the current value, reschedule
311                self.pending_value = Some(self.val);
312                let remaining = self.dly
313                    - self
314                        .last_send_time
315                        .map(|t| t.elapsed().as_secs_f64())
316                        .unwrap_or(0.0);
317                let delay = std::time::Duration::from_secs_f64(remaining.max(0.001));
318                actions.push(ProcessAction::ReprocessAfter(delay));
319                return Ok(ProcessOutcome {
320                    result: RecordProcessResult::Complete,
321                    actions,
322                    device_did_compute: false,
323                });
324            }
325        }
326
327        // Normal processing: check limits
328        match self.check_limits(self.val) {
329            Ok(clamped) => {
330                self.oval = self.val;
331                self.val = clamped;
332            }
333            Err(()) => {
334                self.val = self.oval;
335                self.sts = 1; // Error
336                return Ok(ProcessOutcome::complete_with(actions));
337            }
338        }
339
340        // Check if we can send immediately
341        if self.delay_elapsed() {
342            // Send immediately
343            self.send_value(self.val);
344            actions.push(ProcessAction::WriteDbLink {
345                link_field: "OUT",
346                value: EpicsValue::Double(self.sent),
347            });
348
349            // Start delay period if DLY > 0
350            if self.dly > 0.0 {
351                self.delay_active = true;
352                self.wait = 1;
353                // ReprocessAfter: current cycle's OUT write proceeds (SENT is output),
354                // then framework schedules re-process after DLY to drain pending values.
355                let delay = std::time::Duration::from_secs_f64(self.dly);
356                actions.push(ProcessAction::ReprocessAfter(delay));
357                return Ok(ProcessOutcome {
358                    result: RecordProcessResult::Complete,
359                    actions,
360                    device_did_compute: false,
361                });
362            }
363
364            self.wait = 0;
365            Ok(ProcessOutcome::complete_with(actions))
366        } else {
367            // Still in delay from previous send — queue value
368            self.pending_value = Some(self.val);
369            self.wait = 1;
370            self.delay_active = true;
371            let remaining = self.dly
372                - self
373                    .last_send_time
374                    .map(|t| t.elapsed().as_secs_f64())
375                    .unwrap_or(0.0);
376            let delay = std::time::Duration::from_secs_f64(remaining.max(0.001));
377            actions.push(ProcessAction::ReprocessAfter(delay));
378            Ok(ProcessOutcome {
379                result: RecordProcessResult::Complete,
380                actions,
381                device_did_compute: false,
382            })
383        }
384    }
385
386    fn can_device_write(&self) -> bool {
387        true
388    }
389
390    fn special(&mut self, field: &str, after: bool) -> CaResult<()> {
391        if !after {
392            return Ok(());
393        }
394        match field {
395            "DLY" => {
396                if self.dly < 0.0 {
397                    self.dly = 0.0;
398                }
399            }
400            "DRVLH" | "DRVLL" => {
401                self.limit_flag = self.drvlh > self.drvll;
402                if !self.limit_flag {
403                    self.drvls = 0; // Normal
404                }
405            }
406            _ => {}
407        }
408        Ok(())
409    }
410
411    fn get_field(&self, name: &str) -> Option<EpicsValue> {
412        match name {
413            "VAL" => Some(EpicsValue::Double(self.val)),
414            "OVAL" => Some(EpicsValue::Double(self.oval)),
415            "SENT" => Some(EpicsValue::Double(self.sent)),
416            "OSENT" => Some(EpicsValue::Double(self.osent)),
417            "WAIT" => Some(EpicsValue::Short(self.wait)),
418            "HOPR" => Some(EpicsValue::Double(self.hopr)),
419            "LOPR" => Some(EpicsValue::Double(self.lopr)),
420            "DRVLH" => Some(EpicsValue::Double(self.drvlh)),
421            "DRVLL" => Some(EpicsValue::Double(self.drvll)),
422            "DRVLS" => Some(EpicsValue::Short(self.drvls)),
423            "DRVLC" => Some(EpicsValue::Short(self.drvlc)),
424            "VER" => Some(EpicsValue::String(self.ver.clone())),
425            "STS" => Some(EpicsValue::Short(self.sts)),
426            "PREC" => Some(EpicsValue::Short(self.prec)),
427            "DPREC" => Some(EpicsValue::Short(self.dprec)),
428            "DLY" => Some(EpicsValue::Double(self.dly)),
429            "OUT" => Some(EpicsValue::String(self.out.clone())),
430            "OV" => Some(EpicsValue::Short(self.ov)),
431            "SINP" => Some(EpicsValue::String(self.sinp.clone())),
432            "SIV" => Some(EpicsValue::Short(self.siv)),
433            "SYNC" => Some(EpicsValue::Short(self.sync)),
434            _ => None,
435        }
436    }
437
438    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
439        match name {
440            "VAL" => match value {
441                EpicsValue::Double(v) => {
442                    self.val = v;
443                    Ok(())
444                }
445                _ => Err(CaError::TypeMismatch(name.into())),
446            },
447            "HOPR" => match value {
448                EpicsValue::Double(v) => {
449                    self.hopr = v;
450                    Ok(())
451                }
452                _ => Err(CaError::TypeMismatch(name.into())),
453            },
454            "LOPR" => match value {
455                EpicsValue::Double(v) => {
456                    self.lopr = v;
457                    Ok(())
458                }
459                _ => Err(CaError::TypeMismatch(name.into())),
460            },
461            "DRVLH" => match value {
462                EpicsValue::Double(v) => {
463                    self.drvlh = v;
464                    Ok(())
465                }
466                _ => Err(CaError::TypeMismatch(name.into())),
467            },
468            "DRVLL" => match value {
469                EpicsValue::Double(v) => {
470                    self.drvll = v;
471                    Ok(())
472                }
473                _ => Err(CaError::TypeMismatch(name.into())),
474            },
475            "DRVLC" => match value {
476                EpicsValue::Short(v) => {
477                    self.drvlc = v;
478                    Ok(())
479                }
480                _ => Err(CaError::TypeMismatch(name.into())),
481            },
482            "PREC" => match value {
483                EpicsValue::Short(v) => {
484                    self.prec = v;
485                    Ok(())
486                }
487                _ => Err(CaError::TypeMismatch(name.into())),
488            },
489            "DPREC" => match value {
490                EpicsValue::Short(v) => {
491                    self.dprec = v;
492                    Ok(())
493                }
494                _ => Err(CaError::TypeMismatch(name.into())),
495            },
496            "DLY" => match value {
497                EpicsValue::Double(v) => {
498                    self.dly = v;
499                    Ok(())
500                }
501                _ => Err(CaError::TypeMismatch(name.into())),
502            },
503            "OUT" => match value {
504                EpicsValue::String(v) => {
505                    self.out = v;
506                    Ok(())
507                }
508                _ => Err(CaError::TypeMismatch(name.into())),
509            },
510            "SINP" => match value {
511                EpicsValue::String(v) => {
512                    self.sinp = v;
513                    Ok(())
514                }
515                _ => Err(CaError::TypeMismatch(name.into())),
516            },
517            "SYNC" => match value {
518                EpicsValue::Short(v) => {
519                    self.sync = v;
520                    Ok(())
521                }
522                _ => Err(CaError::TypeMismatch(name.into())),
523            },
524            // Read-only fields
525            "OVAL" | "SENT" | "OSENT" | "WAIT" | "DRVLS" | "VER" | "STS" | "OV" | "SIV" => {
526                Err(CaError::ReadOnlyField(name.into()))
527            }
528            _ => Err(CaError::FieldNotFound(name.into())),
529        }
530    }
531
532    fn field_list(&self) -> &'static [FieldDesc] {
533        FIELDS
534    }
535
536    fn init_record(&mut self, pass: u8) -> CaResult<()> {
537        if pass == 1 {
538            self.limit_flag = self.drvlh > self.drvll;
539        }
540        Ok(())
541    }
542}