Skip to main content

std_rs/records/
epid.rs

1use std::any::Any;
2use std::time::Instant;
3
4use epics_base_rs::error::{CaError, CaResult};
5use epics_base_rs::server::record::{FieldDesc, ProcessOutcome, Record};
6use epics_base_rs::types::{DbFieldType, EpicsValue};
7
8/// Feedback mode for the epid record.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10#[repr(i16)]
11pub enum FeedbackMode {
12    #[default]
13    Pid = 0,
14    MaxMin = 1,
15}
16
17impl From<i16> for FeedbackMode {
18    fn from(v: i16) -> Self {
19        match v {
20            1 => FeedbackMode::MaxMin,
21            _ => FeedbackMode::Pid,
22        }
23    }
24}
25
26/// Feedback on/off state.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[repr(i16)]
29pub enum FeedbackState {
30    #[default]
31    Off = 0,
32    On = 1,
33}
34
35impl From<i16> for FeedbackState {
36    fn from(v: i16) -> Self {
37        match v {
38            1 => FeedbackState::On,
39            _ => FeedbackState::Off,
40        }
41    }
42}
43
44/// Extended PID feedback control record.
45///
46/// Ported from EPICS std module `epidRecord.c`.
47/// Supports PID and Max/Min feedback modes with anti-windup,
48/// bumpless turn-on, output deadband, and hysteresis-based alarms.
49pub struct EpidRecord {
50    // --- PID control ---
51    /// Setpoint (VAL)
52    pub val: f64,
53    /// Setpoint mode: 0=supervisory, 1=closed_loop (SMSL)
54    pub smsl: i16,
55    /// Setpoint input link (STPL) — resolved by framework
56    pub stpl: String,
57    /// Controlled value input link (INP) — resolved by framework
58    pub inp: String,
59    /// Output link (OUTL) — resolved by framework
60    pub outl: String,
61    /// Readback trigger link (TRIG)
62    pub trig: String,
63    /// Trigger value (TVAL)
64    pub tval: f64,
65    /// Controlled value (CVAL), read-only
66    pub cval: f64,
67    /// Previous controlled value (CVLP), read-only
68    pub cvlp: f64,
69    /// Output value (OVAL), read-only
70    pub oval: f64,
71    /// Previous output value (OVLP), read-only
72    pub ovlp: f64,
73    /// Proportional gain (KP)
74    pub kp: f64,
75    /// Integral gain — repeats per second (KI)
76    pub ki: f64,
77    /// Derivative gain (KD)
78    pub kd: f64,
79    /// Proportional component (P), read-only
80    pub p: f64,
81    /// Previous P (PP), read-only
82    pub pp: f64,
83    /// Integral component (I), writable for bumpless init
84    pub i: f64,
85    /// Previous I (IP)
86    pub ip: f64,
87    /// Derivative component (D), read-only
88    pub d: f64,
89    /// Previous D (DP), read-only
90    pub dp: f64,
91    /// Error = setpoint - controlled value (ERR), read-only
92    pub err: f64,
93    /// Previous error (ERRP), read-only
94    pub errp: f64,
95    /// Delta time in seconds (DT), writable for fast mode
96    pub dt: f64,
97    /// Previous delta time (DTP)
98    pub dtp: f64,
99    /// Minimum delta time between calculations (MDT)
100    pub mdt: f64,
101    /// Feedback mode: PID or MaxMin (FMOD)
102    pub fmod: i16,
103    /// Feedback on/off (FBON)
104    pub fbon: i16,
105    /// Previous feedback on/off (FBOP)
106    pub fbop: i16,
107    /// Output deadband (ODEL)
108    pub odel: f64,
109
110    // --- Display ---
111    /// Display precision (PREC)
112    pub prec: i16,
113    /// Engineering units (EGU)
114    pub egu: String,
115    /// High operating range (HOPR)
116    pub hopr: f64,
117    /// Low operating range (LOPR)
118    pub lopr: f64,
119    /// High drive limit (DRVH)
120    pub drvh: f64,
121    /// Low drive limit (DRVL)
122    pub drvl: f64,
123
124    // --- Alarm ---
125    /// Hihi deviation limit (HIHI)
126    pub hihi: f64,
127    /// Lolo deviation limit (LOLO)
128    pub lolo: f64,
129    /// High deviation limit (HIGH)
130    pub high: f64,
131    /// Low deviation limit (LOW)
132    pub low: f64,
133    /// Hihi severity (HHSV)
134    pub hhsv: i16,
135    /// Lolo severity (LLSV)
136    pub llsv: i16,
137    /// High severity (HSV)
138    pub hsv: i16,
139    /// Low severity (LSV)
140    pub lsv: i16,
141    /// Alarm deadband / hysteresis (HYST)
142    pub hyst: f64,
143    /// Last value alarmed (LALM), read-only
144    pub lalm: f64,
145
146    // --- Monitor deadband ---
147    /// Archive deadband (ADEL)
148    pub adel: f64,
149    /// Monitor deadband (MDEL)
150    pub mdel: f64,
151    /// Last value archived (ALST), read-only
152    pub alst: f64,
153    /// Last value monitored (MLST), read-only
154    pub mlst: f64,
155
156    // --- Internal time tracking ---
157    /// Current time (CT) — used for delta-T computation
158    pub(crate) ct: Instant,
159    /// Previous time (CTP) — tracked for monitor change detection
160    #[allow(dead_code)]
161    pub(crate) ctp: Instant,
162
163    // --- Internal flags ---
164    /// Set by the framework (via set_device_did_compute) to indicate
165    /// device support's read() already performed the PID computation.
166    /// process() checks this to avoid running the built-in PID a second time.
167    device_did_compute: bool,
168}
169
170impl Default for EpidRecord {
171    fn default() -> Self {
172        let now = Instant::now();
173        Self {
174            val: 0.0,
175            smsl: 0,
176            stpl: String::new(),
177            inp: String::new(),
178            outl: String::new(),
179            trig: String::new(),
180            tval: 0.0,
181            cval: 0.0,
182            cvlp: 0.0,
183            oval: 0.0,
184            ovlp: 0.0,
185            kp: 0.0,
186            ki: 0.0,
187            kd: 0.0,
188            p: 0.0,
189            pp: 0.0,
190            i: 0.0,
191            ip: 0.0,
192            d: 0.0,
193            dp: 0.0,
194            err: 0.0,
195            errp: 0.0,
196            dt: 0.0,
197            dtp: 0.0,
198            mdt: 0.0,
199            fmod: 0,
200            fbon: 0,
201            fbop: 0,
202            odel: 0.0,
203            prec: 0,
204            egu: String::new(),
205            hopr: 0.0,
206            lopr: 0.0,
207            drvh: 0.0,
208            drvl: 0.0,
209            hihi: 0.0,
210            lolo: 0.0,
211            high: 0.0,
212            low: 0.0,
213            hhsv: 0,
214            llsv: 0,
215            hsv: 0,
216            lsv: 0,
217            hyst: 0.0,
218            lalm: 0.0,
219            adel: 0.0,
220            mdel: 0.0,
221            alst: 0.0,
222            mlst: 0.0,
223            ct: now,
224            ctp: now,
225            device_did_compute: false,
226        }
227    }
228}
229
230impl EpidRecord {
231    /// Check alarms using hysteresis-based threshold comparison on VAL.
232    /// Ported from epidRecord.c `checkAlarms()`.
233    pub fn check_alarms(&mut self) -> Option<(u16, u16)> {
234        let val = self.val;
235        let hyst = self.hyst;
236        let lalm = self.lalm;
237
238        // HIHI alarm
239        if self.hhsv != 0 {
240            if val >= self.hihi || (lalm == self.hihi && val >= self.hihi - hyst) {
241                self.lalm = self.hihi;
242                return Some((3, self.hhsv as u16)); // HIHI_ALARM
243            }
244        }
245
246        // LOLO alarm
247        if self.llsv != 0 {
248            if val <= self.lolo || (lalm == self.lolo && val <= self.lolo + hyst) {
249                self.lalm = self.lolo;
250                return Some((4, self.llsv as u16)); // LOLO_ALARM
251            }
252        }
253
254        // HIGH alarm
255        if self.hsv != 0 {
256            if val >= self.high || (lalm == self.high && val >= self.high - hyst) {
257                self.lalm = self.high;
258                return Some((1, self.hsv as u16)); // HIGH_ALARM
259            }
260        }
261
262        // LOW alarm
263        if self.lsv != 0 {
264            if val <= self.low || (lalm == self.low && val <= self.low + hyst) {
265                self.lalm = self.low;
266                return Some((2, self.lsv as u16)); // LOW_ALARM
267            }
268        }
269
270        // No alarm
271        self.lalm = val;
272        None
273    }
274
275    /// Update monitor tracking fields. Returns list of fields that changed.
276    /// Ported from epidRecord.c `monitor()`.
277    pub fn update_monitors(&mut self) {
278        // Update previous-value fields for change detection
279        self.ovlp = self.oval;
280        self.pp = self.p;
281        self.ip = self.i;
282        self.dp = self.d;
283        self.dtp = self.dt;
284        self.errp = self.err;
285        self.cvlp = self.cval;
286
287        // VAL deadband tracking
288        if self.mdel == 0.0 || (self.mlst - self.val).abs() > self.mdel {
289            self.mlst = self.val;
290        }
291        if self.adel == 0.0 || (self.alst - self.val).abs() > self.adel {
292            self.alst = self.val;
293        }
294    }
295}
296
297static FIELDS: &[FieldDesc] = &[
298    // PID control
299    FieldDesc {
300        name: "VAL",
301        dbf_type: DbFieldType::Double,
302        read_only: false,
303    },
304    FieldDesc {
305        name: "SMSL",
306        dbf_type: DbFieldType::Short,
307        read_only: false,
308    },
309    FieldDesc {
310        name: "STPL",
311        dbf_type: DbFieldType::String,
312        read_only: false,
313    },
314    FieldDesc {
315        name: "INP",
316        dbf_type: DbFieldType::String,
317        read_only: false,
318    },
319    FieldDesc {
320        name: "OUTL",
321        dbf_type: DbFieldType::String,
322        read_only: false,
323    },
324    FieldDesc {
325        name: "TRIG",
326        dbf_type: DbFieldType::String,
327        read_only: false,
328    },
329    FieldDesc {
330        name: "TVAL",
331        dbf_type: DbFieldType::Double,
332        read_only: false,
333    },
334    FieldDesc {
335        name: "CVAL",
336        dbf_type: DbFieldType::Double,
337        read_only: true,
338    },
339    FieldDesc {
340        name: "CVLP",
341        dbf_type: DbFieldType::Double,
342        read_only: true,
343    },
344    FieldDesc {
345        name: "OVAL",
346        dbf_type: DbFieldType::Double,
347        read_only: true,
348    },
349    FieldDesc {
350        name: "OVLP",
351        dbf_type: DbFieldType::Double,
352        read_only: true,
353    },
354    FieldDesc {
355        name: "KP",
356        dbf_type: DbFieldType::Double,
357        read_only: false,
358    },
359    FieldDesc {
360        name: "KI",
361        dbf_type: DbFieldType::Double,
362        read_only: false,
363    },
364    FieldDesc {
365        name: "KD",
366        dbf_type: DbFieldType::Double,
367        read_only: false,
368    },
369    FieldDesc {
370        name: "P",
371        dbf_type: DbFieldType::Double,
372        read_only: true,
373    },
374    FieldDesc {
375        name: "PP",
376        dbf_type: DbFieldType::Double,
377        read_only: true,
378    },
379    FieldDesc {
380        name: "I",
381        dbf_type: DbFieldType::Double,
382        read_only: false,
383    },
384    FieldDesc {
385        name: "IP",
386        dbf_type: DbFieldType::Double,
387        read_only: true,
388    },
389    FieldDesc {
390        name: "D",
391        dbf_type: DbFieldType::Double,
392        read_only: true,
393    },
394    FieldDesc {
395        name: "DP",
396        dbf_type: DbFieldType::Double,
397        read_only: true,
398    },
399    FieldDesc {
400        name: "ERR",
401        dbf_type: DbFieldType::Double,
402        read_only: true,
403    },
404    FieldDesc {
405        name: "ERRP",
406        dbf_type: DbFieldType::Double,
407        read_only: true,
408    },
409    FieldDesc {
410        name: "DT",
411        dbf_type: DbFieldType::Double,
412        read_only: false,
413    },
414    FieldDesc {
415        name: "DTP",
416        dbf_type: DbFieldType::Double,
417        read_only: true,
418    },
419    FieldDesc {
420        name: "MDT",
421        dbf_type: DbFieldType::Double,
422        read_only: false,
423    },
424    FieldDesc {
425        name: "FMOD",
426        dbf_type: DbFieldType::Short,
427        read_only: false,
428    },
429    FieldDesc {
430        name: "FBON",
431        dbf_type: DbFieldType::Short,
432        read_only: false,
433    },
434    FieldDesc {
435        name: "FBOP",
436        dbf_type: DbFieldType::Short,
437        read_only: true,
438    },
439    FieldDesc {
440        name: "ODEL",
441        dbf_type: DbFieldType::Double,
442        read_only: false,
443    },
444    // Display
445    FieldDesc {
446        name: "PREC",
447        dbf_type: DbFieldType::Short,
448        read_only: false,
449    },
450    FieldDesc {
451        name: "EGU",
452        dbf_type: DbFieldType::String,
453        read_only: false,
454    },
455    FieldDesc {
456        name: "HOPR",
457        dbf_type: DbFieldType::Double,
458        read_only: false,
459    },
460    FieldDesc {
461        name: "LOPR",
462        dbf_type: DbFieldType::Double,
463        read_only: false,
464    },
465    FieldDesc {
466        name: "DRVH",
467        dbf_type: DbFieldType::Double,
468        read_only: false,
469    },
470    FieldDesc {
471        name: "DRVL",
472        dbf_type: DbFieldType::Double,
473        read_only: false,
474    },
475    // Alarm
476    FieldDesc {
477        name: "HIHI",
478        dbf_type: DbFieldType::Double,
479        read_only: false,
480    },
481    FieldDesc {
482        name: "LOLO",
483        dbf_type: DbFieldType::Double,
484        read_only: false,
485    },
486    FieldDesc {
487        name: "HIGH",
488        dbf_type: DbFieldType::Double,
489        read_only: false,
490    },
491    FieldDesc {
492        name: "LOW",
493        dbf_type: DbFieldType::Double,
494        read_only: false,
495    },
496    FieldDesc {
497        name: "HHSV",
498        dbf_type: DbFieldType::Short,
499        read_only: false,
500    },
501    FieldDesc {
502        name: "LLSV",
503        dbf_type: DbFieldType::Short,
504        read_only: false,
505    },
506    FieldDesc {
507        name: "HSV",
508        dbf_type: DbFieldType::Short,
509        read_only: false,
510    },
511    FieldDesc {
512        name: "LSV",
513        dbf_type: DbFieldType::Short,
514        read_only: false,
515    },
516    FieldDesc {
517        name: "HYST",
518        dbf_type: DbFieldType::Double,
519        read_only: false,
520    },
521    FieldDesc {
522        name: "LALM",
523        dbf_type: DbFieldType::Double,
524        read_only: true,
525    },
526    // Monitor deadband
527    FieldDesc {
528        name: "ADEL",
529        dbf_type: DbFieldType::Double,
530        read_only: false,
531    },
532    FieldDesc {
533        name: "MDEL",
534        dbf_type: DbFieldType::Double,
535        read_only: false,
536    },
537    FieldDesc {
538        name: "ALST",
539        dbf_type: DbFieldType::Double,
540        read_only: true,
541    },
542    FieldDesc {
543        name: "MLST",
544        dbf_type: DbFieldType::Double,
545        read_only: true,
546    },
547];
548
549impl Record for EpidRecord {
550    fn record_type(&self) -> &'static str {
551        "epid"
552    }
553
554    fn process(&mut self) -> CaResult<ProcessOutcome> {
555        // In the C code, process() always calls pdset->do_pid() — a custom
556        // device support function unique to the epid record. In Rust, the
557        // framework has a generic DeviceSupport trait with read()/write()
558        // and no custom function pointers.
559        //
560        // For non-"Soft Channel" DTYPs (e.g. "Fast Epid"), the framework
561        // calls DeviceSupport::read() BEFORE process(). That read() runs
562        // the driver-specific PID and sets pid_done = true.
563        //
564        // For "Soft Channel" or no device support, the framework skips
565        // read(), so pid_done stays false and process() runs the built-in
566        // PID here.
567        if !self.device_did_compute {
568            crate::device_support::epid_soft::EpidSoftDeviceSupport::do_pid(self);
569        }
570        self.device_did_compute = false; // Reset for next cycle
571
572        self.check_alarms();
573        self.update_monitors();
574
575        // Device support actions are now merged by the framework
576        let actions = Vec::new();
577        Ok(ProcessOutcome::complete_with(actions))
578    }
579
580    fn get_field(&self, name: &str) -> Option<EpicsValue> {
581        match name {
582            "VAL" => Some(EpicsValue::Double(self.val)),
583            "SMSL" => Some(EpicsValue::Short(self.smsl)),
584            "STPL" => Some(EpicsValue::String(self.stpl.clone())),
585            "INP" => Some(EpicsValue::String(self.inp.clone())),
586            "OUTL" => Some(EpicsValue::String(self.outl.clone())),
587            "TRIG" => Some(EpicsValue::String(self.trig.clone())),
588            "TVAL" => Some(EpicsValue::Double(self.tval)),
589            "CVAL" => Some(EpicsValue::Double(self.cval)),
590            "CVLP" => Some(EpicsValue::Double(self.cvlp)),
591            "OVAL" => Some(EpicsValue::Double(self.oval)),
592            "OVLP" => Some(EpicsValue::Double(self.ovlp)),
593            "KP" => Some(EpicsValue::Double(self.kp)),
594            "KI" => Some(EpicsValue::Double(self.ki)),
595            "KD" => Some(EpicsValue::Double(self.kd)),
596            "P" => Some(EpicsValue::Double(self.p)),
597            "PP" => Some(EpicsValue::Double(self.pp)),
598            "I" => Some(EpicsValue::Double(self.i)),
599            "IP" => Some(EpicsValue::Double(self.ip)),
600            "D" => Some(EpicsValue::Double(self.d)),
601            "DP" => Some(EpicsValue::Double(self.dp)),
602            "ERR" => Some(EpicsValue::Double(self.err)),
603            "ERRP" => Some(EpicsValue::Double(self.errp)),
604            "DT" => Some(EpicsValue::Double(self.dt)),
605            "DTP" => Some(EpicsValue::Double(self.dtp)),
606            "MDT" => Some(EpicsValue::Double(self.mdt)),
607            "FMOD" => Some(EpicsValue::Short(self.fmod)),
608            "FBON" => Some(EpicsValue::Short(self.fbon)),
609            "FBOP" => Some(EpicsValue::Short(self.fbop)),
610            "ODEL" => Some(EpicsValue::Double(self.odel)),
611            "PREC" => Some(EpicsValue::Short(self.prec)),
612            "EGU" => Some(EpicsValue::String(self.egu.clone())),
613            "HOPR" => Some(EpicsValue::Double(self.hopr)),
614            "LOPR" => Some(EpicsValue::Double(self.lopr)),
615            "DRVH" => Some(EpicsValue::Double(self.drvh)),
616            "DRVL" => Some(EpicsValue::Double(self.drvl)),
617            "HIHI" => Some(EpicsValue::Double(self.hihi)),
618            "LOLO" => Some(EpicsValue::Double(self.lolo)),
619            "HIGH" => Some(EpicsValue::Double(self.high)),
620            "LOW" => Some(EpicsValue::Double(self.low)),
621            "HHSV" => Some(EpicsValue::Short(self.hhsv)),
622            "LLSV" => Some(EpicsValue::Short(self.llsv)),
623            "HSV" => Some(EpicsValue::Short(self.hsv)),
624            "LSV" => Some(EpicsValue::Short(self.lsv)),
625            "HYST" => Some(EpicsValue::Double(self.hyst)),
626            "LALM" => Some(EpicsValue::Double(self.lalm)),
627            "ADEL" => Some(EpicsValue::Double(self.adel)),
628            "MDEL" => Some(EpicsValue::Double(self.mdel)),
629            "ALST" => Some(EpicsValue::Double(self.alst)),
630            "MLST" => Some(EpicsValue::Double(self.mlst)),
631            _ => None,
632        }
633    }
634
635    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
636        match name {
637            "VAL" => match value {
638                EpicsValue::Double(v) => {
639                    self.val = v;
640                    Ok(())
641                }
642                _ => Err(CaError::TypeMismatch(name.into())),
643            },
644            "SMSL" => match value {
645                EpicsValue::Short(v) => {
646                    self.smsl = v;
647                    Ok(())
648                }
649                _ => Err(CaError::TypeMismatch(name.into())),
650            },
651            "STPL" => match value {
652                EpicsValue::String(v) => {
653                    self.stpl = v;
654                    Ok(())
655                }
656                _ => Err(CaError::TypeMismatch(name.into())),
657            },
658            "INP" => match value {
659                EpicsValue::String(v) => {
660                    self.inp = v;
661                    Ok(())
662                }
663                _ => Err(CaError::TypeMismatch(name.into())),
664            },
665            "OUTL" => match value {
666                EpicsValue::String(v) => {
667                    self.outl = v;
668                    Ok(())
669                }
670                _ => Err(CaError::TypeMismatch(name.into())),
671            },
672            "TRIG" => match value {
673                EpicsValue::String(v) => {
674                    self.trig = v;
675                    Ok(())
676                }
677                _ => Err(CaError::TypeMismatch(name.into())),
678            },
679            "TVAL" => match value {
680                EpicsValue::Double(v) => {
681                    self.tval = v;
682                    Ok(())
683                }
684                _ => Err(CaError::TypeMismatch(name.into())),
685            },
686            "KP" => match value {
687                EpicsValue::Double(v) => {
688                    self.kp = v;
689                    Ok(())
690                }
691                _ => Err(CaError::TypeMismatch(name.into())),
692            },
693            "KI" => match value {
694                EpicsValue::Double(v) => {
695                    self.ki = v;
696                    Ok(())
697                }
698                _ => Err(CaError::TypeMismatch(name.into())),
699            },
700            "KD" => match value {
701                EpicsValue::Double(v) => {
702                    self.kd = v;
703                    Ok(())
704                }
705                _ => Err(CaError::TypeMismatch(name.into())),
706            },
707            "I" => match value {
708                EpicsValue::Double(v) => {
709                    self.i = v;
710                    Ok(())
711                }
712                _ => Err(CaError::TypeMismatch(name.into())),
713            },
714            "IP" => match value {
715                EpicsValue::Double(v) => {
716                    self.ip = v;
717                    Ok(())
718                }
719                _ => Err(CaError::TypeMismatch(name.into())),
720            },
721            "DT" => match value {
722                EpicsValue::Double(v) => {
723                    self.dt = v;
724                    Ok(())
725                }
726                _ => Err(CaError::TypeMismatch(name.into())),
727            },
728            "MDT" => match value {
729                EpicsValue::Double(v) => {
730                    self.mdt = v;
731                    Ok(())
732                }
733                _ => Err(CaError::TypeMismatch(name.into())),
734            },
735            "FMOD" => match value {
736                EpicsValue::Short(v) => {
737                    self.fmod = v;
738                    Ok(())
739                }
740                _ => Err(CaError::TypeMismatch(name.into())),
741            },
742            "FBON" => match value {
743                EpicsValue::Short(v) => {
744                    self.fbon = v;
745                    Ok(())
746                }
747                _ => Err(CaError::TypeMismatch(name.into())),
748            },
749            "ODEL" => match value {
750                EpicsValue::Double(v) => {
751                    self.odel = v;
752                    Ok(())
753                }
754                _ => Err(CaError::TypeMismatch(name.into())),
755            },
756            "PREC" => match value {
757                EpicsValue::Short(v) => {
758                    self.prec = v;
759                    Ok(())
760                }
761                _ => Err(CaError::TypeMismatch(name.into())),
762            },
763            "EGU" => match value {
764                EpicsValue::String(v) => {
765                    self.egu = v;
766                    Ok(())
767                }
768                _ => Err(CaError::TypeMismatch(name.into())),
769            },
770            "HOPR" => match value {
771                EpicsValue::Double(v) => {
772                    self.hopr = v;
773                    Ok(())
774                }
775                _ => Err(CaError::TypeMismatch(name.into())),
776            },
777            "LOPR" => match value {
778                EpicsValue::Double(v) => {
779                    self.lopr = v;
780                    Ok(())
781                }
782                _ => Err(CaError::TypeMismatch(name.into())),
783            },
784            "DRVH" => match value {
785                EpicsValue::Double(v) => {
786                    self.drvh = v;
787                    Ok(())
788                }
789                _ => Err(CaError::TypeMismatch(name.into())),
790            },
791            "DRVL" => match value {
792                EpicsValue::Double(v) => {
793                    self.drvl = v;
794                    Ok(())
795                }
796                _ => Err(CaError::TypeMismatch(name.into())),
797            },
798            "HIHI" => match value {
799                EpicsValue::Double(v) => {
800                    self.hihi = v;
801                    Ok(())
802                }
803                _ => Err(CaError::TypeMismatch(name.into())),
804            },
805            "LOLO" => match value {
806                EpicsValue::Double(v) => {
807                    self.lolo = v;
808                    Ok(())
809                }
810                _ => Err(CaError::TypeMismatch(name.into())),
811            },
812            "HIGH" => match value {
813                EpicsValue::Double(v) => {
814                    self.high = v;
815                    Ok(())
816                }
817                _ => Err(CaError::TypeMismatch(name.into())),
818            },
819            "LOW" => match value {
820                EpicsValue::Double(v) => {
821                    self.low = v;
822                    Ok(())
823                }
824                _ => Err(CaError::TypeMismatch(name.into())),
825            },
826            "HHSV" => match value {
827                EpicsValue::Short(v) => {
828                    self.hhsv = v;
829                    Ok(())
830                }
831                _ => Err(CaError::TypeMismatch(name.into())),
832            },
833            "LLSV" => match value {
834                EpicsValue::Short(v) => {
835                    self.llsv = v;
836                    Ok(())
837                }
838                _ => Err(CaError::TypeMismatch(name.into())),
839            },
840            "HSV" => match value {
841                EpicsValue::Short(v) => {
842                    self.hsv = v;
843                    Ok(())
844                }
845                _ => Err(CaError::TypeMismatch(name.into())),
846            },
847            "LSV" => match value {
848                EpicsValue::Short(v) => {
849                    self.lsv = v;
850                    Ok(())
851                }
852                _ => Err(CaError::TypeMismatch(name.into())),
853            },
854            "HYST" => match value {
855                EpicsValue::Double(v) => {
856                    self.hyst = v;
857                    Ok(())
858                }
859                _ => Err(CaError::TypeMismatch(name.into())),
860            },
861            "ADEL" => match value {
862                EpicsValue::Double(v) => {
863                    self.adel = v;
864                    Ok(())
865                }
866                _ => Err(CaError::TypeMismatch(name.into())),
867            },
868            "MDEL" => match value {
869                EpicsValue::Double(v) => {
870                    self.mdel = v;
871                    Ok(())
872                }
873                _ => Err(CaError::TypeMismatch(name.into())),
874            },
875            // Read-only fields
876            "CVAL" | "CVLP" | "OVAL" | "OVLP" | "P" | "PP" | "D" | "DP" | "ERR" | "ERRP"
877            | "DTP" | "FBOP" | "LALM" | "ALST" | "MLST" => Err(CaError::ReadOnlyField(name.into())),
878            _ => Err(CaError::FieldNotFound(name.into())),
879        }
880    }
881
882    fn field_list(&self) -> &'static [FieldDesc] {
883        FIELDS
884    }
885
886    fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
887        Some(self)
888    }
889
890    fn set_device_did_compute(&mut self, did_compute: bool) {
891        self.device_did_compute = did_compute;
892    }
893
894    fn put_field_internal(
895        &mut self,
896        name: &str,
897        value: EpicsValue,
898    ) -> epics_base_rs::error::CaResult<()> {
899        // Bypass read-only checks for framework-internal writes (ReadDbLink).
900        // This allows the framework to write to CVAL, OVAL, etc. from link resolution.
901        match name {
902            "CVAL" => match value {
903                EpicsValue::Double(v) => {
904                    self.cval = v;
905                    Ok(())
906                }
907                _ => Err(CaError::TypeMismatch(name.into())),
908            },
909            "OVAL" => match value {
910                EpicsValue::Double(v) => {
911                    self.oval = v;
912                    Ok(())
913                }
914                _ => Err(CaError::TypeMismatch(name.into())),
915            },
916            "P" => match value {
917                EpicsValue::Double(v) => {
918                    self.p = v;
919                    Ok(())
920                }
921                _ => Err(CaError::TypeMismatch(name.into())),
922            },
923            "D" => match value {
924                EpicsValue::Double(v) => {
925                    self.d = v;
926                    Ok(())
927                }
928                _ => Err(CaError::TypeMismatch(name.into())),
929            },
930            "ERR" => match value {
931                EpicsValue::Double(v) => {
932                    self.err = v;
933                    Ok(())
934                }
935                _ => Err(CaError::TypeMismatch(name.into())),
936            },
937            _ => self.put_field(name, value),
938        }
939    }
940
941    fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
942        // INP -> CVAL is always resolved.
943        // STPL -> VAL is only resolved when SMSL == closed_loop (1).
944        // In supervisory mode (SMSL=0), the operator sets VAL directly
945        // and STPL must not overwrite it.
946        if self.smsl == 1 {
947            // closed_loop: fetch setpoint from STPL into VAL
948            static WITH_STPL: &[(&str, &str)] = &[("STPL", "VAL"), ("INP", "CVAL")];
949            WITH_STPL
950        } else {
951            // supervisory: VAL is set by operator, don't fetch STPL
952            static WITHOUT_STPL: &[(&str, &str)] = &[("INP", "CVAL")];
953            WITHOUT_STPL
954        }
955    }
956
957    fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
958        // OUTL -> OVAL (output link)
959        static LINKS: &[(&str, &str)] = &[("OUTL", "OVAL")];
960        LINKS
961    }
962}