Skip to main content

spvirit_server/
types.rs

1//! Server-specific record and IOC types.
2//!
3//! Shared Normative Type definitions (ScalarValue, NtScalar, NtPayload, etc.)
4//! live in the `spvirit-types` crate and are re-exported here for convenience.
5
6use std::collections::HashMap;
7use std::time::Duration;
8
9pub use spvirit_types::*;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum RecordType {
13    Ai,
14    Ao,
15    Bi,
16    Bo,
17    StringIn,
18    StringOut,
19    Waveform,
20    Aai,
21    Aao,
22    SubArray,
23    NtTable,
24    NtNdArray,
25    Mbbi,
26    Mbbo,
27    Generic,
28    LongIn,
29    LongOut,
30}
31
32impl RecordType {
33    // TODO(follow-up): .db parsing — longin/longout are not yet recognized
34    // here; wiring `.db`-loaded longin/longout records needs db.rs
35    // construction work (out of scope for the handle-API-only gap task).
36    pub fn from_db_name(name: &str) -> Option<Self> {
37        match name.to_ascii_lowercase().as_str() {
38            "ai" => Some(Self::Ai),
39            "ao" => Some(Self::Ao),
40            "bi" => Some(Self::Bi),
41            "bo" => Some(Self::Bo),
42            "stringin" => Some(Self::StringIn),
43            "stringout" => Some(Self::StringOut),
44            "waveform" => Some(Self::Waveform),
45            "aai" => Some(Self::Aai),
46            "aao" => Some(Self::Aao),
47            "subarray" => Some(Self::SubArray),
48            "mbbi" | "ntenum" => Some(Self::Mbbi),
49            "mbbo" => Some(Self::Mbbo),
50            _ => None,
51        }
52    }
53
54    pub fn is_output(&self) -> bool {
55        matches!(
56            self,
57            Self::Ao | Self::Bo | Self::StringOut | Self::Aao | Self::Mbbo | Self::LongOut
58        )
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ScanMode {
64    Passive,
65    Periodic(Duration),
66    Event(String),
67    IoEvent(String),
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub enum LinkExpr {
72    Constant(ScalarValue),
73    DbLink {
74        target: String,
75        process_passive: bool,
76        maximize_severity: bool,
77    },
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum OutputMode {
82    Supervisory,
83    ClosedLoop,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct DbCommonState {
88    pub desc: String,
89    pub scan: ScanMode,
90    pub pini: bool,
91    pub phas: i32,
92    pub pact: bool,
93    pub disa: bool,
94    pub sdis: Option<LinkExpr>,
95    pub diss: i32,
96    pub flnk: Option<LinkExpr>,
97}
98
99impl Default for DbCommonState {
100    fn default() -> Self {
101        Self {
102            desc: String::new(),
103            scan: ScanMode::Passive,
104            pini: false,
105            phas: 0,
106            pact: false,
107            disa: false,
108            sdis: None,
109            diss: 0,
110            flnk: None,
111        }
112    }
113}
114
115#[derive(Debug, Clone, PartialEq)]
116pub enum RecordData {
117    Ai {
118        nt: NtScalar,
119        inp: Option<LinkExpr>,
120        siml: Option<LinkExpr>,
121        siol: Option<LinkExpr>,
122        simm: bool,
123    },
124    Ao {
125        nt: NtScalar,
126        out: Option<LinkExpr>,
127        dol: Option<LinkExpr>,
128        omsl: OutputMode,
129        drvl: Option<f64>,
130        drvh: Option<f64>,
131        oroc: Option<f64>,
132        siml: Option<LinkExpr>,
133        siol: Option<LinkExpr>,
134        simm: bool,
135    },
136    Bi {
137        nt: NtScalar,
138        inp: Option<LinkExpr>,
139        znam: String,
140        onam: String,
141        siml: Option<LinkExpr>,
142        siol: Option<LinkExpr>,
143        simm: bool,
144    },
145    Bo {
146        nt: NtScalar,
147        out: Option<LinkExpr>,
148        dol: Option<LinkExpr>,
149        omsl: OutputMode,
150        znam: String,
151        onam: String,
152        siml: Option<LinkExpr>,
153        siol: Option<LinkExpr>,
154        simm: bool,
155    },
156    StringIn {
157        nt: NtScalar,
158        inp: Option<LinkExpr>,
159        siml: Option<LinkExpr>,
160        siol: Option<LinkExpr>,
161        simm: bool,
162    },
163    StringOut {
164        nt: NtScalar,
165        out: Option<LinkExpr>,
166        dol: Option<LinkExpr>,
167        omsl: OutputMode,
168        siml: Option<LinkExpr>,
169        siol: Option<LinkExpr>,
170        simm: bool,
171    },
172    Waveform {
173        nt: NtScalarArray,
174        inp: Option<LinkExpr>,
175        ftvl: String,
176        nelm: usize,
177        nord: usize,
178    },
179    Aai {
180        nt: NtScalarArray,
181        inp: Option<LinkExpr>,
182        ftvl: String,
183        nelm: usize,
184        nord: usize,
185    },
186    Aao {
187        nt: NtScalarArray,
188        out: Option<LinkExpr>,
189        dol: Option<LinkExpr>,
190        omsl: OutputMode,
191        ftvl: String,
192        nelm: usize,
193        nord: usize,
194    },
195    SubArray {
196        nt: NtScalarArray,
197        inp: Option<LinkExpr>,
198        ftvl: String,
199        malm: usize,
200        nelm: usize,
201        nord: usize,
202        indx: usize,
203    },
204    NtTable {
205        nt: NtTable,
206        inp: Option<LinkExpr>,
207        out: Option<LinkExpr>,
208        omsl: OutputMode,
209    },
210    NtNdArray {
211        nt: NtNdArray,
212        inp: Option<LinkExpr>,
213        out: Option<LinkExpr>,
214        omsl: OutputMode,
215    },
216    NtEnum {
217        nt: NtEnum,
218        inp: Option<LinkExpr>,
219        out: Option<LinkExpr>,
220        omsl: OutputMode,
221    },
222    Generic {
223        struct_id: String,
224        fields: Vec<(String, PvValue)>,
225        inp: Option<LinkExpr>,
226        out: Option<LinkExpr>,
227        omsl: OutputMode,
228    },
229}
230
231impl RecordData {
232    pub fn nt(&self) -> &NtScalar {
233        match self {
234            Self::Ai { nt, .. } => nt,
235            Self::Ao { nt, .. } => nt,
236            Self::Bi { nt, .. } => nt,
237            Self::Bo { nt, .. } => nt,
238            Self::StringIn { nt, .. } => nt,
239            Self::StringOut { nt, .. } => nt,
240            _ => panic!("record variant does not expose NtScalar"),
241        }
242    }
243
244    pub fn nt_mut(&mut self) -> &mut NtScalar {
245        match self {
246            Self::Ai { nt, .. } => nt,
247            Self::Ao { nt, .. } => nt,
248            Self::Bi { nt, .. } => nt,
249            Self::Bo { nt, .. } => nt,
250            Self::StringIn { nt, .. } => nt,
251            Self::StringOut { nt, .. } => nt,
252            _ => panic!("record variant does not expose NtScalar"),
253        }
254    }
255
256    pub fn payload(&self) -> NtPayload {
257        match self {
258            Self::Ai { nt, .. }
259            | Self::Ao { nt, .. }
260            | Self::Bi { nt, .. }
261            | Self::Bo { nt, .. }
262            | Self::StringIn { nt, .. }
263            | Self::StringOut { nt, .. } => NtPayload::Scalar(nt.clone()),
264            Self::Waveform { nt, .. }
265            | Self::Aai { nt, .. }
266            | Self::Aao { nt, .. }
267            | Self::SubArray { nt, .. } => NtPayload::ScalarArray(nt.clone()),
268            Self::NtTable { nt, .. } => NtPayload::Table(nt.clone()),
269            Self::NtNdArray { nt, .. } => NtPayload::NdArray(nt.clone()),
270            Self::NtEnum { nt, .. } => NtPayload::Enum(nt.clone()),
271            Self::Generic {
272                struct_id, fields, ..
273            } => NtPayload::Generic {
274                struct_id: struct_id.clone(),
275                fields: fields.clone(),
276            },
277        }
278    }
279}
280
281#[derive(Debug, Clone, PartialEq)]
282pub struct RecordInstance {
283    pub name: String,
284    pub record_type: RecordType,
285    pub common: DbCommonState,
286    pub data: RecordData,
287    pub raw_fields: HashMap<String, String>,
288}
289
290impl RecordInstance {
291    pub fn writable(&self) -> bool {
292        if self.record_type.is_output() {
293            return true;
294        }
295        match &self.data {
296            RecordData::Ai { simm: true, .. } => true,
297            RecordData::Waveform { .. } => true,
298            RecordData::NtTable { .. } => true,
299            RecordData::NtNdArray { .. } => true,
300            RecordData::NtEnum { .. } => true,
301            RecordData::Generic { .. } => true,
302            _ => false,
303        }
304    }
305
306    pub fn to_ntpayload(&self) -> NtPayload {
307        self.data.payload()
308    }
309
310    pub fn to_ntscalar(&self) -> NtScalar {
311        match self.to_ntpayload() {
312            NtPayload::Scalar(nt) => nt,
313            NtPayload::ScalarArray(nt) => {
314                let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.value.len() as i32));
315                scalar.display_description = "Array length".to_string();
316                scalar
317            }
318            NtPayload::Table(nt) => {
319                let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.columns.len() as i32));
320                scalar.display_description = "Table columns".to_string();
321                scalar
322            }
323            NtPayload::NdArray(nt) => {
324                let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.dimension.len() as i32));
325                scalar.display_description = "NDArray dimensions".to_string();
326                scalar
327            }
328            NtPayload::Enum(nt) => {
329                let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.index));
330                scalar.display_description = nt.selected().unwrap_or("").to_string();
331                scalar
332            }
333            NtPayload::Generic { fields, .. } => {
334                let mut scalar = NtScalar::from_value(ScalarValue::I32(fields.len() as i32));
335                scalar.display_description = "Generic structure".to_string();
336                scalar
337            }
338        }
339    }
340    //
341    pub fn nt_mut(&mut self) -> &mut NtScalar {
342        self.data.nt_mut()
343    }
344
345    /// Fallible mutable access to the record's `NtScalar`, for record variants
346    /// that carry one (Ai, Ao, Bi, Bo, StringIn, StringOut). `None` otherwise.
347    pub(crate) fn nt_scalar_mut(&mut self) -> Option<&mut NtScalar> {
348        match &mut self.data {
349            RecordData::Ai { nt, .. }
350            | RecordData::Ao { nt, .. }
351            | RecordData::Bi { nt, .. }
352            | RecordData::Bo { nt, .. }
353            | RecordData::StringIn { nt, .. }
354            | RecordData::StringOut { nt, .. } => Some(nt),
355            _ => None,
356        }
357    }
358
359    pub fn current_value(&self) -> ScalarValue {
360        match self.to_ntpayload() {
361            NtPayload::Scalar(nt) => nt.value,
362            NtPayload::ScalarArray(nt) => ScalarValue::I32(nt.value.len() as i32),
363            NtPayload::Table(nt) => ScalarValue::I32(nt.columns.len() as i32),
364            NtPayload::NdArray(nt) => ScalarValue::I32(nt.dimension.len() as i32),
365            NtPayload::Enum(nt) => ScalarValue::I32(nt.index),
366            NtPayload::Generic { fields, .. } => ScalarValue::I32(fields.len() as i32),
367        }
368    }
369
370    pub fn set_scalar_value(&mut self, value: ScalarValue, compute_alarms: bool) -> bool {
371        if let RecordData::NtEnum { nt, .. } = &mut self.data {
372            let idx = match &value {
373                ScalarValue::I32(i) => *i,
374                ScalarValue::I16(i) => *i as i32,
375                ScalarValue::I8(i) => *i as i32,
376                ScalarValue::I64(i) => *i as i32,
377                _ => return false,
378            };
379            if idx < 0 || (idx as usize) >= nt.choices.len() {
380                return false; // out-of-range index — reject, keep value
381            }
382            if nt.index == idx {
383                return false;
384            }
385            nt.index = idx;
386            return true;
387        }
388
389        let nt = match &mut self.data {
390            RecordData::Ai { nt, .. }
391            | RecordData::Ao { nt, .. }
392            | RecordData::Bi { nt, .. }
393            | RecordData::Bo { nt, .. }
394            | RecordData::StringIn { nt, .. }
395            | RecordData::StringOut { nt, .. } => nt,
396            _ => return false,
397        };
398
399        let changed = match (&mut nt.value, value) {
400            (ScalarValue::Bool(current), ScalarValue::Bool(v)) => {
401                if *current == v {
402                    false
403                } else {
404                    *current = v;
405                    true
406                }
407            }
408            (ScalarValue::I32(current), ScalarValue::I32(v)) => {
409                if *current == v {
410                    false
411                } else {
412                    *current = v;
413                    true
414                }
415            }
416            (ScalarValue::F64(current), ScalarValue::F64(v)) => {
417                if (*current - v).abs() < f64::EPSILON {
418                    false
419                } else {
420                    *current = v;
421                    true
422                }
423            }
424            (ScalarValue::Str(current), ScalarValue::Str(v)) => {
425                if *current == v {
426                    false
427                } else {
428                    *current = v;
429                    true
430                }
431            }
432            (ScalarValue::Bool(current), ScalarValue::I32(v)) => {
433                let next = v != 0;
434                if *current == next {
435                    false
436                } else {
437                    *current = next;
438                    true
439                }
440            }
441            (ScalarValue::Bool(current), ScalarValue::F64(v)) => {
442                let next = v != 0.0;
443                if *current == next {
444                    false
445                } else {
446                    *current = next;
447                    true
448                }
449            }
450            (ScalarValue::I32(current), ScalarValue::Bool(v)) => {
451                let next = if v { 1 } else { 0 };
452                if *current == next {
453                    false
454                } else {
455                    *current = next;
456                    true
457                }
458            }
459            (ScalarValue::I32(current), ScalarValue::F64(v)) => {
460                let next = v as i32;
461                if *current == next {
462                    false
463                } else {
464                    *current = next;
465                    true
466                }
467            }
468            (ScalarValue::F64(current), ScalarValue::Bool(v)) => {
469                let next = if v { 1.0 } else { 0.0 };
470                if (*current - next).abs() < f64::EPSILON {
471                    false
472                } else {
473                    *current = next;
474                    true
475                }
476            }
477            (ScalarValue::F64(current), ScalarValue::I32(v)) => {
478                let next = v as f64;
479                if (*current - next).abs() < f64::EPSILON {
480                    false
481                } else {
482                    *current = next;
483                    true
484                }
485            }
486            (ScalarValue::Str(current), ScalarValue::Bool(v)) => {
487                let next = if v { "1" } else { "0" }.to_string();
488                if *current == next {
489                    false
490                } else {
491                    *current = next;
492                    true
493                }
494            }
495            (ScalarValue::Str(current), ScalarValue::I32(v)) => {
496                let next = v.to_string();
497                if *current == next {
498                    false
499                } else {
500                    *current = next;
501                    true
502                }
503            }
504            (ScalarValue::Str(current), ScalarValue::F64(v)) => {
505                let next = v.to_string();
506                if *current == next {
507                    false
508                } else {
509                    *current = next;
510                    true
511                }
512            }
513            (ScalarValue::Bool(current), ScalarValue::Str(v)) => {
514                let next = parse_bool_like(&v).unwrap_or(*current);
515                if *current == next {
516                    false
517                } else {
518                    *current = next;
519                    true
520                }
521            }
522            (ScalarValue::I32(current), ScalarValue::Str(v)) => {
523                let next = v.parse::<i32>().unwrap_or(*current);
524                if *current == next {
525                    false
526                } else {
527                    *current = next;
528                    true
529                }
530            }
531            (ScalarValue::F64(current), ScalarValue::Str(v)) => {
532                let next = v.parse::<f64>().unwrap_or(*current);
533                if (*current - next).abs() < f64::EPSILON {
534                    false
535                } else {
536                    *current = next;
537                    true
538                }
539            }
540            // Handle all remaining numeric ScalarValue variants by coercing to
541            // the target's type via f64.
542            (target, other) => {
543                let as_f64 = match &other {
544                    ScalarValue::I8(v) => *v as f64,
545                    ScalarValue::I16(v) => *v as f64,
546                    ScalarValue::I64(v) => *v as f64,
547                    ScalarValue::U8(v) => *v as f64,
548                    ScalarValue::U16(v) => *v as f64,
549                    ScalarValue::U32(v) => *v as f64,
550                    ScalarValue::U64(v) => *v as f64,
551                    ScalarValue::F32(v) => *v as f64,
552                    _ => return false,
553                };
554                match target {
555                    ScalarValue::Bool(current) => {
556                        let next = as_f64 != 0.0;
557                        if *current == next {
558                            false
559                        } else {
560                            *current = next;
561                            true
562                        }
563                    }
564                    ScalarValue::I32(current) => {
565                        let next = as_f64 as i32;
566                        if *current == next {
567                            false
568                        } else {
569                            *current = next;
570                            true
571                        }
572                    }
573                    ScalarValue::F64(current) => {
574                        if (*current - as_f64).abs() < f64::EPSILON {
575                            false
576                        } else {
577                            *current = as_f64;
578                            true
579                        }
580                    }
581                    ScalarValue::Str(current) => {
582                        let next = as_f64.to_string();
583                        if *current == next {
584                            false
585                        } else {
586                            *current = next;
587                            true
588                        }
589                    }
590                    _ => false,
591                }
592            }
593        };
594        if changed && compute_alarms {
595            nt.update_alarm_from_value();
596        }
597        changed
598    }
599
600    pub fn set_array_value(&mut self, value: ScalarArrayValue) -> bool {
601        let (nt, nord, nelm) = match &mut self.data {
602            RecordData::Waveform { nt, nord, nelm, .. }
603            | RecordData::Aai { nt, nord, nelm, .. }
604            | RecordData::Aao { nt, nord, nelm, .. }
605            | RecordData::SubArray { nt, nord, nelm, .. } => (nt, nord, *nelm),
606            _ => return false,
607        };
608
609        let mut next = value;
610        truncate_scalar_array_to_nelm(&mut next, nelm);
611        if nt.value == next {
612            return false;
613        }
614
615        *nord = next.len();
616        nt.value = next;
617        true
618    }
619
620    pub fn set_nt_payload(&mut self, payload: NtPayload) -> bool {
621        match (&mut self.data, payload) {
622            (
623                RecordData::Ai { nt, .. }
624                | RecordData::Ao { nt, .. }
625                | RecordData::Bi { nt, .. }
626                | RecordData::Bo { nt, .. }
627                | RecordData::StringIn { nt, .. }
628                | RecordData::StringOut { nt, .. },
629                NtPayload::Scalar(next),
630            ) => {
631                if *nt == next {
632                    false
633                } else {
634                    *nt = next;
635                    true
636                }
637            }
638            (
639                RecordData::Waveform { nt, nord, nelm, .. }
640                | RecordData::Aai { nt, nord, nelm, .. }
641                | RecordData::Aao { nt, nord, nelm, .. }
642                | RecordData::SubArray { nt, nord, nelm, .. },
643                NtPayload::ScalarArray(mut next),
644            ) => {
645                truncate_scalar_array_to_nelm(&mut next.value, *nelm);
646                let next_len = next.value.len();
647                if *nt == next {
648                    false
649                } else {
650                    *nord = next_len;
651                    *nt = next;
652                    true
653                }
654            }
655            (RecordData::NtTable { nt, .. }, NtPayload::Table(next)) => {
656                if next.validate().is_err() || *nt == next {
657                    false
658                } else {
659                    *nt = next;
660                    true
661                }
662            }
663            (RecordData::NtNdArray { nt, .. }, NtPayload::NdArray(next)) => {
664                if next.validate().is_err() || *nt == next {
665                    false
666                } else {
667                    *nt = next;
668                    true
669                }
670            }
671            (RecordData::NtEnum { nt, .. }, NtPayload::Enum(next)) => {
672                if *nt == next {
673                    false
674                } else {
675                    *nt = next;
676                    true
677                }
678            }
679            (
680                RecordData::Generic {
681                    struct_id, fields, ..
682                },
683                NtPayload::Generic {
684                    struct_id: next_id,
685                    fields: next_fields,
686                },
687            ) => {
688                if *struct_id == next_id && *fields == next_fields {
689                    false
690                } else {
691                    *struct_id = next_id;
692                    *fields = next_fields;
693                    true
694                }
695            }
696            _ => false,
697        }
698    }
699}
700
701fn truncate_scalar_array_to_nelm(value: &mut ScalarArrayValue, nelm: usize) {
702    match value {
703        ScalarArrayValue::Bool(v) => v.truncate(nelm),
704        ScalarArrayValue::I8(v) => v.truncate(nelm),
705        ScalarArrayValue::I16(v) => v.truncate(nelm),
706        ScalarArrayValue::I32(v) => v.truncate(nelm),
707        ScalarArrayValue::I64(v) => v.truncate(nelm),
708        ScalarArrayValue::U8(v) => v.truncate(nelm),
709        ScalarArrayValue::U16(v) => v.truncate(nelm),
710        ScalarArrayValue::U32(v) => v.truncate(nelm),
711        ScalarArrayValue::U64(v) => v.truncate(nelm),
712        ScalarArrayValue::F32(v) => v.truncate(nelm),
713        ScalarArrayValue::F64(v) => v.truncate(nelm),
714        ScalarArrayValue::Str(v) => v.truncate(nelm),
715    }
716}
717
718fn parse_bool_like(input: &str) -> Option<bool> {
719    match input.trim().to_ascii_lowercase().as_str() {
720        "1" | "true" | "yes" | "on" => Some(true),
721        "0" | "false" | "no" | "off" => Some(false),
722        _ => None,
723    }
724}