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