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