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}