Skip to main content

std_rs/snl/
delay_do.rs

1//! Delay-and-Do state machine — native Rust port of `delayDo.st`.
2//!
3//! Implements a state machine that waits for a standby condition,
4//! monitors an active condition, and after the active condition
5//! clears (with a configurable delay), triggers an action.
6//!
7//! # State Machine
8//!
9//! ```text
10//!   init ──► idle ◄──────────────────────────┐
11//!            │  ▲                              │
12//!            │  └── maybeStandby ◄── disable  │
13//!            ▼                       ▲        │
14//!         standby ──► maybeWait ──► waiting ──► action
15//!            ▲            │           │
16//!            │            ▼           ▼
17//!            │          idle       active ──► waiting
18//!            └──────────────────────┘
19//! ```
20
21use std::time::{Duration, Instant};
22
23/// States of the delay-do state machine.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DelayDoState {
26    Init,
27    Disable,
28    MaybeStandby,
29    Idle,
30    Standby,
31    MaybeWait,
32    Active,
33    Waiting,
34    Action,
35}
36
37impl DelayDoState {
38    /// True where the state's `when` clauses are exhaustive, so SNL leaves it
39    /// on the same evaluation that entered it rather than waiting for a new
40    /// event: `init`'s `pvConnectCount() == pvAssignCount()`
41    /// (`delayDo.st:35`), `maybeStandby`'s standby/active/!standby triple
42    /// (`:58-74`), `maybeWait`'s active/efTest/!efTest triple (`:121-138`),
43    /// and `action`'s bare `when ()` (`:201`). A runner must keep stepping
44    /// while this holds.
45    pub fn is_transient(self) -> bool {
46        matches!(
47            self,
48            DelayDoState::Init
49                | DelayDoState::MaybeStandby
50                | DelayDoState::MaybeWait
51                | DelayDoState::Action
52        )
53    }
54
55    /// True where entering the state writes its name to `{P}{R}:state`.
56    /// `maybeStandby` and `maybeWait` are the two exceptions, and
57    /// deliberately so — "the state doesn't last long enough"
58    /// (`delayDo.st:51-52`, `:112-113`) is why their `PVPUTSTR` calls are
59    /// commented out upstream.
60    pub fn is_published(self) -> bool {
61        !matches!(self, DelayDoState::MaybeStandby | DelayDoState::MaybeWait)
62    }
63}
64
65impl std::fmt::Display for DelayDoState {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            DelayDoState::Init => write!(f, "init"),
69            DelayDoState::Disable => write!(f, "disable"),
70            DelayDoState::MaybeStandby => write!(f, "maybeStandby"),
71            DelayDoState::Idle => write!(f, "idle"),
72            DelayDoState::Standby => write!(f, "standby"),
73            DelayDoState::MaybeWait => write!(f, "maybeWait"),
74            DelayDoState::Active => write!(f, "active"),
75            DelayDoState::Waiting => write!(f, "waiting"),
76            DelayDoState::Action => write!(f, "action"),
77        }
78    }
79}
80
81/// Input signals for the delay-do state machine.
82///
83/// Each `_changed` field is a *monitor event*, not a level comparison: SNL's
84/// `sync` posts the event flag on every monitor callback for the variable,
85/// including one that carries the same value (an alarm-only update) or a fall
86/// back to zero. Pass `true` on the step a callback arrived, whatever the new
87/// value is; [`DelayDoController::step`] latches it exactly as SNL does.
88#[derive(Debug, Clone, Copy)]
89pub struct DelayDoInputs {
90    /// Enable/disable control
91    pub enable: bool,
92    /// A monitor event arrived on "enable" this step (SNL `enable_mon`)
93    pub enable_changed: bool,
94    /// Standby condition
95    pub standby: bool,
96    /// A monitor event arrived on "standby" this step (SNL `standby_mon`)
97    pub standby_changed: bool,
98    /// Active condition
99    pub active: bool,
100    /// A monitor event arrived on "active" this step (SNL `active_mon`)
101    pub active_changed: bool,
102}
103
104/// An SNL event flag (`evflag`), latched.
105///
106/// STD-12: `EvFlag(_VAR_)` (`seqPVmacros.h:131-134`) expands to
107/// `monitor _VAR_; evflag _VAR_##_mon; sync _VAR_ _VAR_##_mon`, so the flag is
108/// raised by *any* monitor event on the variable and stays raised until an
109/// `efTestAndClear` or `efClear` evaluates — across state transitions included.
110/// Reading a per-step "changed" input directly models an edge instead, which
111/// drops every event arriving in a state whose clauses do not test that flag,
112/// and keeps a stale one wherever C would have cleared it. Both directions were
113/// observable in the ported transition table, so all three flags go through
114/// this type rather than the cited one alone.
115#[derive(Debug, Clone, Copy, Default)]
116struct EventFlag(bool);
117
118impl EventFlag {
119    /// SNL `sync`: a monitor callback arrived, whatever value it carried.
120    fn sync(&mut self, monitor_event: bool) {
121        self.0 |= monitor_event;
122    }
123
124    /// SNL `efTest` — read without clearing.
125    fn test(self) -> bool {
126        self.0
127    }
128
129    /// SNL `efTestAndClear` — read and clear. Clears whether or not it was
130    /// raised, and only when the clause that names it is actually reached, so
131    /// it must stay the left operand of every `&&` that mirrors C's.
132    fn test_and_clear(&mut self) -> bool {
133        std::mem::replace(&mut self.0, false)
134    }
135
136    /// SNL `efClear`.
137    fn clear(&mut self) {
138        self.0 = false;
139    }
140}
141
142/// Output actions from the delay-do state machine.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum DelayDoAction {
145    /// No action this step.
146    None,
147    /// Process the action sequence (doSeq).
148    ProcessAction,
149}
150
151/// The delay-do controller.
152pub struct DelayDoController {
153    pub state: DelayDoState,
154    /// Delay period before triggering the action.
155    pub delay_period: Duration,
156    /// Whether to resume waiting when re-entering from standby.
157    resume_waiting: bool,
158    /// SNL `enable_mon` / `standby_mon` / `active_mon`.
159    enable_mon: EventFlag,
160    standby_mon: EventFlag,
161    active_mon: EventFlag,
162    /// `waiting`'s armed `delay(delayPeriod)` clause (`delayDo.st:189`):
163    /// when the state was entered, and the `delay_period` the clause was
164    /// evaluated with AT entry. SNL evaluates a `delay()` argument once, on
165    /// entry, so a monitor on `{P}{R}:delay` that lands mid-wait retimes the
166    /// NEXT wait and not the one in flight. `Some` exactly in `waiting`.
167    wait: Option<(Instant, Duration)>,
168}
169
170impl Default for DelayDoController {
171    fn default() -> Self {
172        Self {
173            state: DelayDoState::Init,
174            delay_period: Duration::from_secs(0),
175            resume_waiting: false,
176            enable_mon: EventFlag::default(),
177            standby_mon: EventFlag::default(),
178            active_mon: EventFlag::default(),
179            wait: None,
180        }
181    }
182}
183
184impl DelayDoController {
185    pub fn new(delay_secs: f64) -> Self {
186        Self {
187            delay_period: epics_base_rs::runtime::time::duration_from_secs(delay_secs),
188            ..Default::default()
189        }
190    }
191
192    /// Advance the state machine given current inputs.
193    /// Returns the action to take (if any) and the new state.
194    pub fn step(&mut self, inputs: &DelayDoInputs) -> (DelayDoAction, DelayDoState) {
195        let action;
196
197        // SNL `sync` delivers every monitor event to its flag before any state's
198        // clauses run, whatever state the machine is in — that is what makes the
199        // flags survive the transient states (`init`, `maybeStandby`,
200        // `maybeWait`, `action`) whose clauses name none of them.
201        self.enable_mon.sync(inputs.enable_changed);
202        self.standby_mon.sync(inputs.standby_changed);
203        self.active_mon.sync(inputs.active_changed);
204
205        match self.state {
206            DelayDoState::Init => {
207                action = DelayDoAction::None;
208                self.resume_waiting = false;
209                self.state = DelayDoState::Idle;
210            }
211
212            DelayDoState::Disable => {
213                action = DelayDoAction::None;
214                if self.enable_mon.test_and_clear() && inputs.enable {
215                    // delayDo.st:49 — only events after re-enabling may act.
216                    self.active_mon.clear();
217                    self.state = DelayDoState::MaybeStandby;
218                }
219            }
220
221            DelayDoState::MaybeStandby => {
222                action = DelayDoAction::None;
223                if inputs.standby {
224                    self.state = DelayDoState::Standby;
225                } else if inputs.active {
226                    self.state = DelayDoState::Active;
227                } else {
228                    self.state = DelayDoState::Idle;
229                }
230            }
231
232            DelayDoState::Idle => {
233                action = DelayDoAction::None;
234                if self.enable_mon.test_and_clear() && !inputs.enable {
235                    self.state = DelayDoState::Disable;
236                } else if self.standby_mon.test_and_clear() && inputs.standby {
237                    self.state = DelayDoState::Standby;
238                } else if self.active_mon.test_and_clear() && inputs.active {
239                    self.state = DelayDoState::Active;
240                }
241            }
242
243            DelayDoState::Standby => {
244                action = DelayDoAction::None;
245                // `standby` names no active_mon clause, so the flag accumulates
246                // here — that accumulation is the whole point of `maybeWait`.
247                if self.enable_mon.test_and_clear() && !inputs.enable {
248                    self.resume_waiting = false;
249                    self.state = DelayDoState::Disable;
250                } else if self.standby_mon.test_and_clear() && !inputs.standby {
251                    self.state = DelayDoState::MaybeWait;
252                }
253            }
254
255            DelayDoState::MaybeWait => {
256                action = DelayDoAction::None;
257                if inputs.active {
258                    self.state = DelayDoState::Active;
259                } else if self.active_mon.test() || self.resume_waiting {
260                    // delayDo.st:130-132 — `efTest` then an explicit `efClear`.
261                    self.active_mon.clear();
262                    self.wait = Some((Instant::now(), self.delay_period));
263                    self.state = DelayDoState::Waiting;
264                } else {
265                    self.state = DelayDoState::Idle;
266                }
267            }
268
269            DelayDoState::Active => {
270                action = DelayDoAction::None;
271                if self.enable_mon.test_and_clear() && !inputs.enable {
272                    self.state = DelayDoState::Disable;
273                } else if self.standby_mon.test_and_clear() && inputs.standby {
274                    self.state = DelayDoState::Standby;
275                } else if self.active_mon.test_and_clear() && !inputs.active {
276                    self.wait = Some((Instant::now(), self.delay_period));
277                    self.state = DelayDoState::Waiting;
278                }
279            }
280
281            DelayDoState::Waiting => {
282                if self.enable_mon.test_and_clear() && !inputs.enable {
283                    action = DelayDoAction::None;
284                    self.state = DelayDoState::Disable;
285                    self.wait = None;
286                } else if self.standby_mon.test_and_clear() && inputs.standby {
287                    action = DelayDoAction::None;
288                    self.resume_waiting = true;
289                    self.state = DelayDoState::Standby;
290                    self.wait = None;
291                } else if self.active_mon.test_and_clear() && inputs.active {
292                    action = DelayDoAction::None;
293                    self.state = DelayDoState::Active;
294                    self.wait = None;
295                } else if let Some((start, period)) = self.wait {
296                    if start.elapsed() >= period {
297                        self.resume_waiting = false;
298                        self.wait = None;
299                        self.state = DelayDoState::Action;
300                        action = DelayDoAction::None;
301                    } else {
302                        action = DelayDoAction::None;
303                    }
304                } else {
305                    action = DelayDoAction::None;
306                }
307            }
308
309            DelayDoState::Action => {
310                action = DelayDoAction::ProcessAction;
311                self.state = DelayDoState::Idle;
312            }
313        }
314
315        (action, self.state)
316    }
317
318    /// SNL `when ( delay( delayPeriod ) )` (`delayDo.st:189`) — how long a
319    /// runner must wait before re-evaluating, or `None` where no clause is
320    /// time-based and monitors are the only wake-up. `wait` is set on every
321    /// entry to `waiting` and cleared on every exit from it, so this answers
322    /// `Some` exactly in the one state that has a delay clause.
323    pub fn delay_remaining(&self) -> Option<Duration> {
324        let (start, period) = self.wait?;
325        Some(period.saturating_sub(start.elapsed()))
326    }
327}
328
329// ---------------------------------------------------------------------------
330// Runner — binds the state machine above to `delayDo.db`
331// ---------------------------------------------------------------------------
332
333use epics_base_rs::server::database::PvDatabase;
334use epics_base_rs::server::database::db_access::{DbChannel, DbMultiMonitor, alloc_origin};
335
336/// The `P` and `R` macros of
337/// `program delayDo("name=delayDo,P=xxx:,R=delayDo1")` (`delayDo.st:1`).
338#[derive(Debug, Clone)]
339pub struct DelayDoConfig {
340    pub prefix: String,
341    pub record: String,
342}
343
344impl DelayDoConfig {
345    pub fn new(prefix: &str, record: &str) -> Self {
346        Self {
347            prefix: prefix.to_string(),
348            record: record.to_string(),
349        }
350    }
351
352    /// `{P}{R}:<leaf>` — the shape every `PV(...)` line in `delayDo.st` uses
353    /// (`:22-28`), and the one `delayDo.db` declares its records with.
354    pub fn pv(&self, leaf: &str) -> String {
355        format!("{}{}:{}", self.prefix, self.record, leaf)
356    }
357}
358
359/// `DEBUG_PRINT(level, msg)` (`seqPVmacros.h:231-236`) — printed only while
360/// `{P}{R}:debug` is at or above the level, with the program name in the
361/// header as `DEBUG_PRINT_HEADER` writes it.
362fn debug_print(debug_flag: i32, level: i32, msg: &str) {
363    if debug_flag >= level {
364        println!("<delayDo.st,{level},delayDo> {msg}");
365    }
366}
367
368/// Run `delayDo.st` against the records `delayDo.db` loaded.
369///
370/// The state machine above is pure; this is the half that gives it PVs. It
371/// owns the two things SNL's runtime owns and the controller cannot: when to
372/// evaluate, and what to write.
373///
374/// **When to evaluate.** SNL leaves a state whose `when` clauses are
375/// exhaustive on the same evaluation that entered it, so a stable state is
376/// reached by stepping while [`DelayDoState::is_transient`] holds. It then
377/// blocks — on monitors alone, or on whichever of a monitor and the armed
378/// `delay(delayPeriod)` clause comes first. `{P}{R}:delay` and `{P}{R}:debug`
379/// appear in no `when` condition, only inside action blocks, so a monitor on
380/// either updates the runner and does NOT re-evaluate: an evaluation runs
381/// `efTestAndClear` on the flags its clauses name, and one triggered by a
382/// variable no clause tests would consume an event that had not been acted
383/// on.
384///
385/// One deviation, and it is loud rather than silent: a PV that `delayDo.db`
386/// never loaded leaves C in `init` forever, because `pvConnectCount()` never
387/// reaches `pvAssignCount()` (`:35`). Here it is an error return, so the
388/// caller's `eprintln!` names it.
389pub async fn run(
390    config: DelayDoConfig,
391    db: PvDatabase,
392) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
393    let origin = alloc_origin();
394
395    // `PV(..., EvFlag)` and `PV(..., Monitor)` — the five assigned inputs.
396    let pv_enable = config.pv("enable");
397    let pv_standby = config.pv("standbyCalc");
398    let pv_active = config.pv("activeCalc");
399    let pv_delay = config.pv("delay");
400    let pv_debug = config.pv("debug");
401    let monitored = vec![
402        pv_enable.clone(),
403        pv_standby.clone(),
404        pv_active.clone(),
405        pv_delay.clone(),
406        pv_debug.clone(),
407    ];
408
409    let ch_enable = DbChannel::with_origin(&db, &pv_enable, origin);
410    let ch_standby = DbChannel::with_origin(&db, &pv_standby, origin);
411    let ch_active = DbChannel::with_origin(&db, &pv_active, origin);
412    let ch_delay = DbChannel::with_origin(&db, &pv_delay, origin);
413    let ch_debug = DbChannel::with_origin(&db, &pv_debug, origin);
414
415    // `PV(..., NoMon)` — the two outputs. `doSeq` is assigned to the field
416    // `.PROC` (`:27`), so the put processes the sseq rather than setting a
417    // value on it.
418    let ch_state = DbChannel::with_origin(&db, &config.pv("state"), origin);
419    let ch_doseq = DbChannel::with_origin(&db, &format!("{}.PROC", config.pv("doSeq")), origin);
420
421    let mut monitor = DbMultiMonitor::new_filtered(&db, &monitored, origin).await;
422    if monitor.sub_count() != monitored.len() {
423        return Err(format!(
424            "delayDo: {} of the {} PVs it assigns are not in the database ({})",
425            monitored.len() - monitor.sub_count(),
426            monitored.len(),
427            monitored.join(", ")
428        )
429        .into());
430    }
431
432    let mut debug_flag = ch_debug.get_i32().await;
433    let mut ctrl = DelayDoController::new(ch_delay.get_f64().await);
434    // SNL's `monitor` delivers the variable's current value at connect, so the
435    // machine starts on levels rather than on zeroes. `enable` is a `short`
436    // and the two calcs are `int` (`:23-25`), and C truncates on the way in —
437    // a calc result of 0.5 is a false `standby`, not a true one.
438    let mut inputs = DelayDoInputs {
439        enable: ch_enable.get_i16().await != 0,
440        enable_changed: false,
441        standby: ch_standby.get_i32().await != 0,
442        standby_changed: false,
443        active: ch_active.get_i32().await != 0,
444        active_changed: false,
445    };
446
447    loop {
448        loop {
449            let previous = ctrl.state;
450            let (action, state) = ctrl.step(&inputs);
451            // One evaluation consumes the monitor events it was given; a
452            // transient state's re-evaluation is not a second arrival.
453            inputs.enable_changed = false;
454            inputs.standby_changed = false;
455            inputs.active_changed = false;
456
457            if action == DelayDoAction::ProcessAction {
458                // `PVPUT(doSeq, 1)` before `PVPUTSTR(seqState, "idle")`
459                // (`:204-206`) — the sseq runs, then the state PV catches up.
460                let _ = ch_doseq.put_i32_process(1).await;
461            }
462            if state != previous {
463                debug_print(debug_flag, 3, &format!("{previous} -> {state}"));
464                if state.is_published() {
465                    let _ = ch_state.put_string_process(&state.to_string()).await;
466                }
467            }
468            if !state.is_transient() {
469                break;
470            }
471        }
472
473        loop {
474            let woken = match ctrl.delay_remaining() {
475                Some(remaining) => tokio::time::timeout(remaining, monitor.wait_change())
476                    .await
477                    .ok(),
478                None => Some(monitor.wait_change().await),
479            };
480            let Some((pv, value)) = woken else {
481                // The armed `delay(delayPeriod)` clause came first.
482                break;
483            };
484            if pv == pv_enable {
485                inputs.enable = (value as i16) != 0;
486                inputs.enable_changed = true;
487                break;
488            } else if pv == pv_standby {
489                inputs.standby = (value as i32) != 0;
490                inputs.standby_changed = true;
491                break;
492            } else if pv == pv_active {
493                inputs.active = (value as i32) != 0;
494                inputs.active_changed = true;
495                break;
496            } else if pv == pv_delay {
497                ctrl.delay_period = epics_base_rs::runtime::time::duration_from_secs(value);
498            } else if pv == pv_debug {
499                debug_flag = value as i32;
500            }
501        }
502    }
503}