Skip to main content

spvirit_types/
lib.rs

1//! Shared Normative Type (NT) data model types.
2//!
3//! These types represent the PVAccess Normative Types used across the
4//! codec, client tools, server, and packet capture subsystems.
5
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum ScalarValue {
10    Bool(bool),
11    I8(i8),
12    I16(i16),
13    I32(i32),
14    I64(i64),
15    U8(u8),
16    U16(u16),
17    U32(u32),
18    U64(u64),
19    F32(f32),
20    F64(f64),
21    Str(String),
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum ScalarArrayValue {
26    Bool(Vec<bool>),
27    I8(Vec<i8>),
28    I16(Vec<i16>),
29    I32(Vec<i32>),
30    I64(Vec<i64>),
31    U8(Vec<u8>),
32    U16(Vec<u16>),
33    U32(Vec<u32>),
34    U64(Vec<u64>),
35    F32(Vec<f32>),
36    F64(Vec<f64>),
37    Str(Vec<String>),
38}
39
40impl ScalarArrayValue {
41    pub fn len(&self) -> usize {
42        match self {
43            Self::Bool(v) => v.len(),
44            Self::I8(v) => v.len(),
45            Self::I16(v) => v.len(),
46            Self::I32(v) => v.len(),
47            Self::I64(v) => v.len(),
48            Self::U8(v) => v.len(),
49            Self::U16(v) => v.len(),
50            Self::U32(v) => v.len(),
51            Self::U64(v) => v.len(),
52            Self::F32(v) => v.len(),
53            Self::F64(v) => v.len(),
54            Self::Str(v) => v.len(),
55        }
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.len() == 0
60    }
61
62    pub fn element_size_bytes(&self) -> usize {
63        match self {
64            Self::Bool(_) => 1,
65            Self::I8(_) => 1,
66            Self::I16(_) => 2,
67            Self::I32(_) => 4,
68            Self::I64(_) => 8,
69            Self::U8(_) => 1,
70            Self::U16(_) => 2,
71            Self::U32(_) => 4,
72            Self::U64(_) => 8,
73            Self::F32(_) => 4,
74            Self::F64(_) => 8,
75            Self::Str(v) => v.iter().map(|s| s.len()).sum(),
76        }
77    }
78
79    pub fn type_label(&self) -> &'static str {
80        match self {
81            Self::Bool(_) => "boolean[]",
82            Self::I8(_) => "byte[]",
83            Self::I16(_) => "short[]",
84            Self::I32(_) => "int[]",
85            Self::I64(_) => "long[]",
86            Self::U8(_) => "ubyte[]",
87            Self::U16(_) => "ushort[]",
88            Self::U32(_) => "uint[]",
89            Self::U64(_) => "ulong[]",
90            Self::F32(_) => "float[]",
91            Self::F64(_) => "double[]",
92            Self::Str(_) => "string[]",
93        }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Default)]
98pub struct NtAlarm {
99    pub severity: i32,
100    pub status: i32,
101    pub message: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Default)]
105pub struct NtTimeStamp {
106    pub seconds_past_epoch: i64,
107    pub nanoseconds: i32,
108    pub user_tag: i32,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub struct NtDisplay {
113    pub limit_low: f64,
114    pub limit_high: f64,
115    pub description: String,
116    pub units: String,
117    pub precision: i32,
118}
119
120impl Default for NtDisplay {
121    fn default() -> Self {
122        Self {
123            limit_low: 0.0,
124            limit_high: 0.0,
125            description: String::new(),
126            units: String::new(),
127            precision: 0,
128        }
129    }
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct NtControl {
134    pub limit_low: f64,
135    pub limit_high: f64,
136    pub min_step: f64,
137}
138
139impl Default for NtControl {
140    fn default() -> Self {
141        Self {
142            limit_low: 0.0,
143            limit_high: 0.0,
144            min_step: 0.0,
145        }
146    }
147}
148
149#[derive(Debug, Clone, PartialEq)]
150pub struct NtScalar {
151    pub value: ScalarValue,
152    pub alarm_severity: i32,
153    pub alarm_status: i32,
154    pub alarm_message: String,
155    pub alarm_low: Option<f64>,
156    pub alarm_high: Option<f64>,
157    pub alarm_lolo: Option<f64>,
158    pub alarm_hihi: Option<f64>,
159    pub display_low: f64,
160    pub display_high: f64,
161    pub display_description: String,
162    pub display_precision: i32,
163    pub display_form_index: i32,
164    pub display_form_choices: Vec<String>,
165    pub control_low: f64,
166    pub control_high: f64,
167    pub control_min_step: f64,
168    pub units: String,
169    pub value_alarm_active: bool,
170    pub value_alarm_low_alarm_limit: f64,
171    pub value_alarm_low_warning_limit: f64,
172    pub value_alarm_high_warning_limit: f64,
173    pub value_alarm_high_alarm_limit: f64,
174    pub value_alarm_low_alarm_severity: i32,
175    pub value_alarm_low_warning_severity: i32,
176    pub value_alarm_high_warning_severity: i32,
177    pub value_alarm_high_alarm_severity: i32,
178    pub value_alarm_hysteresis: u8,
179}
180
181impl NtScalar {
182    pub fn from_value(value: ScalarValue) -> Self {
183        Self {
184            value,
185            alarm_severity: 0,
186            alarm_status: 0,
187            alarm_message: String::new(),
188            alarm_low: None,
189            alarm_high: None,
190            alarm_lolo: None,
191            alarm_hihi: None,
192            display_low: 0.0,
193            display_high: 0.0,
194            display_description: String::new(),
195            display_precision: 0,
196            display_form_index: 0,
197            display_form_choices: default_form_choices(),
198            control_low: 0.0,
199            control_high: 0.0,
200            control_min_step: 0.0,
201            units: String::new(),
202            value_alarm_active: false,
203            value_alarm_low_alarm_limit: 0.0,
204            value_alarm_low_warning_limit: 0.0,
205            value_alarm_high_warning_limit: 0.0,
206            value_alarm_high_alarm_limit: 0.0,
207            value_alarm_low_alarm_severity: 0,
208            value_alarm_low_warning_severity: 0,
209            value_alarm_high_warning_severity: 0,
210            value_alarm_high_alarm_severity: 0,
211            value_alarm_hysteresis: 0,
212        }
213    }
214
215    pub fn with_limits(mut self, low: f64, high: f64) -> Self {
216        self.display_low = low;
217        self.display_high = high;
218        self.control_low = low;
219        self.control_high = high;
220        self
221    }
222
223    pub fn with_units(mut self, units: String) -> Self {
224        self.units = units;
225        self
226    }
227
228    pub fn with_description(mut self, description: String) -> Self {
229        self.display_description = description;
230        self
231    }
232
233    pub fn with_precision(mut self, precision: i32) -> Self {
234        self.display_precision = precision;
235        self
236    }
237
238    pub fn with_alarm_limits(
239        mut self,
240        low: Option<f64>,
241        high: Option<f64>,
242        lolo: Option<f64>,
243        hihi: Option<f64>,
244    ) -> Self {
245        self.alarm_low = low;
246        self.alarm_high = high;
247        self.alarm_lolo = lolo;
248        self.alarm_hihi = hihi;
249        if let Some(v) = low {
250            self.value_alarm_low_warning_limit = v;
251        }
252        if let Some(v) = high {
253            self.value_alarm_high_warning_limit = v;
254        }
255        if let Some(v) = lolo {
256            self.value_alarm_low_alarm_limit = v;
257        }
258        if let Some(v) = hihi {
259            self.value_alarm_high_alarm_limit = v;
260        }
261        self
262    }
263
264    pub fn update_alarm_from_value(&mut self) {
265        let val = match self.value {
266            ScalarValue::F64(v) => v,
267            ScalarValue::F32(v) => v as f64,
268            ScalarValue::I8(v) => v as f64,
269            ScalarValue::I16(v) => v as f64,
270            ScalarValue::I32(v) => v as f64,
271            ScalarValue::I64(v) => v as f64,
272            ScalarValue::U8(v) => v as f64,
273            ScalarValue::U16(v) => v as f64,
274            ScalarValue::U32(v) => v as f64,
275            ScalarValue::U64(v) => v as f64,
276            _ => {
277                self.alarm_severity = 0;
278                self.alarm_status = 0;
279                self.alarm_message.clear();
280                return;
281            }
282        };
283
284        let mut severity = 0;
285        let mut message = String::new();
286
287        if let Some(hihi) = self.alarm_hihi {
288            if val >= hihi {
289                severity = 2;
290                message = "HIHI".to_string();
291            }
292        }
293        if severity == 0 {
294            if let Some(high) = self.alarm_high {
295                if val >= high {
296                    severity = 1;
297                    message = "HIGH".to_string();
298                }
299            }
300        }
301        if severity == 0 {
302            if let Some(lolo) = self.alarm_lolo {
303                if val <= lolo {
304                    severity = 2;
305                    message = "LOLO".to_string();
306                }
307            }
308        }
309        if severity == 0 {
310            if let Some(low) = self.alarm_low {
311                if val <= low {
312                    severity = 1;
313                    message = "LOW".to_string();
314                }
315            }
316        }
317
318        if severity == 0 {
319            self.alarm_severity = 0;
320            self.alarm_status = 0;
321            self.alarm_message.clear();
322        } else {
323            self.alarm_severity = severity;
324            self.alarm_status = 1;
325            self.alarm_message = message;
326        }
327    }
328}
329
330#[derive(Debug, Clone, PartialEq)]
331pub struct NtScalarArray {
332    pub value: ScalarArrayValue,
333    pub alarm: NtAlarm,
334    pub time_stamp: NtTimeStamp,
335    pub display: NtDisplay,
336    pub control: NtControl,
337}
338
339impl NtScalarArray {
340    pub fn from_value(value: ScalarArrayValue) -> Self {
341        Self {
342            value,
343            alarm: NtAlarm::default(),
344            time_stamp: NtTimeStamp::default(),
345            display: NtDisplay::default(),
346            control: NtControl::default(),
347        }
348    }
349}
350
351#[derive(Debug, Clone, PartialEq)]
352pub struct NtTableColumn {
353    pub name: String,
354    pub values: ScalarArrayValue,
355}
356
357#[derive(Debug, Clone, PartialEq)]
358pub struct NtTable {
359    pub labels: Vec<String>,
360    pub columns: Vec<NtTableColumn>,
361    pub descriptor: Option<String>,
362    pub alarm: Option<NtAlarm>,
363    pub time_stamp: Option<NtTimeStamp>,
364}
365
366impl NtTable {
367    pub fn validate(&self) -> Result<(), String> {
368        let mut expected_len: Option<usize> = None;
369        for col in &self.columns {
370            let len = col.values.len();
371            if let Some(expected) = expected_len {
372                if expected != len {
373                    return Err(format!(
374                        "table column '{}' length {} does not match expected {}",
375                        col.name, len, expected
376                    ));
377                }
378            } else {
379                expected_len = Some(len);
380            }
381        }
382        Ok(())
383    }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct NdCodec {
388    pub name: String,
389    pub parameters: HashMap<String, String>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct NdDimension {
394    pub size: i32,
395    pub offset: i32,
396    pub full_size: i32,
397    pub binning: i32,
398    pub reverse: bool,
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct NtAttribute {
403    pub name: String,
404    pub value: ScalarValue,
405    pub descriptor: String,
406    pub source_type: i32,
407    pub source: String,
408}
409
410#[derive(Debug, Clone, PartialEq)]
411pub struct NtNdArray {
412    pub value: ScalarArrayValue,
413    pub codec: NdCodec,
414    pub compressed_size: i64,
415    pub uncompressed_size: i64,
416    pub dimension: Vec<NdDimension>,
417    pub unique_id: i32,
418    pub data_time_stamp: NtTimeStamp,
419    pub attribute: Vec<NtAttribute>,
420    pub descriptor: Option<String>,
421    pub alarm: Option<NtAlarm>,
422    pub time_stamp: Option<NtTimeStamp>,
423    pub display: Option<NtDisplay>,
424}
425
426impl NtNdArray {
427    pub fn validate(&self) -> Result<(), String> {
428        if self
429            .attribute
430            .iter()
431            .any(|a| a.descriptor.trim().is_empty())
432        {
433            return Err("ntndarray attribute descriptor must be set".to_string());
434        }
435        let element_size = self.value.element_size_bytes().max(1) as i64;
436        let logical_elements = self
437            .dimension
438            .iter()
439            .map(|d| d.size.max(0) as i64)
440            .product::<i64>()
441            .max(0);
442        let expected_uncompressed = logical_elements.saturating_mul(element_size);
443        if self.uncompressed_size > 0 && self.uncompressed_size != expected_uncompressed {
444            return Err(format!(
445                "uncompressed_size {} does not match dimension*element_size {}",
446                self.uncompressed_size, expected_uncompressed
447            ));
448        }
449        if self.compressed_size > 0 && self.compressed_size > self.uncompressed_size {
450            return Err(format!(
451                "compressed_size {} cannot exceed uncompressed_size {}",
452                self.compressed_size, self.uncompressed_size
453            ));
454        }
455        Ok(())
456    }
457}
458
459#[derive(Debug, Clone, PartialEq)]
460pub enum NtPayload {
461    Scalar(NtScalar),
462    ScalarArray(NtScalarArray),
463    Table(NtTable),
464    NdArray(NtNdArray),
465}
466
467pub(crate) fn default_form_choices() -> Vec<String> {
468    vec![
469        "Default".to_string(),
470        "String".to_string(),
471        "Binary".to_string(),
472        "Decimal".to_string(),
473        "Hex".to_string(),
474        "Exponential".to_string(),
475        "Engineering".to_string(),
476    ]
477}