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