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        }
294    }
295    // 
296    pub fn nt_mut(&mut self) -> &mut NtScalar {
297        self.data.nt_mut()
298    }
299
300    pub fn current_value(&self) -> ScalarValue {
301        match self.to_ntpayload() {
302            NtPayload::Scalar(nt) => nt.value,
303            NtPayload::ScalarArray(nt) => ScalarValue::I32(nt.value.len() as i32),
304            NtPayload::Table(nt) => ScalarValue::I32(nt.columns.len() as i32),
305            NtPayload::NdArray(nt) => ScalarValue::I32(nt.dimension.len() as i32),
306        }
307    }
308
309    pub fn set_scalar_value(&mut self, value: ScalarValue, compute_alarms: bool) -> bool {
310        let nt = match &mut self.data {
311            RecordData::Ai { nt, .. }
312            | RecordData::Ao { nt, .. }
313            | RecordData::Bi { nt, .. }
314            | RecordData::Bo { nt, .. }
315            | RecordData::StringIn { nt, .. }
316            | RecordData::StringOut { nt, .. } => nt,
317            _ => return false,
318        };
319
320        let changed = match (&mut nt.value, value) {
321            (ScalarValue::Bool(current), ScalarValue::Bool(v)) => {
322                if *current == v {
323                    false
324                } else {
325                    *current = v;
326                    true
327                }
328            }
329            (ScalarValue::I32(current), ScalarValue::I32(v)) => {
330                if *current == v {
331                    false
332                } else {
333                    *current = v;
334                    true
335                }
336            }
337            (ScalarValue::F64(current), ScalarValue::F64(v)) => {
338                if (*current - v).abs() < f64::EPSILON {
339                    false
340                } else {
341                    *current = v;
342                    true
343                }
344            }
345            (ScalarValue::Str(current), ScalarValue::Str(v)) => {
346                if *current == v {
347                    false
348                } else {
349                    *current = v;
350                    true
351                }
352            }
353            (ScalarValue::Bool(current), ScalarValue::I32(v)) => {
354                let next = v != 0;
355                if *current == next {
356                    false
357                } else {
358                    *current = next;
359                    true
360                }
361            }
362            (ScalarValue::Bool(current), ScalarValue::F64(v)) => {
363                let next = v != 0.0;
364                if *current == next {
365                    false
366                } else {
367                    *current = next;
368                    true
369                }
370            }
371            (ScalarValue::I32(current), ScalarValue::Bool(v)) => {
372                let next = if v { 1 } else { 0 };
373                if *current == next {
374                    false
375                } else {
376                    *current = next;
377                    true
378                }
379            }
380            (ScalarValue::I32(current), ScalarValue::F64(v)) => {
381                let next = v as i32;
382                if *current == next {
383                    false
384                } else {
385                    *current = next;
386                    true
387                }
388            }
389            (ScalarValue::F64(current), ScalarValue::Bool(v)) => {
390                let next = if v { 1.0 } else { 0.0 };
391                if (*current - next).abs() < f64::EPSILON {
392                    false
393                } else {
394                    *current = next;
395                    true
396                }
397            }
398            (ScalarValue::F64(current), ScalarValue::I32(v)) => {
399                let next = v as f64;
400                if (*current - next).abs() < f64::EPSILON {
401                    false
402                } else {
403                    *current = next;
404                    true
405                }
406            }
407            (ScalarValue::Str(current), ScalarValue::Bool(v)) => {
408                let next = if v { "1" } else { "0" }.to_string();
409                if *current == next {
410                    false
411                } else {
412                    *current = next;
413                    true
414                }
415            }
416            (ScalarValue::Str(current), ScalarValue::I32(v)) => {
417                let next = v.to_string();
418                if *current == next {
419                    false
420                } else {
421                    *current = next;
422                    true
423                }
424            }
425            (ScalarValue::Str(current), ScalarValue::F64(v)) => {
426                let next = v.to_string();
427                if *current == next {
428                    false
429                } else {
430                    *current = next;
431                    true
432                }
433            }
434            (ScalarValue::Bool(current), ScalarValue::Str(v)) => {
435                let next = parse_bool_like(&v).unwrap_or(*current);
436                if *current == next {
437                    false
438                } else {
439                    *current = next;
440                    true
441                }
442            }
443            (ScalarValue::I32(current), ScalarValue::Str(v)) => {
444                let next = v.parse::<i32>().unwrap_or(*current);
445                if *current == next {
446                    false
447                } else {
448                    *current = next;
449                    true
450                }
451            }
452            (ScalarValue::F64(current), ScalarValue::Str(v)) => {
453                let next = v.parse::<f64>().unwrap_or(*current);
454                if (*current - next).abs() < f64::EPSILON {
455                    false
456                } else {
457                    *current = next;
458                    true
459                }
460            }
461            // Handle all remaining numeric ScalarValue variants by coercing to
462            // the target's type via f64.
463            (target, other) => {
464                let as_f64 = match &other {
465                    ScalarValue::I8(v) => *v as f64,
466                    ScalarValue::I16(v) => *v as f64,
467                    ScalarValue::I64(v) => *v as f64,
468                    ScalarValue::U8(v) => *v as f64,
469                    ScalarValue::U16(v) => *v as f64,
470                    ScalarValue::U32(v) => *v as f64,
471                    ScalarValue::U64(v) => *v as f64,
472                    ScalarValue::F32(v) => *v as f64,
473                    _ => return false,
474                };
475                match target {
476                    ScalarValue::Bool(current) => {
477                        let next = as_f64 != 0.0;
478                        if *current == next { false } else { *current = next; true }
479                    }
480                    ScalarValue::I32(current) => {
481                        let next = as_f64 as i32;
482                        if *current == next { false } else { *current = next; true }
483                    }
484                    ScalarValue::F64(current) => {
485                        if (*current - as_f64).abs() < f64::EPSILON { false } else { *current = as_f64; true }
486                    }
487                    ScalarValue::Str(current) => {
488                        let next = as_f64.to_string();
489                        if *current == next { false } else { *current = next; true }
490                    }
491                    _ => false,
492                }
493            }
494        };
495        if changed && compute_alarms {
496            nt.update_alarm_from_value();
497        }
498        changed
499    }
500}
501
502fn parse_bool_like(input: &str) -> Option<bool> {
503    match input.trim().to_ascii_lowercase().as_str() {
504        "1" | "true" | "yes" | "on" => Some(true),
505        "0" | "false" | "no" | "off" => Some(false),
506        _ => None,
507    }
508}