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