Skip to main content

spvirit/
nt.rs

1//! Python wrappers for Normative Type structs.
2
3use pyo3::prelude::*;
4use pyo3::types::{PyDict, PyList};
5use spvirit_types::{
6    NtAlarm, NtControl, NtDisplay, NtNdArray, NtPayload, NtScalar, NtScalarArray, NtTable,
7    NtTimeStamp, PvValue,
8};
9
10use crate::convert::{py_to_scalar, py_to_scalar_array, scalar_array_to_py, scalar_to_py};
11
12// ─── PyAlarm ─────────────────────────────────────────────────────────────────
13
14/// Alarm substructure: severity (0=NO_ALARM, 1=MINOR, 2=MAJOR, 3=INVALID),
15/// status, message. Read-only properties.
16#[pyclass(name = "Alarm")]
17#[derive(Clone)]
18pub struct PyAlarm {
19    /// Alarm severity: 0=NO_ALARM, 1=MINOR, 2=MAJOR, 3=INVALID.
20    #[pyo3(get)]
21    pub severity: i32,
22    /// Alarm status code.
23    #[pyo3(get)]
24    pub status: i32,
25    /// Alarm message text.
26    #[pyo3(get)]
27    pub message: String,
28}
29
30impl From<&NtAlarm> for PyAlarm {
31    fn from(a: &NtAlarm) -> Self {
32        Self {
33            severity: a.severity,
34            status: a.status,
35            message: a.message.clone(),
36        }
37    }
38}
39
40#[pymethods]
41impl PyAlarm {
42    #[new]
43    #[pyo3(signature = (severity=0, status=0, message=String::new()))]
44    fn py_new(severity: i32, status: i32, message: String) -> Self {
45        Self {
46            severity,
47            status,
48            message,
49        }
50    }
51
52    fn __repr__(&self) -> String {
53        format!(
54            "Alarm(severity={}, status={}, message={:?})",
55            self.severity, self.status, self.message
56        )
57    }
58}
59
60// ─── PyTimeStamp ─────────────────────────────────────────────────────────────
61
62/// Time stamp substructure: seconds_past_epoch, nanoseconds, user_tag.
63#[pyclass(name = "TimeStamp")]
64#[derive(Clone)]
65pub struct PyTimeStamp {
66    /// Seconds since the Unix epoch.
67    #[pyo3(get)]
68    pub seconds_past_epoch: i64,
69    /// Nanoseconds part of the time stamp.
70    #[pyo3(get)]
71    pub nanoseconds: i32,
72    /// Application-defined user tag.
73    #[pyo3(get)]
74    pub user_tag: i32,
75}
76
77impl From<&NtTimeStamp> for PyTimeStamp {
78    fn from(ts: &NtTimeStamp) -> Self {
79        Self {
80            seconds_past_epoch: ts.seconds_past_epoch,
81            nanoseconds: ts.nanoseconds,
82            user_tag: ts.user_tag,
83        }
84    }
85}
86
87#[pymethods]
88impl PyTimeStamp {
89    #[new]
90    #[pyo3(signature = (seconds_past_epoch=0, nanoseconds=0, user_tag=0))]
91    fn py_new(seconds_past_epoch: i64, nanoseconds: i32, user_tag: i32) -> Self {
92        Self {
93            seconds_past_epoch,
94            nanoseconds,
95            user_tag,
96        }
97    }
98
99    fn __repr__(&self) -> String {
100        format!(
101            "TimeStamp(seconds={}, ns={})",
102            self.seconds_past_epoch, self.nanoseconds
103        )
104    }
105}
106
107// ─── PyDisplay ───────────────────────────────────────────────────────────────
108
109/// Display metadata substructure: limits, description, units, precision.
110#[pyclass(name = "Display")]
111#[derive(Clone)]
112pub struct PyDisplay {
113    /// Lower display limit.
114    #[pyo3(get)]
115    pub limit_low: f64,
116    /// Upper display limit.
117    #[pyo3(get)]
118    pub limit_high: f64,
119    /// Human-readable description.
120    #[pyo3(get)]
121    pub description: String,
122    /// Engineering units string.
123    #[pyo3(get)]
124    pub units: String,
125    /// Display precision (decimal places).
126    #[pyo3(get)]
127    pub precision: i32,
128}
129
130impl From<&NtDisplay> for PyDisplay {
131    fn from(d: &NtDisplay) -> Self {
132        Self {
133            limit_low: d.limit_low,
134            limit_high: d.limit_high,
135            description: d.description.clone(),
136            units: d.units.clone(),
137            precision: d.precision,
138        }
139    }
140}
141
142#[pymethods]
143impl PyDisplay {
144    #[new]
145    #[pyo3(signature = (limit_low=0.0, limit_high=0.0, description=String::new(), units=String::new(), precision=0))]
146    fn py_new(
147        limit_low: f64,
148        limit_high: f64,
149        description: String,
150        units: String,
151        precision: i32,
152    ) -> Self {
153        Self {
154            limit_low,
155            limit_high,
156            description,
157            units,
158            precision,
159        }
160    }
161
162    fn __repr__(&self) -> String {
163        format!(
164            "Display(low={}, high={}, units={:?})",
165            self.limit_low, self.limit_high, self.units
166        )
167    }
168}
169
170// ─── PyControl ───────────────────────────────────────────────────────────────
171
172/// Control metadata substructure: write limits and minimum step.
173#[pyclass(name = "Control")]
174#[derive(Clone)]
175pub struct PyControl {
176    /// Lower control (write) limit.
177    #[pyo3(get)]
178    pub limit_low: f64,
179    /// Upper control (write) limit.
180    #[pyo3(get)]
181    pub limit_high: f64,
182    /// Minimum step between accepted values.
183    #[pyo3(get)]
184    pub min_step: f64,
185}
186
187impl From<&NtControl> for PyControl {
188    fn from(c: &NtControl) -> Self {
189        Self {
190            limit_low: c.limit_low,
191            limit_high: c.limit_high,
192            min_step: c.min_step,
193        }
194    }
195}
196
197#[pymethods]
198impl PyControl {
199    #[new]
200    #[pyo3(signature = (limit_low=0.0, limit_high=0.0, min_step=0.0))]
201    fn py_new(limit_low: f64, limit_high: f64, min_step: f64) -> Self {
202        Self {
203            limit_low,
204            limit_high,
205            min_step,
206        }
207    }
208
209    fn __repr__(&self) -> String {
210        format!(
211            "Control(low={}, high={}, min_step={})",
212            self.limit_low, self.limit_high, self.min_step
213        )
214    }
215}
216
217// ─── PyNtScalar ──────────────────────────────────────────────────────────────
218
219/// Full-fidelity NTScalar payload: value plus alarm, display, and control
220/// metadata. Used with `Store.get_nt`/`Store.put_nt`, Python sources, and
221/// `Notifier.notify`.
222#[pyclass(name = "NtScalar")]
223pub struct PyNtScalar {
224    inner: NtScalar,
225}
226
227impl PyNtScalar {
228    pub fn new(inner: NtScalar) -> Self {
229        Self { inner }
230    }
231}
232
233#[pymethods]
234impl PyNtScalar {
235    /// Create an NtScalar from a Python value with optional metadata.
236    #[new]
237    #[pyo3(signature = (value, units=String::new(), display_low=0.0, display_high=0.0, display_description=String::new(), display_precision=0, control_low=0.0, control_high=0.0, control_min_step=0.0, alarm_severity=0, alarm_status=0, alarm_message=String::new()))]
238    fn py_new(
239        value: &Bound<'_, PyAny>,
240        units: String,
241        display_low: f64,
242        display_high: f64,
243        display_description: String,
244        display_precision: i32,
245        control_low: f64,
246        control_high: f64,
247        control_min_step: f64,
248        alarm_severity: i32,
249        alarm_status: i32,
250        alarm_message: String,
251    ) -> PyResult<Self> {
252        let sv = py_to_scalar(value)?;
253        let mut nt = NtScalar::from_value(sv);
254        nt.units = units;
255        nt.display_low = display_low;
256        nt.display_high = display_high;
257        nt.display_description = display_description;
258        nt.display_precision = display_precision;
259        nt.control_low = control_low;
260        nt.control_high = control_high;
261        nt.control_min_step = control_min_step;
262        nt.alarm_severity = alarm_severity;
263        nt.alarm_status = alarm_status;
264        nt.alarm_message = alarm_message;
265        Ok(Self { inner: nt })
266    }
267
268    /// Scalar value as a Python object.
269    #[getter]
270    fn value(&self, py: Python<'_>) -> PyObject {
271        scalar_to_py(py, &self.inner.value)
272    }
273
274    /// Alarm severity: 0=NO_ALARM, 1=MINOR, 2=MAJOR, 3=INVALID.
275    #[getter]
276    fn alarm_severity(&self) -> i32 {
277        self.inner.alarm_severity
278    }
279
280    /// Alarm status code.
281    #[getter]
282    fn alarm_status(&self) -> i32 {
283        self.inner.alarm_status
284    }
285
286    /// Alarm message text.
287    #[getter]
288    fn alarm_message(&self) -> &str {
289        &self.inner.alarm_message
290    }
291
292    /// Engineering units string.
293    #[getter]
294    fn units(&self) -> &str {
295        &self.inner.units
296    }
297
298    /// Lower display limit.
299    #[getter]
300    fn display_low(&self) -> f64 {
301        self.inner.display_low
302    }
303
304    /// Upper display limit.
305    #[getter]
306    fn display_high(&self) -> f64 {
307        self.inner.display_high
308    }
309
310    /// Display description text.
311    #[getter]
312    fn display_description(&self) -> &str {
313        &self.inner.display_description
314    }
315
316    /// Display precision (decimal places).
317    #[getter]
318    fn display_precision(&self) -> i32 {
319        self.inner.display_precision
320    }
321
322    /// Lower control (write) limit.
323    #[getter]
324    fn control_low(&self) -> f64 {
325        self.inner.control_low
326    }
327
328    /// Upper control (write) limit.
329    #[getter]
330    fn control_high(&self) -> f64 {
331        self.inner.control_high
332    }
333
334    /// Minimum step between accepted values.
335    #[getter]
336    fn control_min_step(&self) -> f64 {
337        self.inner.control_min_step
338    }
339
340    fn __repr__(&self, py: Python<'_>) -> String {
341        let val = scalar_to_py(py, &self.inner.value);
342        format!("NtScalar(value={}, units={:?})", val, self.inner.units)
343    }
344}
345
346// ─── PyNtScalarArray ─────────────────────────────────────────────────────────
347
348/// Full-fidelity NTScalarArray payload: array value plus alarm, time stamp,
349/// display, and control metadata.
350#[pyclass(name = "NtScalarArray")]
351pub struct PyNtScalarArray {
352    inner: NtScalarArray,
353}
354
355impl PyNtScalarArray {
356    pub fn new(inner: NtScalarArray) -> Self {
357        Self { inner }
358    }
359}
360
361#[pymethods]
362impl PyNtScalarArray {
363    /// Create an NtScalarArray from a Python list/bytes.
364    #[new]
365    fn py_new(value: &Bound<'_, PyAny>) -> PyResult<Self> {
366        let arr = py_to_scalar_array(value)?;
367        Ok(Self {
368            inner: NtScalarArray::from_value(arr),
369        })
370    }
371
372    /// Array value as a Python list (or bytes for byte arrays).
373    #[getter]
374    fn value(&self, py: Python<'_>) -> PyObject {
375        scalar_array_to_py(py, &self.inner.value)
376    }
377
378    /// Alarm substructure.
379    #[getter]
380    fn alarm(&self) -> PyAlarm {
381        PyAlarm::from(&self.inner.alarm)
382    }
383
384    /// Time stamp substructure.
385    #[getter]
386    fn time_stamp(&self) -> PyTimeStamp {
387        PyTimeStamp::from(&self.inner.time_stamp)
388    }
389
390    /// Display metadata substructure.
391    #[getter]
392    fn display(&self) -> PyDisplay {
393        PyDisplay::from(&self.inner.display)
394    }
395
396    /// Control metadata substructure.
397    #[getter]
398    fn control(&self) -> PyControl {
399        PyControl::from(&self.inner.control)
400    }
401
402    fn __repr__(&self, py: Python<'_>) -> String {
403        let val = scalar_array_to_py(py, &self.inner.value);
404        format!("NtScalarArray(value={})", val)
405    }
406}
407
408// ─── PyNtTable ───────────────────────────────────────────────────────────────
409
410/// NTTable payload: column labels, columns, and optional metadata. Has no
411/// Python constructor — returned by reads such as `Store.get_nt`.
412#[pyclass(name = "NtTable")]
413pub struct PyNtTable {
414    inner: NtTable,
415}
416
417impl PyNtTable {
418    pub fn new(inner: NtTable) -> Self {
419        Self { inner }
420    }
421}
422
423#[pymethods]
424impl PyNtTable {
425    /// Column labels as a list of strings.
426    #[getter]
427    fn labels(&self, py: Python<'_>) -> PyResult<PyObject> {
428        let items: Vec<PyObject> = self
429            .inner
430            .labels
431            .iter()
432            .map(|s| pyo3::types::PyString::new(py, s).into_any().unbind())
433            .collect();
434        Ok(PyList::new(py, &items)?.into_any().unbind())
435    }
436
437    /// Return columns as a dict of {name: list}.
438    fn columns(&self, py: Python<'_>) -> PyResult<PyObject> {
439        let dict = pyo3::types::PyDict::new(py);
440        for col in &self.inner.columns {
441            dict.set_item(&col.name, scalar_array_to_py(py, &col.values))?;
442        }
443        Ok(dict.into_any().unbind())
444    }
445
446    /// Optional descriptor string, or None.
447    #[getter]
448    fn descriptor(&self) -> Option<&str> {
449        self.inner.descriptor.as_deref()
450    }
451
452    /// Optional alarm substructure, or None.
453    #[getter]
454    fn alarm(&self) -> Option<PyAlarm> {
455        self.inner.alarm.as_ref().map(PyAlarm::from)
456    }
457
458    /// Optional time stamp substructure, or None.
459    #[getter]
460    fn time_stamp(&self) -> Option<PyTimeStamp> {
461        self.inner.time_stamp.as_ref().map(PyTimeStamp::from)
462    }
463
464    fn __repr__(&self) -> String {
465        format!(
466            "NtTable(labels={:?}, columns={})",
467            self.inner.labels,
468            self.inner.columns.len()
469        )
470    }
471}
472
473// ─── PyNtNdArray ─────────────────────────────────────────────────────────────
474
475/// NTNDArray payload: flat array data plus dimensions and image metadata.
476/// Has no Python constructor — returned by reads such as `Store.get_nt`.
477#[pyclass(name = "NtNdArray")]
478pub struct PyNtNdArray {
479    inner: NtNdArray,
480}
481
482impl PyNtNdArray {
483    pub fn new(inner: NtNdArray) -> Self {
484        Self { inner }
485    }
486}
487
488#[pymethods]
489impl PyNtNdArray {
490    /// Flat array data as a Python list (or bytes for byte arrays).
491    #[getter]
492    fn value(&self, py: Python<'_>) -> PyObject {
493        scalar_array_to_py(py, &self.inner.value)
494    }
495
496    /// Unique frame identifier.
497    #[getter]
498    fn unique_id(&self) -> i32 {
499        self.inner.unique_id
500    }
501
502    /// Compressed data size in bytes.
503    #[getter]
504    fn compressed_size(&self) -> i64 {
505        self.inner.compressed_size
506    }
507
508    /// Uncompressed data size in bytes.
509    #[getter]
510    fn uncompressed_size(&self) -> i64 {
511        self.inner.uncompressed_size
512    }
513
514    /// Return dimensions as a list of dicts, one per dimension, with keys
515    /// `size`, `offset`, `full_size`, `binning`, and `reverse`.
516    fn dimensions(&self, py: Python<'_>) -> PyResult<PyObject> {
517        let items: Vec<PyObject> = self
518            .inner
519            .dimension
520            .iter()
521            .map(|d| {
522                let dict = pyo3::types::PyDict::new(py);
523                dict.set_item("size", d.size).expect("set");
524                dict.set_item("offset", d.offset).expect("set");
525                dict.set_item("full_size", d.full_size).expect("set");
526                dict.set_item("binning", d.binning).expect("set");
527                dict.set_item("reverse", d.reverse).expect("set");
528                dict.into_any().unbind()
529            })
530            .collect();
531        Ok(PyList::new(py, &items)?.into_any().unbind())
532    }
533
534    /// Time stamp of the data acquisition.
535    #[getter]
536    fn data_time_stamp(&self) -> PyTimeStamp {
537        PyTimeStamp::from(&self.inner.data_time_stamp)
538    }
539
540    fn __repr__(&self) -> String {
541        let dims: Vec<i32> = self.inner.dimension.iter().map(|d| d.size).collect();
542        format!(
543            "NtNdArray(unique_id={}, dims={:?})",
544            self.inner.unique_id, dims
545        )
546    }
547}
548
549// ─── NtPayload → Python wrapper ──────────────────────────────────────────────
550
551/// Extract the Rust `NtPayload` from a Python NT object.
552pub fn py_to_nt_payload(obj: &Bound<'_, PyAny>) -> PyResult<NtPayload> {
553    if let Ok(s) = obj.downcast::<PyNtScalar>() {
554        Ok(NtPayload::Scalar(s.borrow().inner.clone()))
555    } else if let Ok(a) = obj.downcast::<PyNtScalarArray>() {
556        Ok(NtPayload::ScalarArray(a.borrow().inner.clone()))
557    } else if let Ok(t) = obj.downcast::<PyNtTable>() {
558        Ok(NtPayload::Table(t.borrow().inner.clone()))
559    } else if let Ok(n) = obj.downcast::<PyNtNdArray>() {
560        Ok(NtPayload::NdArray(n.borrow().inner.clone()))
561    } else {
562        Err(pyo3::exceptions::PyTypeError::new_err(
563            "expected NtScalar, NtScalarArray, NtTable, or NtNdArray",
564        ))
565    }
566}
567
568pub fn nt_payload_to_py(py: Python<'_>, payload: NtPayload) -> PyObject {
569    match payload {
570        NtPayload::Scalar(s) => PyNtScalar::new(s)
571            .into_pyobject(py)
572            .expect("NtScalar")
573            .into_any()
574            .unbind(),
575        NtPayload::ScalarArray(a) => PyNtScalarArray::new(a)
576            .into_pyobject(py)
577            .expect("NtScalarArray")
578            .into_any()
579            .unbind(),
580        NtPayload::Table(t) => PyNtTable::new(t)
581            .into_pyobject(py)
582            .expect("NtTable")
583            .into_any()
584            .unbind(),
585        NtPayload::NdArray(n) => PyNtNdArray::new(n)
586            .into_pyobject(py)
587            .expect("NtNdArray")
588            .into_any()
589            .unbind(),
590        NtPayload::Enum(e) => {
591            let d = PyDict::new(py);
592            d.set_item("index", e.index).ok();
593            d.set_item("choices", &e.choices).ok();
594            d.set_item("selected", e.selected().unwrap_or("")).ok();
595            d.unbind().into_any()
596        }
597        NtPayload::Generic { struct_id, fields } => {
598            let d = PyDict::new(py);
599            d.set_item("struct_id", &struct_id).ok();
600            for (name, val) in &fields {
601                d.set_item(name, pvvalue_to_py(py, val)).ok();
602            }
603            d.unbind().into_any()
604        }
605    }
606}
607
608fn pvvalue_to_py(py: Python<'_>, val: &PvValue) -> PyObject {
609    match val {
610        PvValue::Scalar(s) => scalar_to_py(py, s),
611        PvValue::ScalarArray(a) => scalar_array_to_py(py, a),
612        PvValue::Structure { struct_id, fields } => {
613            let d = PyDict::new(py);
614            d.set_item("struct_id", struct_id).ok();
615            for (name, v) in fields {
616                d.set_item(name, pvvalue_to_py(py, v)).ok();
617            }
618            d.unbind().into_any()
619        }
620    }
621}