Skip to main content

std_rs/device_support/
epid_fast.rs

1use std::sync::{Arc, Mutex};
2use std::time::Instant;
3
4use epics_base_rs::error::CaResult;
5use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport};
6use epics_base_rs::server::record::Record;
7
8use crate::records::epid::EpidRecord;
9
10/// Fast Epid device support using asyn driver for high-speed (1+ kHz) PID.
11///
12/// Ported from `devEpidFast.c`. The PID computation runs in a background
13/// tokio task driven by asyn interrupt callbacks, not during record
14/// processing. The record merely copies parameters to/from the fast
15/// computation thread.
16///
17/// # Architecture
18///
19/// ```text
20/// ┌─────────────┐    interrupt     ┌──────────────────┐
21/// │ asyn driver  │ ──────────────► │ PID callback task │
22/// │ (input ADC)  │    (new cval)   │  (tokio::spawn)   │
23/// └─────────────┘                  │  runs do_pid()    │
24///                                  │  writes output    │
25///       ┌──────────────────────────┤  to output driver │
26///       │  shared EpidFastPvt      └──────────────────┘
27///       │  (Arc<Mutex>)                    ▲
28///       ▼                                  │
29/// ┌─────────────┐  read()         params   │
30/// │ EpidRecord   │ ◄─────── copy ──────────┘
31/// │ (process)    │ ────────► copy ──────────►
32/// └─────────────┘  results
33/// ```
34///
35/// The `start_callback_loop()` method spawns the background task.
36/// Call it after connecting to the asyn input port.
37pub struct EpidFastDeviceSupport {
38    pvt: Arc<Mutex<EpidFastPvt>>,
39}
40
41/// Private state for the fast PID loop, shared between the
42/// record process thread and the interrupt callback task.
43pub struct EpidFastPvt {
44    // PID parameters (copied from record on each process cycle)
45    pub kp: f64,
46    pub ki: f64,
47    pub kd: f64,
48    pub drvh: f64,
49    pub drvl: f64,
50    pub val: f64, // setpoint
51    pub fbon: bool,
52
53    // PID state (updated by callback, read by record process)
54    pub cval: f64,
55    pub oval: f64,
56    pub err: f64,
57    pub p: f64,
58    pub i: f64,
59    pub d: f64,
60    pub dt: f64,
61    pub ct: Instant,
62    pub fbop: bool,
63
64    // Averaging — C `devEpidFast.c` `epidFastPvt`
65    /// Interval (seconds) between successive driver data callbacks.
66    /// C `callbackInterval`, set by `intervalCallback` from the driver
67    /// and also used directly as `dt` in `do_PID` (devEpidFast.c:430).
68    pub callback_interval: f64,
69    /// Requested time-per-point (C `timePerPointRequested`), copied from
70    /// the record's `DT` field by `update_params` (devEpidFast.c:320).
71    pub time_per_point_requested: f64,
72    /// Actual time-per-point achieved (C `timePerPointActual`) —
73    /// `num_average * callback_interval`.
74    pub time_per_point_actual: f64,
75    pub num_average: u32,
76    pub accumulated: f64,
77    pub count: u32,
78
79    // Output port writer (set by start_callback_loop)
80    pub output_writer: Option<Arc<Mutex<dyn FnMut(f64) + Send>>>,
81
82    /// Output port reader — C `devEpidFast.c:446-448` reads the actual
83    /// current value of the output (DAC) on the feedback OFF->ON edge
84    /// via `pPvt->pfloat64Output->read(...)` so the integral term is
85    /// seeded bumplessly from the hardware's real output, not the
86    /// last value the loop happened to command. When `None` (no
87    /// output-port reader wired), the bumpless edge falls back to the
88    /// last commanded `oval` — see `do_pid`.
89    pub output_reader: Option<Arc<Mutex<dyn FnMut() -> Option<f64> + Send>>>,
90}
91
92impl Default for EpidFastPvt {
93    fn default() -> Self {
94        let now = Instant::now();
95        Self {
96            // C `devEpidFast.c:121-123` init_record seeds KP=1 and
97            // inverted drive limits (lowLimit=1, highLimit=-1) so that
98            // before `update_params` runs the output clamp is a no-op.
99            kp: 1.0,
100            ki: 0.0,
101            kd: 0.0,
102            drvh: -1.0,
103            drvl: 1.0,
104            val: 0.0,
105            fbon: false,
106            cval: 0.0,
107            oval: 0.0,
108            err: 0.0,
109            p: 0.0,
110            i: 0.0,
111            d: 0.0,
112            dt: 0.0,
113            ct: now,
114            fbop: false,
115            callback_interval: 0.0,
116            time_per_point_requested: 0.0,
117            time_per_point_actual: 0.0,
118            num_average: 1,
119            accumulated: 0.0,
120            count: 0,
121            output_writer: None,
122            output_reader: None,
123        }
124    }
125}
126
127impl EpidFastPvt {
128    /// Recompute the number of points to average and the resulting
129    /// actual time-per-point. C `devEpidFast.c::computeNumAverage`
130    /// (devEpidFast.c:356-362):
131    /// `numAverage = 0.5 + timePerPointRequested/callbackInterval`,
132    /// clamped to `>= 1`, then `timePerPointActual = numAverage *
133    /// callbackInterval`.
134    pub fn compute_num_average(&mut self) {
135        let n = if self.callback_interval > 0.0 {
136            (0.5 + self.time_per_point_requested / self.callback_interval) as i64
137        } else {
138            1
139        };
140        self.num_average = n.max(1) as u32;
141        self.time_per_point_actual = self.num_average as f64 * self.callback_interval;
142    }
143
144    /// Callback from the driver when the data-callback interval changes.
145    /// C `devEpidFast.c::intervalCallback` (devEpidFast.c:367-375):
146    /// updates `callbackInterval` then recomputes `numAverage`.
147    pub fn interval_callback(&mut self, seconds: f64) {
148        self.callback_interval = seconds;
149        self.compute_num_average();
150    }
151
152    /// Execute one PID cycle on new data. Called from the interrupt callback task.
153    /// After computing the output, writes to the output port if configured.
154    ///
155    /// Mirrors C `devEpidFast.c::dataCallback` (averaging) +
156    /// `do_PID` (devEpidFast.c:379-482).
157    pub fn do_pid(&mut self, new_cval: f64) {
158        // Averaging — C `dataCallback` (devEpidFast.c:379-398).
159        // C shortcuts when numAverage == 1 (no need to accumulate).
160        let cval = if self.num_average <= 1 {
161            new_cval
162        } else {
163            self.accumulated += new_cval;
164            self.count += 1;
165            if self.count < self.num_average {
166                return;
167            }
168            // C divides averageStore by `accumulated` (the running count),
169            // which equals `count` here.
170            let avg = self.accumulated / self.count as f64;
171            self.accumulated = 0.0;
172            self.count = 0;
173            avg
174        };
175
176        self.cval = cval;
177
178        // C `do_PID` (devEpidFast.c:430) uses `dt = pPvt->callbackInterval`
179        // — the configured driver callback interval, NOT a measured
180        // wall-clock difference. C devEpidFast keeps no `ct` timestamp.
181        let dt = self.callback_interval;
182        self.dt = self.time_per_point_actual;
183        self.ct = Instant::now();
184
185        let ep = self.err;
186        let mut oval = self.oval;
187
188        // C `do_PID` (devEpidFast.c:400-482) runs the PID algorithm
189        // UNCONDITIONALLY — `epidFastPvt` has no `fmod` field and
190        // `do_PID` never branches on FMOD. FMOD/MaxMin is honoured only
191        // by the Soft device supports (`devEpidSoft.c:137`,
192        // `devEpidSoftCallback.c:173` have `switch (pepid->fmod)`); the
193        // Fast support ignores FMOD entirely.
194        let e = self.val - cval;
195        let de = e - ep;
196        self.p = self.kp * e;
197        let di = self.kp * self.ki * e * dt;
198
199        if self.fbon {
200            if !self.fbop {
201                // Bumpless OFF->ON — C `devEpidFast.c:445-448`:
202                //   pPvt->pfloat64Output->read(pPvt->float64OutputPvt,
203                //       pPvt->pfloat64OutputAsynUser, &pPvt->I);
204                // The integral term is seeded from the output port's
205                // *actual current value* (the real DAC output) so the
206                // loop turns on without a bump. When an `output_reader`
207                // is wired, read it; otherwise fall back to the last
208                // commanded `oval` (the closest available estimate).
209                self.i = match &self.output_reader {
210                    Some(reader) => reader
211                        .lock()
212                        .ok()
213                        .and_then(|mut r| r())
214                        .unwrap_or(self.oval),
215                    None => self.oval,
216                };
217            } else if (oval > self.drvl && oval < self.drvh)
218                || (oval >= self.drvh && di < 0.0)
219                || (oval <= self.drvl && di > 0.0)
220            {
221                self.i += di;
222                // C `devEpidFast.c:455-456` does two sequential
223                // `if` clamps (low then high). `f64::clamp`
224                // panics when min > max, which happens before
225                // `update_params` seeds the limits — replicate
226                // the panic-free sequential form.
227                if self.i < self.drvl {
228                    self.i = self.drvl;
229                }
230                if self.i > self.drvh {
231                    self.i = self.drvh;
232                }
233            }
234        }
235        if self.ki == 0.0 {
236            self.i = 0.0;
237        }
238        self.d = if dt > 0.0 {
239            self.kp * self.kd * (de / dt)
240        } else {
241            0.0
242        };
243        self.err = e;
244        oval = self.p + self.i + self.d;
245
246        // Clamp output — C `devEpidFast.c:464-465` sequential `if`
247        // clamps (panic-free vs `f64::clamp` when drvl > drvh).
248        if oval > self.drvh {
249            oval = self.drvh;
250        }
251        if oval < self.drvl {
252            oval = self.drvl;
253        }
254        self.oval = oval;
255        self.fbop = self.fbon;
256
257        // Write output to hardware if configured
258        if self.fbon {
259            if let Some(ref writer) = self.output_writer {
260                if let Ok(mut w) = writer.lock() {
261                    w(self.oval);
262                }
263            }
264        }
265    }
266}
267
268impl Default for EpidFastDeviceSupport {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl EpidFastDeviceSupport {
275    pub fn new() -> Self {
276        Self {
277            pvt: Arc::new(Mutex::new(EpidFastPvt::default())),
278        }
279    }
280
281    /// Get a handle to the shared PID state for callback registration.
282    pub fn pvt(&self) -> Arc<Mutex<EpidFastPvt>> {
283        Arc::clone(&self.pvt)
284    }
285
286    /// Wire the output-port readback used for bumpless transfer.
287    ///
288    /// C `devEpidFast.c:446-448` reads the actual current output value
289    /// (`pfloat64Output->read`) on the feedback OFF->ON edge to seed
290    /// the integral term. Supply a closure that returns the output
291    /// port's current value; without it the bumpless edge falls back
292    /// to the last commanded `OVAL`.
293    pub fn set_output_reader(&self, reader: Arc<Mutex<dyn FnMut() -> Option<f64> + Send>>) {
294        if let Ok(mut p) = self.pvt.lock() {
295            p.output_reader = Some(reader);
296        }
297    }
298
299    /// Wire the asyn output port: install both the output writer and
300    /// the bumpless-transfer output reader from a single asyn Float64
301    /// port handle.
302    ///
303    /// This mirrors C `devEpidFast.c` exactly. `init_record`
304    /// (devEpidFast.c:264-303) connects to *one* output asyn port and
305    /// captures `pPvt->pfloat64Output` / `float64OutputPvt`. `do_PID`
306    /// then uses that same interface for both directions:
307    ///
308    /// * the bumpless OFF->ON edge reads the output port's present
309    ///   value — `pPvt->pfloat64Output->read(...)` (devEpidFast.c:446-448);
310    /// * the feedback write — `pPvt->pfloat64Output->write(...)`
311    ///   (devEpidFast.c:471-473).
312    ///
313    /// `sync_io` is the asyn Float64 output port handle and `reason`
314    /// the parameter index (asyn `drvUser` "outputDataString" channel,
315    /// devEpidFast.c:296-303). The reader calls `read_float64(reason)`
316    /// on it; a failed read yields `None`, so `do_pid` falls back to
317    /// the last commanded `OVAL` safety net.
318    pub fn set_output_port(&self, sync_io: asyn_rs::sync_io::SyncIOHandle, reason: usize) {
319        let sync_io = Arc::new(sync_io);
320        let writer_io = Arc::clone(&sync_io);
321        let reader_io = Arc::clone(&sync_io);
322
323        let writer: Arc<Mutex<dyn FnMut(f64) + Send>> = Arc::new(Mutex::new(move |v: f64| {
324            // C `devEpidFast.c:471-478` — write the output, log on error.
325            let _ = writer_io.write_float64(reason, v);
326        }));
327        let reader: Arc<Mutex<dyn FnMut() -> Option<f64> + Send>> =
328            Arc::new(Mutex::new(move || reader_io.read_float64(reason).ok()));
329
330        if let Ok(mut p) = self.pvt.lock() {
331            p.output_writer = Some(writer);
332            p.output_reader = Some(reader);
333        }
334    }
335
336    /// Start the interrupt-driven PID callback loop.
337    ///
338    /// Spawns a tokio task that receives new readback values from `input_rx`
339    /// and runs `do_pid()` on each. This is the high-speed PID path that
340    /// runs at the interrupt rate (1kHz+), independent of record processing.
341    ///
342    /// `input_rx`: receives new controlled-variable values from the input driver
343    /// `output_fn`: called with each new output value (writes to output driver)
344    ///
345    /// This bare-closure form leaves the bumpless-transfer `output_reader`
346    /// untouched — wire it separately with [`set_output_reader`] or
347    /// [`set_output_port`], otherwise the OFF->ON edge falls back to the
348    /// last commanded `OVAL`. When the output is a real asyn Float64
349    /// port, prefer [`start_callback_loop_with_port`], which installs
350    /// both writer and reader from the same port handle (mirroring C
351    /// `devEpidFast.c` `pPvt->pfloat64Output`).
352    ///
353    /// [`set_output_reader`]: Self::set_output_reader
354    /// [`set_output_port`]: Self::set_output_port
355    /// [`start_callback_loop_with_port`]: Self::start_callback_loop_with_port
356    pub fn start_callback_loop(
357        &self,
358        mut input_rx: tokio::sync::mpsc::Receiver<f64>,
359        output_fn: Arc<Mutex<dyn FnMut(f64) + Send>>,
360    ) {
361        let pvt = Arc::clone(&self.pvt);
362
363        // Store the output writer in pvt
364        {
365            let mut p = pvt.lock().unwrap();
366            p.output_writer = Some(output_fn);
367        }
368
369        tokio::spawn(async move {
370            while let Some(new_cval) = input_rx.recv().await {
371                let mut p = pvt.lock().unwrap();
372                p.do_pid(new_cval);
373            }
374        });
375    }
376
377    /// Start the PID callback loop driven by an asyn Float64 output port.
378    ///
379    /// Identical to [`start_callback_loop`] but takes the output asyn
380    /// port handle directly and installs *both* the output writer and
381    /// the bumpless-transfer output reader from it via
382    /// [`set_output_port`] — mirroring C `devEpidFast.c` `init_record`,
383    /// which captures a single `pPvt->pfloat64Output` interface and uses
384    /// it for both `read` (devEpidFast.c:446-448) and `write`
385    /// (devEpidFast.c:471-473).
386    ///
387    /// `output_sync_io` is the asyn Float64 output port handle and
388    /// `output_reason` the parameter index for the output channel.
389    ///
390    /// [`start_callback_loop`]: Self::start_callback_loop
391    /// [`set_output_port`]: Self::set_output_port
392    pub fn start_callback_loop_with_port(
393        &self,
394        input_rx: tokio::sync::mpsc::Receiver<f64>,
395        output_sync_io: asyn_rs::sync_io::SyncIOHandle,
396        output_reason: usize,
397    ) {
398        self.set_output_port(output_sync_io, output_reason);
399
400        let pvt = Arc::clone(&self.pvt);
401        let mut input_rx = input_rx;
402        tokio::spawn(async move {
403            while let Some(new_cval) = input_rx.recv().await {
404                let mut p = pvt.lock().unwrap();
405                p.do_pid(new_cval);
406            }
407        });
408    }
409
410    /// Start from an asyn interrupt subscription.
411    ///
412    /// Subscribes to Float64 interrupts from the given broadcast sender
413    /// and feeds them into the PID callback loop.
414    ///
415    /// This bare-closure form leaves the bumpless-transfer `output_reader`
416    /// untouched — wire it separately with [`set_output_reader`] or
417    /// [`set_output_port`], or use [`start_from_asyn_interrupts_with_port`]
418    /// to install both writer and reader from one asyn output port.
419    ///
420    /// [`set_output_reader`]: Self::set_output_reader
421    /// [`set_output_port`]: Self::set_output_port
422    /// [`start_from_asyn_interrupts_with_port`]: Self::start_from_asyn_interrupts_with_port
423    pub fn start_from_asyn_interrupts(
424        &self,
425        interrupt_rx: tokio::sync::broadcast::Receiver<asyn_rs::interrupt::InterruptValue>,
426        input_reason: usize,
427        output_fn: Arc<Mutex<dyn FnMut(f64) + Send>>,
428    ) {
429        {
430            let mut p = self.pvt.lock().unwrap();
431            p.output_writer = Some(output_fn);
432        }
433        self.spawn_interrupt_loop(interrupt_rx, input_reason);
434    }
435
436    /// Start from an asyn interrupt subscription, driven by an asyn
437    /// Float64 output port.
438    ///
439    /// Identical to [`start_from_asyn_interrupts`] but takes the output
440    /// asyn port handle directly and installs *both* the output writer
441    /// and the bumpless-transfer output reader from it via
442    /// [`set_output_port`] — mirroring C `devEpidFast.c` `init_record`,
443    /// which captures a single `pPvt->pfloat64Output` interface and uses
444    /// it for both `read` (devEpidFast.c:446-448) and `write`
445    /// (devEpidFast.c:471-473).
446    ///
447    /// [`start_from_asyn_interrupts`]: Self::start_from_asyn_interrupts
448    /// [`set_output_port`]: Self::set_output_port
449    pub fn start_from_asyn_interrupts_with_port(
450        &self,
451        interrupt_rx: tokio::sync::broadcast::Receiver<asyn_rs::interrupt::InterruptValue>,
452        input_reason: usize,
453        output_sync_io: asyn_rs::sync_io::SyncIOHandle,
454        output_reason: usize,
455    ) {
456        self.set_output_port(output_sync_io, output_reason);
457        self.spawn_interrupt_loop(interrupt_rx, input_reason);
458    }
459
460    /// Spawn the tokio task that feeds asyn Float64 interrupts into
461    /// `do_pid`. Shared by both `start_from_asyn_interrupts` variants.
462    fn spawn_interrupt_loop(
463        &self,
464        mut interrupt_rx: tokio::sync::broadcast::Receiver<asyn_rs::interrupt::InterruptValue>,
465        input_reason: usize,
466    ) {
467        let pvt = Arc::clone(&self.pvt);
468        tokio::spawn(async move {
469            loop {
470                match interrupt_rx.recv().await {
471                    Ok(iv) => {
472                        if iv.reason == input_reason {
473                            let v = match &iv.value {
474                                asyn_rs::param::ParamValue::Float64(f) => Some(*f),
475                                asyn_rs::param::ParamValue::Int32(i) => Some(*i as f64),
476                                asyn_rs::param::ParamValue::Int64(i) => Some(*i as f64),
477                                _ => None,
478                            };
479                            if let Some(v) = v {
480                                let mut p = pvt.lock().unwrap();
481                                p.do_pid(v);
482                            }
483                        }
484                    }
485                    Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
486                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
487                        // Dropped some interrupts — continue
488                    }
489                }
490            }
491        });
492    }
493
494    /// Copy parameters from record to fast PID state.
495    ///
496    /// C `devEpidFast.c::update_params` (devEpidFast.c:312-354): when the
497    /// record's `DT` field (requested time-per-point) differs from the
498    /// achieved `timePerPointActual`, the requested value is adopted and
499    /// `numAverage` is recomputed.
500    fn update_params_from_record(&self, epid: &EpidRecord) {
501        let mut pvt = self.pvt.lock().unwrap();
502        // C devEpidFast.c:319-322 — recompute averaging if DT changed.
503        if epid.dt != pvt.time_per_point_actual {
504            pvt.time_per_point_requested = epid.dt;
505            pvt.compute_num_average();
506        }
507        pvt.kp = epid.kp;
508        pvt.ki = epid.ki;
509        pvt.kd = epid.kd;
510        pvt.drvh = epid.drvh;
511        pvt.drvl = epid.drvl;
512        pvt.val = epid.val;
513        pvt.fbon = epid.fbon != 0;
514        // C `devEpidFast.c::update_params` (devEpidFast.c:332-339) copies
515        // FBON/DRVH/DRVL/KP/KI/KD/VAL only — `epidFastPvt` has no `fmod`
516        // field and the Fast support never reads FMOD.
517    }
518
519    /// Copy computed results from fast PID state back to record.
520    fn update_record_from_params(&self, epid: &mut EpidRecord) {
521        let pvt = self.pvt.lock().unwrap();
522        epid.cval = pvt.cval;
523        epid.oval = pvt.oval;
524        epid.err = pvt.err;
525        epid.p = pvt.p;
526        epid.i = pvt.i;
527        epid.d = pvt.d;
528        epid.dt = pvt.dt;
529        epid.fbop = if pvt.fbop { 1 } else { 0 };
530    }
531}
532
533impl DeviceSupport for EpidFastDeviceSupport {
534    fn dtyp(&self) -> &str {
535        "Fast Epid"
536    }
537
538    fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
539        let epid = record
540            .as_any_mut()
541            .and_then(|a| a.downcast_mut::<EpidRecord>())
542            .expect("EpidFastDeviceSupport requires an EpidRecord");
543
544        // Copy parameters to fast PID (so callback loop uses latest gains)
545        self.update_params_from_record(epid);
546        // Copy latest results back to record (for display/alarm)
547        self.update_record_from_params(epid);
548        Ok(DeviceReadOutcome::computed())
549    }
550
551    fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
552        Ok(())
553    }
554}