1use std::future::Future;
29use std::pin::Pin;
30use std::sync::Arc;
31
32use pyo3::prelude::*;
33use pyo3::types::{PyDict, PyList, PyTuple};
34use tokio::sync::mpsc;
35
36use spvirit_codec::spvd_decode::{
37 DecodedValue, FieldDesc, FieldType, StructureDesc, TypeCode,
38};
39use spvirit_server::monitor::MonitorRegistry;
40use spvirit_server::pvstore::{PvInfo, Source};
41use spvirit_types::NtPayload;
42
43use crate::convert::decoded_to_py;
44use crate::nt::{nt_payload_to_py, py_to_nt_payload};
45use crate::runtime::RUNTIME;
46
47fn parse_type_code(s: &str) -> Option<TypeCode> {
50 Some(match s {
51 "boolean" | "bool" => TypeCode::Boolean,
52 "byte" | "int8" | "i8" => TypeCode::Int8,
53 "short" | "int16" | "i16" => TypeCode::Int16,
54 "int" | "int32" | "i32" => TypeCode::Int32,
55 "long" | "int64" | "i64" => TypeCode::Int64,
56 "ubyte" | "uint8" | "u8" => TypeCode::UInt8,
57 "ushort" | "uint16" | "u16" => TypeCode::UInt16,
58 "uint" | "uint32" | "u32" => TypeCode::UInt32,
59 "ulong" | "uint64" | "u64" => TypeCode::UInt64,
60 "float" | "float32" | "f32" => TypeCode::Float32,
61 "double" | "float64" | "f64" => TypeCode::Float64,
62 _ => return None,
63 })
64}
65
66fn parse_field_type(s: &str) -> PyResult<FieldType> {
69 let trimmed = s.trim();
70 if let Some(base) = trimmed.strip_suffix("[]") {
72 let base = base.trim();
73 if base == "string" || base == "str" {
74 return Ok(FieldType::StringArray);
75 }
76 if let Some(tc) = parse_type_code(base) {
77 return Ok(FieldType::ScalarArray(tc));
78 }
79 return Err(pyo3::exceptions::PyValueError::new_err(format!(
80 "unknown array element type: {base:?}"
81 )));
82 }
83 if trimmed == "string" || trimmed == "str" {
84 return Ok(FieldType::String);
85 }
86 if trimmed == "any" || trimmed == "variant" {
87 return Ok(FieldType::Variant);
88 }
89 if let Some(tc) = parse_type_code(trimmed) {
90 return Ok(FieldType::Scalar(tc));
91 }
92 Err(pyo3::exceptions::PyValueError::new_err(format!(
93 "unknown field type: {trimmed:?}"
94 )))
95}
96
97fn dict_to_structure_desc(
99 struct_id: Option<String>,
100 fields: &Bound<'_, PyDict>,
101) -> PyResult<StructureDesc> {
102 let mut desc_fields = Vec::with_capacity(fields.len());
103 for (key, val) in fields.iter() {
104 let name: String = key.extract()?;
105 let type_str: String = val.extract()?;
106 let field_type = parse_field_type(&type_str)?;
107 desc_fields.push(FieldDesc { name, field_type });
108 }
109 Ok(StructureDesc {
110 struct_id,
111 fields: desc_fields,
112 })
113}
114
115#[pyclass(name = "PvInfo")]
124#[derive(Clone)]
125pub struct PyPvInfo {
126 pub inner: PvInfo,
127}
128
129#[pymethods]
130impl PyPvInfo {
131 #[new]
136 #[pyo3(signature = (struct_id, fields, writable=false))]
137 fn new(
138 struct_id: String,
139 fields: &Bound<'_, PyDict>,
140 writable: bool,
141 ) -> PyResult<Self> {
142 let desc = dict_to_structure_desc(Some(struct_id), fields)?;
143 Ok(Self {
144 inner: PvInfo {
145 descriptor: desc,
146 writable,
147 },
148 })
149 }
150
151 #[staticmethod]
153 #[pyo3(signature = (type_str, writable=false))]
154 fn nt_scalar(type_str: &str, writable: bool) -> PyResult<Self> {
155 let field_type = parse_field_type(type_str)?;
156 Ok(Self {
157 inner: PvInfo {
158 descriptor: StructureDesc {
159 struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
160 fields: vec![FieldDesc {
161 name: "value".to_string(),
162 field_type,
163 }],
164 },
165 writable,
166 },
167 })
168 }
169
170 #[staticmethod]
173 #[pyo3(signature = (element_type, writable=false))]
174 fn nt_scalar_array(element_type: &str, writable: bool) -> PyResult<Self> {
175 let array_spec = format!("{element_type}[]");
176 let field_type = parse_field_type(&array_spec)?;
177 Ok(Self {
178 inner: PvInfo {
179 descriptor: StructureDesc {
180 struct_id: Some("epics:nt/NTScalarArray:1.0".to_string()),
181 fields: vec![FieldDesc {
182 name: "value".to_string(),
183 field_type,
184 }],
185 },
186 writable,
187 },
188 })
189 }
190
191 #[getter]
192 fn writable(&self) -> bool {
193 self.inner.writable
194 }
195
196 #[getter]
197 fn struct_id(&self) -> Option<String> {
198 self.inner.descriptor.struct_id.clone()
199 }
200
201 fn __repr__(&self) -> String {
202 format!(
203 "PvInfo(struct_id={:?}, fields={}, writable={})",
204 self.inner.descriptor.struct_id,
205 self.inner.descriptor.fields.len(),
206 self.inner.writable
207 )
208 }
209}
210
211fn py_to_pv_info(obj: &Bound<'_, PyAny>) -> PyResult<PvInfo> {
213 if let Ok(info) = obj.downcast::<PyPvInfo>() {
214 return Ok(info.borrow().inner.clone());
215 }
216 if let Ok(dict) = obj.downcast::<PyDict>() {
217 let struct_id: Option<String> = match dict.get_item("struct_id")? {
218 Some(v) if !v.is_none() => Some(v.extract()?),
219 _ => None,
220 };
221 let writable: bool = match dict.get_item("writable")? {
222 Some(v) if !v.is_none() => v.extract()?,
223 _ => false,
224 };
225 let fields_obj = dict
226 .get_item("fields")?
227 .ok_or_else(|| {
228 pyo3::exceptions::PyValueError::new_err("PvInfo dict missing 'fields'")
229 })?;
230 let fields = fields_obj.downcast::<PyDict>().map_err(|_| {
231 pyo3::exceptions::PyTypeError::new_err("PvInfo 'fields' must be a dict")
232 })?;
233 let desc = dict_to_structure_desc(struct_id, fields)?;
234 return Ok(PvInfo {
235 descriptor: desc,
236 writable,
237 });
238 }
239 Err(pyo3::exceptions::PyTypeError::new_err(
240 "expected PvInfo instance or dict with 'struct_id'/'fields'/'writable'",
241 ))
242}
243
244#[pyclass(name = "Notifier")]
252#[derive(Clone)]
253pub struct PyNotifier {
254 registry: Arc<MonitorRegistry>,
255}
256
257impl PyNotifier {
258 pub fn new(registry: Arc<MonitorRegistry>) -> Self {
259 Self { registry }
260 }
261}
262
263#[pymethods]
264impl PyNotifier {
265 fn notify(&self, py: Python<'_>, pv_name: String, nt: &Bound<'_, PyAny>) -> PyResult<()> {
270 let payload = py_to_nt_payload(nt)?;
271 let registry = self.registry.clone();
272 py.allow_threads(|| {
273 if let Ok(handle) = tokio::runtime::Handle::try_current() {
276 handle.spawn(async move {
277 registry.notify_monitors(&pv_name, &payload).await;
278 });
279 } else {
280 RUNTIME.block_on(async move {
281 registry.notify_monitors(&pv_name, &payload).await;
282 });
283 }
284 });
285 Ok(())
286 }
287
288 fn __repr__(&self) -> String {
289 "Notifier(<MonitorRegistry>)".to_string()
290 }
291}
292
293pub struct PySourceAdapter {
296 obj: Arc<PyObject>,
297}
298
299impl PySourceAdapter {
300 pub fn new(obj: PyObject) -> Self {
301 Self { obj: Arc::new(obj) }
302 }
303
304 pub fn invoke_on_start(&self, notifier: PyNotifier) {
306 let obj = self.obj.clone();
307 Python::with_gil(|py| {
308 let b = obj.bind(py);
309 if let Ok(method) = b.getattr("on_start") {
310 let py_notifier = match notifier.into_pyobject(py) {
311 Ok(n) => n,
312 Err(e) => {
313 tracing::error!("Notifier.into_pyobject: {e}");
314 return;
315 }
316 };
317 if let Err(e) = method.call1((py_notifier,)) {
318 tracing::error!("source.on_start error: {e}");
319 }
320 }
321 });
322 }
323}
324
325fn asyncio_loop(py: Python<'_>) -> PyResult<PyObject> {
334 static LOOP: std::sync::OnceLock<PyObject> = std::sync::OnceLock::new();
335 if let Some(l) = LOOP.get() {
336 return Ok(l.clone_ref(py));
337 }
338 let asyncio = py.import("asyncio")?;
339 let loop_obj: PyObject = asyncio.getattr("new_event_loop")?.call0()?.unbind();
340 let loop_for_thread = loop_obj.clone_ref(py);
341 std::thread::Builder::new()
342 .name("spvirit-asyncio".into())
343 .spawn(move || {
344 Python::with_gil(|py| {
345 let l = loop_for_thread.bind(py);
346 if let Err(e) = l.call_method0("run_forever") {
348 tracing::error!("asyncio loop exited: {}", e);
349 }
350 });
351 })
352 .expect("spawn asyncio thread");
353 let out = loop_obj.clone_ref(py);
358 let _ = LOOP.set(loop_obj);
359 Ok(out)
360}
361
362async fn call_py_await(
365 obj: Arc<PyObject>,
366 method: &'static str,
367 build_args: impl for<'py> FnOnce(Python<'py>) -> PyResult<Bound<'py, PyTuple>> + Send,
368) -> PyResult<PyObject> {
369 enum Outcome {
373 Value(PyObject),
374 Future(PyObject),
375 }
376 let outcome: Outcome = Python::with_gil(|py| -> PyResult<Outcome> {
377 let args = build_args(py)?;
378 let ret = obj.call_method1(py, method, args)?;
379 let bound = ret.bind(py);
380 let is_awaitable = bound.hasattr("__await__").unwrap_or(false);
381 if !is_awaitable {
382 return Ok(Outcome::Value(ret));
383 }
384 let loop_obj = asyncio_loop(py)?;
385 let asyncio = py.import("asyncio")?;
386 let fut = asyncio
387 .getattr("run_coroutine_threadsafe")?
388 .call1((bound, loop_obj.bind(py)))?;
389 Ok(Outcome::Future(fut.unbind()))
390 })?;
391 match outcome {
392 Outcome::Value(v) => Ok(v),
393 Outcome::Future(fut) => {
394 Python::with_gil(|py| -> PyResult<PyObject> {
398 Ok(fut.call_method0(py, "result")?)
399 })
400 }
401 }
402}
403
404fn log_err(method: &str, e: impl std::fmt::Display) {
405 tracing::error!("PySource.{}: {}", method, e);
406}
407
408impl Source for PySourceAdapter {
409 fn claim<'a>(
410 &'a self,
411 name: &str,
412 ) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + 'a>> {
413 let obj = self.obj.clone();
414 let name = name.to_string();
415 Box::pin(async move {
416 let ret = match call_py_await(obj, "claim", move |py| {
417 PyTuple::new(py, &[name.into_pyobject(py)?.into_any()])
418 })
419 .await
420 {
421 Ok(r) => r,
422 Err(e) => {
423 log_err("claim", e);
424 return None;
425 }
426 };
427 Python::with_gil(|py| {
428 let b = ret.bind(py);
429 if b.is_none() {
430 return None;
431 }
432 match py_to_pv_info(b) {
433 Ok(info) => Some(info),
434 Err(e) => {
435 log_err("claim", e);
436 None
437 }
438 }
439 })
440 })
441 }
442
443 fn get<'a>(
444 &'a self,
445 name: &str,
446 ) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + 'a>> {
447 let obj = self.obj.clone();
448 let name = name.to_string();
449 Box::pin(async move {
450 let ret = match call_py_await(obj, "get", move |py| {
451 PyTuple::new(py, &[name.into_pyobject(py)?.into_any()])
452 })
453 .await
454 {
455 Ok(r) => r,
456 Err(e) => {
457 log_err("get", e);
458 return None;
459 }
460 };
461 Python::with_gil(|py| {
462 let b = ret.bind(py);
463 if b.is_none() {
464 return None;
465 }
466 match py_to_nt_payload(b) {
467 Ok(p) => Some(p),
468 Err(e) => {
469 log_err("get", e);
470 None
471 }
472 }
473 })
474 })
475 }
476
477 fn put<'a>(
478 &'a self,
479 name: &str,
480 value: &DecodedValue,
481 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + 'a>> {
482 let obj = self.obj.clone();
483 let name = name.to_string();
484 let value = value.clone();
485 Box::pin(async move {
486 let name_for_call = name.clone();
488 let ret = call_py_await(obj, "put", move |py| {
489 let v = decoded_to_py(py, &value);
490 PyTuple::new(
491 py,
492 &[
493 name_for_call.into_pyobject(py)?.into_any(),
494 v.into_bound(py),
495 ],
496 )
497 })
498 .await
499 .map_err(|e| format!("{e}"))?;
500
501 Python::with_gil(|py| -> Result<Vec<(String, NtPayload)>, String> {
507 let b = ret.bind(py);
508 if b.is_none() {
509 return Ok(Vec::new());
510 }
511 if let Ok(p) = py_to_nt_payload(b) {
513 return Ok(vec![(name.clone(), p)]);
514 }
515 if let Ok(d) = b.downcast::<PyDict>() {
517 let mut out = Vec::with_capacity(d.len());
518 for (k, v) in d.iter() {
519 let key: String = k.extract().map_err(|e| format!("put dict key: {e}"))?;
520 let payload = py_to_nt_payload(&v).map_err(|e| {
521 format!("put dict value for '{key}': {e}")
522 })?;
523 out.push((key, payload));
524 }
525 return Ok(out);
526 }
527 if let Ok(list) = b.downcast::<PyList>() {
529 let mut out = Vec::with_capacity(list.len());
530 for item in list.iter() {
531 let t = item
532 .downcast::<PyTuple>()
533 .map_err(|_| "put list item must be (name, payload)".to_string())?;
534 if t.len() != 2 {
535 return Err("put list tuple must have 2 elements".to_string());
536 }
537 let key: String =
538 t.get_item(0).and_then(|x| x.extract()).map_err(|e| format!("{e}"))?;
539 let payload = t
540 .get_item(1)
541 .map_err(|e| format!("{e}"))
542 .and_then(|x| py_to_nt_payload(&x).map_err(|e| format!("{e}")))?;
543 out.push((key, payload));
544 }
545 return Ok(out);
546 }
547 Err(format!(
548 "put() must return None, NtPayload, dict, or list of tuples; got {}",
549 b.get_type().name().map(|n| n.to_string()).unwrap_or_default()
550 ))
551 })
552 })
553 }
554
555 fn subscribe<'a>(
556 &'a self,
557 _name: &str,
558 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + 'a>> {
559 Box::pin(async { None })
562 }
563
564 fn rpc<'a>(
565 &'a self,
566 name: &str,
567 args: &DecodedValue,
568 ) -> Pin<Box<dyn Future<Output = Result<NtPayload, String>> + Send + 'a>> {
569 let obj = self.obj.clone();
570 let name = name.to_string();
571 let args = args.clone();
572 Box::pin(async move {
573 let has_rpc = Python::with_gil(|py| {
575 obj.bind(py).hasattr("rpc").unwrap_or(false)
576 });
577 if !has_rpc {
578 return Err("RPC not supported".to_string());
579 }
580 let ret = call_py_await(obj, "rpc", move |py| {
581 let args_py = decoded_to_py(py, &args);
582 PyTuple::new(
583 py,
584 &[
585 name.into_pyobject(py)?.into_any(),
586 args_py.into_bound(py),
587 ],
588 )
589 })
590 .await
591 .map_err(|e| format!("{e}"))?;
592 Python::with_gil(|py| -> Result<NtPayload, String> {
593 py_to_nt_payload(ret.bind(py)).map_err(|e| format!("{e}"))
594 })
595 })
596 }
597
598 fn names<'a>(
599 &'a self,
600 ) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + 'a>> {
601 let obj = self.obj.clone();
602 Box::pin(async move {
603 let has = Python::with_gil(|py| obj.bind(py).hasattr("names").unwrap_or(false));
605 if !has {
606 return Vec::new();
607 }
608 let ret = match call_py_await(obj, "names", |py| Ok(PyTuple::empty(py))).await {
609 Ok(r) => r,
610 Err(e) => {
611 log_err("names", e);
612 return Vec::new();
613 }
614 };
615 Python::with_gil(|py| {
616 let b = ret.bind(py);
617 if b.is_none() {
618 return Vec::new();
619 }
620 match b.extract::<Vec<String>>() {
621 Ok(v) => v,
622 Err(e) => {
623 log_err("names", e);
624 Vec::new()
625 }
626 }
627 })
628 })
629 }
630}
631
632pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
635 m.add_class::<PyPvInfo>()?;
636 m.add_class::<PyNotifier>()?;
637 Ok(())
638}
639
640#[allow(dead_code)]
642pub(crate) fn _ensure_used(py: Python<'_>, p: NtPayload) -> PyObject {
643 nt_payload_to_py(py, p)
644}