1use std::sync::Mutex;
4
5use pyo3::IntoPyObjectExt;
6use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyTypeError};
7use pyo3::prelude::*;
8
9use spvirit_server::pv::{AnyPv, Pv, PvArray, PvError};
10
11use crate::convert::{py_to_scalar_array, scalar_array_to_py};
12use crate::runtime::{block_on_py, future_into_py};
13
14pub(crate) fn pv_err(e: PvError) -> PyErr {
15 match e {
16 PvError::Unbound => PyRuntimeError::new_err(e.to_string()),
17 PvError::NotFound(_) => PyKeyError::new_err(e.to_string()),
18 PvError::TypeMismatch { .. } => PyTypeError::new_err(e.to_string()),
19 PvError::PutRejected(_) => crate::errors::PutRejectedError::new_err(e.to_string()),
20 }
21}
22
23#[derive(Clone)]
24pub(crate) enum PvKind {
25 F64(Pv<f64>),
26 Bool(Pv<bool>),
27 I32(Pv<i32>),
28 Str(Pv<String>),
29 Array(PvArray),
30}
31
32#[pyclass(name = "Pv")]
35#[derive(Clone)]
36pub struct PyPv {
37 pub(crate) kind: PvKind,
38}
39
40impl PyPv {
41 pub(crate) fn any(&self) -> AnyPv {
42 match &self.kind {
43 PvKind::F64(p) => AnyPv::from(p.clone()),
44 PvKind::Bool(p) => AnyPv::from(p.clone()),
45 PvKind::I32(p) => AnyPv::from(p.clone()),
46 PvKind::Str(p) => AnyPv::from(p.clone()),
47 PvKind::Array(a) => AnyPv::from(a.clone()),
48 }
49 }
50}
51
52#[pymethods]
53impl PyPv {
54 #[getter]
56 fn name(&self) -> &str {
57 match &self.kind {
58 PvKind::F64(p) => p.name(),
59 PvKind::Bool(p) => p.name(),
60 PvKind::I32(p) => p.name(),
61 PvKind::Str(p) => p.name(),
62 PvKind::Array(p) => p.name(),
63 }
64 }
65
66 fn __repr__(&self) -> String {
67 let ty = match &self.kind {
68 PvKind::F64(_) => "float",
69 PvKind::Bool(_) => "bool",
70 PvKind::I32(_) => "int",
71 PvKind::Str(_) => "str",
72 PvKind::Array(_) => "array",
73 };
74 format!("<spvirit.Pv '{}' ({ty})>", self.name())
75 }
76
77 fn set(&self, py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult<()> {
79 match &self.kind {
80 PvKind::F64(p) => {
81 let v: f64 = value.extract()?;
82 block_on_py(py, p.set(v)).map_err(pv_err)
83 }
84 PvKind::Bool(p) => {
85 let v: bool = value.extract()?;
86 block_on_py(py, p.set(v)).map_err(pv_err)
87 }
88 PvKind::I32(p) => {
89 let v: i32 = value.extract()?;
90 block_on_py(py, p.set(v)).map_err(pv_err)
91 }
92 PvKind::Str(p) => {
93 let v: String = value.extract()?;
94 block_on_py(py, p.set(v)).map_err(pv_err)
95 }
96 PvKind::Array(p) => {
97 let v = py_to_scalar_array(value)?;
98 block_on_py(py, p.set(v)).map_err(pv_err)
99 }
100 }
101 }
102
103 fn get(&self, py: Python<'_>) -> PyResult<PyObject> {
105 match &self.kind {
106 PvKind::F64(p) => {
107 let v = block_on_py(py, p.get()).map_err(pv_err)?;
108 v.into_py_any(py)
109 }
110 PvKind::Bool(p) => {
111 let v = block_on_py(py, p.get()).map_err(pv_err)?;
112 v.into_py_any(py)
113 }
114 PvKind::I32(p) => {
115 let v = block_on_py(py, p.get()).map_err(pv_err)?;
116 v.into_py_any(py)
117 }
118 PvKind::Str(p) => {
119 let v = block_on_py(py, p.get()).map_err(pv_err)?;
120 v.into_py_any(py)
121 }
122 PvKind::Array(p) => {
123 let v = block_on_py(py, p.get()).map_err(pv_err)?;
124 Ok(scalar_array_to_py(py, &v))
125 }
126 }
127 }
128
129 fn set_async<'py>(
131 &self,
132 py: Python<'py>,
133 value: &Bound<'py, PyAny>,
134 ) -> PyResult<Bound<'py, PyAny>> {
135 match &self.kind {
136 PvKind::F64(p) => {
137 let v: f64 = value.extract()?;
138 let handle = p.clone();
139 future_into_py(py, async move {
140 handle.set(v).await.map_err(pv_err)?;
141 Python::with_gil(|py| py.None().into_py_any(py))
142 })
143 }
144 PvKind::Bool(p) => {
145 let v: bool = value.extract()?;
146 let handle = p.clone();
147 future_into_py(py, async move {
148 handle.set(v).await.map_err(pv_err)?;
149 Python::with_gil(|py| py.None().into_py_any(py))
150 })
151 }
152 PvKind::I32(p) => {
153 let v: i32 = value.extract()?;
154 let handle = p.clone();
155 future_into_py(py, async move {
156 handle.set(v).await.map_err(pv_err)?;
157 Python::with_gil(|py| py.None().into_py_any(py))
158 })
159 }
160 PvKind::Str(p) => {
161 let v: String = value.extract()?;
162 let handle = p.clone();
163 future_into_py(py, async move {
164 handle.set(v).await.map_err(pv_err)?;
165 Python::with_gil(|py| py.None().into_py_any(py))
166 })
167 }
168 PvKind::Array(p) => {
169 let v = py_to_scalar_array(value)?;
170 let handle = p.clone();
171 future_into_py(py, async move {
172 handle.set(v).await.map_err(pv_err)?;
173 Python::with_gil(|py| py.None().into_py_any(py))
174 })
175 }
176 }
177 }
178
179 fn get_async<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
181 match &self.kind {
182 PvKind::F64(p) => {
183 let handle = p.clone();
184 future_into_py(py, async move {
185 let v = handle.get().await.map_err(pv_err)?;
186 Python::with_gil(|py| v.into_py_any(py))
187 })
188 }
189 PvKind::Bool(p) => {
190 let handle = p.clone();
191 future_into_py(py, async move {
192 let v = handle.get().await.map_err(pv_err)?;
193 Python::with_gil(|py| v.into_py_any(py))
194 })
195 }
196 PvKind::I32(p) => {
197 let handle = p.clone();
198 future_into_py(py, async move {
199 let v = handle.get().await.map_err(pv_err)?;
200 Python::with_gil(|py| v.into_py_any(py))
201 })
202 }
203 PvKind::Str(p) => {
204 let handle = p.clone();
205 future_into_py(py, async move {
206 let v = handle.get().await.map_err(pv_err)?;
207 Python::with_gil(|py| v.into_py_any(py))
208 })
209 }
210 PvKind::Array(p) => {
211 let handle = p.clone();
212 future_into_py(py, async move {
213 let v = handle.get().await.map_err(pv_err)?;
214 Ok(Python::with_gil(|py| scalar_array_to_py(py, &v)))
215 })
216 }
217 }
218 }
219
220 #[pyo3(signature = (severity, status, message=""))]
223 fn set_alarm(&self, py: Python<'_>, severity: i32, status: i32, message: &str) -> PyResult<()> {
224 match &self.kind {
225 PvKind::F64(p) => {
226 block_on_py(py, p.set_alarm(severity, status, message)).map_err(pv_err)
227 }
228 PvKind::Bool(p) => {
229 block_on_py(py, p.set_alarm(severity, status, message)).map_err(pv_err)
230 }
231 PvKind::I32(p) => {
232 block_on_py(py, p.set_alarm(severity, status, message)).map_err(pv_err)
233 }
234 PvKind::Str(p) => {
235 block_on_py(py, p.set_alarm(severity, status, message)).map_err(pv_err)
236 }
237 PvKind::Array(p) => {
238 block_on_py(py, p.set_alarm(severity, status, message)).map_err(pv_err)
239 }
240 }
241 }
242
243 fn on_put(&self, py: Python<'_>, callback: PyObject) -> PyResult<PyObject> {
252 if matches!(&self.kind, PvKind::Array(_)) {
253 return Err(PyTypeError::new_err(
254 "on_put/scan not supported on array PVs",
255 ));
256 }
257 match &self.kind {
258 PvKind::F64(p) => {
259 let handle = p.clone();
260 let cb = callback.clone_ref(py);
261 let _ = p.clone().on_put(move |_pv, v: f64| {
262 py_on_put(&cb, PvKind::F64(handle.clone()), PutVal::F64(v))
263 });
264 }
265 PvKind::Bool(p) => {
266 let handle = p.clone();
267 let cb = callback.clone_ref(py);
268 let _ = p.clone().on_put(move |_pv, v: bool| {
269 py_on_put(&cb, PvKind::Bool(handle.clone()), PutVal::Bool(v))
270 });
271 }
272 PvKind::I32(p) => {
273 let handle = p.clone();
274 let cb = callback.clone_ref(py);
275 let _ = p.clone().on_put(move |_pv, v: i32| {
276 py_on_put(&cb, PvKind::I32(handle.clone()), PutVal::I32(v))
277 });
278 }
279 PvKind::Str(p) => {
280 let handle = p.clone();
281 let cb = callback.clone_ref(py);
282 let _ = p.clone().on_put(move |_pv, v: String| {
283 py_on_put(&cb, PvKind::Str(handle.clone()), PutVal::Str(v))
284 });
285 }
286 PvKind::Array(_) => unreachable!("Array on_put rejected above"),
287 }
288 Ok(callback)
289 }
290
291 #[pyo3(signature = (period, callback=None))]
301 fn scan(&self, py: Python<'_>, period: f64, callback: Option<PyObject>) -> PyResult<PyObject> {
302 if matches!(&self.kind, PvKind::Array(_)) {
303 return Err(PyTypeError::new_err(
304 "on_put/scan not supported on array PVs",
305 ));
306 }
307 match callback {
308 Some(cb) => {
309 register_scan(self, period, cb.clone_ref(py));
310 Ok(cb)
311 }
312 None => {
313 let dec = ScanDecorator {
314 pv: self.clone(),
315 period,
316 };
317 dec.into_py_any(py)
318 }
319 }
320 }
321}
322
323#[pyclass]
326pub struct ScanDecorator {
327 pv: PyPv,
328 period: f64,
329}
330
331#[pymethods]
332impl ScanDecorator {
333 fn __call__(&self, py: Python<'_>, callback: PyObject) -> PyResult<PyObject> {
334 register_scan(&self.pv, self.period, callback.clone_ref(py));
335 Ok(callback)
336 }
337}
338
339fn register_scan(pv: &PyPv, period_secs: f64, cb: PyObject) {
340 let dur = std::time::Duration::from_secs_f64(period_secs);
341 match &pv.kind {
342 PvKind::F64(p) => {
343 let cache = Mutex::new(None);
344 let _ = p
345 .clone()
346 .scan(dur, move |h| scan_bridge_f64(&cb, &cache, h));
347 }
348 PvKind::Bool(p) => {
349 let cache = Mutex::new(None);
350 let _ = p
351 .clone()
352 .scan(dur, move |h| scan_bridge_bool(&cb, &cache, h));
353 }
354 PvKind::I32(p) => {
355 let cache = Mutex::new(None);
356 let _ = p
357 .clone()
358 .scan(dur, move |h| scan_bridge_i32(&cb, &cache, h));
359 }
360 PvKind::Str(p) => {
361 let cache = Mutex::new(None);
362 let _ = p
363 .clone()
364 .scan(dur, move |h| scan_bridge_str(&cb, &cache, h));
365 }
366 PvKind::Array(_) => unreachable!("Array scan rejected in PyPv::scan"),
367 }
368}
369
370macro_rules! scan_bridge_fn {
384 ($fname:ident, $ty:ty, $kind:ident, $default:expr) => {
385 fn $fname(cb: &PyObject, cache: &Mutex<Option<$ty>>, h: &Pv<$ty>) -> $ty {
386 Python::with_gil(|py| {
387 let pv = PyPv {
388 kind: PvKind::$kind(h.clone()),
389 };
390 let result = match cb.call1(py, (pv,)) {
391 Ok(ret) if ret.is_none(py) => None,
392 Ok(ret) => ret.extract::<$ty>(py).ok(),
393 Err(e) => {
394 tracing::error!("scan callback error: {e}");
395 None
396 }
397 };
398 let mut guard = cache.lock().unwrap();
399 match result {
400 Some(v) => {
401 *guard = Some(v.clone());
402 v
403 }
404 None => guard.clone().unwrap_or_else(|| $default),
405 }
406 })
407 }
408 };
409}
410
411scan_bridge_fn!(scan_bridge_f64, f64, F64, 0.0f64);
412scan_bridge_fn!(scan_bridge_bool, bool, Bool, false);
413scan_bridge_fn!(scan_bridge_i32, i32, I32, 0i32);
414scan_bridge_fn!(scan_bridge_str, String, Str, String::new());
415
416pub(crate) enum PutVal {
417 F64(f64),
418 Bool(bool),
419 I32(i32),
420 Str(String),
421}
422
423fn py_on_put(cb: &PyObject, kind: PvKind, val: PutVal) -> Result<(), String> {
425 Python::with_gil(|py| {
426 let pv = PyPv { kind };
427 let arg = match val {
428 PutVal::F64(v) => v.into_py_any(py),
429 PutVal::Bool(v) => v.into_py_any(py),
430 PutVal::I32(v) => v.into_py_any(py),
431 PutVal::Str(v) => v.into_py_any(py),
432 }
433 .map_err(|e| e.to_string())?;
434 match cb.call1(py, (pv, arg)) {
435 Err(e) => Err(e.to_string()),
436 Ok(ret) => {
437 if matches!(ret.extract::<bool>(py), Ok(false)) {
438 Err("rejected by on_put".to_string())
439 } else {
440 Ok(())
441 }
442 }
443 }
444 })
445}
446
447#[allow(clippy::too_many_arguments)]
449fn apply_opts<T: spvirit_server::pv::PvScalar>(
450 mut pv: Pv<T>,
451 units: Option<String>,
452 prec: Option<i32>,
453 desc: Option<String>,
454 adel: Option<f64>,
455 mdel: Option<f64>,
456 drive_limits: Option<(f64, f64)>,
457 alarm_limits: Option<(f64, f64, f64, f64)>,
458) -> Pv<T> {
459 if let Some(u) = units {
460 pv = pv.units(u);
461 }
462 if let Some(p) = prec {
463 pv = pv.prec(p);
464 }
465 if let Some(d) = desc {
466 pv = pv.desc(d);
467 }
468 if let Some(a) = adel {
469 pv = pv.adel(a);
470 }
471 if let Some(m) = mdel {
472 pv = pv.mdel(m);
473 }
474 if let Some((lo, hi)) = drive_limits {
475 pv = pv.drive_limits(lo, hi);
476 }
477 if let Some((lolo, low, high, hihi)) = alarm_limits {
478 pv = pv.alarm_limits(lolo, low, high, hihi);
479 }
480 pv
481}
482
483macro_rules! pv_ctor {
484 ($fname:ident, $ctor:path, $init_ty:ty, $kind:ident, $doc:literal) => {
485 #[pyfunction]
486 #[pyo3(signature = (name, initial, *, units=None, prec=None, desc=None,
487 adel=None, mdel=None, drive_limits=None, alarm_limits=None))]
488 #[doc = $doc]
489 #[allow(clippy::too_many_arguments)]
490 pub fn $fname(
491 name: String,
492 initial: $init_ty,
493 units: Option<String>,
494 prec: Option<i32>,
495 desc: Option<String>,
496 adel: Option<f64>,
497 mdel: Option<f64>,
498 drive_limits: Option<(f64, f64)>,
499 alarm_limits: Option<(f64, f64, f64, f64)>,
500 ) -> PyPv {
501 let pv = apply_opts(
502 $ctor(name, initial),
503 units,
504 prec,
505 desc,
506 adel,
507 mdel,
508 drive_limits,
509 alarm_limits,
510 );
511 PyPv {
512 kind: PvKind::$kind(pv),
513 }
514 }
515 };
516}
517
518pv_ctor!(
519 ai,
520 Pv::ai,
521 f64,
522 F64,
523 "Analog input (read-only over the wire)."
524);
525pv_ctor!(ao, Pv::ao, f64, F64, "Analog output (writable).");
526pv_ctor!(
527 bi,
528 Pv::bi,
529 bool,
530 Bool,
531 "Binary input (read-only over the wire)."
532);
533pv_ctor!(bo, Pv::bo, bool, Bool, "Binary output (writable).");
534pv_ctor!(
535 string_in,
536 Pv::string_in,
537 String,
538 Str,
539 "String input (read-only over the wire)."
540);
541pv_ctor!(
542 string_out,
543 Pv::string_out,
544 String,
545 Str,
546 "String output (writable)."
547);
548pv_ctor!(
549 longin,
550 Pv::longin,
551 i32,
552 I32,
553 "32-bit integer input (read-only over the wire)."
554);
555pv_ctor!(
556 longout,
557 Pv::longout,
558 i32,
559 I32,
560 "32-bit integer output (writable)."
561);
562
563#[pyfunction]
568#[pyo3(signature = (name, choices, initial, *, desc=None))]
569pub fn mbbi(name: String, choices: Vec<String>, initial: i32, desc: Option<String>) -> PyPv {
570 let mut pv = Pv::mbbi(name, choices, initial);
571 if let Some(d) = desc {
572 pv = pv.desc(d);
573 }
574 PyPv {
575 kind: PvKind::I32(pv),
576 }
577}
578
579#[pyfunction]
584#[pyo3(signature = (name, choices, initial, *, desc=None))]
585pub fn mbbo(name: String, choices: Vec<String>, initial: i32, desc: Option<String>) -> PyPv {
586 let mut pv = Pv::mbbo(name, choices, initial);
587 if let Some(d) = desc {
588 pv = pv.desc(d);
589 }
590 PyPv {
591 kind: PvKind::I32(pv),
592 }
593}
594
595#[pyfunction]
598pub fn waveform(name: String, data: &Bound<'_, PyAny>) -> PyResult<PyPv> {
599 let arr = py_to_scalar_array(data)?;
600 Ok(PyPv {
601 kind: PvKind::Array(PvArray::waveform(name, arr)),
602 })
603}
604
605#[pyfunction]
608pub fn aai(name: String, data: &Bound<'_, PyAny>) -> PyResult<PyPv> {
609 let arr = py_to_scalar_array(data)?;
610 Ok(PyPv {
611 kind: PvKind::Array(PvArray::aai(name, arr)),
612 })
613}
614
615#[pyfunction]
618pub fn aao(name: String, data: &Bound<'_, PyAny>) -> PyResult<PyPv> {
619 let arr = py_to_scalar_array(data)?;
620 Ok(PyPv {
621 kind: PvKind::Array(PvArray::aao(name, arr)),
622 })
623}
624
625#[pyfunction]
634pub fn calc(py: Python<'_>, name: String, inputs: Vec<PyPv>, callback: PyObject) -> PyResult<PyPv> {
635 let mut fs: Vec<Pv<f64>> = Vec::with_capacity(inputs.len());
636 for p in &inputs {
637 match &p.kind {
638 PvKind::F64(h) => fs.push(h.clone()),
639 _ => {
640 return Err(PyTypeError::new_err(
641 "calc inputs must all be float PVs (ai/ao)",
642 ));
643 }
644 }
645 }
646 let refs: Vec<&Pv<f64>> = fs.iter().collect();
647 let cb = callback.clone_ref(py);
648 let out = Pv::calc(name, &refs, move |vals: &[f64]| {
649 Python::with_gil(|py| {
650 let called = pyo3::types::PyList::new(py, vals)
651 .and_then(|l| cb.call1(py, (l,)))
652 .and_then(|ret| ret.extract::<f64>(py));
653 match called {
654 Ok(v) => v,
655 Err(e) => {
656 tracing::error!("calc callback failed, posting 0.0: {e}");
657 0.0
658 }
659 }
660 })
661 });
662 Ok(PyPv {
663 kind: PvKind::F64(out),
664 })
665}
666
667#[pyfunction]
673#[pyo3(signature = (name, initial, *, units=None, prec=None, desc=None,
674 adel=None, mdel=None, drive_limits=None, alarm_limits=None))]
675#[allow(clippy::too_many_arguments)]
676pub fn pv(
677 name: String,
678 initial: &Bound<'_, PyAny>,
679 units: Option<String>,
680 prec: Option<i32>,
681 desc: Option<String>,
682 adel: Option<f64>,
683 mdel: Option<f64>,
684 drive_limits: Option<(f64, f64)>,
685 alarm_limits: Option<(f64, f64, f64, f64)>,
686) -> PyResult<PyPv> {
687 use pyo3::types::{PyBool, PyBytes, PyFloat, PyInt, PyList, PyString};
688 let kind = if initial.is_instance_of::<PyBool>() {
689 PvKind::Bool(apply_opts(
690 Pv::bo(name, initial.extract::<bool>()?),
691 units,
692 prec,
693 desc,
694 adel,
695 mdel,
696 drive_limits,
697 alarm_limits,
698 ))
699 } else if initial.is_instance_of::<PyInt>() {
700 PvKind::I32(apply_opts(
701 Pv::longout(name, initial.extract::<i32>()?),
702 units,
703 prec,
704 desc,
705 adel,
706 mdel,
707 drive_limits,
708 alarm_limits,
709 ))
710 } else if initial.is_instance_of::<PyList>() || initial.is_instance_of::<PyBytes>() {
711 if units.is_some()
714 || prec.is_some()
715 || desc.is_some()
716 || adel.is_some()
717 || mdel.is_some()
718 || drive_limits.is_some()
719 || alarm_limits.is_some()
720 {
721 return Err(PyTypeError::new_err(
722 "metadata options (units/prec/desc/adel/mdel/drive_limits/alarm_limits) \
723 are not supported for array PVs",
724 ));
725 }
726 let arr = py_to_scalar_array(initial)?;
727 PvKind::Array(PvArray::waveform(name, arr))
728 } else if initial.is_instance_of::<PyFloat>() {
729 PvKind::F64(apply_opts(
730 Pv::ao(name, initial.extract::<f64>()?),
731 units,
732 prec,
733 desc,
734 adel,
735 mdel,
736 drive_limits,
737 alarm_limits,
738 ))
739 } else if initial.is_instance_of::<PyString>() {
740 PvKind::Str(apply_opts(
741 Pv::string_out(name, initial.extract::<String>()?),
742 units,
743 prec,
744 desc,
745 adel,
746 mdel,
747 drive_limits,
748 alarm_limits,
749 ))
750 } else {
751 return Err(PyTypeError::new_err(format!(
752 "cannot infer PV type from initial value of type {}",
753 initial.get_type().name()?
754 )));
755 };
756 Ok(PyPv { kind })
757}