Skip to main content

spvirit/
pv.rs

1//! Typed PV handles for Python — wraps `spvirit_server::pv::Pv<T>`.
2
3use 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/// Typed handle to a PV record. Create with `spvirit.ai(...)`, `spvirit.ao(...)`,
33/// etc.; serve with `spvirit.Server(pvs=[...])`; then `set()`/`get()` freely.
34#[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    /// PV name.
55    #[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    /// Write a value through the full posting pipeline (blocking, GIL released).
78    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    /// Read the current value, typed (blocking, GIL released).
104    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    /// Write a value through the full posting pipeline (async variant of `set`).
130    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    /// Read the current value, typed (async variant of `get`).
180    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    /// Explicitly set the record's alarm severity/status/message, independent
221    /// of its value.
222    #[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    /// Attach a PUT handler: `pv.on_put(fn)` or `@pv.on_put`.
244    ///
245    /// The callback is invoked as `callback(pv, value)` whenever a PVAccess
246    /// client writes to this PV, BEFORE the value is applied. Returning
247    /// `False` or raising rejects the PUT on the wire (the client's `put`
248    /// raises); any other return accepts it. Returns the callback unchanged
249    /// (decorator protocol), so it works both as a plain method call and as
250    /// `@pv.on_put`.
251    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    /// Periodic scan: `pv.scan(period, fn)` or `@pv.scan(period=0.1)`.
292    ///
293    /// `fn(pv)` returns the new value. Returning `None` re-posts the last
294    /// value this scan produced (the type default — 0.0/false/0/"" — before
295    /// the first scanned value); it does not read whatever the PV's current
296    /// value happens to be (e.g. from `pv.set(...)` called elsewhere). Prefer
297    /// returning a value from the callback, or calling `pv.set()` explicitly.
298    /// Must be attached BEFORE the PV is served (`Server(...)`); attaching
299    /// afterwards is a silent no-op (core logs a warning).
300    #[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/// Decorator-factory returned by `pv.scan(period=...)` when called without a
324/// callback: `@pv.scan(period=0.1)` then registers the decorated function.
325#[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
370/// Shared scan-bridge shape, one per scalar type (kept as small distinct fns
371/// rather than a fully generic helper — `PyPv`'s `PvKind` wrapping and the
372/// per-type `extract::<T>` calls don't factor cleanly through a trait without
373/// more machinery than four ~10-line functions warrant).
374///
375/// `Handle::current().block_on` is NOT usable here to read "the current
376/// value": the scan closure runs synchronously inside the async scan task,
377/// and blocking on it panics. Instead each closure owns a
378/// `Mutex<Option<T>>` cache: a value returned by the Python callback is
379/// cached and returned; `None`/an unextractable return/an exception falls
380/// back to the cached last value, or a type default (0.0/false/0/"") on the
381/// very first call. This keeps the `None`-means-"leave PV alone" contract
382/// honest without ever blocking inside the runtime.
383macro_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
423/// Bridge a wire PUT into a Python callback. Exception or `False` → reject.
424fn 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/// Shared keyword options applied to any typed handle.
448#[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/// Multi-bit binary input (enum, read-only). Value is the choice index;
564/// out-of-range writes are rejected. Enum records ignore the common
565/// `NtScalar` options (`units`/`prec`/`adel`/`mdel`/limits) — only `desc`
566/// is accepted.
567#[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/// Multi-bit binary output (enum, writable). Value is the choice index;
580/// out-of-range writes are rejected. Enum records ignore the common
581/// `NtScalar` options (`units`/`prec`/`adel`/`mdel`/limits) — only `desc`
582/// is accepted.
583#[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/// Array record (writable over the wire). `data` is a list of bool/int/
596/// float/str, or `bytes` for a `U8` array.
597#[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/// Analog array input (read-only over the wire). `data` is a list of bool/
606/// int/float/str, or `bytes` for a `U8` array.
607#[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/// Analog array output (writable). `data` is a list of bool/int/float/str,
616/// or `bytes` for a `U8` array.
617#[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/// A derived (read-only) float PV recomputed whenever any input changes.
626///
627/// `inputs` must all be float PVs (`ai`/`ao`); anything else raises
628/// `TypeError`. `callback(values: list[float]) -> float` is invoked with the
629/// inputs' current values in order; exceptions or non-float returns are
630/// logged and treated as `0.0`. Must be attached BEFORE the PVs are served
631/// (`Server(...)`); attaching afterwards is a silent no-op (core logs a
632/// warning).
633#[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/// Build a typed PV, inferring the record type from `initial`'s Python type:
668/// `bool` -> `bo`, `int` -> `longout`, `float` -> `ao`, `str` -> `string_out`,
669/// `list`/`bytes` -> `waveform`. Note `bool` is checked before `int` since
670/// `isinstance(True, int)` is `True` in Python. Any other type raises
671/// `TypeError`.
672#[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        // Array records carry no scalar metadata; reject rather than
712        // silently discard the options.
713        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}