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