1use std::net::SocketAddr;
14use std::ops::ControlFlow;
15use std::sync::atomic::{AtomicU32, Ordering};
16use std::time::Duration;
17
18use serde_json::Value;
19use tokio::io::{AsyncReadExt, AsyncWriteExt};
20use tokio::net::tcp::OwnedWriteHalf;
21use tokio::task::JoinHandle;
22use tokio::time::{Instant, interval};
23
24use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
25use spvirit_codec::spvd_decode::{DecodedValue, PvdDecoder, StructureDesc};
26use spvirit_codec::spvd_encode::encode_pv_request;
27use spvirit_codec::spvirit_encode::{
28 encode_control_message, encode_get_field_request, encode_monitor_request, encode_put_request,
29};
30
31use crate::client::{ChannelConn, ensure_status_ok, establish_channel, pvget as low_level_pvget};
32use crate::put_encode::encode_put_payload;
33use crate::search::resolve_pv_server;
34use crate::transport::{read_packet, read_until};
35use crate::types::{PvGetError, PvGetResult, PvOptions};
36
37const PVA_VERSION: u8 = 2;
39const QOS_INIT: u8 = 0x08;
41
42static NEXT_IOID: AtomicU32 = AtomicU32::new(1);
43fn alloc_ioid() -> u32 {
44 NEXT_IOID.fetch_add(1, Ordering::Relaxed)
45}
46
47fn build_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
53 if fields.is_empty() {
54 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
56 } else {
57 encode_pv_request(fields, is_be)
58 }
59}
60
61pub struct PvaClientBuilder {
72 udp_port: u16,
73 tcp_port: u16,
74 timeout: Duration,
75 no_broadcast: bool,
76 name_servers: Vec<SocketAddr>,
77 authnz_user: Option<String>,
78 authnz_host: Option<String>,
79 server_addr: Option<SocketAddr>,
80 search_addr: Option<std::net::IpAddr>,
81 bind_addr: Option<std::net::IpAddr>,
82 debug: bool,
83}
84
85impl PvaClientBuilder {
86 fn new() -> Self {
87 Self {
88 udp_port: 5076,
89 tcp_port: 5075,
90 timeout: Duration::from_secs(5),
91 no_broadcast: false,
92 name_servers: Vec::new(),
93 authnz_user: None,
94 authnz_host: None,
95 server_addr: None,
96 search_addr: None,
97 bind_addr: None,
98 debug: false,
99 }
100 }
101
102 pub fn port(mut self, port: u16) -> Self {
104 self.tcp_port = port;
105 self
106 }
107
108 pub fn udp_port(mut self, port: u16) -> Self {
110 self.udp_port = port;
111 self
112 }
113
114 pub fn timeout(mut self, timeout: Duration) -> Self {
116 self.timeout = timeout;
117 self
118 }
119
120 pub fn no_broadcast(mut self) -> Self {
122 self.no_broadcast = true;
123 self
124 }
125
126 pub fn name_server(mut self, addr: SocketAddr) -> Self {
128 self.name_servers.push(addr);
129 self
130 }
131
132 pub fn authnz_user(mut self, user: impl Into<String>) -> Self {
134 self.authnz_user = Some(user.into());
135 self
136 }
137
138 pub fn authnz_host(mut self, host: impl Into<String>) -> Self {
140 self.authnz_host = Some(host.into());
141 self
142 }
143
144 pub fn server_addr(mut self, addr: SocketAddr) -> Self {
146 self.server_addr = Some(addr);
147 self
148 }
149
150 pub fn search_addr(mut self, addr: std::net::IpAddr) -> Self {
152 self.search_addr = Some(addr);
153 self
154 }
155
156 pub fn bind_addr(mut self, addr: std::net::IpAddr) -> Self {
158 self.bind_addr = Some(addr);
159 self
160 }
161
162 pub fn debug(mut self) -> Self {
164 self.debug = true;
165 self
166 }
167
168 pub fn build(self) -> PvaClient {
170 PvaClient {
171 udp_port: self.udp_port,
172 tcp_port: self.tcp_port,
173 timeout: self.timeout,
174 no_broadcast: self.no_broadcast,
175 name_servers: self.name_servers,
176 authnz_user: self.authnz_user,
177 authnz_host: self.authnz_host,
178 server_addr: self.server_addr,
179 search_addr: self.search_addr,
180 bind_addr: self.bind_addr,
181 debug: self.debug,
182 }
183 }
184}
185
186#[derive(Clone, Debug)]
198pub struct PvaClient {
199 udp_port: u16,
200 tcp_port: u16,
201 timeout: Duration,
202 no_broadcast: bool,
203 name_servers: Vec<SocketAddr>,
204 authnz_user: Option<String>,
205 authnz_host: Option<String>,
206 server_addr: Option<SocketAddr>,
207 search_addr: Option<std::net::IpAddr>,
208 bind_addr: Option<std::net::IpAddr>,
209 debug: bool,
210}
211
212impl PvaClient {
213 pub fn builder() -> PvaClientBuilder {
215 PvaClientBuilder::new()
216 }
217
218 fn opts(&self, pv_name: &str) -> PvOptions {
220 let mut o = PvOptions::new(pv_name.to_string());
221 o.udp_port = self.udp_port;
222 o.tcp_port = self.tcp_port;
223 o.timeout = self.timeout;
224 o.no_broadcast = self.no_broadcast;
225 o.name_servers.clone_from(&self.name_servers);
226 o.authnz_user.clone_from(&self.authnz_user);
227 o.authnz_host.clone_from(&self.authnz_host);
228 o.server_addr = self.server_addr;
229 o.search_addr = self.search_addr;
230 o.bind_addr = self.bind_addr;
231 o.debug = self.debug;
232 o
233 }
234
235 async fn open_channel(&self, pv_name: &str) -> Result<ChannelConn, PvGetError> {
237 let opts = self.opts(pv_name);
238 let target = resolve_pv_server(&opts).await?;
239 establish_channel(target, &opts).await
240 }
241
242 pub async fn pvget(&self, pv_name: &str) -> Result<PvGetResult, PvGetError> {
246 let opts = self.opts(pv_name);
247 low_level_pvget(&opts).await
248 }
249
250 pub async fn pvget_fields(
252 &self,
253 pv_name: &str,
254 fields: &[&str],
255 ) -> Result<PvGetResult, PvGetError> {
256 let opts = self.opts(pv_name);
257 crate::client::pvget_fields(&opts, fields).await
258 }
259
260 pub async fn pvput(&self, pv_name: &str, value: impl Into<Value>) -> Result<(), PvGetError> {
271 self.pvput_fields(pv_name, value, &["value"]).await
274 }
275
276 pub async fn pvput_fields(
281 &self,
282 pv_name: &str,
283 value: impl Into<Value>,
284 fields: &[&str],
285 ) -> Result<(), PvGetError> {
286 let json_val = value.into();
287 let ChannelConn {
288 mut stream,
289 sid,
290 version: _,
291 is_be,
292 ..
293 } = self.open_channel(pv_name).await?;
294
295 let ioid = alloc_ioid();
296
297 let pv_request = build_pv_request(fields, is_be);
299 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
300 stream.write_all(&init).await?;
301
302 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
304 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
305 })
306 .await?;
307
308 let desc = decode_init_introspection(&init_bytes, "PUT")?;
309
310 let payload = encode_put_payload(&desc, &json_val, is_be)
312 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
313 let req = encode_put_request(sid, ioid, 0x00, &payload, PVA_VERSION, is_be);
314 stream.write_all(&req).await?;
315
316 let resp_bytes = read_until(
318 &mut stream,
319 self.timeout,
320 |cmd| matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.subcmd == 0x00),
321 )
322 .await?;
323 ensure_status_ok(&resp_bytes, is_be, "PUT")?;
324
325 Ok(())
326 }
327
328 pub async fn open_put_channel(&self, pv_name: &str) -> Result<PvaChannel, PvGetError> {
336 self.open_put_channel_fields(pv_name, &["value"]).await
337 }
338
339 pub async fn open_put_channel_fields(
343 &self,
344 pv_name: &str,
345 fields: &[&str],
346 ) -> Result<PvaChannel, PvGetError> {
347 let ChannelConn {
348 mut stream,
349 sid,
350 version,
351 is_be,
352 ..
353 } = self.open_channel(pv_name).await?;
354
355 let ioid = alloc_ioid();
356
357 let pv_request = build_pv_request(fields, is_be);
359 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
360 stream.write_all(&init).await?;
361
362 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
363 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
364 })
365 .await?;
366
367 let desc = decode_init_introspection(&init_bytes, "PUT")?;
368
369 let (mut reader, writer) = stream.into_split();
371 let reader_is_be = is_be;
372 let reader_handle = tokio::spawn(async move {
373 loop {
374 let mut header = [0u8; 8];
375 if reader.read_exact(&mut header).await.is_err() {
376 break;
377 }
378 let hdr = spvirit_codec::epics_decode::PvaHeader::new(&header);
379 let len = if hdr.flags.is_control {
380 0usize
381 } else {
382 hdr.payload_length as usize
383 };
384 let mut payload = vec![0u8; len];
385 if len > 0 && reader.read_exact(&mut payload).await.is_err() {
386 break;
387 }
388 if hdr.command == 11 && !hdr.flags.is_control && len >= 5 {
389 if let Some(st) =
390 spvirit_codec::epics_decode::decode_status(&payload[5..], reader_is_be).0
391 {
392 if st.code != 0 {
393 let msg = st.message.unwrap_or_else(|| format!("code={}", st.code));
394 eprintln!("PvaChannel put error: {msg}");
395 }
396 }
397 }
398 }
399 });
400
401 Ok(PvaChannel {
402 writer,
403 sid,
404 ioid,
405 version,
406 is_be,
407 put_desc: desc,
408 echo_token: 1,
409 last_echo: Instant::now(),
410 _reader_handle: reader_handle,
411 })
412 }
413
414 pub async fn pvmonitor<F>(&self, pv_name: &str, callback: F) -> Result<(), PvGetError>
430 where
431 F: FnMut(&DecodedValue) -> ControlFlow<()>,
432 {
433 self.pvmonitor_fields(pv_name, &[], callback).await
436 }
437
438 pub async fn pvmonitor_fields<F>(
444 &self,
445 pv_name: &str,
446 fields: &[&str],
447 mut callback: F,
448 ) -> Result<(), PvGetError>
449 where
450 F: FnMut(&DecodedValue) -> ControlFlow<()>,
451 {
452 let ChannelConn {
453 mut stream,
454 sid,
455 version: _,
456 is_be,
457 ..
458 } = self.open_channel(pv_name).await?;
459
460 let ioid = alloc_ioid();
461 let decoder = PvdDecoder::new(is_be);
462
463 let pv_request = build_pv_request(fields, is_be);
465 let init = encode_monitor_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
466 stream.write_all(&init).await?;
467
468 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
470 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 13 && (op.subcmd & 0x08) != 0)
471 })
472 .await?;
473
474 let field_desc = decode_init_introspection(&init_bytes, "MONITOR")?;
475
476 let start = encode_monitor_request(sid, ioid, 0x44, &[], PVA_VERSION, is_be);
478 stream.write_all(&start).await?;
479
480 let mut echo_interval = interval(Duration::from_secs(10));
482 let mut echo_token: u32 = 1;
483
484 loop {
485 tokio::select! {
486 _ = echo_interval.tick() => {
487 let msg = encode_control_message(false, is_be, PVA_VERSION, 3, echo_token);
488 echo_token = echo_token.wrapping_add(1);
489 let _ = stream.write_all(&msg).await;
490 }
491 res = read_packet(&mut stream, self.timeout) => {
492 let bytes = match res {
493 Ok(b) => b,
494 Err(PvGetError::Timeout(_)) => continue,
495 Err(e) => return Err(e),
496 };
497 let mut pkt = PvaPacket::new(&bytes);
498 if let Some(PvaPacketCommand::Op(op)) = pkt.decode_payload() {
499 if op.command == 13 && op.ioid == ioid && op.subcmd == 0x00 {
500 let payload = &bytes[8..]; let pos = 5; if let Some((decoded, _)) =
503 decoder.decode_structure_with_bitset(&payload[pos..], &field_desc)
504 {
505 if callback(&decoded).is_break() {
506 return Ok(());
507 }
508 }
509 }
510 }
511 }
512 }
513 }
514 }
515
516 pub async fn pvinfo(&self, pv_name: &str) -> Result<StructureDesc, PvGetError> {
520 let result = self.pvinfo_full(pv_name).await?;
521 Ok(result.0)
522 }
523
524 pub async fn pvinfo_full(
526 &self,
527 pv_name: &str,
528 ) -> Result<(StructureDesc, SocketAddr), PvGetError> {
529 let ChannelConn {
530 mut stream,
531 sid,
532 version: _,
533 is_be,
534 server_addr,
535 } = self.open_channel(pv_name).await?;
536
537 let ioid = alloc_ioid();
538 let msg = encode_get_field_request(sid, ioid, None, PVA_VERSION, is_be);
539 stream.write_all(&msg).await?;
540
541 let resp_bytes = read_until(
542 &mut stream,
543 self.timeout,
544 |cmd| matches!(cmd, PvaPacketCommand::GetField(_)),
545 )
546 .await?;
547
548 let mut pkt = PvaPacket::new(&resp_bytes);
549 let cmd = pkt
550 .decode_payload()
551 .ok_or_else(|| PvGetError::Decode("GET_FIELD response decode failed".to_string()))?;
552 match cmd {
553 PvaPacketCommand::GetField(payload) => {
554 if let Some(ref st) = payload.status {
555 if st.is_error() {
556 let msg = st
557 .message
558 .clone()
559 .unwrap_or_else(|| format!("code={}", st.code));
560 return Err(PvGetError::Protocol(format!("GET_FIELD error: {msg}")));
561 }
562 }
563 let desc = payload
564 .introspection
565 .ok_or_else(|| PvGetError::Decode("missing GET_FIELD introspection".to_string()))?;
566 Ok((desc, server_addr))
567 }
568 _ => Err(PvGetError::Protocol(
569 "unexpected GET_FIELD response".to_string(),
570 )),
571 }
572 }
573
574 pub async fn pvlist(&self, server_addr: SocketAddr) -> Result<Vec<String>, PvGetError> {
578 let opts = self.opts("__pvlist");
579 crate::pvlist::pvlist(&opts, server_addr).await
580 }
581
582 pub async fn pvlist_with_fallback(
586 &self,
587 server_addr: SocketAddr,
588 ) -> Result<(Vec<String>, crate::pvlist::PvListSource), PvGetError> {
589 let opts = self.opts("__pvlist");
590 crate::pvlist::pvlist_with_fallback(&opts, server_addr).await
591 }
592}
593
594pub struct PvaChannel {
612 writer: OwnedWriteHalf,
613 sid: u32,
614 ioid: u32,
615 version: u8,
616 is_be: bool,
617 put_desc: StructureDesc,
618 echo_token: u32,
619 last_echo: Instant,
620 _reader_handle: JoinHandle<()>,
621}
622
623impl PvaChannel {
624 pub async fn put(&mut self, value: impl Into<Value>) -> Result<(), PvGetError> {
629 if self.last_echo.elapsed() >= Duration::from_secs(10) {
631 let msg = encode_control_message(false, self.is_be, self.version, 3, self.echo_token);
632 self.echo_token = self.echo_token.wrapping_add(1);
633 let _ = self.writer.write_all(&msg).await;
634 self.last_echo = Instant::now();
635 }
636
637 let json_val = value.into();
638 let payload = encode_put_payload(&self.put_desc, &json_val, self.is_be)
639 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
640 let req = encode_put_request(
641 self.sid,
642 self.ioid,
643 0x00,
644 &payload,
645 self.version,
646 self.is_be,
647 );
648 self.writer.write_all(&req).await?;
649 Ok(())
650 }
651
652 pub fn introspection(&self) -> &StructureDesc {
654 &self.put_desc
655 }
656}
657
658impl Drop for PvaChannel {
659 fn drop(&mut self) {
660 self._reader_handle.abort();
661 }
662}
663
664pub async fn pvput(opts: &PvOptions, value: impl Into<Value>) -> Result<(), PvGetError> {
674 let client = client_from_opts(opts);
675 client.pvput(&opts.pv_name, value).await
676}
677
678pub async fn pvmonitor<F>(opts: &PvOptions, callback: F) -> Result<(), PvGetError>
684where
685 F: FnMut(&DecodedValue) -> ControlFlow<()>,
686{
687 let client = client_from_opts(opts);
688 client.pvmonitor(&opts.pv_name, callback).await
689}
690
691pub async fn pvmonitor_fields<F>(
693 opts: &PvOptions,
694 fields: &[&str],
695 callback: F,
696) -> Result<(), PvGetError>
697where
698 F: FnMut(&DecodedValue) -> ControlFlow<()>,
699{
700 let client = client_from_opts(opts);
701 client.pvmonitor_fields(&opts.pv_name, fields, callback).await
702}
703
704pub async fn pvput_fields(
706 opts: &PvOptions,
707 value: impl Into<Value>,
708 fields: &[&str],
709) -> Result<(), PvGetError> {
710 let client = client_from_opts(opts);
711 client.pvput_fields(&opts.pv_name, value, fields).await
712}
713
714pub async fn pvinfo(opts: &PvOptions) -> Result<StructureDesc, PvGetError> {
716 let client = client_from_opts(opts);
717 client.pvinfo(&opts.pv_name).await
718}
719
720pub fn client_from_opts(opts: &PvOptions) -> PvaClient {
724 let mut b = PvaClient::builder()
725 .port(opts.tcp_port)
726 .udp_port(opts.udp_port)
727 .timeout(opts.timeout);
728 if opts.no_broadcast {
729 b = b.no_broadcast();
730 }
731 for ns in &opts.name_servers {
732 b = b.name_server(*ns);
733 }
734 if let Some(ref u) = opts.authnz_user {
735 b = b.authnz_user(u.clone());
736 }
737 if let Some(ref h) = opts.authnz_host {
738 b = b.authnz_host(h.clone());
739 }
740 if let Some(addr) = opts.server_addr {
741 b = b.server_addr(addr);
742 }
743 if let Some(addr) = opts.search_addr {
744 b = b.search_addr(addr);
745 }
746 if let Some(addr) = opts.bind_addr {
747 b = b.bind_addr(addr);
748 }
749 if opts.debug {
750 b = b.debug();
751 }
752 b.build()
753}
754
755pub fn decode_init_introspection(raw: &[u8], label: &str) -> Result<StructureDesc, PvGetError> {
757 let mut pkt = PvaPacket::new(raw);
758 let cmd = pkt
759 .decode_payload()
760 .ok_or_else(|| PvGetError::Decode(format!("{label} init response decode failed")))?;
761
762 match cmd {
763 PvaPacketCommand::Op(op) => {
764 if let Some(ref st) = op.status {
765 if st.is_error() {
766 let msg = st
767 .message
768 .clone()
769 .unwrap_or_else(|| format!("code={}", st.code));
770 return Err(PvGetError::Protocol(format!("{label} init error: {msg}")));
771 }
772 }
773 op.introspection
774 .ok_or_else(|| PvGetError::Decode(format!("missing {label} introspection")))
775 }
776 _ => Err(PvGetError::Protocol(format!(
777 "unexpected {label} init response"
778 ))),
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[test]
787 fn builder_defaults() {
788 let c = PvaClient::builder().build();
789 assert_eq!(c.tcp_port, 5075);
790 assert_eq!(c.udp_port, 5076);
791 assert_eq!(c.timeout, Duration::from_secs(5));
792 assert!(!c.no_broadcast);
793 assert!(c.name_servers.is_empty());
794 }
795
796 #[test]
797 fn builder_overrides() {
798 let c = PvaClient::builder()
799 .port(9075)
800 .udp_port(9076)
801 .timeout(Duration::from_secs(10))
802 .no_broadcast()
803 .name_server("127.0.0.1:5075".parse().unwrap())
804 .authnz_user("testuser")
805 .authnz_host("testhost")
806 .build();
807 assert_eq!(c.tcp_port, 9075);
808 assert_eq!(c.udp_port, 9076);
809 assert_eq!(c.timeout, Duration::from_secs(10));
810 assert!(c.no_broadcast);
811 assert_eq!(c.name_servers.len(), 1);
812 assert_eq!(c.authnz_user.as_deref(), Some("testuser"));
813 assert_eq!(c.authnz_host.as_deref(), Some("testhost"));
814 }
815
816 #[test]
817 fn opts_inherits_client_config() {
818 let c = PvaClient::builder()
819 .port(9075)
820 .udp_port(9076)
821 .timeout(Duration::from_secs(10))
822 .no_broadcast()
823 .build();
824 let o = c.opts("TEST:PV");
825 assert_eq!(o.pv_name, "TEST:PV");
826 assert_eq!(o.tcp_port, 9075);
827 assert_eq!(o.udp_port, 9076);
828 assert_eq!(o.timeout, Duration::from_secs(10));
829 assert!(o.no_broadcast);
830 }
831
832 #[test]
833 fn client_from_opts_roundtrip() {
834 let mut opts = PvOptions::new("X:Y".into());
835 opts.tcp_port = 8075;
836 opts.udp_port = 8076;
837 opts.timeout = Duration::from_secs(3);
838 opts.no_broadcast = true;
839 let c = client_from_opts(&opts);
840 assert_eq!(c.tcp_port, 8075);
841 assert_eq!(c.udp_port, 8076);
842 assert!(c.no_broadcast);
843 }
844
845 #[test]
846 fn pv_get_options_alias_works() {
847 let opts: crate::types::PvGetOptions = PvOptions::new("ALIAS:TEST".into());
849 assert_eq!(opts.pv_name, "ALIAS:TEST");
850 }
851}