Skip to main content

std_rs/device_support/
epid_soft_callback.rs

1use epics_base_rs::error::CaResult;
2use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport};
3use epics_base_rs::server::record::{LinkType, ProcessAction, Record, link_field_type};
4use epics_base_rs::types::EpicsValue;
5
6use crate::records::epid::EpidRecord;
7
8/// Async Soft Channel device support for the epid record.
9///
10/// Same PID algorithm as `EpidSoftDeviceSupport`, but with an
11/// asynchronous readback trigger via the TRIG link.
12///
13/// Processing flow:
14/// 1. First pass (triggered=false): Write TVAL to TRIG link via
15///    ProcessAction::WriteDbLink, and request a re-process via
16///    ProcessAction::ReprocessAfter(1ms). The TRIG write triggers
17///    the readback hardware to update the INP PV.
18/// 2. Second pass (triggered=true): INP has been updated by the
19///    triggered readback. Run PID with the fresh CVAL.
20///
21/// Ported from `devEpidSoftCallback.c`.
22pub struct EpidSoftCallbackDeviceSupport {
23    /// Whether the trigger has been sent and we're on the second pass.
24    triggered: bool,
25}
26
27impl Default for EpidSoftCallbackDeviceSupport {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl EpidSoftCallbackDeviceSupport {
34    pub fn new() -> Self {
35        Self { triggered: false }
36    }
37}
38
39impl DeviceSupport for EpidSoftCallbackDeviceSupport {
40    fn dtyp(&self) -> &str {
41        "Epid Async Soft"
42    }
43
44    fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
45        let epid = record
46            .as_any_mut()
47            .and_then(|a| a.downcast_mut::<EpidRecord>())
48            .expect("EpidSoftCallbackDeviceSupport requires an EpidRecord");
49
50        if !self.triggered {
51            // C `devEpidSoftCallback.c:116-147` — execute the
52            // readback-trigger link, then branch on its TYPE:
53            //
54            //   if (ptriglink->type != CA_LINK) {
55            //       status = dbPutLink(ptriglink,DBR_DOUBLE,&pepid->tval,1);
56            //       ...                       // fall through to PID
57            //   } else {
58            //       status = dbCaPutLinkCallback(ptriglink,...);
59            //       pepid->pact = TRUE;       // wait for the callback
60            //       return(0);
61            //   }
62            //
63            // A DB (or CONSTANT/empty) TRIG link is written synchronously
64            // and the record falls straight through to the PID compute
65            // in the SAME process pass — there is no `pact`. A CA TRIG
66            // link cannot be waited on synchronously, so the trigger is
67            // fired, the record is re-processed after the callback, and
68            // the PID compute is deferred to that second pass.
69            match link_field_type(&epid.trig) {
70                LinkType::Ca => {
71                    // CA TRIG link: fire the trigger and arrange a
72                    // re-process — the PID compute happens on the
73                    // second pass. C `devEpidSoftCallback.c:143-145`
74                    // sets `pepid->pact = TRUE` and `return(0)`, and
75                    // C `epidRecord.c:207` then returns BEFORE the
76                    // process tail (`checkAlarms` / `monitor` /
77                    // `recGblFwdLink`) — the trigger pass runs NONE of
78                    // the tail.
79                    //
80                    // The Rust framework runs `read()` before
81                    // `process()`, so `read()` cannot itself
82                    // short-circuit the cycle. Mark the record as a
83                    // CA-trigger pass; `EpidRecord::process` consumes
84                    // the flag and returns `ProcessOutcome::
85                    // async_pending()`, so the framework skips the
86                    // alarm/timestamp/snapshot/OUT/FLNK tail this
87                    // cycle. The trigger pass performs NO PID compute,
88                    // so `did_compute` is `false` (the actions below
89                    // are still merged and executed by the framework).
90                    let actions = vec![
91                        ProcessAction::WriteDbLink {
92                            link_field: "TRIG",
93                            value: EpicsValue::Double(epid.tval),
94                        },
95                        ProcessAction::ReprocessAfter(std::time::Duration::from_millis(1)),
96                    ];
97                    epid.set_ca_trig_pending();
98                    self.triggered = true;
99                    return Ok(DeviceReadOutcome {
100                        actions,
101                        did_compute: false,
102                    });
103                }
104                LinkType::Db => {
105                    // DB TRIG link: the trigger write is C's
106                    // `dbPutLink(ptriglink, ...)` (`devEpidSoftCallback
107                    // .c:121-127`) — a *synchronous* write that
108                    // processes the triggered source, and it must land
109                    // BEFORE C's `dbGetLink(&pepid->inp, ...)` reads
110                    // CVAL (`devEpidSoftCallback.c:151`).
111                    //
112                    // `read()` runs after the framework's `INP -> CVAL`
113                    // input-link fetch, so emitting the TRIG write here
114                    // would land a cycle late. Instead `EpidRecord::
115                    // pre_input_link_actions` emits it as a pre-input
116                    // action — the framework executes that strictly
117                    // before the input-link fetch. By the time this
118                    // `read()` runs, the trigger has already fired and
119                    // CVAL already holds the freshly-triggered value;
120                    // just run the PID, in this same pass (no `pact`).
121                    super::epid_soft::EpidSoftDeviceSupport::do_pid(epid);
122                    return Ok(DeviceReadOutcome::computed());
123                }
124                // CONSTANT / empty / Other TRIG link — `type != CA_LINK`,
125                // so still synchronous: nothing to trigger, run PID now.
126                LinkType::Constant | LinkType::Empty | LinkType::Other => {}
127            }
128        }
129
130        // Second pass (CA path completed), or a non-CA link with no
131        // trigger to fire: execute PID.
132        self.triggered = false;
133        super::epid_soft::EpidSoftDeviceSupport::do_pid(epid);
134        Ok(DeviceReadOutcome::computed())
135    }
136
137    fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
138        Ok(())
139    }
140}