1use std::net::SocketAddr;
9use std::sync::Arc;
10use std::time::Duration;
11
12use pyo3::prelude::*;
13use tokio::io::AsyncWriteExt;
14use tokio::sync::Mutex;
15
16use spvirit_client::client::{
17 ChannelConn, encode_get_request, encode_monitor_request, encode_put_request, ensure_status_ok,
18 establish_channel,
19};
20use spvirit_client::pva_client::decode_init_introspection;
21use spvirit_client::transport::{read_packet, read_until};
22use spvirit_client::types::{PvGetError, PvGetOptions};
23use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
24use spvirit_codec::spvd_decode::PvdDecoder;
25use spvirit_codec::spvd_encode::encode_pv_request;
26use spvirit_codec::spvirit_encode::encode_control_message;
27
28use crate::codec::PyStructureDesc;
29use crate::convert::{decoded_to_py, py_to_json};
30use crate::errors::to_py_err;
31use crate::runtime::{block_on_py, future_into_py};
32
33const PVA_VERSION: u8 = 2;
34const QOS_INIT: u8 = 0x08;
35
36fn build_pv_request(fields: &[String], is_be: bool) -> Vec<u8> {
40 if fields.is_empty() {
41 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
42 } else {
43 let refs: Vec<&str> = fields.iter().map(String::as_str).collect();
44 encode_pv_request(&refs, is_be)
45 }
46}
47
48fn normalize_fields(py: Python<'_>, fields: Option<PyObject>) -> PyResult<Vec<String>> {
54 let Some(obj) = fields else {
55 return Ok(Vec::new());
56 };
57 let bound = obj.bind(py);
58 if let Ok(s) = bound.extract::<String>() {
59 return Ok(vec![s]);
60 }
61 bound.extract::<Vec<String>>()
62}
63
64struct ChannelState {
67 conn: Option<ChannelConn>,
68 pv_name: String,
69 timeout: Duration,
70 next_ioid: u32,
71}
72
73impl ChannelState {
74 fn alloc_ioid(&mut self) -> u32 {
75 let v = self.next_ioid;
76 self.next_ioid = self.next_ioid.wrapping_add(1).max(1);
77 v
78 }
79
80 fn conn_mut(&mut self) -> Result<&mut ChannelConn, PvGetError> {
81 self.conn
82 .as_mut()
83 .ok_or_else(|| PvGetError::Protocol("channel is closed".to_string()))
84 }
85}
86
87#[pyclass(name = "Channel", module = "spvirit.lowlevel")]
88pub struct PyChannel {
89 state: Arc<Mutex<ChannelState>>,
90}
91
92fn parse_addr(addr: &str) -> PyResult<SocketAddr> {
93 addr.parse()
94 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("invalid address: {e}")))
95}
96
97async fn do_connect(
98 pv_name: String,
99 server_addr: SocketAddr,
100 timeout: Duration,
101) -> Result<ChannelState, PvGetError> {
102 let mut opts = PvGetOptions::new(pv_name.clone());
103 opts.timeout = timeout;
104 opts.server_addr = Some(server_addr);
105 let conn = establish_channel(server_addr, &opts).await?;
106 Ok(ChannelState {
107 conn: Some(conn),
108 pv_name,
109 timeout,
110 next_ioid: 1,
111 })
112}
113
114async fn run_get(
115 state: Arc<Mutex<ChannelState>>,
116 fields: Vec<String>,
117) -> Result<
118 (
119 String,
120 spvirit_codec::spvd_decode::DecodedValue,
121 Vec<u8>,
122 Vec<u8>,
123 ),
124 PvGetError,
125> {
126 let mut guard = state.lock().await;
127 let timeout = guard.timeout;
128 let ioid = guard.alloc_ioid();
129 let pv_name = guard.pv_name.clone();
130 let conn = guard.conn_mut()?;
131 let is_be = conn.is_be;
132 let version = conn.version;
133 let sid = conn.sid;
134
135 let pv_request = if fields.is_empty() {
136 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
137 } else {
138 build_pv_request(&fields, is_be)
139 };
140
141 let get_init = encode_get_request(sid, ioid, QOS_INIT, &pv_request, version, is_be);
142 conn.stream.write_all(&get_init).await?;
143
144 let init_resp = read_until(&mut conn.stream, timeout, |cmd| {
145 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
146 })
147 .await?;
148 let desc = decode_init_introspection(&init_resp, "GET")?;
149
150 let get_data = encode_get_request(sid, ioid, 0x00, &[], version, is_be);
151 conn.stream.write_all(&get_data).await?;
152
153 let data_resp = read_until(&mut conn.stream, timeout, |cmd| {
154 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && op.ioid == ioid && op.subcmd == 0x00)
155 })
156 .await?;
157 let mut pkt = PvaPacket::new(&data_resp);
158 let cmd = pkt
159 .decode_payload()
160 .ok_or_else(|| PvGetError::Protocol("get data decode failed".to_string()))?;
161 match cmd {
162 PvaPacketCommand::Op(mut op) => {
163 op.decode_with_field_desc(&desc, is_be);
164 let value = op
165 .decoded_value
166 .ok_or_else(|| PvGetError::Decode("no decoded value".to_string()))?;
167 Ok((pv_name, value, data_resp, op.body))
168 }
169 _ => Err(PvGetError::Protocol(
170 "unexpected get data response".to_string(),
171 )),
172 }
173}
174
175async fn run_put(
176 state: Arc<Mutex<ChannelState>>,
177 json_val: serde_json::Value,
178 fields: Vec<String>,
179) -> Result<(), PvGetError> {
180 let mut guard = state.lock().await;
181 let timeout = guard.timeout;
182 let ioid = guard.alloc_ioid();
183 let conn = guard.conn_mut()?;
184 let is_be = conn.is_be;
185 let sid = conn.sid;
186
187 let pv_request = build_pv_request(&fields, is_be);
188 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
189 conn.stream.write_all(&init).await?;
190
191 let init_resp = read_until(&mut conn.stream, timeout, |cmd| {
192 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
193 })
194 .await?;
195 let desc = decode_init_introspection(&init_resp, "PUT")?;
196
197 let payload = spvirit_client::put_encode::encode_put_payload(&desc, &json_val, is_be)
198 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
199 let req = encode_put_request(sid, ioid, 0x00, &payload, PVA_VERSION, is_be);
200 conn.stream.write_all(&req).await?;
201
202 let resp = read_until(&mut conn.stream, timeout, |cmd| {
203 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.ioid == ioid && op.subcmd == 0x00)
204 })
205 .await?;
206 ensure_status_ok(&resp, is_be, "PUT")?;
207 Ok(())
208}
209
210async fn run_introspect(
211 state: Arc<Mutex<ChannelState>>,
212) -> Result<spvirit_codec::spvd_decode::StructureDesc, PvGetError> {
213 let mut guard = state.lock().await;
215 let timeout = guard.timeout;
216 let ioid = guard.alloc_ioid();
217 let conn = guard.conn_mut()?;
218 let is_be = conn.is_be;
219 let version = conn.version;
220 let sid = conn.sid;
221
222 let pv_request = vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00];
223 let init = encode_get_request(sid, ioid, QOS_INIT, &pv_request, version, is_be);
224 conn.stream.write_all(&init).await?;
225 let init_resp = read_until(&mut conn.stream, timeout, |cmd| {
226 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
227 })
228 .await?;
229 decode_init_introspection(&init_resp, "GET")
230}
231
232async fn run_monitor(
236 state: Arc<Mutex<ChannelState>>,
237 callback: PyObject,
238 fields: Vec<String>,
239) -> Result<(), PvGetError> {
240 let mut guard = state.lock().await;
241 let timeout = guard.timeout;
242 let ioid = guard.alloc_ioid();
243 let conn = guard.conn_mut()?;
244 let is_be = conn.is_be;
245 let sid = conn.sid;
246
247 let decoder = PvdDecoder::new(is_be);
248 let pv_request = build_pv_request(&fields, is_be);
249 let init = encode_monitor_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
250 conn.stream.write_all(&init).await?;
251
252 let init_resp = read_until(&mut conn.stream, timeout, |cmd| {
253 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 13 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
254 })
255 .await?;
256 let field_desc = decode_init_introspection(&init_resp, "MONITOR")?;
257
258 let start = encode_monitor_request(sid, ioid, 0x44, &[], PVA_VERSION, is_be);
259 conn.stream.write_all(&start).await?;
260
261 let mut echo_interval = tokio::time::interval(Duration::from_secs(10));
262 echo_interval.tick().await; let mut echo_token: u32 = 1;
264
265 loop {
266 tokio::select! {
267 _ = echo_interval.tick() => {
268 let msg = encode_control_message(false, is_be, PVA_VERSION, 3, echo_token);
269 echo_token = echo_token.wrapping_add(1);
270 let _ = conn.stream.write_all(&msg).await;
271 }
272 res = read_packet(&mut conn.stream, timeout) => {
273 let bytes = match res {
274 Ok(b) => b,
275 Err(PvGetError::Timeout(_)) => continue,
276 Err(e) => return Err(e),
277 };
278 let mut pkt = PvaPacket::new(&bytes);
279 if let Some(PvaPacketCommand::Op(op)) = pkt.decode_payload() {
280 if op.command == 13 && op.ioid == ioid && op.subcmd == 0x00 {
281 let payload = &bytes[8..];
282 let pos = 5;
283 if let Some((decoded, _)) =
284 decoder.decode_structure_with_bitset(&payload[pos..], &field_desc)
285 {
286 let keep_going = Python::with_gil(|py| {
287 let v = decoded_to_py(py, &decoded);
288 match callback.call1(py, (v,)) {
289 Ok(ret) => ret.extract::<bool>(py).unwrap_or(true),
290 Err(_) => false,
291 }
292 });
293 if !keep_going {
294 return Ok(());
295 }
296 }
297 }
298 }
299 }
300 }
301 }
302}
303
304#[pymethods]
305impl PyChannel {
306 #[staticmethod]
308 #[pyo3(signature = (pv_name, server_addr, timeout=5.0))]
309 fn connect(
310 py: Python<'_>,
311 pv_name: String,
312 server_addr: String,
313 timeout: f64,
314 ) -> PyResult<PyChannel> {
315 let sa = parse_addr(&server_addr)?;
316 let dur = Duration::from_secs_f64(timeout);
317 let state = block_on_py(py, do_connect(pv_name, sa, dur)).map_err(to_py_err)?;
318 Ok(PyChannel {
319 state: Arc::new(Mutex::new(state)),
320 })
321 }
322
323 #[staticmethod]
325 #[pyo3(signature = (pv_name, server_addr, timeout=5.0))]
326 fn connect_async<'py>(
327 py: Python<'py>,
328 pv_name: String,
329 server_addr: String,
330 timeout: f64,
331 ) -> PyResult<Bound<'py, PyAny>> {
332 let sa = parse_addr(&server_addr)?;
333 let dur = Duration::from_secs_f64(timeout);
334 future_into_py(py, async move {
335 let state = do_connect(pv_name, sa, dur).await.map_err(to_py_err)?;
336 Ok(PyChannel {
337 state: Arc::new(Mutex::new(state)),
338 })
339 })
340 }
341
342 #[getter]
343 fn pv_name(&self, py: Python<'_>) -> String {
344 py.allow_threads(|| {
345 let guard = self.state.blocking_lock();
346 guard.pv_name.clone()
347 })
348 }
349
350 #[getter]
351 fn is_open(&self, py: Python<'_>) -> bool {
352 py.allow_threads(|| {
353 let guard = self.state.blocking_lock();
354 guard.conn.is_some()
355 })
356 }
357
358 #[getter]
359 fn server_addr(&self, py: Python<'_>) -> Option<String> {
360 py.allow_threads(|| {
361 let guard = self.state.blocking_lock();
362 guard.conn.as_ref().map(|c| c.server_addr.to_string())
363 })
364 }
365
366 #[getter]
367 fn sid(&self, py: Python<'_>) -> Option<u32> {
368 py.allow_threads(|| {
369 let guard = self.state.blocking_lock();
370 guard.conn.as_ref().map(|c| c.sid)
371 })
372 }
373
374 #[pyo3(signature = (fields=None))]
377 fn get(
378 &self,
379 py: Python<'_>,
380 fields: Option<Vec<String>>,
381 ) -> PyResult<crate::client::PyGetResult> {
382 let state = self.state.clone();
383 let fields = fields.unwrap_or_default();
384 let (pv_name, value, raw_pva, raw_pvd) =
385 block_on_py(py, run_get(state, fields)).map_err(to_py_err)?;
386 let py_val = decoded_to_py(py, &value);
387 Ok(crate::client::PyGetResult::new(
388 pv_name, py_val, raw_pva, raw_pvd,
389 ))
390 }
391
392 #[pyo3(signature = (fields=None))]
393 fn get_async<'py>(
394 &self,
395 py: Python<'py>,
396 fields: Option<Vec<String>>,
397 ) -> PyResult<Bound<'py, PyAny>> {
398 let state = self.state.clone();
399 let fields = fields.unwrap_or_default();
400 future_into_py(py, async move {
401 let (pv_name, value, raw_pva, raw_pvd) =
402 run_get(state, fields).await.map_err(to_py_err)?;
403 Python::with_gil(|py| {
404 let py_val = decoded_to_py(py, &value);
405 Ok(Py::new(
406 py,
407 crate::client::PyGetResult::new(pv_name, py_val, raw_pva, raw_pvd),
408 )?)
409 })
410 })
411 }
412
413 #[pyo3(signature = (value, fields=None))]
419 fn put(&self, py: Python<'_>, value: PyObject, fields: Option<PyObject>) -> PyResult<()> {
420 let state = self.state.clone();
421 let json = py_to_json(value.bind(py))?;
422 let fields = normalize_fields(py, fields)?;
423 block_on_py(py, run_put(state, json, fields)).map_err(to_py_err)
424 }
425
426 #[pyo3(signature = (value, fields=None))]
427 fn put_async<'py>(
428 &self,
429 py: Python<'py>,
430 value: PyObject,
431 fields: Option<PyObject>,
432 ) -> PyResult<Bound<'py, PyAny>> {
433 let state = self.state.clone();
434 let json = py_to_json(value.bind(py))?;
435 let fields = normalize_fields(py, fields)?;
436 future_into_py(py, async move {
437 run_put(state, json, fields).await.map_err(to_py_err)?;
438 Ok(())
439 })
440 }
441
442 fn introspect(&self, py: Python<'_>) -> PyResult<PyStructureDesc> {
444 let state = self.state.clone();
445 let desc = block_on_py(py, run_introspect(state)).map_err(to_py_err)?;
446 Ok(PyStructureDesc::from_inner(desc))
447 }
448
449 fn introspect_async<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
450 let state = self.state.clone();
451 future_into_py(py, async move {
452 let desc = run_introspect(state).await.map_err(to_py_err)?;
453 Python::with_gil(|py| Py::new(py, PyStructureDesc::from_inner(desc)))
454 })
455 }
456
457 #[pyo3(signature = (callback, fields=None))]
462 fn monitor(
463 &self,
464 py: Python<'_>,
465 callback: PyObject,
466 fields: Option<Vec<String>>,
467 ) -> PyResult<()> {
468 let state = self.state.clone();
469 let fields = fields.unwrap_or_default();
470 let _ = py;
471 crate::runtime::RUNTIME
472 .block_on(run_monitor(state, callback, fields))
473 .map_err(to_py_err)
474 }
475
476 fn close(&self, py: Python<'_>) -> PyResult<()> {
479 py.allow_threads(|| {
480 let mut guard = self.state.blocking_lock();
481 guard.conn.take();
482 });
483 Ok(())
484 }
485
486 fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
487 slf
488 }
489
490 #[pyo3(signature = (_exc_type=None, _exc=None, _tb=None))]
491 fn __exit__(
492 &self,
493 py: Python<'_>,
494 _exc_type: Option<PyObject>,
495 _exc: Option<PyObject>,
496 _tb: Option<PyObject>,
497 ) -> PyResult<bool> {
498 self.close(py)?;
499 Ok(false)
500 }
501
502 fn __repr__(&self, py: Python<'_>) -> String {
503 py.allow_threads(|| {
504 let guard = self.state.blocking_lock();
505 let addr = guard
506 .conn
507 .as_ref()
508 .map(|c| c.server_addr.to_string())
509 .unwrap_or_else(|| "<closed>".to_string());
510 format!("Channel(pv_name={:?}, server={})", guard.pv_name, addr)
511 })
512 }
513
514 #[pyo3(signature = (timeout=None))]
516 fn read_packet(
517 &self,
518 py: Python<'_>,
519 timeout: Option<f64>,
520 ) -> PyResult<crate::packet::PyPacket> {
521 let state = self.state.clone();
522 let override_timeout = timeout.map(Duration::from_secs_f64);
523 let bytes = block_on_py(py, async move {
524 let mut guard = state.lock().await;
525 let t = override_timeout.unwrap_or(guard.timeout);
526 let conn = guard.conn_mut()?;
527 read_packet(&mut conn.stream, t).await
528 })
529 .map_err(to_py_err)?;
530 Ok(crate::packet::PyPacket::from_bytes(bytes))
531 }
532
533 #[pyo3(signature = (timeout=None))]
534 fn read_packet_async<'py>(
535 &self,
536 py: Python<'py>,
537 timeout: Option<f64>,
538 ) -> PyResult<Bound<'py, PyAny>> {
539 let state = self.state.clone();
540 let override_timeout = timeout.map(Duration::from_secs_f64);
541 future_into_py(py, async move {
542 let bytes = {
543 let mut guard = state.lock().await;
544 let t = override_timeout.unwrap_or(guard.timeout);
545 let conn = guard.conn_mut().map_err(to_py_err)?;
546 read_packet(&mut conn.stream, t).await.map_err(to_py_err)?
547 };
548 Python::with_gil(|py| Py::new(py, crate::packet::PyPacket::from_bytes(bytes)))
549 })
550 }
551
552 #[pyo3(signature = (predicate, timeout=None, max_frames=None))]
555 fn read_until(
556 &self,
557 py: Python<'_>,
558 predicate: PyObject,
559 timeout: Option<f64>,
560 max_frames: Option<usize>,
561 ) -> PyResult<crate::packet::PyPacket> {
562 let state = self.state.clone();
563 let override_timeout = timeout.map(Duration::from_secs_f64);
564 let max = max_frames.unwrap_or(usize::MAX);
565 let mut seen = 0usize;
566 loop {
567 if seen >= max {
568 return Err(pyo3::exceptions::PyRuntimeError::new_err(
569 "read_until: max_frames reached",
570 ));
571 }
572 seen += 1;
573 let bytes = {
574 let state = state.clone();
575 block_on_py(py, async move {
576 let mut guard = state.lock().await;
577 let t = override_timeout.unwrap_or(guard.timeout);
578 let conn = guard.conn_mut()?;
579 read_packet(&mut conn.stream, t).await
580 })
581 .map_err(to_py_err)?
582 };
583 let pkt = crate::packet::PyPacket::from_bytes(bytes);
584 let (matched, pkt_back) = Python::with_gil(|py| -> PyResult<(bool, _)> {
585 let pkt_obj = Py::new(py, pkt)?;
586 let result = predicate.call1(py, (pkt_obj.clone_ref(py),))?;
587 let matched = result.extract::<bool>(py).unwrap_or(false);
588 Ok((matched, pkt_obj))
589 })?;
590 if matched {
591 let pkt: crate::packet::PyPacket = Python::with_gil(|py| -> PyResult<_> {
593 let bound = pkt_back.bind(py);
594 let data: Vec<u8> = bound.borrow().raw().to_vec();
595 Ok(crate::packet::PyPacket::from_bytes(data))
596 })?;
597 return Ok(pkt);
598 }
599 }
600 }
601}
602
603pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
605 let py = parent.py();
606 let m = PyModule::new(py, "lowlevel")?;
607 m.add_class::<PyChannel>()?;
608 m.add_class::<crate::packet::PyPacket>()?;
609 crate::discovery::register(&m)?;
610 parent.add_submodule(&m)?;
611 py.import("sys")?
612 .getattr("modules")?
613 .set_item("spvirit.lowlevel", &m)?;
614 Ok(())
615}