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