1use 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 pub time_stamp: Option<NtTimeStamp>,
186}
187
188impl NtScalar {
189 pub fn from_value(value: ScalarValue) -> Self {
190 Self {
191 value,
192 alarm_severity: 0,
193 alarm_status: 0,
194 alarm_message: String::new(),
195 alarm_low: None,
196 alarm_high: None,
197 alarm_lolo: None,
198 alarm_hihi: None,
199 display_low: 0.0,
200 display_high: 0.0,
201 display_description: String::new(),
202 display_precision: 0,
203 display_form_index: 0,
204 display_form_choices: default_form_choices(),
205 control_low: 0.0,
206 control_high: 0.0,
207 control_min_step: 0.0,
208 units: String::new(),
209 value_alarm_active: false,
210 value_alarm_low_alarm_limit: 0.0,
211 value_alarm_low_warning_limit: 0.0,
212 value_alarm_high_warning_limit: 0.0,
213 value_alarm_high_alarm_limit: 0.0,
214 value_alarm_low_alarm_severity: 0,
215 value_alarm_low_warning_severity: 0,
216 value_alarm_high_warning_severity: 0,
217 value_alarm_high_alarm_severity: 0,
218 value_alarm_hysteresis: 0,
219 time_stamp: None,
220 }
221 }
222
223 pub fn with_timestamp(mut self, seconds_past_epoch: i64, nanoseconds: i32) -> Self {
228 self.time_stamp = Some(NtTimeStamp {
229 seconds_past_epoch,
230 nanoseconds,
231 user_tag: 0,
232 });
233 self
234 }
235
236 pub fn with_limits(mut self, low: f64, high: f64) -> Self {
237 self.display_low = low;
238 self.display_high = high;
239 self.control_low = low;
240 self.control_high = high;
241 self
242 }
243
244 pub fn with_units(mut self, units: String) -> Self {
245 self.units = units;
246 self
247 }
248
249 pub fn with_description(mut self, description: String) -> Self {
250 self.display_description = description;
251 self
252 }
253
254 pub fn with_precision(mut self, precision: i32) -> Self {
255 self.display_precision = precision;
256 self
257 }
258
259 pub fn with_alarm_limits(
260 mut self,
261 low: Option<f64>,
262 high: Option<f64>,
263 lolo: Option<f64>,
264 hihi: Option<f64>,
265 ) -> Self {
266 self.alarm_low = low;
267 self.alarm_high = high;
268 self.alarm_lolo = lolo;
269 self.alarm_hihi = hihi;
270 if let Some(v) = low {
271 self.value_alarm_low_warning_limit = v;
272 }
273 if let Some(v) = high {
274 self.value_alarm_high_warning_limit = v;
275 }
276 if let Some(v) = lolo {
277 self.value_alarm_low_alarm_limit = v;
278 }
279 if let Some(v) = hihi {
280 self.value_alarm_high_alarm_limit = v;
281 }
282 self
283 }
284
285 pub fn update_alarm_from_value(&mut self) {
286 let val = match self.value {
287 ScalarValue::F64(v) => v,
288 ScalarValue::F32(v) => v as f64,
289 ScalarValue::I8(v) => v as f64,
290 ScalarValue::I16(v) => v as f64,
291 ScalarValue::I32(v) => v as f64,
292 ScalarValue::I64(v) => v as f64,
293 ScalarValue::U8(v) => v as f64,
294 ScalarValue::U16(v) => v as f64,
295 ScalarValue::U32(v) => v as f64,
296 ScalarValue::U64(v) => v as f64,
297 _ => {
298 self.alarm_severity = 0;
299 self.alarm_status = 0;
300 self.alarm_message.clear();
301 return;
302 }
303 };
304
305 let mut severity = 0;
306 let mut message = String::new();
307
308 if let Some(hihi) = self.alarm_hihi {
309 if val >= hihi {
310 severity = 2;
311 message = "HIHI".to_string();
312 }
313 }
314 if severity == 0 {
315 if let Some(high) = self.alarm_high {
316 if val >= high {
317 severity = 1;
318 message = "HIGH".to_string();
319 }
320 }
321 }
322 if severity == 0 {
323 if let Some(lolo) = self.alarm_lolo {
324 if val <= lolo {
325 severity = 2;
326 message = "LOLO".to_string();
327 }
328 }
329 }
330 if severity == 0 {
331 if let Some(low) = self.alarm_low {
332 if val <= low {
333 severity = 1;
334 message = "LOW".to_string();
335 }
336 }
337 }
338
339 if severity == 0 {
340 self.alarm_severity = 0;
341 self.alarm_status = 0;
342 self.alarm_message.clear();
343 } else {
344 self.alarm_severity = severity;
345 self.alarm_status = 1;
346 self.alarm_message = message;
347 }
348 }
349}
350
351#[derive(Debug, Clone, PartialEq)]
352pub struct NtScalarArray {
353 pub value: ScalarArrayValue,
354 pub alarm: NtAlarm,
355 pub time_stamp: NtTimeStamp,
356 pub display: NtDisplay,
357 pub control: NtControl,
358}
359
360impl NtScalarArray {
361 pub fn from_value(value: ScalarArrayValue) -> Self {
362 Self {
363 value,
364 alarm: NtAlarm::default(),
365 time_stamp: NtTimeStamp::default(),
366 display: NtDisplay::default(),
367 control: NtControl::default(),
368 }
369 }
370}
371
372#[derive(Debug, Clone, PartialEq)]
373pub struct NtTableColumn {
374 pub name: String,
375 pub values: ScalarArrayValue,
376}
377
378#[derive(Debug, Clone, PartialEq)]
379pub struct NtTable {
380 pub labels: Vec<String>,
381 pub columns: Vec<NtTableColumn>,
382 pub descriptor: Option<String>,
383 pub alarm: Option<NtAlarm>,
384 pub time_stamp: Option<NtTimeStamp>,
385}
386
387impl NtTable {
388 pub fn validate(&self) -> Result<(), String> {
389 let mut expected_len: Option<usize> = None;
390 for col in &self.columns {
391 let len = col.values.len();
392 if let Some(expected) = expected_len {
393 if expected != len {
394 return Err(format!(
395 "table column '{}' length {} does not match expected {}",
396 col.name, len, expected
397 ));
398 }
399 } else {
400 expected_len = Some(len);
401 }
402 }
403 Ok(())
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
408pub struct NdCodec {
409 pub name: String,
410 pub parameters: HashMap<String, String>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct NdDimension {
415 pub size: i32,
416 pub offset: i32,
417 pub full_size: i32,
418 pub binning: i32,
419 pub reverse: bool,
420}
421
422#[derive(Debug, Clone, PartialEq)]
423pub struct NtAttribute {
424 pub name: String,
425 pub value: ScalarValue,
426 pub descriptor: String,
427 pub source_type: i32,
428 pub source: String,
429}
430
431#[derive(Debug, Clone, PartialEq)]
432pub struct NtNdArray {
433 pub value: ScalarArrayValue,
434 pub codec: NdCodec,
435 pub compressed_size: i64,
436 pub uncompressed_size: i64,
437 pub dimension: Vec<NdDimension>,
438 pub unique_id: i32,
439 pub data_time_stamp: NtTimeStamp,
440 pub attribute: Vec<NtAttribute>,
441 pub descriptor: Option<String>,
442 pub alarm: Option<NtAlarm>,
443 pub time_stamp: Option<NtTimeStamp>,
444 pub display: Option<NtDisplay>,
445}
446
447impl NtNdArray {
448 pub fn empty() -> Self {
451 Self {
452 value: ScalarArrayValue::U8(vec![]),
453 codec: NdCodec {
454 name: String::new(),
455 parameters: Default::default(),
456 },
457 compressed_size: 0,
458 uncompressed_size: 0,
459 dimension: vec![],
460 unique_id: 0,
461 data_time_stamp: NtTimeStamp {
462 seconds_past_epoch: 0,
463 nanoseconds: 0,
464 user_tag: 0,
465 },
466 attribute: vec![],
467 descriptor: None,
468 alarm: None,
469 time_stamp: None,
470 display: None,
471 }
472 }
473
474 pub fn validate(&self) -> Result<(), String> {
475 if self
476 .attribute
477 .iter()
478 .any(|a| a.descriptor.trim().is_empty())
479 {
480 return Err("ntndarray attribute descriptor must be set".to_string());
481 }
482 let element_size = self.value.element_size_bytes().max(1) as i64;
483 let logical_elements = self
484 .dimension
485 .iter()
486 .map(|d| d.size.max(0) as i64)
487 .product::<i64>()
488 .max(0);
489 let expected_uncompressed = logical_elements.saturating_mul(element_size);
490 if self.uncompressed_size > 0 && self.uncompressed_size != expected_uncompressed {
491 return Err(format!(
492 "uncompressed_size {} does not match dimension*element_size {}",
493 self.uncompressed_size, expected_uncompressed
494 ));
495 }
496 if self.compressed_size > 0 && self.compressed_size > self.uncompressed_size {
497 return Err(format!(
498 "compressed_size {} cannot exceed uncompressed_size {}",
499 self.compressed_size, self.uncompressed_size
500 ));
501 }
502 Ok(())
503 }
504}
505
506#[derive(Debug, Clone, PartialEq)]
524pub struct NtEnum {
525 pub index: i32,
526 pub choices: Vec<String>,
527 pub alarm: NtAlarm,
528 pub time_stamp: NtTimeStamp,
529}
530
531impl NtEnum {
532 pub fn new(index: i32, choices: Vec<String>) -> Self {
533 Self {
534 index,
535 choices,
536 alarm: NtAlarm::default(),
537 time_stamp: NtTimeStamp::default(),
538 }
539 }
540
541 pub fn selected(&self) -> Option<&str> {
544 if self.index >= 0 {
545 self.choices.get(self.index as usize).map(|s| s.as_str())
546 } else {
547 None
548 }
549 }
550}
551
552#[derive(Debug, Clone, PartialEq)]
560pub enum PvValue {
561 Scalar(ScalarValue),
562 ScalarArray(ScalarArrayValue),
563 Structure {
564 struct_id: String,
565 fields: Vec<(String, PvValue)>,
566 },
567}
568
569#[derive(Debug, Clone, PartialEq)]
570pub enum NtPayload {
571 Scalar(NtScalar),
572 ScalarArray(NtScalarArray),
573 Table(NtTable),
574 NdArray(NtNdArray),
575 Enum(NtEnum),
576 Generic {
578 struct_id: String,
579 fields: Vec<(String, PvValue)>,
580 },
581}
582
583pub(crate) fn default_form_choices() -> Vec<String> {
584 vec![
585 "Default".to_string(),
586 "String".to_string(),
587 "Binary".to_string(),
588 "Decimal".to_string(),
589 "Hex".to_string(),
590 "Exponential".to_string(),
591 "Engineering".to_string(),
592 ]
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn from_value_has_no_timestamp_by_default() {
601 let nt = NtScalar::from_value(ScalarValue::F64(1.0));
602 assert_eq!(nt.time_stamp, None);
603 }
604
605 #[test]
606 fn with_timestamp_sets_stored_timestamp() {
607 let nt = NtScalar::from_value(ScalarValue::F64(1.0)).with_timestamp(1_700_000_000, 250);
608 assert_eq!(
609 nt.time_stamp,
610 Some(NtTimeStamp {
611 seconds_past_epoch: 1_700_000_000,
612 nanoseconds: 250,
613 user_tag: 0,
614 })
615 );
616 }
617}