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]
186 fn writable(&self) -> bool {
187 self.inner.writable
188 }
189
190 #[getter]
191 fn struct_id(&self) -> Option<String> {
192 self.inner.descriptor.struct_id.clone()
193 }
194
195 fn __repr__(&self) -> String {
196 format!(
197 "PvInfo(struct_id={:?}, fields={}, writable={})",
198 self.inner.descriptor.struct_id,
199 self.inner.descriptor.fields.len(),
200 self.inner.writable
201 )
202 }
203}
204
205fn py_to_pv_info(obj: &Bound<'_, PyAny>) -> PyResult<PvInfo> {
207 if let Ok(info) = obj.downcast::<PyPvInfo>() {
208 return Ok(info.borrow().inner.clone());
209 }
210 if let Ok(dict) = obj.downcast::<PyDict>() {
211 let struct_id: Option<String> = match dict.get_item("struct_id")? {
212 Some(v) if !v.is_none() => Some(v.extract()?),
213 _ => None,
214 };
215 let writable: bool = match dict.get_item("writable")? {
216 Some(v) if !v.is_none() => v.extract()?,
217 _ => false,
218 };
219 let fields_obj = dict.get_item("fields")?.ok_or_else(|| {
220 pyo3::exceptions::PyValueError::new_err("PvInfo dict missing 'fields'")
221 })?;
222 let fields = fields_obj.downcast::<PyDict>().map_err(|_| {
223 pyo3::exceptions::PyTypeError::new_err("PvInfo 'fields' must be a dict")
224 })?;
225 let desc = dict_to_structure_desc(struct_id, fields)?;
226 return Ok(PvInfo {
227 descriptor: desc,
228 writable,
229 });
230 }
231 Err(pyo3::exceptions::PyTypeError::new_err(
232 "expected PvInfo instance or dict with 'struct_id'/'fields'/'writable'",
233 ))
234}
235
236#[pyclass(name = "Notifier")]
244#[derive(Clone)]
245pub struct PyNotifier {
246 registry: Arc<MonitorRegistry>,
247}
248
249impl PyNotifier {
250 pub fn new(registry: Arc<MonitorRegistry>) -> Self {
251 Self { registry }
252 }
253}
254
255#[pymethods]
256impl PyNotifier {
257 fn notify(&self, py: Python<'_>, pv_name: String, nt: &Bound<'_, PyAny>) -> PyResult<()> {
262 let payload = py_to_nt_payload(nt)?;
263 let registry = self.registry.clone();
264 py.allow_threads(|| {
265 if let Ok(handle) = tokio::runtime::Handle::try_current() {
268 handle.spawn(async move {
269 registry.notify_monitors(&pv_name, &payload).await;
270 });
271 } else {
272 RUNTIME.block_on(async move {
273 registry.notify_monitors(&pv_name, &payload).await;
274 });
275 }
276 });
277 Ok(())
278 }
279
280 fn __repr__(&self) -> String {
281 "Notifier(<MonitorRegistry>)".to_string()
282 }
283}
284
285pub struct PySourceAdapter {
288 obj: Arc<PyObject>,
289}
290
291impl PySourceAdapter {
292 pub fn new(obj: PyObject) -> Self {
293 Self { obj: Arc::new(obj) }
294 }
295
296 pub fn invoke_on_start(&self, notifier: PyNotifier) {
298 let obj = self.obj.clone();
299 Python::with_gil(|py| {
300 let b = obj.bind(py);
301 if let Ok(method) = b.getattr("on_start") {
302 let py_notifier = match notifier.into_pyobject(py) {
303 Ok(n) => n,
304 Err(e) => {
305 tracing::error!("Notifier.into_pyobject: {e}");
306 return;
307 }
308 };
309 if let Err(e) = method.call1((py_notifier,)) {
310 tracing::error!("source.on_start error: {e}");
311 }
312 }
313 });
314 }
315}
316
317fn asyncio_loop(py: Python<'_>) -> PyResult<PyObject> {
326 static LOOP: std::sync::OnceLock<PyObject> = std::sync::OnceLock::new();
327 if let Some(l) = LOOP.get() {
328 return Ok(l.clone_ref(py));
329 }
330 let asyncio = py.import("asyncio")?;
331 let loop_obj: PyObject = asyncio.getattr("new_event_loop")?.call0()?.unbind();
332 let loop_for_thread = loop_obj.clone_ref(py);
333 std::thread::Builder::new()
334 .name("spvirit-asyncio".into())
335 .spawn(move || {
336 Python::with_gil(|py| {
337 let l = loop_for_thread.bind(py);
338 if let Err(e) = l.call_method0("run_forever") {
340 tracing::error!("asyncio loop exited: {}", e);
341 }
342 });
343 })
344 .expect("spawn asyncio thread");
345 let out = loop_obj.clone_ref(py);
350 let _ = LOOP.set(loop_obj);
351 Ok(out)
352}
353
354async fn call_py_await(
357 obj: Arc<PyObject>,
358 method: &'static str,
359 build_args: impl for<'py> FnOnce(Python<'py>) -> PyResult<Bound<'py, PyTuple>> + Send,
360) -> PyResult<PyObject> {
361 enum Outcome {
365 Value(PyObject),
366 Future(PyObject),
367 }
368 let outcome: Outcome = Python::with_gil(|py| -> PyResult<Outcome> {
369 let args = build_args(py)?;
370 let ret = obj.call_method1(py, method, args)?;
371 let bound = ret.bind(py);
372 let is_awaitable = bound.hasattr("__await__").unwrap_or(false);
373 if !is_awaitable {
374 return Ok(Outcome::Value(ret));
375 }
376 let loop_obj = asyncio_loop(py)?;
377 let asyncio = py.import("asyncio")?;
378 let fut = asyncio
379 .getattr("run_coroutine_threadsafe")?
380 .call1((bound, loop_obj.bind(py)))?;
381 Ok(Outcome::Future(fut.unbind()))
382 })?;
383 match outcome {
384 Outcome::Value(v) => Ok(v),
385 Outcome::Future(fut) => {
386 Python::with_gil(|py| -> PyResult<PyObject> { Ok(fut.call_method0(py, "result")?) })
390 }
391 }
392}
393
394fn log_err(method: &str, e: impl std::fmt::Display) {
395 tracing::error!("PySource.{}: {}", method, e);
396}
397
398impl Source for PySourceAdapter {
399 fn claim<'a>(
400 &'a self,
401 name: &str,
402 ) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + 'a>> {
403 let obj = self.obj.clone();
404 let name = name.to_string();
405 Box::pin(async move {
406 let ret = match call_py_await(obj, "claim", move |py| {
407 PyTuple::new(py, &[name.into_pyobject(py)?.into_any()])
408 })
409 .await
410 {
411 Ok(r) => r,
412 Err(e) => {
413 log_err("claim", e);
414 return None;
415 }
416 };
417 Python::with_gil(|py| {
418 let b = ret.bind(py);
419 if b.is_none() {
420 return None;
421 }
422 match py_to_pv_info(b) {
423 Ok(info) => Some(info),
424 Err(e) => {
425 log_err("claim", e);
426 None
427 }
428 }
429 })
430 })
431 }
432
433 fn get<'a>(
434 &'a self,
435 name: &str,
436 ) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + 'a>> {
437 let obj = self.obj.clone();
438 let name = name.to_string();
439 Box::pin(async move {
440 let ret = match call_py_await(obj, "get", move |py| {
441 PyTuple::new(py, &[name.into_pyobject(py)?.into_any()])
442 })
443 .await
444 {
445 Ok(r) => r,
446 Err(e) => {
447 log_err("get", e);
448 return None;
449 }
450 };
451 Python::with_gil(|py| {
452 let b = ret.bind(py);
453 if b.is_none() {
454 return None;
455 }
456 match py_to_nt_payload(b) {
457 Ok(p) => Some(p),
458 Err(e) => {
459 log_err("get", e);
460 None
461 }
462 }
463 })
464 })
465 }
466
467 fn put<'a>(
468 &'a self,
469 name: &str,
470 value: &DecodedValue,
471 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + 'a>> {
472 let obj = self.obj.clone();
473 let name = name.to_string();
474 let value = value.clone();
475 Box::pin(async move {
476 let name_for_call = name.clone();
478 let ret = call_py_await(obj, "put", move |py| {
479 let v = decoded_to_py(py, &value);
480 PyTuple::new(
481 py,
482 &[
483 name_for_call.into_pyobject(py)?.into_any(),
484 v.into_bound(py),
485 ],
486 )
487 })
488 .await
489 .map_err(|e| format!("{e}"))?;
490
491 Python::with_gil(|py| -> Result<Vec<(String, NtPayload)>, String> {
497 let b = ret.bind(py);
498 if b.is_none() {
499 return Ok(Vec::new());
500 }
501 if let Ok(p) = py_to_nt_payload(b) {
503 return Ok(vec![(name.clone(), p)]);
504 }
505 if let Ok(d) = b.downcast::<PyDict>() {
507 let mut out = Vec::with_capacity(d.len());
508 for (k, v) in d.iter() {
509 let key: String = k.extract().map_err(|e| format!("put dict key: {e}"))?;
510 let payload = py_to_nt_payload(&v)
511 .map_err(|e| format!("put dict value for '{key}': {e}"))?;
512 out.push((key, payload));
513 }
514 return Ok(out);
515 }
516 if let Ok(list) = b.downcast::<PyList>() {
518 let mut out = Vec::with_capacity(list.len());
519 for item in list.iter() {
520 let t = item
521 .downcast::<PyTuple>()
522 .map_err(|_| "put list item must be (name, payload)".to_string())?;
523 if t.len() != 2 {
524 return Err("put list tuple must have 2 elements".to_string());
525 }
526 let key: String = t
527 .get_item(0)
528 .and_then(|x| x.extract())
529 .map_err(|e| format!("{e}"))?;
530 let payload = t
531 .get_item(1)
532 .map_err(|e| format!("{e}"))
533 .and_then(|x| py_to_nt_payload(&x).map_err(|e| format!("{e}")))?;
534 out.push((key, payload));
535 }
536 return Ok(out);
537 }
538 Err(format!(
539 "put() must return None, NtPayload, dict, or list of tuples; got {}",
540 b.get_type()
541 .name()
542 .map(|n| n.to_string())
543 .unwrap_or_default()
544 ))
545 })
546 })
547 }
548
549 fn subscribe<'a>(
550 &'a self,
551 _name: &str,
552 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + 'a>> {
553 Box::pin(async { None })
556 }
557
558 fn rpc<'a>(
559 &'a self,
560 name: &str,
561 args: &DecodedValue,
562 ) -> Pin<Box<dyn Future<Output = Result<NtPayload, String>> + Send + 'a>> {
563 let obj = self.obj.clone();
564 let name = name.to_string();
565 let args = args.clone();
566 Box::pin(async move {
567 let has_rpc = Python::with_gil(|py| obj.bind(py).hasattr("rpc").unwrap_or(false));
569 if !has_rpc {
570 return Err("RPC not supported".to_string());
571 }
572 let ret = call_py_await(obj, "rpc", move |py| {
573 let args_py = decoded_to_py(py, &args);
574 PyTuple::new(
575 py,
576 &[name.into_pyobject(py)?.into_any(), args_py.into_bound(py)],
577 )
578 })
579 .await
580 .map_err(|e| format!("{e}"))?;
581 Python::with_gil(|py| -> Result<NtPayload, String> {
582 py_to_nt_payload(ret.bind(py)).map_err(|e| format!("{e}"))
583 })
584 })
585 }
586
587 fn names<'a>(&'a self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + 'a>> {
588 let obj = self.obj.clone();
589 Box::pin(async move {
590 let has = Python::with_gil(|py| obj.bind(py).hasattr("names").unwrap_or(false));
592 if !has {
593 return Vec::new();
594 }
595 let ret = match call_py_await(obj, "names", |py| Ok(PyTuple::empty(py))).await {
596 Ok(r) => r,
597 Err(e) => {
598 log_err("names", e);
599 return Vec::new();
600 }
601 };
602 Python::with_gil(|py| {
603 let b = ret.bind(py);
604 if b.is_none() {
605 return Vec::new();
606 }
607 match b.extract::<Vec<String>>() {
608 Ok(v) => v,
609 Err(e) => {
610 log_err("names", e);
611 Vec::new()
612 }
613 }
614 })
615 })
616 }
617}
618
619pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
622 m.add_class::<PyPvInfo>()?;
623 m.add_class::<PyNotifier>()?;
624 Ok(())
625}
626
627#[allow(dead_code)]
629pub(crate) fn _ensure_used(py: Python<'_>, p: NtPayload) -> PyObject {
630 nt_payload_to_py(py, p)
631}