1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use tokio::sync::{RwLock, mpsc};
10use tracing::debug;
11
12use std::future::Future;
13use std::pin::Pin;
14
15use spvirit_codec::spvd_decode::{DecodedValue, FieldDesc, FieldType, StructureDesc, TypeCode};
16use spvirit_types::{NtPayload, ScalarArrayValue, ScalarValue};
17
18use crate::apply::{
19 apply_alarm_update, apply_control_update, apply_display_update, apply_scalar_array_put,
20 apply_value_update,
21};
22use crate::monitor::MonitorRegistry;
23use crate::pvstore::{PvInfo, Source};
24use crate::types::{RecordData, RecordInstance};
25
26pub type OnPutCallback = Arc<dyn Fn(&str, &DecodedValue) + Send + Sync>;
28
29pub type ScanCallback = Arc<dyn Fn(&str) -> ScalarValue + Send + Sync>;
31
32pub type LinkCallback = Arc<dyn Fn(&[ScalarValue]) -> ScalarValue + Send + Sync>;
34
35pub(crate) struct LinkDef {
37 pub output: String,
38 pub inputs: Vec<String>,
39 pub compute: LinkCallback,
40}
41
42struct PvEntry {
43 record: RecordInstance,
44 subscribers: Vec<mpsc::Sender<NtPayload>>,
45 last_posted: Option<f64>,
49}
50
51pub struct SimplePvStore {
53 pvs: RwLock<HashMap<String, PvEntry>>,
54 on_put: HashMap<String, OnPutCallback>,
55 links: Vec<LinkDef>,
56 compute_alarms: bool,
57 registry: RwLock<Option<Arc<MonitorRegistry>>>,
58}
59
60impl SimplePvStore {
61 pub(crate) fn new(
62 records: HashMap<String, RecordInstance>,
63 on_put: HashMap<String, OnPutCallback>,
64 links: Vec<LinkDef>,
65 compute_alarms: bool,
66 ) -> Self {
67 let pvs = records
68 .into_iter()
69 .map(|(name, record)| {
70 let last_posted = initial_posted(&record);
71 (
72 name,
73 PvEntry {
74 record,
75 subscribers: Vec::new(),
76 last_posted,
77 },
78 )
79 })
80 .collect();
81 Self {
82 pvs: RwLock::new(pvs),
83 on_put,
84 links,
85 compute_alarms,
86 registry: RwLock::new(None),
87 }
88 }
89
90 pub async fn set_registry(&self, registry: Arc<MonitorRegistry>) {
93 *self.registry.write().await = Some(registry);
94 }
95
96 pub async fn insert(&self, name: String, record: RecordInstance) {
98 let mut pvs = self.pvs.write().await;
99 let last_posted = initial_posted(&record);
100 pvs.insert(
101 name,
102 PvEntry {
103 record,
104 subscribers: Vec::new(),
105 last_posted,
106 },
107 );
108 }
109
110 pub async fn get_value(&self, name: &str) -> Option<ScalarValue> {
112 let pvs = self.pvs.read().await;
113 pvs.get(name).map(|e| e.record.current_value())
114 }
115
116 pub async fn get_record(&self, name: &str) -> Option<RecordInstance> {
121 let pvs = self.pvs.read().await;
122 pvs.get(name).map(|e| e.record.clone())
123 }
124
125 pub async fn get_nt(&self, name: &str) -> Option<NtPayload> {
127 let pvs = self.pvs.read().await;
128 pvs.get(name).map(|e| e.record.to_ntpayload())
129 }
130
131 pub async fn set_value(&self, name: &str, value: ScalarValue) -> bool {
133 if self.set_value_inner(name, value).await {
134 self.evaluate_links(name).await;
135 true
136 } else {
137 false
138 }
139 }
140
141 pub async fn set_array_value(&self, name: &str, value: ScalarArrayValue) -> bool {
143 if self.set_array_value_inner(name, value).await {
144 self.evaluate_links(name).await;
145 true
146 } else {
147 false
148 }
149 }
150
151 pub async fn put_nt(&self, name: &str, payload: NtPayload) -> bool {
153 if self.put_nt_inner(name, payload).await {
154 self.evaluate_links(name).await;
155 true
156 } else {
157 false
158 }
159 }
160
161 async fn set_value_inner(&self, name: &str, value: ScalarValue) -> bool {
164 let payload = {
165 let mut pvs = self.pvs.write().await;
166 if let Some(entry) = pvs.get_mut(name) {
167 let prev_severity = entry.record.to_ntscalar().alarm_severity;
168 let changed = entry.record.set_scalar_value(value, self.compute_alarms);
169 if changed {
170 if !should_post_update(entry, prev_severity) {
171 return true;
175 }
176 let payload = entry.record.to_ntpayload();
177 entry
178 .subscribers
179 .retain(|tx| tx.try_send(payload.clone()).is_ok());
180 Some(payload)
181 } else {
182 None
183 }
184 } else {
185 return false;
186 }
187 };
188
189 if let Some(payload) = payload {
190 let reg = self.registry.read().await;
192 if let Some(registry) = reg.as_ref() {
193 registry.notify_monitors(name, &payload).await;
194 }
195 true
196 } else {
197 false
198 }
199 }
200
201 async fn set_array_value_inner(&self, name: &str, value: ScalarArrayValue) -> bool {
204 let payload = {
205 let mut pvs = self.pvs.write().await;
206 if let Some(entry) = pvs.get_mut(name) {
207 let changed = entry.record.set_array_value(value);
208 if changed {
209 let payload = entry.record.to_ntpayload();
210 entry
211 .subscribers
212 .retain(|tx| tx.try_send(payload.clone()).is_ok());
213 Some(payload)
214 } else {
215 None
216 }
217 } else {
218 return false;
219 }
220 };
221
222 if let Some(payload) = payload {
223 let reg = self.registry.read().await;
225 if let Some(registry) = reg.as_ref() {
226 registry.notify_monitors(name, &payload).await;
227 }
228 true
229 } else {
230 false
231 }
232 }
233
234 async fn put_nt_inner(&self, name: &str, payload: NtPayload) -> bool {
237 let payload = {
238 let mut pvs = self.pvs.write().await;
239 if let Some(entry) = pvs.get_mut(name) {
240 let changed = entry.record.set_nt_payload(payload);
241 if changed {
242 let payload = entry.record.to_ntpayload();
243 entry
244 .subscribers
245 .retain(|tx| tx.try_send(payload.clone()).is_ok());
246 Some(payload)
247 } else {
248 None
249 }
250 } else {
251 return false;
252 }
253 };
254
255 if let Some(payload) = payload {
256 let reg = self.registry.read().await;
258 if let Some(registry) = reg.as_ref() {
259 registry.notify_monitors(name, &payload).await;
260 }
261 true
262 } else {
263 false
264 }
265 }
266
267 async fn evaluate_links(&self, changed_pv: &str) {
270 if self.links.is_empty() {
271 return;
272 }
273 let mut queue = vec![changed_pv.to_string()];
274 let mut visited = HashSet::new();
275
276 while let Some(pv) = queue.pop() {
277 if !visited.insert(pv.clone()) {
278 debug!("Circular link detected for PV '{}', skipping", pv);
279 continue;
280 }
281 for link in &self.links {
282 if !link.inputs.iter().any(|i| i == &pv) {
283 continue;
284 }
285 let values = {
287 let pvs = self.pvs.read().await;
288 link.inputs
289 .iter()
290 .map(|n| {
291 pvs.get(n)
292 .map(|e| e.record.current_value())
293 .unwrap_or(ScalarValue::F64(0.0))
294 })
295 .collect::<Vec<_>>()
296 };
297 let new_val = (link.compute)(&values);
298 if self.set_value_inner(&link.output, new_val).await {
299 queue.push(link.output.clone());
300 }
301 }
302 }
303 }
304
305 pub async fn pv_names(&self) -> Vec<String> {
307 let pvs = self.pvs.read().await;
308 pvs.keys().cloned().collect()
309 }
310}
311
312impl Source for SimplePvStore {
313 fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
314 let name = name.to_string();
315 Box::pin(async move {
316 let pvs = self.pvs.read().await;
317 let entry = pvs.get(&name)?;
318 let descriptor = descriptor_for_payload(&entry.record.to_ntpayload());
319 Some(PvInfo {
320 descriptor,
321 writable: entry.record.writable(),
322 })
323 })
324 }
325
326 fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
327 let name = name.to_string();
328 Box::pin(async move {
329 let pvs = self.pvs.read().await;
330 pvs.get(&name).map(|e| e.record.to_ntpayload())
331 })
332 }
333
334 fn put(
335 &self,
336 name: &str,
337 value: &DecodedValue,
338 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
339 let name = name.to_string();
340 let value = value.clone();
341 Box::pin(async move {
342 let result = {
343 let mut pvs = self.pvs.write().await;
344 let entry = pvs
345 .get_mut(&name)
346 .ok_or_else(|| format!("PV '{}' not found", name))?;
347
348 if !entry.record.writable() {
349 return Err(format!("PV '{}' is not writable", name));
350 }
351
352 let prev_severity = entry.record.to_ntscalar().alarm_severity;
353 let changed = apply_put_to_record(&mut entry.record, &value, self.compute_alarms);
354 if !changed {
355 return Ok(vec![]);
356 }
357
358 if should_post_update(entry, prev_severity) {
359 let payload = entry.record.to_ntpayload();
360 entry
361 .subscribers
362 .retain(|tx| tx.try_send(payload.clone()).is_ok());
363 Some((name.clone(), payload))
364 } else {
365 None
367 }
368 }; if let Some(cb) = self.on_put.get(&name) {
372 let cb = cb.clone();
373 let n = name.clone();
374 let v = value.clone();
375 tokio::spawn(async move { cb(&n, &v) });
376 }
377
378 self.evaluate_links(&name).await;
380
381 Ok(result.into_iter().collect())
382 })
383 }
384
385 fn subscribe(
386 &self,
387 name: &str,
388 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
389 let name = name.to_string();
390 Box::pin(async move {
391 let mut pvs = self.pvs.write().await;
392 let entry = pvs.get_mut(&name)?;
393 let (tx, rx) = mpsc::channel(64);
394 entry.subscribers.push(tx);
395 Some(rx)
396 })
397 }
398
399 fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
400 Box::pin(async move {
401 let pvs = self.pvs.read().await;
402 pvs.keys().cloned().collect()
403 })
404 }
405}
406
407fn scalar_as_f64(v: &ScalarValue) -> Option<f64> {
411 Some(match v {
412 ScalarValue::I8(x) => *x as f64,
413 ScalarValue::I16(x) => *x as f64,
414 ScalarValue::I32(x) => *x as f64,
415 ScalarValue::I64(x) => *x as f64,
416 ScalarValue::U8(x) => *x as f64,
417 ScalarValue::U16(x) => *x as f64,
418 ScalarValue::U32(x) => *x as f64,
419 ScalarValue::U64(x) => *x as f64,
420 ScalarValue::F32(x) => *x as f64,
421 ScalarValue::F64(x) => *x,
422 ScalarValue::Bool(_) | ScalarValue::Str(_) => return None,
423 })
424}
425
426fn initial_posted(record: &RecordInstance) -> Option<f64> {
430 match record.to_ntpayload() {
431 NtPayload::Scalar(nt) => scalar_as_f64(&nt.value),
432 _ => None,
433 }
434}
435
436fn should_post_update(entry: &mut PvEntry, prev_severity: i32) -> bool {
444 let new_f = match entry.record.to_ntpayload() {
445 NtPayload::Scalar(nt) => match scalar_as_f64(&nt.value) {
446 Some(f) => f,
447 None => return true,
448 },
449 _ => return true,
450 };
451 let mdel = crate::record_fields::mdel_of(&entry.record);
452 let severity_changed = entry.record.to_ntscalar().alarm_severity != prev_severity;
453 let within_deadband = mdel > 0.0
454 && !severity_changed
455 && entry
456 .last_posted
457 .is_some_and(|last| (new_f - last).abs() < mdel);
458 if within_deadband {
459 return false;
460 }
461 entry.last_posted = Some(new_f);
462 true
463}
464
465fn apply_put_to_record(
467 record: &mut RecordInstance,
468 value: &DecodedValue,
469 compute_alarms: bool,
470) -> bool {
471 let fields = match value {
472 DecodedValue::Structure(f) => f,
473 other => {
474 return apply_put_to_record(
476 record,
477 &DecodedValue::Structure(vec![("value".to_string(), other.clone())]),
478 compute_alarms,
479 );
480 }
481 };
482
483 let mut changed = false;
484
485 match &mut record.data {
486 RecordData::Ai { nt, .. }
487 | RecordData::Ao { nt, .. }
488 | RecordData::Bi { nt, .. }
489 | RecordData::Bo { nt, .. }
490 | RecordData::StringIn { nt, .. }
491 | RecordData::StringOut { nt, .. } => {
492 for (name, val) in fields {
493 match name.as_str() {
494 "value" => {
495 changed |= apply_value_update(nt, val, compute_alarms);
496 }
497 "alarm" => {
498 changed |= apply_alarm_update(nt, val);
499 }
500 "display" => {
501 changed |= apply_display_update(nt, val);
502 }
503 "control" => {
504 changed |= apply_control_update(nt, val);
505 }
506 _ => {}
507 }
508 }
509 }
510 RecordData::Waveform { nt, nord, .. }
511 | RecordData::Aai { nt, nord, .. }
512 | RecordData::Aao { nt, nord, .. }
513 | RecordData::SubArray { nt, nord, .. } => {
514 changed = apply_scalar_array_put(nt, nord, value);
515 }
516 RecordData::NtTable { .. } | RecordData::NtNdArray { .. } => {
517 debug!("PUT to NtTable/NtNdArray not yet supported in SimplePvStore");
519 }
520 RecordData::NtEnum { nt, .. } => {
521 for (name, val) in fields {
523 if name == "value" {
524 let idx = match val {
525 DecodedValue::Int32(v) => Some(*v),
526 DecodedValue::Int64(v) => Some(*v as i32),
527 DecodedValue::Int16(v) => Some(*v as i32),
528 DecodedValue::Int8(v) => Some(*v as i32),
529 DecodedValue::Float64(v) => Some(*v as i32),
530 _ => None,
531 };
532 if let Some(idx) = idx {
533 if nt.index != idx {
534 nt.index = idx;
535 changed = true;
536 }
537 }
538 }
539 }
540 }
541 RecordData::Generic { .. } => {
542 debug!("PUT to Generic not yet supported in SimplePvStore");
543 }
544 }
545
546 changed
547}
548
549pub fn descriptor_for_payload(payload: &NtPayload) -> StructureDesc {
552 match payload {
553 NtPayload::Scalar(nt) => nt_scalar_desc(&nt.value),
554 NtPayload::ScalarArray(arr) => nt_scalar_array_desc(&arr.value),
555 _ => StructureDesc::new(),
556 }
557}
558
559fn value_type_code(sv: &ScalarValue) -> TypeCode {
560 match sv {
561 ScalarValue::Bool(_) => TypeCode::Boolean,
562 ScalarValue::I8(_) => TypeCode::Int8,
563 ScalarValue::I16(_) => TypeCode::Int16,
564 ScalarValue::I32(_) => TypeCode::Int32,
565 ScalarValue::I64(_) => TypeCode::Int64,
566 ScalarValue::U8(_) => TypeCode::UInt8,
567 ScalarValue::U16(_) => TypeCode::UInt16,
568 ScalarValue::U32(_) => TypeCode::UInt32,
569 ScalarValue::U64(_) => TypeCode::UInt64,
570 ScalarValue::F32(_) => TypeCode::Float32,
571 ScalarValue::F64(_) => TypeCode::Float64,
572 ScalarValue::Str(_) => TypeCode::String,
573 }
574}
575
576fn array_type_code(sav: &ScalarArrayValue) -> TypeCode {
577 match sav {
578 ScalarArrayValue::Bool(_) => TypeCode::Boolean,
579 ScalarArrayValue::I8(_) => TypeCode::Int8,
580 ScalarArrayValue::I16(_) => TypeCode::Int16,
581 ScalarArrayValue::I32(_) => TypeCode::Int32,
582 ScalarArrayValue::I64(_) => TypeCode::Int64,
583 ScalarArrayValue::U8(_) => TypeCode::UInt8,
584 ScalarArrayValue::U16(_) => TypeCode::UInt16,
585 ScalarArrayValue::U32(_) => TypeCode::UInt32,
586 ScalarArrayValue::U64(_) => TypeCode::UInt64,
587 ScalarArrayValue::F32(_) => TypeCode::Float32,
588 ScalarArrayValue::F64(_) => TypeCode::Float64,
589 ScalarArrayValue::Str(_) => TypeCode::String,
590 }
591}
592
593fn nt_scalar_desc(sv: &ScalarValue) -> StructureDesc {
594 let tc = value_type_code(sv);
595 StructureDesc {
596 struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
597 fields: vec![
598 FieldDesc {
599 name: "value".to_string(),
600 field_type: FieldType::Scalar(tc),
601 },
602 FieldDesc {
603 name: "alarm".to_string(),
604 field_type: FieldType::Structure(alarm_desc()),
605 },
606 FieldDesc {
607 name: "timeStamp".to_string(),
608 field_type: FieldType::Structure(timestamp_desc()),
609 },
610 FieldDesc {
611 name: "display".to_string(),
612 field_type: FieldType::Structure(display_desc()),
613 },
614 FieldDesc {
615 name: "control".to_string(),
616 field_type: FieldType::Structure(control_desc()),
617 },
618 FieldDesc {
619 name: "valueAlarm".to_string(),
620 field_type: FieldType::Structure(value_alarm_desc()),
621 },
622 ],
623 }
624}
625
626fn nt_scalar_array_desc(sav: &ScalarArrayValue) -> StructureDesc {
627 let tc = array_type_code(sav);
628 StructureDesc {
629 struct_id: Some("epics:nt/NTScalarArray:1.0".to_string()),
630 fields: vec![
631 FieldDesc {
632 name: "value".to_string(),
633 field_type: FieldType::ScalarArray(tc),
634 },
635 FieldDesc {
636 name: "alarm".to_string(),
637 field_type: FieldType::Structure(alarm_desc()),
638 },
639 FieldDesc {
640 name: "timeStamp".to_string(),
641 field_type: FieldType::Structure(timestamp_desc()),
642 },
643 FieldDesc {
644 name: "display".to_string(),
645 field_type: FieldType::Structure(display_desc()),
646 },
647 FieldDesc {
648 name: "control".to_string(),
649 field_type: FieldType::Structure(control_desc()),
650 },
651 ],
652 }
653}
654
655fn alarm_desc() -> StructureDesc {
656 StructureDesc {
657 struct_id: Some("alarm_t".to_string()),
658 fields: vec![
659 FieldDesc {
660 name: "severity".to_string(),
661 field_type: FieldType::Scalar(TypeCode::Int32),
662 },
663 FieldDesc {
664 name: "status".to_string(),
665 field_type: FieldType::Scalar(TypeCode::Int32),
666 },
667 FieldDesc {
668 name: "message".to_string(),
669 field_type: FieldType::String,
670 },
671 ],
672 }
673}
674
675fn timestamp_desc() -> StructureDesc {
676 StructureDesc {
677 struct_id: Some("time_t".to_string()),
678 fields: vec![
679 FieldDesc {
680 name: "secondsPastEpoch".to_string(),
681 field_type: FieldType::Scalar(TypeCode::Int64),
682 },
683 FieldDesc {
684 name: "nanoseconds".to_string(),
685 field_type: FieldType::Scalar(TypeCode::Int32),
686 },
687 FieldDesc {
688 name: "userTag".to_string(),
689 field_type: FieldType::Scalar(TypeCode::Int32),
690 },
691 ],
692 }
693}
694
695fn display_desc() -> StructureDesc {
696 StructureDesc {
697 struct_id: Some("display_t".to_string()),
698 fields: vec![
699 FieldDesc {
700 name: "limitLow".to_string(),
701 field_type: FieldType::Scalar(TypeCode::Float64),
702 },
703 FieldDesc {
704 name: "limitHigh".to_string(),
705 field_type: FieldType::Scalar(TypeCode::Float64),
706 },
707 FieldDesc {
708 name: "description".to_string(),
709 field_type: FieldType::String,
710 },
711 FieldDesc {
712 name: "units".to_string(),
713 field_type: FieldType::String,
714 },
715 FieldDesc {
716 name: "precision".to_string(),
717 field_type: FieldType::Scalar(TypeCode::Int32),
718 },
719 FieldDesc {
720 name: "form".to_string(),
721 field_type: FieldType::Structure(StructureDesc {
722 struct_id: Some("enum_t".to_string()),
723 fields: vec![
724 FieldDesc {
725 name: "index".to_string(),
726 field_type: FieldType::Scalar(TypeCode::Int32),
727 },
728 FieldDesc {
729 name: "choices".to_string(),
730 field_type: FieldType::StringArray,
731 },
732 ],
733 }),
734 },
735 ],
736 }
737}
738
739fn control_desc() -> StructureDesc {
740 StructureDesc {
741 struct_id: Some("control_t".to_string()),
742 fields: vec![
743 FieldDesc {
744 name: "limitLow".to_string(),
745 field_type: FieldType::Scalar(TypeCode::Float64),
746 },
747 FieldDesc {
748 name: "limitHigh".to_string(),
749 field_type: FieldType::Scalar(TypeCode::Float64),
750 },
751 FieldDesc {
752 name: "minStep".to_string(),
753 field_type: FieldType::Scalar(TypeCode::Float64),
754 },
755 ],
756 }
757}
758
759fn value_alarm_desc() -> StructureDesc {
760 StructureDesc {
761 struct_id: Some("valueAlarm_t".to_string()),
762 fields: vec![
763 FieldDesc {
764 name: "active".to_string(),
765 field_type: FieldType::Scalar(TypeCode::Boolean),
766 },
767 FieldDesc {
768 name: "lowAlarmLimit".to_string(),
769 field_type: FieldType::Scalar(TypeCode::Float64),
770 },
771 FieldDesc {
772 name: "lowWarningLimit".to_string(),
773 field_type: FieldType::Scalar(TypeCode::Float64),
774 },
775 FieldDesc {
776 name: "highWarningLimit".to_string(),
777 field_type: FieldType::Scalar(TypeCode::Float64),
778 },
779 FieldDesc {
780 name: "highAlarmLimit".to_string(),
781 field_type: FieldType::Scalar(TypeCode::Float64),
782 },
783 FieldDesc {
784 name: "lowAlarmSeverity".to_string(),
785 field_type: FieldType::Scalar(TypeCode::Int32),
786 },
787 FieldDesc {
788 name: "lowWarningSeverity".to_string(),
789 field_type: FieldType::Scalar(TypeCode::Int32),
790 },
791 FieldDesc {
792 name: "highWarningSeverity".to_string(),
793 field_type: FieldType::Scalar(TypeCode::Int32),
794 },
795 FieldDesc {
796 name: "highAlarmSeverity".to_string(),
797 field_type: FieldType::Scalar(TypeCode::Int32),
798 },
799 FieldDesc {
800 name: "hysteresis".to_string(),
801 field_type: FieldType::Scalar(TypeCode::UInt8),
802 },
803 ],
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use crate::types::{DbCommonState, RecordType};
811 use spvirit_types::{
812 NdCodec, NdDimension, NtNdArray, NtPayload, NtScalar, NtScalarArray, NtTable,
813 NtTableColumn, ScalarArrayValue, ScalarValue,
814 };
815
816 fn make_ai(name: &str, val: f64) -> RecordInstance {
817 RecordInstance {
818 name: name.to_string(),
819 record_type: RecordType::Ai,
820 common: DbCommonState::default(),
821 data: RecordData::Ai {
822 nt: NtScalar::from_value(ScalarValue::F64(val)),
823 inp: None,
824 siml: None,
825 siol: None,
826 simm: false,
827 },
828 raw_fields: HashMap::new(),
829 }
830 }
831
832 fn make_ao(name: &str, val: f64) -> RecordInstance {
833 RecordInstance {
834 name: name.to_string(),
835 record_type: RecordType::Ao,
836 common: DbCommonState::default(),
837 data: RecordData::Ao {
838 nt: NtScalar::from_value(ScalarValue::F64(val)),
839 out: None,
840 dol: None,
841 omsl: crate::types::OutputMode::Supervisory,
842 drvl: None,
843 drvh: None,
844 oroc: None,
845 siml: None,
846 siol: None,
847 simm: false,
848 },
849 raw_fields: HashMap::new(),
850 }
851 }
852
853 #[tokio::test]
854 async fn mdel_deadband_suppresses_small_monitor_updates() {
855 let recs = crate::db::parse_db(
856 r#"
857record(ao, "DB:AO") {
858 field(VAL, "0.0")
859 field(MDEL, "0.5")
860}"#,
861 )
862 .expect("parse");
863 let store = SimplePvStore::new(recs, HashMap::new(), Vec::new(), false);
864 let mut rx = Source::subscribe(&store, "DB:AO")
865 .await
866 .expect("subscribed");
867
868 assert!(store.set_value("DB:AO", ScalarValue::F64(0.2)).await);
870 assert!(store.set_value("DB:AO", ScalarValue::F64(0.9)).await);
872
873 match rx.recv().await.expect("posted update") {
874 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(0.9)),
875 other => panic!("expected scalar, got {other:?}"),
876 }
877 assert!(rx.try_recv().is_err());
879 assert_eq!(store.get_value("DB:AO").await, Some(ScalarValue::F64(0.9)));
881 }
882
883 fn make_waveform(name: &str, value: ScalarArrayValue) -> RecordInstance {
884 let nelm = value.len();
885 RecordInstance {
886 name: name.to_string(),
887 record_type: RecordType::Waveform,
888 common: DbCommonState::default(),
889 data: RecordData::Waveform {
890 nt: NtScalarArray::from_value(value),
891 inp: None,
892 ftvl: "DOUBLE".to_string(),
893 nelm,
894 nord: nelm,
895 },
896 raw_fields: HashMap::new(),
897 }
898 }
899
900 fn make_nt_table(name: &str) -> RecordInstance {
901 RecordInstance {
902 name: name.to_string(),
903 record_type: RecordType::NtTable,
904 common: DbCommonState::default(),
905 data: RecordData::NtTable {
906 nt: NtTable {
907 labels: vec!["X".to_string(), "Y".to_string()],
908 columns: vec![
909 NtTableColumn {
910 name: "x".to_string(),
911 values: ScalarArrayValue::F64(vec![1.0, 2.0]),
912 },
913 NtTableColumn {
914 name: "y".to_string(),
915 values: ScalarArrayValue::F64(vec![10.0, 20.0]),
916 },
917 ],
918 descriptor: Some("table".to_string()),
919 alarm: None,
920 time_stamp: None,
921 },
922 inp: None,
923 out: None,
924 omsl: crate::types::OutputMode::Supervisory,
925 },
926 raw_fields: HashMap::new(),
927 }
928 }
929
930 fn make_nt_ndarray(name: &str) -> RecordInstance {
931 RecordInstance {
932 name: name.to_string(),
933 record_type: RecordType::NtNdArray,
934 common: DbCommonState::default(),
935 data: RecordData::NtNdArray {
936 nt: NtNdArray {
937 value: ScalarArrayValue::U8(vec![0; 4]),
938 codec: NdCodec {
939 name: "none".to_string(),
940 parameters: HashMap::new(),
941 },
942 compressed_size: 4,
943 uncompressed_size: 4,
944 dimension: vec![NdDimension {
945 size: 2,
946 offset: 0,
947 full_size: 2,
948 binning: 1,
949 reverse: false,
950 }],
951 unique_id: 1,
952 data_time_stamp: Default::default(),
953 attribute: vec![],
954 descriptor: Some("ndarray".to_string()),
955 alarm: None,
956 time_stamp: None,
957 display: None,
958 },
959 inp: None,
960 out: None,
961 omsl: crate::types::OutputMode::Supervisory,
962 },
963 raw_fields: HashMap::new(),
964 }
965 }
966
967 #[tokio::test]
968 async fn has_pv_returns_true_for_existing() {
969 let mut records = HashMap::new();
970 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
971 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
972 assert!(store.claim("TEST:AI").await.is_some());
973 assert!(store.claim("MISSING").await.is_none());
974 }
975
976 #[tokio::test]
977 async fn get_snapshot_returns_payload() {
978 let mut records = HashMap::new();
979 records.insert("TEST:AI".into(), make_ai("TEST:AI", 42.0));
980 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
981 let snap = store.get("TEST:AI").await.unwrap();
982 match snap {
983 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(42.0)),
984 _ => panic!("expected scalar"),
985 }
986 }
987
988 #[tokio::test]
989 async fn put_value_updates_writable_record() {
990 let mut records = HashMap::new();
991 records.insert("TEST:AO".into(), make_ao("TEST:AO", 0.0));
992 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
993
994 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(99.5))]);
995 let result = store.put("TEST:AO", &val).await.unwrap();
996 assert_eq!(result.len(), 1);
997 assert_eq!(result[0].0, "TEST:AO");
998
999 let snap = store.get("TEST:AO").await.unwrap();
1000 match snap {
1001 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(99.5)),
1002 _ => panic!("expected scalar"),
1003 }
1004 }
1005
1006 #[tokio::test]
1007 async fn put_value_rejects_readonly() {
1008 let mut records = HashMap::new();
1009 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1010 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1011
1012 let val = DecodedValue::Float64(5.0);
1013 let err = store.put("TEST:AI", &val).await.unwrap_err();
1014 assert!(err.contains("not writable"));
1015 }
1016
1017 #[tokio::test]
1018 async fn set_value_bypasses_writable_check() {
1019 let mut records = HashMap::new();
1020 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1021 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1022 assert!(store.set_value("TEST:AI", ScalarValue::F64(10.0)).await);
1023 let val = store.get_value("TEST:AI").await.unwrap();
1024 assert_eq!(val, ScalarValue::F64(10.0));
1025 }
1026
1027 #[tokio::test]
1028 async fn set_array_value_updates_all_scalar_array_types() {
1029 let cases: Vec<ScalarArrayValue> = vec![
1030 ScalarArrayValue::Bool(vec![false, true]),
1031 ScalarArrayValue::I8(vec![1, 2]),
1032 ScalarArrayValue::I16(vec![1, 2]),
1033 ScalarArrayValue::I32(vec![1, 2]),
1034 ScalarArrayValue::I64(vec![1, 2]),
1035 ScalarArrayValue::U8(vec![1, 2]),
1036 ScalarArrayValue::U16(vec![1, 2]),
1037 ScalarArrayValue::U32(vec![1, 2]),
1038 ScalarArrayValue::U64(vec![1, 2]),
1039 ScalarArrayValue::F32(vec![1.0, 2.0]),
1040 ScalarArrayValue::F64(vec![1.0, 2.0]),
1041 ScalarArrayValue::Str(vec!["a".to_string(), "b".to_string()]),
1042 ];
1043
1044 for (idx, updated) in cases.into_iter().enumerate() {
1045 let pv = format!("TEST:WF:{idx}");
1046 let mut records = HashMap::new();
1047 records.insert(pv.clone(), make_waveform(&pv, updated.clone()));
1048 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1049
1050 assert!(!store.set_array_value(&pv, updated.clone()).await);
1051
1052 let second = match updated {
1053 ScalarArrayValue::Bool(_) => ScalarArrayValue::Bool(vec![true, false]),
1054 ScalarArrayValue::I8(_) => ScalarArrayValue::I8(vec![3, 4]),
1055 ScalarArrayValue::I16(_) => ScalarArrayValue::I16(vec![3, 4]),
1056 ScalarArrayValue::I32(_) => ScalarArrayValue::I32(vec![3, 4]),
1057 ScalarArrayValue::I64(_) => ScalarArrayValue::I64(vec![3, 4]),
1058 ScalarArrayValue::U8(_) => ScalarArrayValue::U8(vec![3, 4]),
1059 ScalarArrayValue::U16(_) => ScalarArrayValue::U16(vec![3, 4]),
1060 ScalarArrayValue::U32(_) => ScalarArrayValue::U32(vec![3, 4]),
1061 ScalarArrayValue::U64(_) => ScalarArrayValue::U64(vec![3, 4]),
1062 ScalarArrayValue::F32(_) => ScalarArrayValue::F32(vec![3.0, 4.0]),
1063 ScalarArrayValue::F64(_) => ScalarArrayValue::F64(vec![3.0, 4.0]),
1064 ScalarArrayValue::Str(_) => {
1065 ScalarArrayValue::Str(vec!["x".to_string(), "y".to_string()])
1066 }
1067 };
1068
1069 assert!(store.set_array_value(&pv, second.clone()).await);
1070 let snap = store.get(&pv).await.unwrap();
1071 match snap {
1072 NtPayload::ScalarArray(nt) => assert_eq!(nt.value, second),
1073 _ => panic!("expected scalar array"),
1074 }
1075 }
1076 }
1077
1078 #[tokio::test]
1079 async fn get_nt_returns_full_payload() {
1080 let mut records = HashMap::new();
1081 records.insert("TEST:AI".into(), make_ai("TEST:AI", 12.5));
1082 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1083
1084 let nt = store.get_nt("TEST:AI").await.unwrap();
1085 match nt {
1086 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(12.5)),
1087 _ => panic!("expected scalar payload"),
1088 }
1089 }
1090
1091 #[tokio::test]
1092 async fn put_nt_updates_scalar_array_table_and_ndarray() {
1093 let mut records = HashMap::new();
1094 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1095 records.insert(
1096 "TEST:WF".into(),
1097 make_waveform("TEST:WF", ScalarArrayValue::F64(vec![0.0, 0.0])),
1098 );
1099 records.insert("TEST:TBL".into(), make_nt_table("TEST:TBL"));
1100 records.insert("TEST:NDA".into(), make_nt_ndarray("TEST:NDA"));
1101 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1102
1103 assert!(
1104 store
1105 .put_nt(
1106 "TEST:AI",
1107 NtPayload::Scalar(NtScalar::from_value(ScalarValue::F64(5.0))),
1108 )
1109 .await
1110 );
1111 assert!(
1112 store
1113 .put_nt(
1114 "TEST:WF",
1115 NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
1116 3.0, 4.0
1117 ],))),
1118 )
1119 .await
1120 );
1121
1122 let table = NtTable {
1123 labels: vec!["X".to_string(), "Y".to_string()],
1124 columns: vec![
1125 NtTableColumn {
1126 name: "x".to_string(),
1127 values: ScalarArrayValue::F64(vec![2.0, 3.0]),
1128 },
1129 NtTableColumn {
1130 name: "y".to_string(),
1131 values: ScalarArrayValue::F64(vec![20.0, 30.0]),
1132 },
1133 ],
1134 descriptor: Some("updated table".to_string()),
1135 alarm: None,
1136 time_stamp: None,
1137 };
1138 assert!(
1139 store
1140 .put_nt("TEST:TBL", NtPayload::Table(table.clone()))
1141 .await
1142 );
1143
1144 let ndarray = NtNdArray {
1145 value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
1146 codec: NdCodec {
1147 name: "none".to_string(),
1148 parameters: HashMap::new(),
1149 },
1150 compressed_size: 4,
1151 uncompressed_size: 4,
1152 dimension: vec![NdDimension {
1153 size: 4,
1154 offset: 0,
1155 full_size: 4,
1156 binning: 1,
1157 reverse: false,
1158 }],
1159 unique_id: 2,
1160 data_time_stamp: Default::default(),
1161 attribute: vec![],
1162 descriptor: Some("updated ndarray".to_string()),
1163 alarm: None,
1164 time_stamp: None,
1165 display: None,
1166 };
1167 assert!(
1168 store
1169 .put_nt("TEST:NDA", NtPayload::NdArray(ndarray.clone()))
1170 .await
1171 );
1172
1173 assert!(
1174 !store
1175 .put_nt(
1176 "TEST:AI",
1177 NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
1178 1.0
1179 ]))),
1180 )
1181 .await
1182 );
1183
1184 match store.get_nt("TEST:TBL").await.unwrap() {
1185 NtPayload::Table(nt) => assert_eq!(nt, table),
1186 _ => panic!("expected table payload"),
1187 }
1188 match store.get_nt("TEST:NDA").await.unwrap() {
1189 NtPayload::NdArray(nt) => assert_eq!(nt, ndarray),
1190 _ => panic!("expected ndarray payload"),
1191 }
1192 }
1193
1194 #[tokio::test]
1195 async fn descriptor_matches_value_type() {
1196 let mut records = HashMap::new();
1197 records.insert("TEST:AI".into(), make_ai("TEST:AI", 0.0));
1198 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1199 let info = store.claim("TEST:AI").await.unwrap();
1200 assert_eq!(
1201 info.descriptor.struct_id.as_deref(),
1202 Some("epics:nt/NTScalar:1.0")
1203 );
1204 let desc = info.descriptor;
1205 let value_field = desc.field("value").unwrap();
1206 assert!(matches!(
1207 value_field.field_type,
1208 FieldType::Scalar(TypeCode::Float64)
1209 ));
1210 }
1211
1212 #[tokio::test]
1213 async fn subscribe_receives_updates() {
1214 let mut records = HashMap::new();
1215 records.insert("TEST:AO".into(), make_ao("TEST:AO", 0.0));
1216 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1217
1218 let mut rx = Source::subscribe(&store, "TEST:AO").await.unwrap();
1219
1220 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(7.7))]);
1221 store.put("TEST:AO", &val).await.unwrap();
1222
1223 let update = rx.recv().await.unwrap();
1224 match update {
1225 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(7.7)),
1226 _ => panic!("expected scalar"),
1227 }
1228 }
1229
1230 #[tokio::test]
1231 async fn on_put_callback_is_invoked() {
1232 use std::sync::atomic::{AtomicBool, Ordering};
1233
1234 let called = Arc::new(AtomicBool::new(false));
1235 let called2 = called.clone();
1236
1237 let mut records = HashMap::new();
1238 records.insert("CB:AO".into(), make_ao("CB:AO", 0.0));
1239
1240 let mut on_put = HashMap::new();
1241 let cb: OnPutCallback = Arc::new(move |_name, _val| {
1242 called2.store(true, Ordering::SeqCst);
1243 });
1244 on_put.insert("CB:AO".into(), cb);
1245
1246 let store = SimplePvStore::new(records, on_put, vec![], false);
1247 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(1.0))]);
1248 store.put("CB:AO", &val).await.unwrap();
1249
1250 tokio::task::yield_now().await;
1252 tokio::task::yield_now().await;
1253
1254 assert!(called.load(Ordering::SeqCst));
1255 }
1256}