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