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}