1use std::marker::PhantomData;
41use std::sync::{Arc, Mutex};
42
43use spvirit_codec::spvd_decode::DecodedValue;
44use spvirit_types::{NtPayload, ScalarArrayValue, ScalarValue};
45
46use crate::pva_server::{make_array_record, make_output_record, make_scalar_record};
47use crate::simple_store::SimplePvStore;
48use crate::types::{RecordInstance, RecordType};
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum PvError {
53 Unbound,
55 NotFound(String),
57 TypeMismatch {
59 expected: &'static str,
60 actual: String,
61 },
62 PutRejected(String),
64}
65
66impl std::fmt::Display for PvError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 PvError::Unbound => write!(f, "PV handle is not bound to a server yet"),
70 PvError::NotFound(n) => write!(f, "PV '{n}' not found"),
71 PvError::TypeMismatch { expected, actual } => {
72 write!(
73 f,
74 "PV value type mismatch: expected {expected}, record holds {actual}"
75 )
76 }
77 PvError::PutRejected(msg) => write!(f, "PUT rejected: {msg}"),
78 }
79 }
80}
81
82impl std::error::Error for PvError {}
83
84pub trait PvScalar: Sized + Send + Sync + 'static {
86 const TYPE_NAME: &'static str;
88 fn into_scalar(self) -> ScalarValue;
89 fn from_scalar(v: ScalarValue) -> Option<Self>;
90
91 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
101 Self::from_scalar(crate::convert::decoded_to_scalar_value(dv))
102 }
103}
104
105impl PvScalar for f64 {
106 const TYPE_NAME: &'static str = "f64";
107 fn into_scalar(self) -> ScalarValue {
108 ScalarValue::F64(self)
109 }
110 fn from_scalar(v: ScalarValue) -> Option<Self> {
111 match v {
112 ScalarValue::F64(x) => Some(x),
113 ScalarValue::F32(x) => Some(x as f64),
114 _ => None,
115 }
116 }
117 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
118 crate::convert::decoded_to_f64(dv)
119 }
120}
121
122impl PvScalar for bool {
123 const TYPE_NAME: &'static str = "bool";
124 fn into_scalar(self) -> ScalarValue {
125 ScalarValue::Bool(self)
126 }
127 fn from_scalar(v: ScalarValue) -> Option<Self> {
128 match v {
129 ScalarValue::Bool(b) => Some(b),
130 _ => None,
131 }
132 }
133 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
134 crate::convert::decoded_to_bool(dv)
135 }
136}
137
138impl PvScalar for i32 {
139 const TYPE_NAME: &'static str = "i32";
140 fn into_scalar(self) -> ScalarValue {
141 ScalarValue::I32(self)
142 }
143 fn from_scalar(v: ScalarValue) -> Option<Self> {
144 match v {
145 ScalarValue::I32(x) => Some(x),
146 ScalarValue::I16(x) => Some(x as i32),
147 ScalarValue::I8(x) => Some(x as i32),
148 _ => None,
149 }
150 }
151 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
152 crate::convert::decoded_to_i32(dv)
153 }
154}
155
156impl PvScalar for String {
157 const TYPE_NAME: &'static str = "String";
158 fn into_scalar(self) -> ScalarValue {
159 ScalarValue::Str(self)
160 }
161 fn from_scalar(v: ScalarValue) -> Option<Self> {
162 match v {
163 ScalarValue::Str(s) => Some(s),
164 _ => None,
165 }
166 }
167 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
168 crate::convert::decoded_to_string(dv)
169 }
170}
171
172pub(crate) struct PendingDef {
173 pub(crate) record: RecordInstance,
174 pub(crate) validator: Option<crate::simple_store::PutValidator>,
175 pub(crate) scan: Option<(std::time::Duration, crate::simple_store::ScanCallback)>,
176 pub(crate) calc: Option<(Vec<String>, crate::simple_store::LinkCallback)>,
177}
178
179pub(crate) enum PvState {
180 Pending(PendingDef),
181 Bound(Arc<SimplePvStore>),
182}
183
184pub(crate) struct PvShared {
185 pub(crate) name: String,
186 pub(crate) state: Mutex<PvState>,
187}
188
189pub struct Pv<T: PvScalar> {
191 pub(crate) shared: Arc<PvShared>,
192 _marker: PhantomData<fn() -> T>,
193}
194
195impl<T: PvScalar> Clone for Pv<T> {
196 fn clone(&self) -> Self {
197 Self {
198 shared: self.shared.clone(),
199 _marker: PhantomData,
200 }
201 }
202}
203
204impl<T: PvScalar> Pv<T> {
205 fn from_record(record: RecordInstance) -> Self {
206 Self {
207 shared: Arc::new(PvShared {
208 name: record.name.clone(),
209 state: Mutex::new(PvState::Pending(PendingDef {
210 record,
211 validator: None,
212 scan: None,
213 calc: None,
214 })),
215 }),
216 _marker: PhantomData,
217 }
218 }
219
220 pub fn name(&self) -> &str {
221 &self.shared.name
222 }
223
224 #[cfg(test)]
227 fn pending_record(&self) -> Option<RecordInstance> {
228 match &*self.shared.state.lock().unwrap() {
229 PvState::Pending(def) => Some(def.record.clone()),
230 PvState::Bound(_) => None,
231 }
232 }
233
234 fn with_record(self, f: impl FnOnce(&mut RecordInstance)) -> Self {
236 {
237 let mut state = self.shared.state.lock().unwrap();
238 match &mut *state {
239 PvState::Pending(def) => f(&mut def.record),
240 PvState::Bound(_) => {
241 tracing::warn!("Pv '{}': option ignored, already bound", self.shared.name)
242 }
243 }
244 }
245 self
246 }
247
248 pub fn units(self, units: impl Into<String>) -> Self {
249 let u = units.into();
250 self.with_record(|r| {
251 if let Some(nt) = r.nt_scalar_mut() {
252 nt.units = u;
253 }
254 })
255 }
256
257 pub fn prec(self, prec: i32) -> Self {
258 self.with_record(|r| {
259 if let Some(nt) = r.nt_scalar_mut() {
260 nt.display_precision = prec;
261 }
262 })
263 }
264
265 pub fn desc(self, desc: impl Into<String>) -> Self {
266 let d = desc.into();
267 self.with_record(|r| {
268 r.common.desc = d.clone();
269 if let Some(nt) = r.nt_scalar_mut() {
270 nt.display_description = d;
271 }
272 })
273 }
274
275 pub fn adel(self, deadband: f64) -> Self {
277 self.with_record(|r| {
278 r.raw_fields.insert("ADEL".into(), trim_float(deadband));
279 })
280 }
281
282 pub fn mdel(self, deadband: f64) -> Self {
284 self.with_record(|r| {
285 r.raw_fields.insert("MDEL".into(), trim_float(deadband));
286 })
287 }
288
289 pub fn drive_limits(self, low: f64, high: f64) -> Self {
290 self.with_record(|r| {
291 if let Some(nt) = r.nt_scalar_mut() {
292 nt.control_low = low;
293 nt.control_high = high;
294 }
295 })
296 }
297
298 pub fn alarm_limits(self, lolo: f64, low: f64, high: f64, hihi: f64) -> Self {
299 self.with_record(|r| {
300 if let Some(nt) = r.nt_scalar_mut() {
301 nt.value_alarm_active = true;
302 nt.value_alarm_low_alarm_limit = lolo;
303 nt.value_alarm_low_warning_limit = low;
304 nt.value_alarm_high_warning_limit = high;
305 nt.value_alarm_high_alarm_limit = hihi;
306 }
307 })
308 }
309
310 pub fn on_put<F>(self, f: F) -> Self
313 where
314 F: Fn(&Pv<T>, T) -> Result<(), String> + Send + Sync + 'static,
315 {
316 let handle = self.clone();
317 let validator: crate::simple_store::PutValidator = Arc::new(move |_name, dv| {
318 let scalar_dv = unwrap_value_field(dv);
324 let typed = T::from_decoded(scalar_dv)
325 .ok_or_else(|| format!("expected {}, got {scalar_dv:?}", T::TYPE_NAME))?;
326 f(&handle, typed)
327 });
328 {
329 let mut state = self.shared.state.lock().unwrap();
330 match &mut *state {
331 PvState::Pending(def) => def.validator = Some(validator),
332 PvState::Bound(_) => {
333 tracing::warn!("Pv '{}': on_put ignored, already bound", self.shared.name)
334 }
335 }
336 }
337 self
338 }
339
340 pub fn scan<F>(self, period: std::time::Duration, f: F) -> Self
342 where
343 F: Fn(&Pv<T>) -> T + Send + Sync + 'static,
344 {
345 let handle = self.clone();
346 let cb: crate::simple_store::ScanCallback = Arc::new(move |_name| f(&handle).into_scalar());
347 {
348 let mut state = self.shared.state.lock().unwrap();
349 if let PvState::Pending(def) = &mut *state {
350 def.scan = Some((period, cb));
351 } else {
352 tracing::warn!("Pv '{}': scan ignored, already bound", self.shared.name);
353 }
354 }
355 self
356 }
357}
358
359impl Pv<f64> {
360 pub fn calc<F>(name: impl Into<String>, inputs: &[&Pv<f64>], f: F) -> Self
362 where
363 F: Fn(&[f64]) -> f64 + Send + Sync + 'static,
364 {
365 let name = name.into();
366 let input_names: Vec<String> = inputs.iter().map(|p| p.shared.name.clone()).collect();
367 let initial = ScalarValue::F64(0.0);
368 let pv = Self::from_record(make_scalar_record(&name, RecordType::Ai, initial));
369 let compute: crate::simple_store::LinkCallback = Arc::new(move |values| {
370 let floats: Vec<f64> = values
371 .iter()
372 .map(|v| f64::from_scalar(v.clone()).unwrap_or(0.0))
373 .collect();
374 ScalarValue::F64(f(&floats))
375 });
376 if let PvState::Pending(def) = &mut *pv.shared.state.lock().unwrap() {
377 def.calc = Some((input_names, compute));
378 }
379 pv
380 }
381}
382
383fn unwrap_value_field(dv: &DecodedValue) -> &DecodedValue {
387 match dv {
388 DecodedValue::Structure(fields) => fields
389 .iter()
390 .find(|(name, _)| name == "value")
391 .map(|(_, v)| v)
392 .unwrap_or(dv),
393 other => other,
394 }
395}
396
397fn trim_float(v: f64) -> String {
399 if v.fract() == 0.0 && v.abs() < 1e15 {
400 format!("{}", v as i64)
401 } else {
402 format!("{v}")
403 }
404}
405
406impl Pv<f64> {
407 pub fn ai(name: impl Into<String>, initial: f64) -> Self {
408 let name = name.into();
409 Self::from_record(make_scalar_record(
410 &name,
411 RecordType::Ai,
412 ScalarValue::F64(initial),
413 ))
414 }
415 pub fn ao(name: impl Into<String>, initial: f64) -> Self {
416 let name = name.into();
417 Self::from_record(make_output_record(
418 &name,
419 RecordType::Ao,
420 ScalarValue::F64(initial),
421 ))
422 }
423}
424
425impl Pv<bool> {
426 pub fn bi(name: impl Into<String>, initial: bool) -> Self {
427 let name = name.into();
428 Self::from_record(make_scalar_record(
429 &name,
430 RecordType::Bi,
431 ScalarValue::Bool(initial),
432 ))
433 }
434 pub fn bo(name: impl Into<String>, initial: bool) -> Self {
435 let name = name.into();
436 Self::from_record(make_output_record(
437 &name,
438 RecordType::Bo,
439 ScalarValue::Bool(initial),
440 ))
441 }
442}
443
444impl Pv<String> {
445 pub fn string_in(name: impl Into<String>, initial: impl Into<String>) -> Self {
446 let name = name.into();
447 Self::from_record(make_scalar_record(
448 &name,
449 RecordType::StringIn,
450 ScalarValue::Str(initial.into()),
451 ))
452 }
453 pub fn string_out(name: impl Into<String>, initial: impl Into<String>) -> Self {
454 let name = name.into();
455 Self::from_record(make_output_record(
456 &name,
457 RecordType::StringOut,
458 ScalarValue::Str(initial.into()),
459 ))
460 }
461}
462
463impl Pv<i32> {
464 pub fn longin(name: impl Into<String>, initial: i32) -> Self {
466 let name = name.into();
467 Self::from_record(make_scalar_record(
468 &name,
469 RecordType::LongIn,
470 ScalarValue::I32(initial),
471 ))
472 }
473 pub fn longout(name: impl Into<String>, initial: i32) -> Self {
475 let name = name.into();
476 Self::from_record(make_output_record(
477 &name,
478 RecordType::LongOut,
479 ScalarValue::I32(initial),
480 ))
481 }
482 pub fn mbbi(name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
484 Self::from_enum_record(name.into(), choices, initial, RecordType::Mbbi)
485 }
486 pub fn mbbo(name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
488 Self::from_enum_record(name.into(), choices, initial, RecordType::Mbbo)
489 }
490 fn from_enum_record(name: String, choices: Vec<String>, initial: i32, rt: RecordType) -> Self {
491 let data = crate::types::RecordData::NtEnum {
493 nt: spvirit_types::NtEnum::new(initial, choices),
494 inp: None,
495 out: None,
496 omsl: crate::types::OutputMode::Supervisory,
497 };
498 Self::from_record(RecordInstance {
499 name: name.clone(),
500 record_type: rt,
501 common: crate::types::DbCommonState::default(),
502 data,
503 raw_fields: std::collections::HashMap::new(),
504 })
505 }
506}
507
508impl<T: PvScalar> Pv<T> {
509 fn store(&self) -> Result<Arc<SimplePvStore>, PvError> {
510 match &*self.shared.state.lock().unwrap() {
511 PvState::Bound(store) => Ok(store.clone()),
512 PvState::Pending(_) => Err(PvError::Unbound),
513 }
514 }
515
516 pub async fn set(&self, value: T) -> Result<(), PvError> {
519 let store = self.store()?;
520 if store
521 .set_value(&self.shared.name, value.into_scalar())
522 .await
523 {
524 Ok(())
525 } else if store.get_value(&self.shared.name).await.is_some() {
526 Ok(())
531 } else {
532 Err(PvError::NotFound(self.shared.name.clone()))
533 }
534 }
535
536 pub async fn set_alarm(
540 &self,
541 severity: i32,
542 status: i32,
543 message: &str,
544 ) -> Result<(), PvError> {
545 let store = self.store()?;
546 if store
547 .set_alarm(&self.shared.name, severity, status, message)
548 .await
549 {
550 Ok(())
551 } else if store.get_value(&self.shared.name).await.is_some() {
552 Ok(())
553 } else {
554 Err(PvError::NotFound(self.shared.name.clone()))
555 }
556 }
557
558 pub async fn get(&self) -> Result<T, PvError> {
560 let store = self.store()?;
561 let v = store
562 .get_value(&self.shared.name)
563 .await
564 .ok_or_else(|| PvError::NotFound(self.shared.name.clone()))?;
565 let actual = format!("{v:?}");
566 T::from_scalar(v).ok_or(PvError::TypeMismatch {
567 expected: T::TYPE_NAME,
568 actual,
569 })
570 }
571
572 pub(crate) async fn attach(store: &Arc<SimplePvStore>, name: &str) -> Result<Self, PvError> {
574 match store.get_nt(name).await {
582 None => return Err(PvError::NotFound(name.to_string())),
583 Some(NtPayload::Scalar(_)) | Some(NtPayload::Enum(_)) => {}
584 Some(other) => {
585 return Err(PvError::TypeMismatch {
586 expected: T::TYPE_NAME,
587 actual: nt_payload_kind(&other).to_string(),
588 });
589 }
590 }
591 let v = store
592 .get_value(name)
593 .await
594 .ok_or_else(|| PvError::NotFound(name.to_string()))?;
595 let actual = format!("{v:?}");
596 if T::from_scalar(v).is_none() {
597 return Err(PvError::TypeMismatch {
598 expected: T::TYPE_NAME,
599 actual,
600 });
601 }
602 Ok(Self {
603 shared: Arc::new(PvShared {
604 name: name.to_string(),
605 state: Mutex::new(PvState::Bound(store.clone())),
606 }),
607 _marker: PhantomData,
608 })
609 }
610}
611
612fn nt_payload_kind(p: &NtPayload) -> &'static str {
614 match p {
615 NtPayload::Scalar(_) => "Scalar",
616 NtPayload::ScalarArray(_) => "ScalarArray",
617 NtPayload::Table(_) => "Table",
618 NtPayload::NdArray(_) => "NdArray",
619 NtPayload::Enum(_) => "Enum",
620 NtPayload::Generic { .. } => "Generic",
621 }
622}
623
624pub struct PvArray {
628 pub(crate) shared: Arc<PvShared>,
629}
630
631impl Clone for PvArray {
632 fn clone(&self) -> Self {
633 Self {
634 shared: self.shared.clone(),
635 }
636 }
637}
638
639impl PvArray {
640 fn from_record(record: RecordInstance) -> Self {
641 Self {
642 shared: Arc::new(PvShared {
643 name: record.name.clone(),
644 state: Mutex::new(PvState::Pending(PendingDef {
645 record,
646 validator: None,
647 scan: None,
648 calc: None,
649 })),
650 }),
651 }
652 }
653
654 pub fn waveform(name: impl Into<String>, data: ScalarArrayValue) -> Self {
656 let name = name.into();
657 Self::from_record(make_array_record(&name, RecordType::Waveform, data))
658 }
659
660 pub fn aai(name: impl Into<String>, data: ScalarArrayValue) -> Self {
662 let name = name.into();
663 Self::from_record(make_array_record(&name, RecordType::Aai, data))
664 }
665
666 pub fn aao(name: impl Into<String>, data: ScalarArrayValue) -> Self {
668 let name = name.into();
669 Self::from_record(make_array_record(&name, RecordType::Aao, data))
670 }
671
672 pub fn name(&self) -> &str {
673 &self.shared.name
674 }
675
676 #[cfg(test)]
679 fn pending_record(&self) -> Option<RecordInstance> {
680 match &*self.shared.state.lock().unwrap() {
681 PvState::Pending(def) => Some(def.record.clone()),
682 PvState::Bound(_) => None,
683 }
684 }
685
686 fn store(&self) -> Result<Arc<SimplePvStore>, PvError> {
687 match &*self.shared.state.lock().unwrap() {
688 PvState::Bound(store) => Ok(store.clone()),
689 PvState::Pending(_) => Err(PvError::Unbound),
690 }
691 }
692
693 pub async fn set(&self, data: ScalarArrayValue) -> Result<(), PvError> {
695 let store = self.store()?;
696 if store.set_array_value(&self.shared.name, data).await {
697 Ok(())
698 } else {
699 match store.get_nt(&self.shared.name).await {
700 Some(NtPayload::ScalarArray(_)) => Ok(()),
704 Some(other) => Err(PvError::TypeMismatch {
705 expected: "array",
706 actual: nt_payload_kind(&other).to_string(),
707 }),
708 None => Err(PvError::NotFound(self.shared.name.clone())),
709 }
710 }
711 }
712
713 pub async fn set_alarm(
717 &self,
718 severity: i32,
719 status: i32,
720 message: &str,
721 ) -> Result<(), PvError> {
722 let store = self.store()?;
723 if store
724 .set_alarm(&self.shared.name, severity, status, message)
725 .await
726 {
727 Ok(())
728 } else if store.get_nt(&self.shared.name).await.is_some() {
729 Ok(())
730 } else {
731 Err(PvError::NotFound(self.shared.name.clone()))
732 }
733 }
734
735 pub async fn get(&self) -> Result<ScalarArrayValue, PvError> {
737 let store = self.store()?;
738 match store.get_nt(&self.shared.name).await {
739 Some(NtPayload::ScalarArray(nt)) => Ok(nt.value),
740 Some(other) => Err(PvError::TypeMismatch {
741 expected: "array",
742 actual: nt_payload_kind(&other).to_string(),
743 }),
744 None => Err(PvError::NotFound(self.shared.name.clone())),
745 }
746 }
747
748 pub(crate) async fn attach(store: &Arc<SimplePvStore>, name: &str) -> Result<Self, PvError> {
750 match store.get_nt(name).await {
751 Some(NtPayload::ScalarArray(_)) => Ok(Self {
752 shared: Arc::new(PvShared {
753 name: name.to_string(),
754 state: Mutex::new(PvState::Bound(store.clone())),
755 }),
756 }),
757 Some(other) => Err(PvError::TypeMismatch {
758 expected: "array",
759 actual: nt_payload_kind(&other).to_string(),
760 }),
761 None => Err(PvError::NotFound(name.to_string())),
762 }
763 }
764}
765
766impl From<PvArray> for AnyPv {
767 fn from(pv: PvArray) -> Self {
768 Self { shared: pv.shared }
769 }
770}
771
772pub struct AnyPv {
774 pub(crate) shared: Arc<PvShared>,
775}
776
777impl<T: PvScalar> From<Pv<T>> for AnyPv {
778 fn from(pv: Pv<T>) -> Self {
779 Self { shared: pv.shared }
780 }
781}
782
783impl AnyPv {
784 pub(crate) fn take_record(&self) -> Option<RecordInstance> {
786 let state = self.shared.state.lock().unwrap();
787 match &*state {
788 PvState::Pending(def) => Some(def.record.clone()),
789 PvState::Bound(_) => None,
790 }
791 }
792
793 pub(crate) fn bind(&self, store: &Arc<SimplePvStore>) {
795 *self.shared.state.lock().unwrap() = PvState::Bound(store.clone());
796 }
797
798 pub fn name(&self) -> &str {
799 &self.shared.name
800 }
801
802 pub(crate) fn take_validator(&self) -> Option<crate::simple_store::PutValidator> {
804 let mut state = self.shared.state.lock().unwrap();
805 match &mut *state {
806 PvState::Pending(def) => def.validator.take(),
807 PvState::Bound(_) => None,
808 }
809 }
810
811 pub(crate) fn take_scan(
813 &self,
814 ) -> Option<(std::time::Duration, crate::simple_store::ScanCallback)> {
815 let mut state = self.shared.state.lock().unwrap();
816 match &mut *state {
817 PvState::Pending(def) => def.scan.take(),
818 PvState::Bound(_) => None,
819 }
820 }
821
822 pub(crate) fn take_calc(&self) -> Option<(Vec<String>, crate::simple_store::LinkCallback)> {
824 let mut state = self.shared.state.lock().unwrap();
825 match &mut *state {
826 PvState::Pending(def) => def.calc.take(),
827 PvState::Bound(_) => None,
828 }
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835
836 #[test]
837 fn pvscalar_roundtrip_f64() {
838 assert_eq!(f64::from_scalar(ScalarValue::F64(1.5)), Some(1.5));
839 assert!(matches!(1.5f64.into_scalar(), ScalarValue::F64(x) if x == 1.5));
840 }
841
842 #[test]
843 fn pvscalar_f64_accepts_f32_widening() {
844 assert_eq!(f64::from_scalar(ScalarValue::F32(2.0)), Some(2.0));
845 }
846
847 #[test]
848 fn pvscalar_rejects_wrong_variant() {
849 assert_eq!(f64::from_scalar(ScalarValue::Str("x".into())), None);
850 assert_eq!(bool::from_scalar(ScalarValue::F64(1.0)), None);
851 assert_eq!(i32::from_scalar(ScalarValue::Str("1".into())), None);
852 assert_eq!(String::from_scalar(ScalarValue::Bool(true)), None);
853 }
854
855 #[test]
856 fn pverror_display() {
857 let e = PvError::TypeMismatch {
858 expected: "f64",
859 actual: "Str".into(),
860 };
861 assert!(e.to_string().contains("f64"));
862 assert!(PvError::Unbound.to_string().contains("not bound"));
863 }
864
865 #[test]
866 fn ai_constructor_builds_record_template() {
867 let pv = Pv::ai("SIM:TEMP", 22.5).units("C").prec(2).desc("Temp");
868 let rec = pv.pending_record().expect("still pending");
869 assert_eq!(rec.name, "SIM:TEMP");
870 let nt = rec.to_ntscalar();
871 assert_eq!(nt.value, ScalarValue::F64(22.5));
872 assert_eq!(nt.units, "C");
873 assert_eq!(nt.display_precision, 2);
874 assert_eq!(rec.common.desc, "Temp");
875 assert!(!rec.writable(), "ai is read-only over the wire");
876 assert_eq!(pv.name(), "SIM:TEMP");
877 }
878
879 #[test]
880 fn ao_is_writable_with_drive_limits() {
881 let pv = Pv::ao("SIM:SP", 25.0).drive_limits(0.0, 100.0);
882 let rec = pv.pending_record().unwrap();
883 assert!(rec.writable());
884 let nt = rec.to_ntscalar();
885 assert_eq!(nt.control_low, 0.0);
886 assert_eq!(nt.control_high, 100.0);
887 }
888
889 #[test]
890 fn mdel_adel_go_to_raw_fields() {
891 let pv = Pv::ai("SIM:X", 0.0).mdel(0.5).adel(1.0);
892 let rec = pv.pending_record().unwrap();
893 assert_eq!(rec.raw_fields.get("MDEL").map(String::as_str), Some("0.5"));
894 assert_eq!(rec.raw_fields.get("ADEL").map(String::as_str), Some("1"));
895 }
896
897 #[test]
898 fn alarm_limits_set_value_alarm_block() {
899 let pv = Pv::ao("SIM:A", 0.0).alarm_limits(-10.0, -5.0, 5.0, 10.0);
900 let nt = pv.pending_record().unwrap().to_ntscalar();
901 assert_eq!(nt.value_alarm_low_alarm_limit, -10.0);
902 assert_eq!(nt.value_alarm_low_warning_limit, -5.0);
903 assert_eq!(nt.value_alarm_high_warning_limit, 5.0);
904 assert_eq!(nt.value_alarm_high_alarm_limit, 10.0);
905 assert!(nt.value_alarm_active);
906 }
907
908 #[test]
909 fn bool_and_string_constructors() {
910 assert!(Pv::bo("B", true).pending_record().unwrap().writable());
911 assert!(!Pv::bi("B2", false).pending_record().unwrap().writable());
912 let s = Pv::string_in("S", "hello").pending_record().unwrap();
913 assert_eq!(s.to_ntscalar().value, ScalarValue::Str("hello".into()));
914 }
915
916 #[test]
917 fn longin_longout_constructors() {
918 let li = Pv::longin("L:IN", 42);
919 let rec = li.pending_record().unwrap();
920 assert_eq!(rec.record_type, crate::types::RecordType::LongIn);
921 assert_eq!(rec.to_ntscalar().value, ScalarValue::I32(42));
922 assert!(!rec.writable());
923
924 let lo = Pv::longout("L:OUT", 7).drive_limits(0.0, 1000.0);
925 let rec = lo.pending_record().unwrap();
926 assert_eq!(rec.record_type, crate::types::RecordType::LongOut);
927 assert!(rec.writable());
928 assert_eq!(rec.to_ntscalar().control_high, 1000.0);
929 }
930
931 #[tokio::test]
932 async fn longout_set_get_roundtrip() {
933 let store = empty_store();
934 let pv = Pv::longout("L:RT", 1);
935 let any: AnyPv = pv.clone().into();
936 let rec = any.take_record().unwrap();
937 store.insert(rec.name.clone(), rec).await;
938 any.bind(&store);
939 pv.set(99).await.unwrap();
940 assert_eq!(pv.get().await, Ok(99));
941 }
942
943 #[test]
944 fn mbbi_mbbo_constructors() {
945 let m = Pv::mbbi("M:I", vec!["Off".into(), "On".into(), "Auto".into()], 1);
946 let rec = m.pending_record().unwrap();
947 assert_eq!(rec.record_type, crate::types::RecordType::Mbbi);
948 assert_eq!(rec.current_value(), ScalarValue::I32(1));
949
950 let o = Pv::mbbo("M:O", vec!["A".into(), "B".into()], 0);
951 assert!(o.pending_record().unwrap().writable());
952 }
953
954 #[tokio::test]
955 async fn mbbo_set_get_index_with_bounds() {
956 let store = empty_store();
957 let pv = Pv::mbbo("M:RT", vec!["Stop".into(), "Run".into(), "Fault".into()], 0);
958 let any: AnyPv = pv.clone().into();
959 let rec = any.take_record().unwrap();
960 store.insert(rec.name.clone(), rec).await;
961 any.bind(&store);
962
963 pv.set(2).await.unwrap();
964 assert_eq!(pv.get().await, Ok(2));
965 let _ = pv.set(7).await;
968 assert_eq!(pv.get().await, Ok(2));
969 }
970
971 #[test]
972 fn set_scalar_value_on_raw_nt_enum_record() {
973 let mut rec = Pv::mbbo("M:RAW", vec!["Off".into(), "On".into(), "Auto".into()], 0)
974 .pending_record()
975 .unwrap();
976 assert!(rec.set_scalar_value(ScalarValue::I32(2), true));
978 assert_eq!(rec.current_value(), ScalarValue::I32(2));
979 assert!(!rec.set_scalar_value(ScalarValue::I32(2), true));
981 assert!(!rec.set_scalar_value(ScalarValue::I32(7), true));
983 assert_eq!(rec.current_value(), ScalarValue::I32(2));
984 assert!(!rec.set_scalar_value(ScalarValue::I32(-1), true));
985 assert_eq!(rec.current_value(), ScalarValue::I32(2));
986 }
987
988 #[tokio::test]
989 async fn pv_array_roundtrip_and_serve() {
990 let wf = PvArray::waveform("W:1", ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]));
991 let server = crate::pva_server::PvaServer::serve([AnyPv::from(wf.clone())])
992 .build()
993 .await;
994 assert_eq!(
995 wf.get().await,
996 Ok(ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]))
997 );
998 wf.set(ScalarArrayValue::F64(vec![4.0, 5.0])).await.unwrap();
999 match wf.get().await.unwrap() {
1000 ScalarArrayValue::F64(v) => assert_eq!(v, vec![4.0, 5.0]),
1001 other => panic!("wrong kind: {other:?}"),
1002 }
1003 let h = server.array_pv("W:1").await.unwrap();
1005 assert!(matches!(h.get().await.unwrap(), ScalarArrayValue::F64(_)));
1006 assert!(matches!(
1008 server.pv::<f64>("W:1").await,
1009 Err(PvError::TypeMismatch { .. })
1010 ));
1011 let t = Pv::ai("W:S", 1.0);
1013 let s2 = crate::pva_server::PvaServer::serve([AnyPv::from(t)])
1014 .build()
1015 .await;
1016 assert!(matches!(
1017 s2.array_pv("W:S").await,
1018 Err(PvError::TypeMismatch { .. })
1019 ));
1020 }
1021
1022 #[test]
1023 fn aai_read_only_aao_writable() {
1024 let a = PvArray::aai("W:AI", ScalarArrayValue::I32(vec![1]));
1025 assert!(a.pending_record().is_some());
1026 assert!(!AnyPv::from(a).take_record().unwrap().writable());
1027 let b = PvArray::aao("W:AO", ScalarArrayValue::I32(vec![1]));
1028 assert!(AnyPv::from(b).take_record().unwrap().writable());
1029 }
1030
1031 #[tokio::test]
1032 async fn scalar_attach_rejects_array_backed_record() {
1033 let store = empty_store();
1038 let wf = PvArray::waveform("W:GUARD", ScalarArrayValue::I32(vec![1, 2, 3]));
1039 let any: AnyPv = wf.into();
1040 let rec = any.take_record().unwrap();
1041 store.insert(rec.name.clone(), rec).await;
1042
1043 let bad = Pv::<i32>::attach(&store, "W:GUARD").await;
1044 assert!(matches!(bad, Err(PvError::TypeMismatch { .. })));
1045 }
1046
1047 fn empty_store() -> Arc<SimplePvStore> {
1048 Arc::new(SimplePvStore::new(
1049 std::collections::HashMap::new(),
1050 std::collections::HashMap::new(),
1051 Vec::new(),
1052 false,
1053 ))
1054 }
1055
1056 #[tokio::test]
1057 async fn set_get_before_bind_errors() {
1058 let pv = Pv::ai("SIM:X", 1.0);
1059 assert_eq!(pv.set(2.0).await, Err(PvError::Unbound));
1060 assert_eq!(pv.get().await, Err(PvError::Unbound));
1061 }
1062
1063 #[tokio::test]
1064 async fn bind_then_set_get_roundtrip() {
1065 let store = empty_store();
1066 let pv = Pv::ai("SIM:X", 1.0).units("mm");
1067 let any: AnyPv = pv.clone().into();
1068 let rec = any.take_record().expect("pending record");
1069 store.insert(rec.name.clone(), rec).await;
1070 any.bind(&store);
1071
1072 assert_eq!(pv.get().await, Ok(1.0));
1073 pv.set(2.5).await.unwrap();
1074 assert_eq!(pv.get().await, Ok(2.5));
1075 assert_eq!(pv.clone().get().await, Ok(2.5));
1077 }
1078
1079 #[tokio::test]
1080 async fn attach_mints_typed_handle_and_checks_type() {
1081 let store = empty_store();
1082 let src = Pv::ai("SIM:Y", 3.0);
1083 let any: AnyPv = src.into();
1084 let rec = any.take_record().unwrap();
1085 store.insert(rec.name.clone(), rec).await;
1086
1087 let h: Pv<f64> = Pv::attach(&store, "SIM:Y").await.unwrap();
1088 assert_eq!(h.get().await, Ok(3.0));
1089
1090 let bad = Pv::<bool>::attach(&store, "SIM:Y").await;
1091 assert!(matches!(bad, Err(PvError::TypeMismatch { .. })));
1092 let missing = Pv::<f64>::attach(&store, "NOPE").await;
1093 assert!(matches!(missing, Err(PvError::NotFound(ref n)) if n == "NOPE"));
1094 }
1095
1096 #[tokio::test]
1097 async fn set_same_value_is_ok_not_not_found() {
1098 let store = empty_store();
1099 let pv = Pv::ai("SIM:Z", 1.0);
1100 let any: AnyPv = pv.clone().into();
1101 let rec = any.take_record().unwrap();
1102 store.insert(rec.name.clone(), rec).await;
1103 any.bind(&store);
1104
1105 assert_eq!(pv.set(2.5).await, Ok(()));
1106 assert_eq!(pv.set(2.5).await, Ok(()));
1108 assert_eq!(pv.get().await, Ok(2.5));
1109 }
1110
1111 #[tokio::test]
1112 async fn set_alarm_posts_and_reads_back() {
1113 let store = empty_store();
1114 let pv = Pv::ai("A:1", 1.0);
1115 let any: AnyPv = pv.clone().into();
1116 let rec = any.take_record().unwrap();
1117 store.insert(rec.name.clone(), rec).await;
1118 any.bind(&store);
1119
1120 let mut rx = crate::pvstore::Source::subscribe(&*store, "A:1")
1122 .await
1123 .unwrap();
1124
1125 pv.set_alarm(2, 3, "sensor dead").await.unwrap();
1126 let rec = store.get_record("A:1").await.unwrap();
1127 let nt = rec.to_ntscalar();
1128 assert_eq!(nt.alarm_severity, 2);
1129 assert_eq!(nt.alarm_status, 3);
1130 assert_eq!(nt.alarm_message, "sensor dead");
1131 let posted = rx.try_recv().expect("alarm change must post");
1133 drop(posted);
1134 pv.set_alarm(2, 3, "sensor dead").await.unwrap();
1136 assert!(rx.try_recv().is_err());
1137 let ghost = Pv::ai("A:GHOST", 0.0);
1139 assert_eq!(ghost.set_alarm(1, 0, "x").await, Err(PvError::Unbound));
1140
1141 let missing: Pv<f64> = Pv::attach(&store, "A:1").await.unwrap();
1142 assert_eq!(missing.get().await, Ok(1.0));
1144 }
1145
1146 #[tokio::test]
1147 async fn set_alarm_missing_record_is_not_found() {
1148 let store = empty_store();
1149 let pv = Pv::ai("A:MISSING", 0.0);
1150 let any: AnyPv = pv.clone().into();
1151 any.bind(&store);
1152 assert_eq!(
1153 pv.set_alarm(1, 1, "x").await,
1154 Err(PvError::NotFound("A:MISSING".into()))
1155 );
1156 }
1157
1158 #[tokio::test]
1159 async fn set_alarm_on_enum_and_array_records() {
1160 let store = empty_store();
1161
1162 let mbbo = Pv::mbbo("M:ALM", vec!["Stop".into(), "Run".into()], 0);
1163 let any: AnyPv = mbbo.clone().into();
1164 let rec = any.take_record().unwrap();
1165 store.insert(rec.name.clone(), rec).await;
1166 any.bind(&store);
1167
1168 let wf = PvArray::waveform("W:ALM", ScalarArrayValue::F64(vec![1.0, 2.0]));
1169 let any: AnyPv = wf.clone().into();
1170 let rec = any.take_record().unwrap();
1171 store.insert(rec.name.clone(), rec).await;
1172 any.bind(&store);
1173
1174 assert!(store.set_alarm("M:ALM", 2, 5, "enum fault").await);
1175 assert!(store.set_alarm("W:ALM", 1, 4, "array fault").await);
1176
1177 match store.get_nt("M:ALM").await.unwrap() {
1178 NtPayload::Enum(nt) => {
1179 assert_eq!(nt.alarm.severity, 2);
1180 assert_eq!(nt.alarm.status, 5);
1181 assert_eq!(nt.alarm.message, "enum fault");
1182 }
1183 other => panic!("expected Enum, got {other:?}"),
1184 }
1185 match store.get_nt("W:ALM").await.unwrap() {
1186 NtPayload::ScalarArray(nt) => {
1187 assert_eq!(nt.alarm.severity, 1);
1188 assert_eq!(nt.alarm.status, 4);
1189 assert_eq!(nt.alarm.message, "array fault");
1190 }
1191 other => panic!("expected ScalarArray, got {other:?}"),
1192 }
1193
1194 assert!(!store.set_alarm("M:ALM", 2, 5, "enum fault").await);
1196 }
1197
1198 #[tokio::test]
1199 async fn on_put_callback_travels_to_store() {
1200 let rejected = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1201 let r2 = rejected.clone();
1202 let pv = Pv::ao("SIM:SP", 1.0).on_put(move |_pv, v: f64| {
1203 if v > 100.0 {
1204 r2.store(true, std::sync::atomic::Ordering::SeqCst);
1205 Err("over limit".into())
1206 } else {
1207 Ok(())
1208 }
1209 });
1210 let any: AnyPv = pv.clone().into();
1211 assert!(any.take_validator().is_some());
1212 }
1213
1214 #[tokio::test]
1215 async fn on_put_wrapper_unwraps_structure_wrapped_scalar_put() {
1216 let store = empty_store();
1222 let seen = std::sync::Arc::new(std::sync::Mutex::new(None));
1223 let seen2 = seen.clone();
1224 let pv = Pv::ao("SIM:WRAP", 1.0).on_put(move |_pv, v: f64| {
1225 *seen2.lock().unwrap() = Some(v);
1226 Ok(())
1227 });
1228 let any: AnyPv = pv.clone().into();
1229 let rec = any.take_record().unwrap();
1230 store.insert(rec.name.clone(), rec).await;
1231 let validator = any.take_validator().expect("validator attached");
1232 any.bind(&store);
1233
1234 let dv = spvirit_codec::spvd_decode::DecodedValue::Structure(vec![(
1235 "value".to_string(),
1236 spvirit_codec::spvd_decode::DecodedValue::Float64(42.0),
1237 )]);
1238 let res = validator("SIM:WRAP", &dv);
1239 assert_eq!(res, Ok(()));
1240 assert_eq!(*seen.lock().unwrap(), Some(42.0));
1241 }
1242}