1use spvirit_codec::spvd_decode::DecodedValue;
4use spvirit_types::*;
5
6use crate::convert::*;
7use crate::types::{RecordData, RecordInstance, now_nt_timestamp};
8
9pub fn apply_value_update(nt: &mut NtScalar, val: &DecodedValue, compute_alarms: bool) -> bool {
11 if let DecodedValue::Structure(fields) = val {
12 if let Some((_, inner)) = fields.iter().find(|(name, _)| name == "value") {
13 return apply_value_update(nt, inner, compute_alarms);
14 }
15 }
16 match &mut nt.value {
17 ScalarValue::Bool(current) => {
18 if let Some(v) = decoded_to_bool(val) {
19 *current = v;
20 if compute_alarms {
21 nt.update_alarm_from_value();
22 }
23 return true;
24 }
25 }
26 ScalarValue::I32(current) => {
27 if let Some(v) = decoded_to_i32(val) {
28 *current = v;
29 if compute_alarms {
30 nt.update_alarm_from_value();
31 }
32 return true;
33 }
34 }
35 ScalarValue::F64(current) => {
36 if let Some(v) = decoded_to_f64(val) {
37 *current = v;
38 if compute_alarms {
39 nt.update_alarm_from_value();
40 }
41 return true;
42 }
43 }
44 ScalarValue::Str(current) => {
45 if let Some(v) = decoded_to_string(val) {
46 *current = v;
47 if compute_alarms {
48 nt.update_alarm_from_value();
49 }
50 return true;
51 }
52 }
53 _ => {
54 if let Some(v) = decoded_to_f64(val) {
55 match &mut nt.value {
56 ScalarValue::I8(c) => {
57 *c = v as i8;
58 }
59 ScalarValue::I16(c) => {
60 *c = v as i16;
61 }
62 ScalarValue::I64(c) => {
63 *c = v as i64;
64 }
65 ScalarValue::U8(c) => {
66 *c = v as u8;
67 }
68 ScalarValue::U16(c) => {
69 *c = v as u16;
70 }
71 ScalarValue::U32(c) => {
72 *c = v as u32;
73 }
74 ScalarValue::U64(c) => {
75 *c = v as u64;
76 }
77 ScalarValue::F32(c) => {
78 *c = v as f32;
79 }
80 _ => return false,
81 }
82 if compute_alarms {
83 nt.update_alarm_from_value();
84 }
85 return true;
86 }
87 }
88 }
89 false
90}
91
92pub fn apply_alarm_update(nt: &mut NtScalar, val: &DecodedValue) -> bool {
94 let DecodedValue::Structure(fields) = val else {
95 return false;
96 };
97 let mut changed = false;
98 for (name, v) in fields {
99 match name.as_str() {
100 "severity" => {
101 if let Some(i) = decoded_to_i32(v) {
102 nt.alarm_severity = i;
103 changed = true;
104 }
105 }
106 "status" => {
107 if let Some(i) = decoded_to_i32(v) {
108 nt.alarm_status = i;
109 changed = true;
110 }
111 }
112 "message" => {
113 if let Some(s) = decoded_to_string(v) {
114 nt.alarm_message = s;
115 changed = true;
116 }
117 }
118 _ => {}
119 }
120 }
121 changed
122}
123
124pub fn apply_display_update(nt: &mut NtScalar, val: &DecodedValue) -> bool {
126 let DecodedValue::Structure(fields) = val else {
127 return false;
128 };
129 let mut changed = false;
130 for (name, v) in fields {
131 match name.as_str() {
132 "low" | "limitLow" => {
133 if let Some(f) = decoded_to_f64(v) {
134 nt.display_low = f;
135 changed = true;
136 }
137 }
138 "high" | "limitHigh" => {
139 if let Some(f) = decoded_to_f64(v) {
140 nt.display_high = f;
141 changed = true;
142 }
143 }
144 "description" => {
145 if let Some(s) = decoded_to_string(v) {
146 nt.display_description = s;
147 changed = true;
148 }
149 }
150 "units" => {
151 if let Some(s) = decoded_to_string(v) {
152 nt.units = s;
153 changed = true;
154 }
155 }
156 "precision" => {
157 if let Some(i) = decoded_to_i32(v) {
158 nt.display_precision = i;
159 changed = true;
160 }
161 }
162 "form" => {
163 if let DecodedValue::Structure(form_fields) = v {
164 let mut updated = false;
165 for (fname, fval) in form_fields {
166 match fname.as_str() {
167 "index" => {
168 if let Some(i) = decoded_to_i32(fval) {
169 nt.display_form_index = i;
170 updated = true;
171 }
172 }
173 "choices" => {
174 if let DecodedValue::Array(items) = fval {
175 let mut choices = Vec::new();
176 for item in items {
177 if let DecodedValue::String(s) = item {
178 choices.push(s.clone());
179 }
180 }
181 if !choices.is_empty() {
182 nt.display_form_choices = choices;
183 updated = true;
184 }
185 }
186 }
187 _ => {}
188 }
189 }
190 if updated {
191 changed = true;
192 }
193 }
194 }
195 _ => {}
196 }
197 }
198 changed
199}
200
201pub fn apply_control_update(nt: &mut NtScalar, val: &DecodedValue) -> bool {
203 let DecodedValue::Structure(fields) = val else {
204 return false;
205 };
206 let mut changed = false;
207 for (name, v) in fields {
208 match name.as_str() {
209 "low" | "limitLow" => {
210 if let Some(f) = decoded_to_f64(v) {
211 nt.control_low = f;
212 changed = true;
213 }
214 }
215 "high" | "limitHigh" => {
216 if let Some(f) = decoded_to_f64(v) {
217 nt.control_high = f;
218 changed = true;
219 }
220 }
221 "minStep" => {
222 if let Some(f) = decoded_to_f64(v) {
223 nt.control_min_step = f;
224 changed = true;
225 }
226 }
227 _ => {}
228 }
229 }
230 changed
231}
232
233pub fn apply_scalar_array_put(
235 nt: &mut NtScalarArray,
236 nord: &mut usize,
237 value: &DecodedValue,
238) -> bool {
239 let field_value = match value {
240 DecodedValue::Structure(fields) => fields
241 .iter()
242 .find(|(name, _)| name == "value")
243 .map(|(_, v)| v)
244 .unwrap_or(value),
245 _ => value,
246 };
247 if let Some(next) = decoded_to_scalar_array(field_value, &nt.value) {
248 let changed = nt.value != next;
249 if changed {
250 *nord = next.len();
251 nt.value = next;
252 }
253 return changed;
254 }
255 false
256}
257
258pub fn apply_table_put(nt: &mut NtTable, value: &DecodedValue) -> bool {
263 let DecodedValue::Structure(fields) = value else {
264 return false;
265 };
266 let mut changed = false;
267 for (name, field_value) in fields {
268 match name.as_str() {
269 "labels" => {
270 if let DecodedValue::Array(items) = field_value {
271 let labels: Vec<String> = items.iter().filter_map(decoded_to_string).collect();
272 if !labels.is_empty() && nt.labels != labels {
273 nt.labels = labels;
274 changed = true;
275 }
276 }
277 }
278 "value" => {
279 if let DecodedValue::Structure(cols) = field_value {
280 for (col_name, col_value) in cols {
281 if let Some(col) = nt.columns.iter_mut().find(|c| c.name == *col_name) {
282 if let Some(next) = decoded_to_scalar_array(col_value, &col.values) {
283 if col.values != next {
284 col.values = next;
285 changed = true;
286 }
287 }
288 }
289 }
290 }
291 }
292 "descriptor" => {
293 if let Some(s) = decoded_to_string(field_value) {
294 let next = if s.is_empty() { None } else { Some(s) };
295 if nt.descriptor != next {
296 nt.descriptor = next;
297 changed = true;
298 }
299 }
300 }
301 "alarm" => {
302 if let Some(alarm) = decode_nt_alarm(field_value) {
303 if nt.alarm.as_ref() != Some(&alarm) {
304 nt.alarm = Some(alarm);
305 changed = true;
306 }
307 }
308 }
309 _ => {}
317 }
318 }
319 changed
320}
321
322pub fn apply_ndarray_put(nt: &mut NtNdArray, value: &DecodedValue) -> bool {
328 let DecodedValue::Structure(fields) = value else {
329 return false;
330 };
331 let mut changed = false;
332 for (name, field_value) in fields {
333 match name.as_str() {
334 "value" => {
335 if let Some(next) = decoded_to_scalar_array(field_value, &nt.value) {
336 if nt.value != next {
337 nt.value = next;
338 changed = true;
339 }
340 }
341 }
342 "compressedSize" => {
343 if let Some(v) = decoded_to_i64(field_value) {
344 if nt.compressed_size != v {
345 nt.compressed_size = v;
346 changed = true;
347 }
348 }
349 }
350 "uncompressedSize" => {
351 if let Some(v) = decoded_to_i64(field_value) {
352 if nt.uncompressed_size != v {
353 nt.uncompressed_size = v;
354 changed = true;
355 }
356 }
357 }
358 "uniqueId" => {
359 if let Some(v) = decoded_to_i32(field_value) {
360 if nt.unique_id != v {
361 nt.unique_id = v;
362 changed = true;
363 }
364 }
365 }
366 "codec" => {
367 if let DecodedValue::Structure(codec_fields) = field_value {
368 for (cname, cval) in codec_fields {
369 if cname == "name" {
370 if let Some(s) = decoded_to_string(cval) {
371 if nt.codec.name != s {
372 nt.codec.name = s;
373 changed = true;
374 }
375 }
376 }
377 }
378 }
379 }
380 "dimension" => {
381 if let DecodedValue::Array(items) = field_value {
382 let dims: Vec<NdDimension> = items
383 .iter()
384 .filter_map(|item| {
385 if let DecodedValue::Structure(fs) = item {
386 Some(NdDimension {
387 size: fs
388 .iter()
389 .find(|(n, _)| n == "size")
390 .and_then(|(_, v)| decoded_to_i32(v))
391 .unwrap_or(0),
392 offset: fs
393 .iter()
394 .find(|(n, _)| n == "offset")
395 .and_then(|(_, v)| decoded_to_i32(v))
396 .unwrap_or(0),
397 full_size: fs
398 .iter()
399 .find(|(n, _)| n == "fullSize")
400 .and_then(|(_, v)| decoded_to_i32(v))
401 .unwrap_or(0),
402 binning: fs
403 .iter()
404 .find(|(n, _)| n == "binning")
405 .and_then(|(_, v)| decoded_to_i32(v))
406 .unwrap_or(1),
407 reverse: fs
408 .iter()
409 .find(|(n, _)| n == "reverse")
410 .and_then(|(_, v)| decoded_to_bool(v))
411 .unwrap_or(false),
412 })
413 } else {
414 None
415 }
416 })
417 .collect();
418 if !dims.is_empty() && nt.dimension != dims {
419 nt.dimension = dims;
420 changed = true;
421 }
422 }
423 }
424 "descriptor" => {
425 if let Some(s) = decoded_to_string(field_value) {
426 let next = if s.is_empty() { None } else { Some(s) };
427 if nt.descriptor != next {
428 nt.descriptor = next;
429 changed = true;
430 }
431 }
432 }
433 "alarm" => {
434 if let Some(alarm) = decode_nt_alarm(field_value) {
435 if nt.alarm.as_ref() != Some(&alarm) {
436 nt.alarm = Some(alarm);
437 changed = true;
438 }
439 }
440 }
441 "display" => {
448 if let Some(display) = decode_nt_display(field_value) {
449 if nt.display.as_ref() != Some(&display) {
450 nt.display = Some(display);
451 changed = true;
452 }
453 }
454 }
455 "attribute" => {
456 if let DecodedValue::Array(items) = field_value {
457 let attrs: Vec<NtAttribute> = items
458 .iter()
459 .filter_map(|item| {
460 if let DecodedValue::Structure(fs) = item {
461 let attr_name = fs
462 .iter()
463 .find(|(n, _)| n == "name")
464 .and_then(|(_, v)| decoded_to_string(v))
465 .unwrap_or_default();
466 let attr_value = fs
467 .iter()
468 .find(|(n, _)| n == "value")
469 .map(|(_, v)| decoded_to_scalar_value(v))
470 .unwrap_or(ScalarValue::I32(0));
471 let descriptor = fs
472 .iter()
473 .find(|(n, _)| n == "descriptor")
474 .and_then(|(_, v)| decoded_to_string(v))
475 .unwrap_or_default();
476 let source_type = fs
477 .iter()
478 .find(|(n, _)| n == "sourceType")
479 .and_then(|(_, v)| decoded_to_i32(v))
480 .unwrap_or(0);
481 let source = fs
482 .iter()
483 .find(|(n, _)| n == "source")
484 .and_then(|(_, v)| decoded_to_string(v))
485 .unwrap_or_default();
486 Some(NtAttribute {
487 name: attr_name,
488 value: attr_value,
489 descriptor,
490 source_type,
491 source,
492 })
493 } else {
494 None
495 }
496 })
497 .collect();
498 if !attrs.is_empty() && nt.attribute != attrs {
499 nt.attribute = attrs;
500 changed = true;
501 }
502 }
503 }
504 _ => {}
505 }
506 }
507 changed
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
512pub struct PutOutcome {
513 pub value_changed: bool,
515 pub client_stamped: bool,
518}
519
520fn client_timestamp(value: &DecodedValue) -> Option<NtTimeStamp> {
526 let DecodedValue::Structure(fields) = value else {
527 return None;
528 };
529 let (_, field) = fields.iter().find(|(name, _)| name == "timeStamp")?;
530 let ts = decode_nt_timestamp(field)?;
531 (ts != NtTimeStamp::default()).then_some(ts)
532}
533
534fn client_data_timestamp(value: &DecodedValue) -> Option<NtTimeStamp> {
538 let DecodedValue::Structure(fields) = value else {
539 return None;
540 };
541 let (_, field) = fields.iter().find(|(name, _)| name == "dataTimeStamp")?;
542 let ts = decode_nt_timestamp(field)?;
543 (ts != NtTimeStamp::default()).then_some(ts)
544}
545
546impl RecordInstance {
547 pub fn apply_put(&mut self, value: &DecodedValue, compute_alarms: bool) -> PutOutcome {
555 let wrapped;
558 let value = match value {
559 DecodedValue::Structure(_) => value,
560 other => {
561 wrapped = DecodedValue::Structure(vec![("value".to_string(), other.clone())]);
562 &wrapped
563 }
564 };
565
566 let value_changed = self.apply_put_fields(value, compute_alarms);
567
568 let client_ts = client_timestamp(value);
569 let client_stamped = client_ts.is_some();
570 self.set_time_stamp(client_ts.unwrap_or_else(now_nt_timestamp));
571
572 if let RecordData::NtNdArray { nt, .. } = &mut self.data {
579 if let Some(data_ts) = client_data_timestamp(value) {
580 nt.data_time_stamp = data_ts;
581 }
582 }
583
584 PutOutcome {
585 value_changed,
586 client_stamped,
587 }
588 }
589
590 fn apply_put_fields(&mut self, value: &DecodedValue, compute_alarms: bool) -> bool {
593 let DecodedValue::Structure(fields) = value else {
594 return false;
595 };
596
597 match &mut self.data {
598 RecordData::Ai { nt, .. }
599 | RecordData::Ao { nt, .. }
600 | RecordData::Bi { nt, .. }
601 | RecordData::Bo { nt, .. }
602 | RecordData::StringIn { nt, .. }
603 | RecordData::StringOut { nt, .. } => {
604 let before = nt.value.clone();
611 let mut changed = false;
612 for (name, val) in fields {
613 match name.as_str() {
614 "value" => {
615 apply_value_update(nt, val, compute_alarms);
616 }
617 "alarm" => changed |= apply_alarm_update(nt, val),
618 "display" => changed |= apply_display_update(nt, val),
619 "control" => changed |= apply_control_update(nt, val),
620 _ => {}
621 }
622 }
623 changed || nt.value != before
624 }
625 RecordData::Waveform { nt, nord, .. }
626 | RecordData::Aai { nt, nord, .. }
627 | RecordData::Aao { nt, nord, .. }
628 | RecordData::SubArray { nt, nord, .. } => apply_scalar_array_put(nt, nord, value),
629 RecordData::NtTable { nt, .. } => apply_table_put(nt, value),
630 RecordData::NtNdArray { nt, .. } => apply_ndarray_put(nt, value),
631 RecordData::NtEnum { nt, .. } => {
632 let mut changed = false;
636 for (name, val) in fields {
637 if name != "value" {
638 continue;
639 }
640 let idx = match val {
641 DecodedValue::Int32(v) => Some(*v),
642 DecodedValue::Int64(v) => Some(*v as i32),
643 DecodedValue::Int16(v) => Some(*v as i32),
644 DecodedValue::Int8(v) => Some(*v as i32),
645 DecodedValue::Float64(v) => Some(*v as i32),
646 _ => None,
647 };
648 if let Some(idx) = idx {
649 if idx < 0 || (idx as usize) >= nt.choices.len() {
650 } else if nt.index != idx {
652 nt.index = idx;
653 changed = true;
654 }
655 }
656 }
657 changed
658 }
659 RecordData::Generic { .. } => false,
660 }
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::types::{DbCommonState, OutputMode, RecordData, RecordInstance, RecordType};
668 use spvirit_types::{NdCodec, NdDimension, NtEnum, NtNdArray, NtScalarArray, ScalarArrayValue};
669 use std::collections::HashMap;
670
671 const OLD: NtTimeStamp = NtTimeStamp {
672 seconds_past_epoch: 1_000,
673 nanoseconds: 0,
674 user_tag: 0,
675 };
676
677 fn ai_record(val: f64) -> RecordInstance {
678 let mut nt = NtScalar::from_value(ScalarValue::F64(val));
679 nt.time_stamp = Some(OLD);
680 RecordInstance {
681 name: "T".to_string(),
682 record_type: RecordType::Ai,
683 common: DbCommonState::default(),
684 data: RecordData::Ai {
685 nt,
686 inp: None,
687 siml: None,
688 siol: None,
689 simm: false,
690 },
691 raw_fields: HashMap::new(),
692 }
693 }
694
695 fn ai_record_with_alarm_limits(
696 val: f64,
697 low: f64,
698 high: f64,
699 lolo: f64,
700 hihi: f64,
701 ) -> RecordInstance {
702 let mut nt = NtScalar::from_value(ScalarValue::F64(val)).with_alarm_limits(
703 Some(low),
704 Some(high),
705 Some(lolo),
706 Some(hihi),
707 );
708 nt.time_stamp = Some(OLD);
709 RecordInstance {
710 name: "ALM".to_string(),
711 record_type: RecordType::Ai,
712 common: DbCommonState::default(),
713 data: RecordData::Ai {
714 nt,
715 inp: None,
716 siml: None,
717 siol: None,
718 simm: false,
719 },
720 raw_fields: HashMap::new(),
721 }
722 }
723
724 fn alarm_of(rec: &RecordInstance) -> (i32, String) {
725 let RecordData::Ai { nt, .. } = &rec.data else {
726 panic!("expected Ai");
727 };
728 (nt.alarm_severity, nt.alarm_message.clone())
729 }
730
731 #[test]
732 fn put_with_compute_alarms_true_sets_hihi_severity() {
733 let mut rec = ai_record_with_alarm_limits(5.0, 0.0, 8.0, -5.0, 15.0);
734 let outcome = rec.apply_put(&put_value(20.0), true);
735
736 assert!(outcome.value_changed);
737 let (severity, message) = alarm_of(&rec);
738 assert_eq!(severity, 2);
739 assert_eq!(message, "HIHI");
740 }
741
742 #[test]
743 fn put_with_compute_alarms_true_sets_high_severity() {
744 let mut rec = ai_record_with_alarm_limits(5.0, 0.0, 8.0, -5.0, 15.0);
745 let outcome = rec.apply_put(&put_value(10.0), true);
746
747 assert!(outcome.value_changed);
748 let (severity, message) = alarm_of(&rec);
749 assert_eq!(severity, 1);
750 assert_eq!(message, "HIGH");
751 }
752
753 #[test]
754 fn put_with_compute_alarms_true_clears_alarm_within_normal_range() {
755 let mut rec = ai_record_with_alarm_limits(20.0, 0.0, 8.0, -5.0, 15.0);
756 rec.apply_put(&put_value(20.0), true);
758 assert_eq!(alarm_of(&rec).0, 2);
759
760 let outcome = rec.apply_put(&put_value(4.0), true);
761
762 assert!(outcome.value_changed);
763 let (severity, message) = alarm_of(&rec);
764 assert_eq!(severity, 0);
765 assert_eq!(message, "");
766 }
767
768 #[test]
769 fn put_with_compute_alarms_false_does_not_update_alarm() {
770 let mut rec = ai_record_with_alarm_limits(5.0, 0.0, 8.0, -5.0, 15.0);
771 let outcome = rec.apply_put(&put_value(20.0), false);
772
773 assert!(outcome.value_changed);
774 assert_eq!(alarm_of(&rec).0, 0);
775 }
776
777 fn stamp_of(rec: &RecordInstance) -> NtTimeStamp {
778 let RecordData::Ai { nt, .. } = &rec.data else {
779 panic!("expected Ai");
780 };
781 nt.time_stamp.clone().expect("stamped")
782 }
783
784 fn ts_field(seconds: i64, nanos: i32) -> DecodedValue {
785 DecodedValue::Structure(vec![
786 ("secondsPastEpoch".to_string(), DecodedValue::Int64(seconds)),
787 ("nanoseconds".to_string(), DecodedValue::Int32(nanos)),
788 ("userTag".to_string(), DecodedValue::Int32(0)),
789 ])
790 }
791
792 fn put_value(v: f64) -> DecodedValue {
793 DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(v))])
794 }
795
796 #[test]
797 fn put_without_timestamp_stamps_server_time() {
798 let mut rec = ai_record(1.0);
799 let outcome = rec.apply_put(&put_value(2.0), false);
800
801 assert!(outcome.value_changed);
802 assert!(!outcome.client_stamped);
803 assert!(stamp_of(&rec).seconds_past_epoch > OLD.seconds_past_epoch);
804 }
805
806 #[test]
807 fn put_with_client_timestamp_keeps_it_verbatim() {
808 let mut rec = ai_record(1.0);
809 let body = DecodedValue::Structure(vec![
810 ("value".to_string(), DecodedValue::Float64(2.0)),
811 ("timeStamp".to_string(), ts_field(5_000, 42)),
812 ]);
813 let outcome = rec.apply_put(&body, false);
814
815 assert!(outcome.client_stamped);
816 assert_eq!(
817 stamp_of(&rec),
818 NtTimeStamp {
819 seconds_past_epoch: 5_000,
820 nanoseconds: 42,
821 user_tag: 0,
822 }
823 );
824 }
825
826 #[test]
827 fn epoch_zero_client_timestamp_falls_back_to_server_time() {
828 let mut rec = ai_record(1.0);
829 let body = DecodedValue::Structure(vec![
830 ("value".to_string(), DecodedValue::Float64(2.0)),
831 ("timeStamp".to_string(), ts_field(0, 0)),
832 ]);
833 let outcome = rec.apply_put(&body, false);
834
835 assert!(!outcome.client_stamped);
836 assert!(stamp_of(&rec).seconds_past_epoch > OLD.seconds_past_epoch);
837 }
838
839 #[test]
840 fn unchanged_value_still_restamps() {
841 let mut rec = ai_record(1.0);
842 let outcome = rec.apply_put(&put_value(1.0), false);
843
844 assert!(!outcome.value_changed);
845 assert!(stamp_of(&rec).seconds_past_epoch > OLD.seconds_past_epoch);
846 }
847
848 #[test]
849 fn bare_scalar_body_is_wrapped_and_stamped() {
850 let mut rec = ai_record(1.0);
851 let outcome = rec.apply_put(&DecodedValue::Float64(3.0), false);
852
853 assert!(outcome.value_changed);
854 assert!(stamp_of(&rec).seconds_past_epoch > OLD.seconds_past_epoch);
855 let RecordData::Ai { nt, .. } = &rec.data else {
856 panic!("expected Ai");
857 };
858 assert_eq!(nt.value, ScalarValue::F64(3.0));
859 }
860
861 #[test]
862 fn unrecognised_fields_still_restamp() {
863 let mut rec = ai_record(1.0);
864 let body =
865 DecodedValue::Structure(vec![("nosuchfield".to_string(), DecodedValue::Int32(1))]);
866 let outcome = rec.apply_put(&body, false);
867
868 assert!(!outcome.value_changed);
869 assert!(stamp_of(&rec).seconds_past_epoch > OLD.seconds_past_epoch);
870 }
871
872 fn waveform_record(vals: Vec<f64>) -> RecordInstance {
873 let mut nt = NtScalarArray::from_value(ScalarArrayValue::F64(vals.clone()));
874 nt.time_stamp = OLD;
875 RecordInstance {
876 name: "W".to_string(),
877 record_type: RecordType::Waveform,
878 common: DbCommonState::default(),
879 data: RecordData::Waveform {
880 nt,
881 nord: vals.len(),
882 nelm: 16,
883 inp: None,
884 ftvl: "DOUBLE".to_string(),
885 },
886 raw_fields: HashMap::new(),
887 }
888 }
889
890 #[test]
891 fn array_put_restamps() {
892 let mut rec = waveform_record(vec![1.0, 2.0]);
893 let body = DecodedValue::Structure(vec![(
894 "value".to_string(),
895 DecodedValue::Array(vec![DecodedValue::Float64(3.0), DecodedValue::Float64(4.0)]),
896 )]);
897 let outcome = rec.apply_put(&body, false);
898
899 assert!(outcome.value_changed);
900 let RecordData::Waveform { nt, .. } = &rec.data else {
901 panic!("expected Waveform");
902 };
903 assert!(nt.time_stamp.seconds_past_epoch > OLD.seconds_past_epoch);
904 }
905
906 #[test]
907 fn array_put_with_unchanged_value_still_restamps() {
908 let mut rec = waveform_record(vec![1.0, 2.0]);
909 let body = DecodedValue::Structure(vec![(
910 "value".to_string(),
911 DecodedValue::Array(vec![DecodedValue::Float64(1.0), DecodedValue::Float64(2.0)]),
912 )]);
913 let outcome = rec.apply_put(&body, false);
914
915 assert!(!outcome.value_changed);
916 let RecordData::Waveform { nt, .. } = &rec.data else {
917 panic!("expected Waveform");
918 };
919 assert!(nt.time_stamp.seconds_past_epoch > OLD.seconds_past_epoch);
920 }
921
922 #[test]
923 fn enum_put_restamps() {
924 let mut nt = NtEnum::new(0, vec!["A".to_string(), "B".to_string()]);
925 nt.time_stamp = OLD;
926 let mut rec = RecordInstance {
927 name: "E".to_string(),
928 record_type: RecordType::Mbbo,
929 common: DbCommonState::default(),
930 data: RecordData::NtEnum {
931 nt,
932 inp: None,
933 out: None,
934 omsl: OutputMode::Supervisory,
935 },
936 raw_fields: HashMap::new(),
937 };
938 let outcome = rec.apply_put(&DecodedValue::Int32(1), false);
939
940 assert!(outcome.value_changed);
941 let RecordData::NtEnum { nt, .. } = &rec.data else {
942 panic!("expected NtEnum");
943 };
944 assert!(nt.time_stamp.seconds_past_epoch > OLD.seconds_past_epoch);
945 }
946
947 fn ndarray_record() -> RecordInstance {
948 let nt = NtNdArray {
949 value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
950 codec: NdCodec {
951 name: "none".to_string(),
952 parameters: HashMap::new(),
953 },
954 compressed_size: 4,
955 uncompressed_size: 4,
956 dimension: vec![NdDimension {
957 size: 4,
958 offset: 0,
959 full_size: 4,
960 binning: 1,
961 reverse: false,
962 }],
963 unique_id: 1,
964 data_time_stamp: OLD,
965 attribute: vec![],
966 descriptor: Some("ndarray".to_string()),
967 alarm: None,
968 time_stamp: Some(OLD),
969 display: None,
970 };
971 RecordInstance {
972 name: "N".to_string(),
973 record_type: RecordType::NtNdArray,
974 common: DbCommonState::default(),
975 data: RecordData::NtNdArray {
976 nt,
977 inp: None,
978 out: None,
979 omsl: OutputMode::Supervisory,
980 },
981 raw_fields: HashMap::new(),
982 }
983 }
984
985 #[test]
986 fn ndarray_put_honours_client_supplied_data_time_stamp() {
987 let mut rec = ndarray_record();
991 let body = DecodedValue::Structure(vec![
992 (
993 "value".to_string(),
994 DecodedValue::Array(vec![
995 DecodedValue::UInt8(5),
996 DecodedValue::UInt8(6),
997 DecodedValue::UInt8(7),
998 DecodedValue::UInt8(8),
999 ]),
1000 ),
1001 ("timeStamp".to_string(), ts_field(5_000, 42)),
1002 ("dataTimeStamp".to_string(), ts_field(9_000, 7)),
1003 ]);
1004 let outcome = rec.apply_put(&body, false);
1005
1006 assert!(outcome.value_changed);
1007 assert!(outcome.client_stamped);
1008 let RecordData::NtNdArray { nt, .. } = &rec.data else {
1009 panic!("expected NtNdArray");
1010 };
1011 assert_eq!(
1012 nt.time_stamp,
1013 Some(NtTimeStamp {
1014 seconds_past_epoch: 5_000,
1015 nanoseconds: 42,
1016 user_tag: 0,
1017 }),
1018 "timeStamp should be the relay's stamp"
1019 );
1020 assert_eq!(
1021 nt.data_time_stamp,
1022 NtTimeStamp {
1023 seconds_past_epoch: 9_000,
1024 nanoseconds: 7,
1025 user_tag: 0,
1026 },
1027 "dataTimeStamp should be the acquisition time, not overwritten by timeStamp"
1028 );
1029 }
1030
1031 #[test]
1032 fn ndarray_put_with_only_timestamp_fields_does_not_report_value_changed() {
1033 let mut rec = ndarray_record();
1039 let body = DecodedValue::Structure(vec![
1040 ("timeStamp".to_string(), ts_field(5_000, 42)),
1041 ("dataTimeStamp".to_string(), ts_field(9_000, 7)),
1042 ]);
1043 let outcome = rec.apply_put(&body, false);
1044
1045 assert!(!outcome.value_changed);
1046 assert!(outcome.client_stamped);
1047 }
1048
1049 #[test]
1050 fn table_put_with_only_timestamp_field_does_not_report_value_changed() {
1051 let mut nt = NtTable {
1052 labels: vec!["a".to_string()],
1053 columns: vec![spvirit_types::NtTableColumn {
1054 name: "a".to_string(),
1055 values: ScalarArrayValue::F64(vec![1.0]),
1056 }],
1057 descriptor: None,
1058 alarm: None,
1059 time_stamp: Some(OLD),
1060 };
1061 nt.time_stamp = Some(OLD);
1062 let mut rec = RecordInstance {
1063 name: "TBL".to_string(),
1064 record_type: RecordType::NtTable,
1065 common: DbCommonState::default(),
1066 data: RecordData::NtTable {
1067 nt,
1068 inp: None,
1069 out: None,
1070 omsl: OutputMode::Supervisory,
1071 },
1072 raw_fields: HashMap::new(),
1073 };
1074 let body = DecodedValue::Structure(vec![("timeStamp".to_string(), ts_field(5_000, 42))]);
1075 let outcome = rec.apply_put(&body, false);
1076
1077 assert!(!outcome.value_changed);
1078 assert!(outcome.client_stamped);
1079 }
1080
1081 #[test]
1082 fn ndarray_put_restamps_both_time_stamp_and_data_time_stamp() {
1083 let mut rec = ndarray_record();
1084 let body = DecodedValue::Structure(vec![(
1085 "value".to_string(),
1086 DecodedValue::Array(vec![
1087 DecodedValue::UInt8(5),
1088 DecodedValue::UInt8(6),
1089 DecodedValue::UInt8(7),
1090 DecodedValue::UInt8(8),
1091 ]),
1092 )]);
1093 let outcome = rec.apply_put(&body, false);
1094
1095 assert!(outcome.value_changed);
1096 let RecordData::NtNdArray { nt, .. } = &rec.data else {
1097 panic!("expected NtNdArray");
1098 };
1099 assert!(
1100 nt.time_stamp.as_ref().expect("stamped").seconds_past_epoch > OLD.seconds_past_epoch
1101 );
1102 assert!(nt.data_time_stamp.seconds_past_epoch > OLD.seconds_past_epoch);
1103 }
1104}