1use std::collections::HashMap;
7use std::time::Duration;
8
9pub use spvirit_types::*;
10
11pub(crate) fn now_nt_timestamp() -> NtTimeStamp {
13 let now = std::time::SystemTime::now()
14 .duration_since(std::time::UNIX_EPOCH)
15 .unwrap_or_default();
16 NtTimeStamp {
17 seconds_past_epoch: now.as_secs() as i64,
18 nanoseconds: now.subsec_nanos() as i32,
19 user_tag: 0,
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum RecordType {
25 Ai,
26 Ao,
27 Bi,
28 Bo,
29 StringIn,
30 StringOut,
31 Waveform,
32 Aai,
33 Aao,
34 SubArray,
35 NtTable,
36 NtNdArray,
37 Mbbi,
38 Mbbo,
39 Generic,
40 LongIn,
41 LongOut,
42}
43
44impl RecordType {
45 pub fn from_db_name(name: &str) -> Option<Self> {
49 match name.to_ascii_lowercase().as_str() {
50 "ai" => Some(Self::Ai),
51 "ao" => Some(Self::Ao),
52 "bi" => Some(Self::Bi),
53 "bo" => Some(Self::Bo),
54 "stringin" => Some(Self::StringIn),
55 "stringout" => Some(Self::StringOut),
56 "waveform" => Some(Self::Waveform),
57 "aai" => Some(Self::Aai),
58 "aao" => Some(Self::Aao),
59 "subarray" => Some(Self::SubArray),
60 "mbbi" | "ntenum" => Some(Self::Mbbi),
61 "mbbo" => Some(Self::Mbbo),
62 _ => None,
63 }
64 }
65
66 pub fn is_output(&self) -> bool {
67 matches!(
68 self,
69 Self::Ao | Self::Bo | Self::StringOut | Self::Aao | Self::Mbbo | Self::LongOut
70 )
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ScanMode {
76 Passive,
77 Periodic(Duration),
78 Event(String),
79 IoEvent(String),
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub enum LinkExpr {
84 Constant(ScalarValue),
85 DbLink {
86 target: String,
87 process_passive: bool,
88 maximize_severity: bool,
89 },
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum OutputMode {
94 Supervisory,
95 ClosedLoop,
96}
97
98#[derive(Debug, Clone, PartialEq)]
99pub struct DbCommonState {
100 pub desc: String,
101 pub scan: ScanMode,
102 pub pini: bool,
103 pub phas: i32,
104 pub pact: bool,
105 pub disa: bool,
106 pub sdis: Option<LinkExpr>,
107 pub diss: i32,
108 pub flnk: Option<LinkExpr>,
109}
110
111impl Default for DbCommonState {
112 fn default() -> Self {
113 Self {
114 desc: String::new(),
115 scan: ScanMode::Passive,
116 pini: false,
117 phas: 0,
118 pact: false,
119 disa: false,
120 sdis: None,
121 diss: 0,
122 flnk: None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq)]
128pub enum RecordData {
129 Ai {
130 nt: NtScalar,
131 inp: Option<LinkExpr>,
132 siml: Option<LinkExpr>,
133 siol: Option<LinkExpr>,
134 simm: bool,
135 },
136 Ao {
137 nt: NtScalar,
138 out: Option<LinkExpr>,
139 dol: Option<LinkExpr>,
140 omsl: OutputMode,
141 drvl: Option<f64>,
142 drvh: Option<f64>,
143 oroc: Option<f64>,
144 siml: Option<LinkExpr>,
145 siol: Option<LinkExpr>,
146 simm: bool,
147 },
148 Bi {
149 nt: NtScalar,
150 inp: Option<LinkExpr>,
151 znam: String,
152 onam: String,
153 siml: Option<LinkExpr>,
154 siol: Option<LinkExpr>,
155 simm: bool,
156 },
157 Bo {
158 nt: NtScalar,
159 out: Option<LinkExpr>,
160 dol: Option<LinkExpr>,
161 omsl: OutputMode,
162 znam: String,
163 onam: String,
164 siml: Option<LinkExpr>,
165 siol: Option<LinkExpr>,
166 simm: bool,
167 },
168 StringIn {
169 nt: NtScalar,
170 inp: Option<LinkExpr>,
171 siml: Option<LinkExpr>,
172 siol: Option<LinkExpr>,
173 simm: bool,
174 },
175 StringOut {
176 nt: NtScalar,
177 out: Option<LinkExpr>,
178 dol: Option<LinkExpr>,
179 omsl: OutputMode,
180 siml: Option<LinkExpr>,
181 siol: Option<LinkExpr>,
182 simm: bool,
183 },
184 Waveform {
185 nt: NtScalarArray,
186 inp: Option<LinkExpr>,
187 ftvl: String,
188 nelm: usize,
189 nord: usize,
190 },
191 Aai {
192 nt: NtScalarArray,
193 inp: Option<LinkExpr>,
194 ftvl: String,
195 nelm: usize,
196 nord: usize,
197 },
198 Aao {
199 nt: NtScalarArray,
200 out: Option<LinkExpr>,
201 dol: Option<LinkExpr>,
202 omsl: OutputMode,
203 ftvl: String,
204 nelm: usize,
205 nord: usize,
206 },
207 SubArray {
208 nt: NtScalarArray,
209 inp: Option<LinkExpr>,
210 ftvl: String,
211 malm: usize,
212 nelm: usize,
213 nord: usize,
214 indx: usize,
215 },
216 NtTable {
217 nt: NtTable,
218 inp: Option<LinkExpr>,
219 out: Option<LinkExpr>,
220 omsl: OutputMode,
221 },
222 NtNdArray {
223 nt: NtNdArray,
224 inp: Option<LinkExpr>,
225 out: Option<LinkExpr>,
226 omsl: OutputMode,
227 },
228 NtEnum {
229 nt: NtEnum,
230 inp: Option<LinkExpr>,
231 out: Option<LinkExpr>,
232 omsl: OutputMode,
233 },
234 Generic {
235 struct_id: String,
236 fields: Vec<(String, PvValue)>,
237 inp: Option<LinkExpr>,
238 out: Option<LinkExpr>,
239 omsl: OutputMode,
240 },
241}
242
243impl RecordData {
244 pub fn nt(&self) -> &NtScalar {
245 match self {
246 Self::Ai { nt, .. } => nt,
247 Self::Ao { nt, .. } => nt,
248 Self::Bi { nt, .. } => nt,
249 Self::Bo { nt, .. } => nt,
250 Self::StringIn { nt, .. } => nt,
251 Self::StringOut { nt, .. } => nt,
252 _ => panic!("record variant does not expose NtScalar"),
253 }
254 }
255
256 pub fn nt_mut(&mut self) -> &mut NtScalar {
257 match self {
258 Self::Ai { nt, .. } => nt,
259 Self::Ao { nt, .. } => nt,
260 Self::Bi { nt, .. } => nt,
261 Self::Bo { nt, .. } => nt,
262 Self::StringIn { nt, .. } => nt,
263 Self::StringOut { nt, .. } => nt,
264 _ => panic!("record variant does not expose NtScalar"),
265 }
266 }
267
268 pub fn payload(&self) -> NtPayload {
269 match self {
270 Self::Ai { nt, .. }
271 | Self::Ao { nt, .. }
272 | Self::Bi { nt, .. }
273 | Self::Bo { nt, .. }
274 | Self::StringIn { nt, .. }
275 | Self::StringOut { nt, .. } => NtPayload::Scalar(nt.clone()),
276 Self::Waveform { nt, .. }
277 | Self::Aai { nt, .. }
278 | Self::Aao { nt, .. }
279 | Self::SubArray { nt, .. } => NtPayload::ScalarArray(nt.clone()),
280 Self::NtTable { nt, .. } => NtPayload::Table(nt.clone()),
281 Self::NtNdArray { nt, .. } => NtPayload::NdArray(nt.clone()),
282 Self::NtEnum { nt, .. } => NtPayload::Enum(nt.clone()),
283 Self::Generic {
284 struct_id, fields, ..
285 } => NtPayload::Generic {
286 struct_id: struct_id.clone(),
287 fields: fields.clone(),
288 },
289 }
290 }
291}
292
293#[derive(Debug, Clone, PartialEq)]
294pub struct RecordInstance {
295 pub name: String,
296 pub record_type: RecordType,
297 pub common: DbCommonState,
298 pub data: RecordData,
299 pub raw_fields: HashMap<String, String>,
300}
301
302impl RecordInstance {
303 pub fn writable(&self) -> bool {
304 if self.record_type.is_output() {
305 return true;
306 }
307 match &self.data {
308 RecordData::Ai { simm: true, .. } => true,
309 RecordData::Waveform { .. } => true,
310 RecordData::NtTable { .. } => true,
311 RecordData::NtNdArray { .. } => true,
312 RecordData::NtEnum { .. } => true,
313 RecordData::Generic { .. } => true,
314 _ => false,
315 }
316 }
317
318 pub fn to_ntpayload(&self) -> NtPayload {
319 self.data.payload()
320 }
321
322 pub fn to_ntscalar(&self) -> NtScalar {
323 match self.to_ntpayload() {
324 NtPayload::Scalar(nt) => nt,
325 NtPayload::ScalarArray(nt) => {
326 let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.value.len() as i32));
327 scalar.display_description = "Array length".to_string();
328 scalar
329 }
330 NtPayload::Table(nt) => {
331 let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.columns.len() as i32));
332 scalar.display_description = "Table columns".to_string();
333 scalar
334 }
335 NtPayload::NdArray(nt) => {
336 let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.dimension.len() as i32));
337 scalar.display_description = "NDArray dimensions".to_string();
338 scalar
339 }
340 NtPayload::Enum(nt) => {
341 let mut scalar = NtScalar::from_value(ScalarValue::I32(nt.index));
342 scalar.display_description = nt.selected().unwrap_or("").to_string();
343 scalar
344 }
345 NtPayload::Generic { fields, .. } => {
346 let mut scalar = NtScalar::from_value(ScalarValue::I32(fields.len() as i32));
347 scalar.display_description = "Generic structure".to_string();
348 scalar
349 }
350 }
351 }
352 pub fn nt_mut(&mut self) -> &mut NtScalar {
354 self.data.nt_mut()
355 }
356
357 pub(crate) fn nt_scalar_mut(&mut self) -> Option<&mut NtScalar> {
360 match &mut self.data {
361 RecordData::Ai { nt, .. }
362 | RecordData::Ao { nt, .. }
363 | RecordData::Bi { nt, .. }
364 | RecordData::Bo { nt, .. }
365 | RecordData::StringIn { nt, .. }
366 | RecordData::StringOut { nt, .. } => Some(nt),
367 _ => None,
368 }
369 }
370
371 pub fn current_value(&self) -> ScalarValue {
372 match self.to_ntpayload() {
373 NtPayload::Scalar(nt) => nt.value,
374 NtPayload::ScalarArray(nt) => ScalarValue::I32(nt.value.len() as i32),
375 NtPayload::Table(nt) => ScalarValue::I32(nt.columns.len() as i32),
376 NtPayload::NdArray(nt) => ScalarValue::I32(nt.dimension.len() as i32),
377 NtPayload::Enum(nt) => ScalarValue::I32(nt.index),
378 NtPayload::Generic { fields, .. } => ScalarValue::I32(fields.len() as i32),
379 }
380 }
381
382 pub(crate) fn stamp_missing_timestamps(&mut self) {
388 let ts = now_nt_timestamp();
389 match &mut self.data {
390 RecordData::Ai { nt, .. }
391 | RecordData::Ao { nt, .. }
392 | RecordData::Bi { nt, .. }
393 | RecordData::Bo { nt, .. }
394 | RecordData::StringIn { nt, .. }
395 | RecordData::StringOut { nt, .. } => {
396 if nt.time_stamp.is_none() {
397 nt.time_stamp = Some(ts);
398 }
399 }
400 RecordData::Waveform { nt, .. }
401 | RecordData::Aai { nt, .. }
402 | RecordData::Aao { nt, .. }
403 | RecordData::SubArray { nt, .. } => {
404 if nt.time_stamp == NtTimeStamp::default() {
405 nt.time_stamp = ts;
406 }
407 }
408 RecordData::NtEnum { nt, .. } => {
409 if nt.time_stamp == NtTimeStamp::default() {
410 nt.time_stamp = ts;
411 }
412 }
413 RecordData::NtTable { nt, .. } => {
414 if nt.time_stamp.is_none() {
415 nt.time_stamp = Some(ts);
416 }
417 }
418 RecordData::NtNdArray { nt, .. } => {
419 if nt.time_stamp.is_none() {
420 nt.time_stamp = Some(ts.clone());
421 }
422 if nt.data_time_stamp == NtTimeStamp::default() {
423 nt.data_time_stamp = ts;
424 }
425 }
426 RecordData::Generic { .. } => {}
427 }
428 }
429
430 pub fn set_time_stamp(&mut self, ts: NtTimeStamp) {
437 match &mut self.data {
438 RecordData::Ai { nt, .. }
439 | RecordData::Ao { nt, .. }
440 | RecordData::Bi { nt, .. }
441 | RecordData::Bo { nt, .. }
442 | RecordData::StringIn { nt, .. }
443 | RecordData::StringOut { nt, .. } => nt.time_stamp = Some(ts),
444 RecordData::Waveform { nt, .. }
445 | RecordData::Aai { nt, .. }
446 | RecordData::Aao { nt, .. }
447 | RecordData::SubArray { nt, .. } => nt.time_stamp = ts,
448 RecordData::NtEnum { nt, .. } => nt.time_stamp = ts,
449 RecordData::NtTable { nt, .. } => nt.time_stamp = Some(ts),
450 RecordData::NtNdArray { nt, .. } => {
451 nt.time_stamp = Some(ts.clone());
452 nt.data_time_stamp = ts;
453 }
454 RecordData::Generic { .. } => {}
455 }
456 }
457
458 pub fn set_scalar_value(&mut self, value: ScalarValue, compute_alarms: bool) -> bool {
459 if let RecordData::NtEnum { nt, .. } = &mut self.data {
460 let idx = match &value {
461 ScalarValue::I32(i) => *i,
462 ScalarValue::I16(i) => *i as i32,
463 ScalarValue::I8(i) => *i as i32,
464 ScalarValue::I64(i) => *i as i32,
465 _ => return false,
466 };
467 if idx < 0 || (idx as usize) >= nt.choices.len() {
468 return false; }
470 if nt.index == idx {
471 return false;
472 }
473 nt.index = idx;
474 nt.time_stamp = now_nt_timestamp();
477 return true;
478 }
479
480 let nt = match &mut self.data {
481 RecordData::Ai { nt, .. }
482 | RecordData::Ao { nt, .. }
483 | RecordData::Bi { nt, .. }
484 | RecordData::Bo { nt, .. }
485 | RecordData::StringIn { nt, .. }
486 | RecordData::StringOut { nt, .. } => nt,
487 _ => return false,
488 };
489
490 let changed = match (&mut nt.value, value) {
491 (ScalarValue::I8(current), ScalarValue::I8(v)) => {
497 if *current == v {
498 false
499 } else {
500 *current = v;
501 true
502 }
503 }
504 (ScalarValue::I16(current), ScalarValue::I16(v)) => {
505 if *current == v {
506 false
507 } else {
508 *current = v;
509 true
510 }
511 }
512 (ScalarValue::I64(current), ScalarValue::I64(v)) => {
513 if *current == v {
514 false
515 } else {
516 *current = v;
517 true
518 }
519 }
520 (ScalarValue::U8(current), ScalarValue::U8(v)) => {
521 if *current == v {
522 false
523 } else {
524 *current = v;
525 true
526 }
527 }
528 (ScalarValue::U16(current), ScalarValue::U16(v)) => {
529 if *current == v {
530 false
531 } else {
532 *current = v;
533 true
534 }
535 }
536 (ScalarValue::U32(current), ScalarValue::U32(v)) => {
537 if *current == v {
538 false
539 } else {
540 *current = v;
541 true
542 }
543 }
544 (ScalarValue::U64(current), ScalarValue::U64(v)) => {
545 if *current == v {
546 false
547 } else {
548 *current = v;
549 true
550 }
551 }
552 (ScalarValue::F32(current), ScalarValue::F32(v)) => {
553 if (*current - v).abs() < f32::EPSILON {
554 false
555 } else {
556 *current = v;
557 true
558 }
559 }
560 (ScalarValue::Bool(current), ScalarValue::Bool(v)) => {
561 if *current == v {
562 false
563 } else {
564 *current = v;
565 true
566 }
567 }
568 (ScalarValue::I32(current), ScalarValue::I32(v)) => {
569 if *current == v {
570 false
571 } else {
572 *current = v;
573 true
574 }
575 }
576 (ScalarValue::F64(current), ScalarValue::F64(v)) => {
577 if (*current - v).abs() < f64::EPSILON {
578 false
579 } else {
580 *current = v;
581 true
582 }
583 }
584 (ScalarValue::Str(current), ScalarValue::Str(v)) => {
585 if *current == v {
586 false
587 } else {
588 *current = v;
589 true
590 }
591 }
592 (ScalarValue::Bool(current), ScalarValue::I32(v)) => {
593 let next = v != 0;
594 if *current == next {
595 false
596 } else {
597 *current = next;
598 true
599 }
600 }
601 (ScalarValue::Bool(current), ScalarValue::F64(v)) => {
602 let next = v != 0.0;
603 if *current == next {
604 false
605 } else {
606 *current = next;
607 true
608 }
609 }
610 (ScalarValue::I32(current), ScalarValue::Bool(v)) => {
611 let next = if v { 1 } else { 0 };
612 if *current == next {
613 false
614 } else {
615 *current = next;
616 true
617 }
618 }
619 (ScalarValue::I32(current), ScalarValue::F64(v)) => {
620 let next = v as i32;
621 if *current == next {
622 false
623 } else {
624 *current = next;
625 true
626 }
627 }
628 (ScalarValue::F64(current), ScalarValue::Bool(v)) => {
629 let next = if v { 1.0 } else { 0.0 };
630 if (*current - next).abs() < f64::EPSILON {
631 false
632 } else {
633 *current = next;
634 true
635 }
636 }
637 (ScalarValue::F64(current), ScalarValue::I32(v)) => {
638 let next = v as f64;
639 if (*current - next).abs() < f64::EPSILON {
640 false
641 } else {
642 *current = next;
643 true
644 }
645 }
646 (ScalarValue::Str(current), ScalarValue::Bool(v)) => {
647 let next = if v { "1" } else { "0" }.to_string();
648 if *current == next {
649 false
650 } else {
651 *current = next;
652 true
653 }
654 }
655 (ScalarValue::Str(current), ScalarValue::I32(v)) => {
656 let next = v.to_string();
657 if *current == next {
658 false
659 } else {
660 *current = next;
661 true
662 }
663 }
664 (ScalarValue::Str(current), ScalarValue::F64(v)) => {
665 let next = v.to_string();
666 if *current == next {
667 false
668 } else {
669 *current = next;
670 true
671 }
672 }
673 (ScalarValue::Bool(current), ScalarValue::Str(v)) => {
674 let next = parse_bool_like(&v).unwrap_or(*current);
675 if *current == next {
676 false
677 } else {
678 *current = next;
679 true
680 }
681 }
682 (ScalarValue::I32(current), ScalarValue::Str(v)) => {
683 let next = v.parse::<i32>().unwrap_or(*current);
684 if *current == next {
685 false
686 } else {
687 *current = next;
688 true
689 }
690 }
691 (ScalarValue::F64(current), ScalarValue::Str(v)) => {
692 let next = v.parse::<f64>().unwrap_or(*current);
693 if (*current - next).abs() < f64::EPSILON {
694 false
695 } else {
696 *current = next;
697 true
698 }
699 }
700 (target, other) => {
703 let as_f64 = match &other {
704 ScalarValue::I8(v) => *v as f64,
705 ScalarValue::I16(v) => *v as f64,
706 ScalarValue::I64(v) => *v as f64,
707 ScalarValue::U8(v) => *v as f64,
708 ScalarValue::U16(v) => *v as f64,
709 ScalarValue::U32(v) => *v as f64,
710 ScalarValue::U64(v) => *v as f64,
711 ScalarValue::F32(v) => *v as f64,
712 ScalarValue::I32(v) => *v as f64,
718 ScalarValue::F64(v) => *v,
719 ScalarValue::Bool(v) => {
720 if *v {
721 1.0
722 } else {
723 0.0
724 }
725 }
726 _ => return false,
727 };
728 let as_i128: Option<i128> = match &other {
734 ScalarValue::I8(v) => Some(*v as i128),
735 ScalarValue::I16(v) => Some(*v as i128),
736 ScalarValue::I32(v) => Some(*v as i128),
737 ScalarValue::I64(v) => Some(*v as i128),
738 ScalarValue::U8(v) => Some(*v as i128),
739 ScalarValue::U16(v) => Some(*v as i128),
740 ScalarValue::U32(v) => Some(*v as i128),
741 ScalarValue::U64(v) => Some(*v as i128),
742 ScalarValue::Bool(v) => Some(if *v { 1 } else { 0 }),
743 _ => None,
744 };
745 match target {
746 ScalarValue::Bool(current) => {
747 let next = as_f64 != 0.0;
748 if *current == next {
749 false
750 } else {
751 *current = next;
752 true
753 }
754 }
755 ScalarValue::I32(current) => {
756 let next = as_f64 as i32;
757 if *current == next {
758 false
759 } else {
760 *current = next;
761 true
762 }
763 }
764 ScalarValue::F64(current) => {
765 if (*current - as_f64).abs() < f64::EPSILON {
766 false
767 } else {
768 *current = as_f64;
769 true
770 }
771 }
772 ScalarValue::Str(current) => {
773 let next = as_f64.to_string();
774 if *current == next {
775 false
776 } else {
777 *current = next;
778 true
779 }
780 }
781 ScalarValue::I8(current) => {
787 let next = as_i128.map(|i| i as i8).unwrap_or(as_f64 as i8);
788 if *current == next {
789 false
790 } else {
791 *current = next;
792 true
793 }
794 }
795 ScalarValue::I16(current) => {
796 let next = as_i128.map(|i| i as i16).unwrap_or(as_f64 as i16);
797 if *current == next {
798 false
799 } else {
800 *current = next;
801 true
802 }
803 }
804 ScalarValue::I64(current) => {
805 let next = as_i128.map(|i| i as i64).unwrap_or(as_f64 as i64);
806 if *current == next {
807 false
808 } else {
809 *current = next;
810 true
811 }
812 }
813 ScalarValue::U8(current) => {
814 let next = as_i128.map(|i| i as u8).unwrap_or(as_f64 as u8);
815 if *current == next {
816 false
817 } else {
818 *current = next;
819 true
820 }
821 }
822 ScalarValue::U16(current) => {
823 let next = as_i128.map(|i| i as u16).unwrap_or(as_f64 as u16);
824 if *current == next {
825 false
826 } else {
827 *current = next;
828 true
829 }
830 }
831 ScalarValue::U32(current) => {
832 let next = as_i128.map(|i| i as u32).unwrap_or(as_f64 as u32);
833 if *current == next {
834 false
835 } else {
836 *current = next;
837 true
838 }
839 }
840 ScalarValue::U64(current) => {
841 let next = as_i128.map(|i| i as u64).unwrap_or(as_f64 as u64);
842 if *current == next {
843 false
844 } else {
845 *current = next;
846 true
847 }
848 }
849 ScalarValue::F32(current) => {
850 let next = as_f64 as f32;
851 if (*current - next).abs() < f32::EPSILON {
852 false
853 } else {
854 *current = next;
855 true
856 }
857 }
858 }
859 }
860 };
861 if changed {
862 nt.time_stamp = Some(now_nt_timestamp());
869 if compute_alarms {
870 nt.update_alarm_from_value();
871 }
872 }
873 changed
874 }
875
876 pub fn set_array_value(&mut self, value: ScalarArrayValue) -> bool {
877 let ts = now_nt_timestamp();
881 if let RecordData::NtNdArray { nt, .. } = &mut self.data {
883 if nt.value == value {
884 return false;
885 }
886 nt.value = value;
887 nt.time_stamp = Some(ts.clone());
888 nt.data_time_stamp = ts;
889 return true;
890 }
891 let (nt, nord, nelm) = match &mut self.data {
892 RecordData::Waveform { nt, nord, nelm, .. }
893 | RecordData::Aai { nt, nord, nelm, .. }
894 | RecordData::Aao { nt, nord, nelm, .. }
895 | RecordData::SubArray { nt, nord, nelm, .. } => (nt, nord, *nelm),
896 _ => return false,
897 };
898
899 let mut next = value;
900 truncate_scalar_array_to_nelm(&mut next, nelm);
901 if nt.value == next {
902 return false;
903 }
904
905 *nord = next.len();
906 nt.value = next;
907 nt.time_stamp = ts;
908 true
909 }
910
911 pub fn set_nt_payload(&mut self, payload: NtPayload) -> bool {
912 match (&mut self.data, payload) {
913 (
914 RecordData::Ai { nt, .. }
915 | RecordData::Ao { nt, .. }
916 | RecordData::Bi { nt, .. }
917 | RecordData::Bo { nt, .. }
918 | RecordData::StringIn { nt, .. }
919 | RecordData::StringOut { nt, .. },
920 NtPayload::Scalar(mut next),
921 ) => {
922 if *nt == next {
923 false
924 } else {
925 if next.time_stamp.is_none() {
929 next.time_stamp = Some(now_nt_timestamp());
930 }
931 *nt = next;
932 true
933 }
934 }
935 (
936 RecordData::Waveform { nt, nord, nelm, .. }
937 | RecordData::Aai { nt, nord, nelm, .. }
938 | RecordData::Aao { nt, nord, nelm, .. }
939 | RecordData::SubArray { nt, nord, nelm, .. },
940 NtPayload::ScalarArray(mut next),
941 ) => {
942 truncate_scalar_array_to_nelm(&mut next.value, *nelm);
943 let next_len = next.value.len();
944 if *nt == next {
945 false
946 } else {
947 if next.time_stamp == NtTimeStamp::default() {
950 next.time_stamp = now_nt_timestamp();
951 }
952 *nord = next_len;
953 *nt = next;
954 true
955 }
956 }
957 (RecordData::NtTable { nt, .. }, NtPayload::Table(mut next)) => {
958 if next.validate().is_err() || *nt == next {
959 false
960 } else {
961 if next.time_stamp.is_none() {
962 next.time_stamp = Some(now_nt_timestamp());
963 }
964 *nt = next;
965 true
966 }
967 }
968 (RecordData::NtNdArray { nt, .. }, NtPayload::NdArray(mut next)) => {
969 if next.validate().is_err() || *nt == next {
970 false
971 } else {
972 let ts = now_nt_timestamp();
973 if next.time_stamp.is_none() {
974 next.time_stamp = Some(ts.clone());
975 }
976 if next.data_time_stamp == NtTimeStamp::default() {
977 next.data_time_stamp = ts;
978 }
979 *nt = next;
980 true
981 }
982 }
983 (RecordData::NtEnum { nt, .. }, NtPayload::Enum(mut next)) => {
984 if *nt == next {
985 false
986 } else {
987 if next.time_stamp == NtTimeStamp::default() {
988 next.time_stamp = now_nt_timestamp();
989 }
990 *nt = next;
991 true
992 }
993 }
994 (
995 RecordData::Generic {
996 struct_id, fields, ..
997 },
998 NtPayload::Generic {
999 struct_id: next_id,
1000 fields: next_fields,
1001 },
1002 ) => {
1003 if *struct_id == next_id && *fields == next_fields {
1004 false
1005 } else {
1006 *struct_id = next_id;
1007 *fields = next_fields;
1008 true
1009 }
1010 }
1011 _ => false,
1012 }
1013 }
1014}
1015
1016fn truncate_scalar_array_to_nelm(value: &mut ScalarArrayValue, nelm: usize) {
1017 match value {
1018 ScalarArrayValue::Bool(v) => v.truncate(nelm),
1019 ScalarArrayValue::I8(v) => v.truncate(nelm),
1020 ScalarArrayValue::I16(v) => v.truncate(nelm),
1021 ScalarArrayValue::I32(v) => v.truncate(nelm),
1022 ScalarArrayValue::I64(v) => v.truncate(nelm),
1023 ScalarArrayValue::U8(v) => v.truncate(nelm),
1024 ScalarArrayValue::U16(v) => v.truncate(nelm),
1025 ScalarArrayValue::U32(v) => v.truncate(nelm),
1026 ScalarArrayValue::U64(v) => v.truncate(nelm),
1027 ScalarArrayValue::F32(v) => v.truncate(nelm),
1028 ScalarArrayValue::F64(v) => v.truncate(nelm),
1029 ScalarArrayValue::Str(v) => v.truncate(nelm),
1030 }
1031}
1032
1033fn parse_bool_like(input: &str) -> Option<bool> {
1034 match input.trim().to_ascii_lowercase().as_str() {
1035 "1" | "true" | "yes" | "on" => Some(true),
1036 "0" | "false" | "no" | "off" => Some(false),
1037 _ => None,
1038 }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044
1045 fn ai_record(val: f64) -> RecordInstance {
1046 RecordInstance {
1047 name: "T".to_string(),
1048 record_type: RecordType::Ai,
1049 common: DbCommonState::default(),
1050 data: RecordData::Ai {
1051 nt: NtScalar::from_value(ScalarValue::F64(val)),
1052 inp: None,
1053 siml: None,
1054 siol: None,
1055 simm: false,
1056 },
1057 raw_fields: std::collections::HashMap::new(),
1058 }
1059 }
1060
1061 #[test]
1062 fn set_time_stamp_overwrites_an_existing_stamp() {
1063 let mut rec = ai_record(1.0);
1064 rec.set_time_stamp(NtTimeStamp {
1065 seconds_past_epoch: 100,
1066 nanoseconds: 0,
1067 user_tag: 0,
1068 });
1069 let replacement = NtTimeStamp {
1070 seconds_past_epoch: 200,
1071 nanoseconds: 7,
1072 user_tag: 3,
1073 };
1074 rec.set_time_stamp(replacement.clone());
1075
1076 let RecordData::Ai { nt, .. } = &rec.data else {
1077 panic!("expected Ai");
1078 };
1079 assert_eq!(nt.time_stamp, Some(replacement));
1080 }
1081
1082 #[test]
1083 fn set_time_stamp_on_generic_record_is_a_no_op() {
1084 let mut rec = RecordInstance {
1085 name: "G".to_string(),
1086 record_type: RecordType::Generic,
1087 common: DbCommonState::default(),
1088 data: RecordData::Generic {
1089 struct_id: "epics:nt/NTScalar:1.0".to_string(),
1090 fields: vec![],
1091 inp: None,
1092 out: None,
1093 omsl: OutputMode::Supervisory,
1094 },
1095 raw_fields: std::collections::HashMap::new(),
1096 };
1097 rec.set_time_stamp(now_nt_timestamp());
1099 let RecordData::Generic { fields, .. } = &rec.data else {
1100 panic!("expected Generic");
1101 };
1102 assert!(fields.is_empty());
1103 }
1104}