Skip to main content

std_rs/device_support/
epid_soft.rs

1use std::time::Instant;
2
3use epics_base_rs::error::CaResult;
4use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport};
5use epics_base_rs::server::record::Record;
6
7use crate::records::epid::EpidRecord;
8
9/// Soft Channel device support for the epid record.
10///
11/// Implements the PID and MaxMin feedback algorithms.
12/// Ported from `devEpidSoft.c`.
13///
14/// PID algorithm:
15/// ```text
16/// E(n) = Setpoint - ControlledValue
17/// P(n) = KP * E(n)
18/// I(n) = I(n-1) + KP * KI * E(n) * dT  (with anti-windup)
19/// D(n) = KP * KD * (E(n) - E(n-1)) / dT
20/// Output = P + I + D
21/// ```
22pub struct EpidSoftDeviceSupport;
23
24impl Default for EpidSoftDeviceSupport {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl EpidSoftDeviceSupport {
31    pub fn new() -> Self {
32        Self
33    }
34
35    /// Execute the PID algorithm on the epid record.
36    /// This is the core computation, equivalent to `do_pid()` in devEpidSoft.c.
37    pub fn do_pid(epid: &mut EpidRecord) {
38        // OUTL-write ownership: clear at entry so every early return
39        // below (CONSTANT INP, sub-MDT) leaves the framework OUTL write
40        // suppressed. C performs the OUTL `dbPutLink` only at the very
41        // end of `do_pid` and only when `fbon` (`devEpidSoft.c:220`); a
42        // sub-MDT (`:125`) or CONSTANT-INP (`:110-112`) return happens
43        // before that line. `epid.set_outl_write(...)` at the success
44        // path re-enables it per `fbon`.
45        epid.set_outl_write(false);
46        // C `devEpidSoft.c:110-112`:
47        //   if (pepid->inp.type == CONSTANT) { /* nothing to control */
48        //       if (recGblSetSevr(pepid,SOFT_ALARM,INVALID_ALARM)) return(0);
49        //   }
50        // A CONSTANT `INP` link is a literal value, not a PV to read —
51        // there is nothing to feed back on, so PID is skipped and the
52        // record is flagged SOFT/INVALID. The framework `check_alarms`
53        // hook raises the severity from this flag.
54        if epics_base_rs::server::record::link_field_type(&epid.inp)
55            == epics_base_rs::server::record::LinkType::Constant
56        {
57            epid.inp_constant = true;
58            return;
59        }
60        epid.inp_constant = false;
61
62        // Previous controlled value: CVLP, maintained by
63        // `EpidRecord::update_monitors()` (`epid.rs` — `self.cvlp = self.cval`).
64        // The MaxMin sign-detection in `fmod==1` needs the value from the
65        // *previous* cycle, not the current CVAL. Reading `epid.cval` for
66        // both would make `e = cval - pcval` identically 0.0. This matches
67        // the in-tree fast path `epid_fast.rs` which uses `pcval = self.cval`
68        // captured before `self.cval = cval`.
69        let pcval = epid.cvlp;
70        let setp = epid.val;
71        let cval = epid.cval;
72
73        // Compute delta time
74        let ctp = epid.ct;
75        let ct = Instant::now();
76        let dt = ct.duration_since(ctp).as_secs_f64();
77
78        // Skip if delta time is less than minimum
79        if dt < epid.mdt {
80            return;
81        }
82
83        let kp = epid.kp;
84        let ki = epid.ki;
85        let kd = epid.kd;
86        let ep = epid.err;
87        let mut oval = epid.oval;
88        let mut p = epid.p;
89        let mut i = epid.i;
90        let mut d = epid.d;
91        // C `devEpidSoft.c:98` declares `double e = 0.;` at function scope.
92        // `devEpidSoft.c:208` writes `pepid->err = e;` *unconditionally*,
93        // regardless of feedback mode. So ERR must always be assigned:
94        //   - PID mode      → e = setp - cval (devEpidSoft.c:139)
95        //   - MaxMin, FB on after the OFF->ON edge → e = cval - pcval
96        //     (devEpidSoft.c:186)
97        //   - MaxMin bumpless edge / MaxMin FB off / invalid mode → e = 0.0
98        //     (the initial value from devEpidSoft.c:98 is never overwritten)
99        let mut e = 0.0_f64;
100
101        match epid.fmod {
102            0 => {
103                // PID mode
104                e = setp - cval;
105                let de = e - ep;
106                p = kp * e;
107
108                // Integral term with sanity checks
109                let di = kp * ki * e * dt;
110                if epid.fbon != 0 {
111                    if epid.fbop == 0 {
112                        // Feedback just transitioned OFF -> ON (bumpless
113                        // turn-on). C `devEpidSoft.c:153-158`:
114                        //   if (pepid->outl.type != CONSTANT) {
115                        //       if (dbGetLink(&pepid->outl,DBR_DOUBLE,&i,..))
116                        //           recGblSetSevr(...,LINK_ALARM,INVALID);
117                        //   }
118                        // — the integral term is seeded from the OUTL
119                        // output link's *actual current value* so the loop
120                        // turns on without a bump. This is C's line, and it
121                        // is reached only AFTER the `dt < mdt` gate above:
122                        // consume the staged readback here, so a gated cycle
123                        // never touches `.I`. `outl_seed` is `None` for a
124                        // CONSTANT/empty OUTL (nothing was read), and then `i`
125                        // keeps its prior value — C's `outl.type != CONSTANT`
126                        // guard. (`i` was loaded from `epid.i` above.)
127                        if let Some(seed) = epid.outl_seed {
128                            i = seed;
129                        }
130                    } else {
131                        // Anti-windup: only accumulate integral if output not saturated,
132                        // or if the integral change would move away from saturation.
133                        if (oval > epid.drvl && oval < epid.drvh)
134                            || (oval >= epid.drvh && di < 0.0)
135                            || (oval <= epid.drvl && di > 0.0)
136                        {
137                            i += di;
138                            if i < epid.drvl {
139                                i = epid.drvl;
140                            }
141                            if i > epid.drvh {
142                                i = epid.drvh;
143                            }
144                        }
145                    }
146                }
147                // If KI is zero, zero the integral term
148                if ki == 0.0 {
149                    i = 0.0;
150                }
151                // Derivative term
152                d = if dt > 0.0 { kp * kd * (de / dt) } else { 0.0 };
153                oval = p + i + d;
154            }
155            1 => {
156                // MaxMin mode
157                if epid.fbon != 0 {
158                    if epid.fbop == 0 {
159                        // Feedback just transitioned OFF -> ON (bumpless
160                        // turn-on). C `devEpidSoft.c:178-184` /
161                        // `devEpidSoftCallback.c:214-220`:
162                        //   if (pepid->outl.type != CONSTANT) {
163                        //       if (dbGetLink(&pepid->outl,DBR_DOUBLE,
164                        //                     &oval,..))
165                        //           recGblSetSevr(...,LINK_ALARM,INVALID);
166                        //   }
167                        // — the output is seeded from the OUTL output link's
168                        // *actual current value*. This is C's line, reached
169                        // only AFTER the `dt < mdt` gate above: consume the
170                        // staged readback here, so a gated cycle never touches
171                        // `.OVAL`. `outl_seed` is `None` for a CONSTANT/empty
172                        // OUTL (nothing was read), and then OVAL keeps its
173                        // prior value — C's `outl.type != CONSTANT` guard.
174                        oval = epid.outl_seed.unwrap_or(epid.oval);
175                    } else {
176                        e = cval - pcval;
177                        let sign = if d > 0.0 { 1.0 } else { -1.0 };
178                        let sign = if (kp > 0.0 && e < 0.0) || (kp < 0.0 && e > 0.0) {
179                            -sign
180                        } else {
181                            sign
182                        };
183                        d = kp * sign;
184                        oval = epid.oval + d;
185                    }
186                }
187            }
188            _ => {
189                tracing::warn!("Invalid feedback mode {} in epid record", epid.fmod);
190            }
191        }
192
193        // Clamp output to drive limits
194        if oval > epid.drvh {
195            oval = epid.drvh;
196        }
197        if oval < epid.drvl {
198            oval = epid.drvl;
199        }
200
201        // Update record fields — C `devEpidSoft.c:206-209`.
202        epid.ct = ct;
203        epid.dt = dt;
204        // C `devEpidSoft.c:208` writes ERR unconditionally for every mode.
205        epid.err = e;
206        epid.cval = cval;
207
208        // Apply output deadband
209        if epid.odel == 0.0 || (epid.oval - oval).abs() > epid.odel {
210            epid.oval = oval;
211        }
212
213        epid.p = p;
214        epid.i = i;
215        epid.d = d;
216        epid.fbop = epid.fbon;
217
218        // C `devEpidSoft.c:218-224` / `devEpidSoftCallback.c:254-258`:
219        //   if (pepid->fbon && (pepid->outl.type != CONSTANT))
220        //       dbPutLink(&pepid->outl, DBR_DOUBLE, &pepid->oval, 1);
221        // The OUTL write is reached only here (after the early returns)
222        // and only with feedback ON. The framework performs the actual
223        // `dbPutLink` via `multi_output_links`; gate it on `fbon`. The
224        // `outl.type != CONSTANT` half is the framework's no-op on a
225        // constant/empty OUTL link.
226        epid.set_outl_write(epid.fbon != 0);
227    }
228}
229
230impl DeviceSupport for EpidSoftDeviceSupport {
231    fn dtyp(&self) -> &str {
232        "Epid Soft"
233    }
234
235    fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
236        let epid = record
237            .as_any_mut()
238            .and_then(|a| a.downcast_mut::<EpidRecord>())
239            .expect("EpidSoftDeviceSupport requires an EpidRecord");
240
241        Self::do_pid(epid);
242        Ok(DeviceReadOutcome::computed())
243    }
244
245    fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
246        Ok(())
247    }
248}