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