1use std::collections::HashMap;
18use std::net::IpAddr;
19use std::sync::Arc;
20use std::time::Duration;
21
22use regex::Regex;
23use tracing::info;
24
25use spvirit_types::{NtEnum, NtScalar, NtScalarArray, NtTable as NtTableType, NtTableColumn, NtNdArray as NtNdArrayType, NdCodec, NdDimension, NtTimeStamp, PvValue, ScalarArrayValue, ScalarValue};
26
27use crate::db::{load_db, parse_db};
28use crate::handler::PvListMode;
29use crate::monitor::MonitorRegistry;
30use crate::pvstore::{Source, SourceRegistry};
31use crate::server::{PvaServerConfig, run_pva_server_with_registry};
32use crate::simple_store::{LinkDef, OnPutCallback, ScanCallback, SimplePvStore};
33use crate::types::{DbCommonState, OutputMode, RecordData, RecordInstance, RecordType};
34
35pub struct PvaServerBuilder {
48 records: HashMap<String, RecordInstance>,
49 on_put: HashMap<String, OnPutCallback>,
50 scans: Vec<(String, Duration, ScanCallback)>,
51 links: Vec<LinkDef>,
52 extra_sources: Vec<(String, i32, Arc<dyn Source>)>,
53 tcp_port: u16,
54 udp_port: u16,
55 listen_ip: Option<IpAddr>,
56 advertise_ip: Option<IpAddr>,
57 compute_alarms: bool,
58 beacon_period_secs: u64,
59 conn_timeout: Duration,
60 pvlist_mode: PvListMode,
61 pvlist_max: usize,
62 pvlist_allow_pattern: Option<Regex>,
63}
64
65impl PvaServerBuilder {
66 fn new() -> Self {
67 Self {
68 records: HashMap::new(),
69 on_put: HashMap::new(),
70 scans: Vec::new(),
71 links: Vec::new(),
72 extra_sources: Vec::new(),
73 tcp_port: 5075,
74 udp_port: 5076,
75 listen_ip: None,
76 advertise_ip: None,
77 compute_alarms: false,
78 beacon_period_secs: 15,
79 conn_timeout: Duration::from_secs(64000),
80 pvlist_mode: PvListMode::List,
81 pvlist_max: 1024,
82 pvlist_allow_pattern: None,
83 }
84 }
85
86 pub fn ai(mut self, name: impl Into<String>, initial: f64) -> Self {
90 let name = name.into();
91 self.records.insert(
92 name.clone(),
93 make_scalar_record(&name, RecordType::Ai, ScalarValue::F64(initial)),
94 );
95 self
96 }
97
98 pub fn ao(mut self, name: impl Into<String>, initial: f64) -> Self {
100 let name = name.into();
101 self.records.insert(
102 name.clone(),
103 make_output_record(&name, RecordType::Ao, ScalarValue::F64(initial)),
104 );
105 self
106 }
107
108 pub fn bi(mut self, name: impl Into<String>, initial: bool) -> Self {
110 let name = name.into();
111 self.records.insert(
112 name.clone(),
113 make_scalar_record(&name, RecordType::Bi, ScalarValue::Bool(initial)),
114 );
115 self
116 }
117
118 pub fn bo(mut self, name: impl Into<String>, initial: bool) -> Self {
120 let name = name.into();
121 self.records.insert(
122 name.clone(),
123 make_output_record(&name, RecordType::Bo, ScalarValue::Bool(initial)),
124 );
125 self
126 }
127
128 pub fn string_in(mut self, name: impl Into<String>, initial: impl Into<String>) -> Self {
130 let name = name.into();
131 self.records.insert(
132 name.clone(),
133 make_scalar_record(
134 &name,
135 RecordType::StringIn,
136 ScalarValue::Str(initial.into()),
137 ),
138 );
139 self
140 }
141
142 pub fn string_out(mut self, name: impl Into<String>, initial: impl Into<String>) -> Self {
144 let name = name.into();
145 self.records.insert(
146 name.clone(),
147 make_output_record(
148 &name,
149 RecordType::StringOut,
150 ScalarValue::Str(initial.into()),
151 ),
152 );
153 self
154 }
155
156 pub fn waveform(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
158 let name = name.into();
159 let ftvl = data.type_label().trim_end_matches("[]").to_string();
160 let nelm = data.len();
161 self.records.insert(
162 name.clone(),
163 RecordInstance {
164 name: name.clone(),
165 record_type: RecordType::Waveform,
166 common: DbCommonState::default(),
167 data: RecordData::Waveform {
168 nt: NtScalarArray::from_value(data),
169 inp: None,
170 ftvl,
171 nelm,
172 nord: nelm,
173 },
174 raw_fields: HashMap::new(),
175 },
176 );
177 self
178 }
179
180 pub fn aai(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
182 let name = name.into();
183 let ftvl = data.type_label().trim_end_matches("[]").to_string();
184 let nelm = data.len();
185 self.records.insert(
186 name.clone(),
187 RecordInstance {
188 name: name.clone(),
189 record_type: RecordType::Aai,
190 common: DbCommonState::default(),
191 data: RecordData::Aai {
192 nt: NtScalarArray::from_value(data),
193 inp: None,
194 ftvl,
195 nelm,
196 nord: nelm,
197 },
198 raw_fields: HashMap::new(),
199 },
200 );
201 self
202 }
203
204 pub fn aao(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
206 let name = name.into();
207 let ftvl = data.type_label().trim_end_matches("[]").to_string();
208 let nelm = data.len();
209 self.records.insert(
210 name.clone(),
211 RecordInstance {
212 name: name.clone(),
213 record_type: RecordType::Aao,
214 common: DbCommonState::default(),
215 data: RecordData::Aao {
216 nt: NtScalarArray::from_value(data),
217 out: None,
218 dol: None,
219 omsl: OutputMode::Supervisory,
220 ftvl,
221 nelm,
222 nord: nelm,
223 },
224 raw_fields: HashMap::new(),
225 },
226 );
227 self
228 }
229
230 pub fn sub_array(
232 mut self,
233 name: impl Into<String>,
234 data: ScalarArrayValue,
235 indx: usize,
236 nelm: usize,
237 ) -> Self {
238 let name = name.into();
239 let ftvl = data.type_label().trim_end_matches("[]").to_string();
240 let malm = data.len();
241 let nord = nelm.min(malm.saturating_sub(indx));
242 self.records.insert(
243 name.clone(),
244 RecordInstance {
245 name: name.clone(),
246 record_type: RecordType::SubArray,
247 common: DbCommonState::default(),
248 data: RecordData::SubArray {
249 nt: NtScalarArray::from_value(data),
250 inp: None,
251 ftvl,
252 malm,
253 nelm,
254 nord,
255 indx,
256 },
257 raw_fields: HashMap::new(),
258 },
259 );
260 self
261 }
262
263 pub fn nt_table(
265 mut self,
266 name: impl Into<String>,
267 columns: Vec<(String, ScalarArrayValue)>,
268 ) -> Self {
269 let name = name.into();
270 let labels: Vec<String> = columns.iter().map(|(n, _)| n.clone()).collect();
271 let cols: Vec<NtTableColumn> = columns
272 .into_iter()
273 .map(|(n, v)| NtTableColumn { name: n, values: v })
274 .collect();
275 self.records.insert(
276 name.clone(),
277 RecordInstance {
278 name: name.clone(),
279 record_type: RecordType::NtTable,
280 common: DbCommonState::default(),
281 data: RecordData::NtTable {
282 nt: NtTableType {
283 labels,
284 columns: cols,
285 descriptor: None,
286 alarm: None,
287 time_stamp: None,
288 },
289 inp: None,
290 out: None,
291 omsl: OutputMode::Supervisory,
292 },
293 raw_fields: HashMap::new(),
294 },
295 );
296 self
297 }
298
299 pub fn nt_ndarray(
301 mut self,
302 name: impl Into<String>,
303 data: ScalarArrayValue,
304 dims: Vec<(i32, i32)>,
305 ) -> Self {
306 let name = name.into();
307 let dimension: Vec<NdDimension> = dims
308 .into_iter()
309 .map(|(size, offset)| NdDimension {
310 size,
311 offset,
312 full_size: size,
313 binning: 1,
314 reverse: false,
315 })
316 .collect();
317 let uncompressed_size =
318 (data.len() * data.element_size_bytes().max(1)) as i64;
319 self.records.insert(
320 name.clone(),
321 RecordInstance {
322 name: name.clone(),
323 record_type: RecordType::NtNdArray,
324 common: DbCommonState::default(),
325 data: RecordData::NtNdArray {
326 nt: NtNdArrayType {
327 value: data,
328 codec: NdCodec {
329 name: String::new(),
330 parameters: Default::default(),
331 },
332 compressed_size: uncompressed_size,
333 uncompressed_size,
334 dimension,
335 unique_id: 0,
336 data_time_stamp: NtTimeStamp {
337 seconds_past_epoch: 0,
338 nanoseconds: 0,
339 user_tag: 0,
340 },
341 attribute: vec![],
342 descriptor: None,
343 alarm: None,
344 time_stamp: None,
345 display: None,
346 },
347 inp: None,
348 out: None,
349 omsl: OutputMode::Supervisory,
350 },
351 raw_fields: HashMap::new(),
352 },
353 );
354 self
355 }
356
357 pub fn mbbi(
359 mut self,
360 name: impl Into<String>,
361 choices: Vec<String>,
362 initial: i32,
363 ) -> Self {
364 let name = name.into();
365 self.records.insert(
366 name.clone(),
367 RecordInstance {
368 name: name.clone(),
369 record_type: RecordType::Mbbi,
370 common: DbCommonState::default(),
371 data: RecordData::NtEnum {
372 nt: NtEnum::new(initial, choices),
373 inp: None,
374 out: None,
375 omsl: OutputMode::Supervisory,
376 },
377 raw_fields: HashMap::new(),
378 },
379 );
380 self
381 }
382
383 pub fn mbbo(
385 mut self,
386 name: impl Into<String>,
387 choices: Vec<String>,
388 initial: i32,
389 ) -> Self {
390 let name = name.into();
391 self.records.insert(
392 name.clone(),
393 RecordInstance {
394 name: name.clone(),
395 record_type: RecordType::Mbbo,
396 common: DbCommonState::default(),
397 data: RecordData::NtEnum {
398 nt: NtEnum::new(initial, choices),
399 inp: None,
400 out: None,
401 omsl: OutputMode::Supervisory,
402 },
403 raw_fields: HashMap::new(),
404 },
405 );
406 self
407 }
408
409 pub fn generic(
411 mut self,
412 name: impl Into<String>,
413 struct_id: impl Into<String>,
414 fields: Vec<(String, PvValue)>,
415 ) -> Self {
416 let name = name.into();
417 self.records.insert(
418 name.clone(),
419 RecordInstance {
420 name: name.clone(),
421 record_type: RecordType::Generic,
422 common: DbCommonState::default(),
423 data: RecordData::Generic {
424 struct_id: struct_id.into(),
425 fields,
426 inp: None,
427 out: None,
428 omsl: OutputMode::Supervisory,
429 },
430 raw_fields: HashMap::new(),
431 },
432 );
433 self
434 }
435
436 pub fn db_file(mut self, path: impl AsRef<str>) -> Self {
440 match load_db(path.as_ref()) {
441 Ok(records) => {
442 self.records.extend(records);
443 }
444 Err(e) => {
445 tracing::error!("Failed to load db file '{}': {}", path.as_ref(), e);
446 }
447 }
448 self
449 }
450
451 pub fn db_string(mut self, content: &str) -> Self {
453 match parse_db(content) {
454 Ok(records) => {
455 self.records.extend(records);
456 }
457 Err(e) => {
458 tracing::error!("Failed to parse db string: {}", e);
459 }
460 }
461 self
462 }
463
464 pub fn on_put<F>(mut self, name: impl Into<String>, callback: F) -> Self
468 where
469 F: Fn(&str, &spvirit_codec::spvd_decode::DecodedValue) + Send + Sync + 'static,
470 {
471 self.on_put.insert(name.into(), Arc::new(callback));
472 self
473 }
474
475 pub fn scan<F>(mut self, name: impl Into<String>, period: Duration, callback: F) -> Self
477 where
478 F: Fn(&str) -> ScalarValue + Send + Sync + 'static,
479 {
480 self.scans.push((name.into(), period, Arc::new(callback)));
481 self
482 }
483
484 pub fn link<F>(mut self, output: impl Into<String>, inputs: &[&str], compute: F) -> Self
499 where
500 F: Fn(&[ScalarValue]) -> ScalarValue + Send + Sync + 'static,
501 {
502 self.links.push(LinkDef {
503 output: output.into(),
504 inputs: inputs.iter().map(|s| s.to_string()).collect(),
505 compute: Arc::new(compute),
506 });
507 self
508 }
509
510 pub fn source(
522 mut self,
523 label: impl Into<String>,
524 order: i32,
525 source: Arc<dyn Source>,
526 ) -> Self {
527 self.extra_sources.push((label.into(), order, source));
528 self
529 }
530
531 pub fn port(mut self, port: u16) -> Self {
535 self.tcp_port = port;
536 self
537 }
538
539 pub fn udp_port(mut self, port: u16) -> Self {
541 self.udp_port = port;
542 self
543 }
544
545 pub fn listen_ip(mut self, ip: IpAddr) -> Self {
547 self.listen_ip = Some(ip);
548 self
549 }
550
551 pub fn advertise_ip(mut self, ip: IpAddr) -> Self {
553 self.advertise_ip = Some(ip);
554 self
555 }
556
557 pub fn compute_alarms(mut self, enabled: bool) -> Self {
559 self.compute_alarms = enabled;
560 self
561 }
562
563 pub fn beacon_period(mut self, secs: u64) -> Self {
565 self.beacon_period_secs = secs;
566 self
567 }
568
569 pub fn conn_timeout(mut self, timeout: Duration) -> Self {
571 self.conn_timeout = timeout;
572 self
573 }
574
575 pub fn pvlist_mode(mut self, mode: PvListMode) -> Self {
577 self.pvlist_mode = mode;
578 self
579 }
580
581 pub fn pvlist_max(mut self, max: usize) -> Self {
583 self.pvlist_max = max;
584 self
585 }
586
587 pub fn pvlist_allow_pattern(mut self, pattern: Regex) -> Self {
589 self.pvlist_allow_pattern = Some(pattern);
590 self
591 }
592
593 pub fn build(self) -> PvaServer {
595 let store = Arc::new(SimplePvStore::new(
596 self.records,
597 self.on_put,
598 self.links,
599 self.compute_alarms,
600 ));
601
602 let mut config = PvaServerConfig::default();
603 config.tcp_port = self.tcp_port;
604 config.udp_port = self.udp_port;
605 config.compute_alarms = self.compute_alarms;
606 if let Some(ip) = self.listen_ip {
607 config.listen_ip = ip;
608 }
609 config.advertise_ip = self.advertise_ip;
610 config.beacon_period_secs = self.beacon_period_secs;
611 config.conn_timeout = self.conn_timeout;
612 config.pvlist_mode = self.pvlist_mode;
613 config.pvlist_max = self.pvlist_max;
614 config.pvlist_allow_pattern = self.pvlist_allow_pattern;
615
616 PvaServer {
617 store,
618 extra_sources: self.extra_sources,
619 config,
620 scans: self.scans,
621 monitor_registry: None,
622 }
623 }
624}
625
626pub struct PvaServer {
647 store: Arc<SimplePvStore>,
648 extra_sources: Vec<(String, i32, Arc<dyn Source>)>,
649 config: PvaServerConfig,
650 scans: Vec<(String, Duration, ScanCallback)>,
651 monitor_registry: Option<Arc<MonitorRegistry>>,
654}
655
656impl PvaServer {
657 pub fn builder() -> PvaServerBuilder {
659 PvaServerBuilder::new()
660 }
661
662 pub fn store(&self) -> &Arc<SimplePvStore> {
664 &self.store
665 }
666
667 pub fn add_source(
679 &mut self,
680 label: impl Into<String>,
681 order: i32,
682 source: Arc<dyn Source>,
683 ) {
684 self.extra_sources.push((label.into(), order, source));
685 }
686
687 pub fn set_monitor_registry(&mut self, registry: Arc<MonitorRegistry>) {
693 self.monitor_registry = Some(registry);
694 }
695
696 pub fn monitor_registry(&mut self) -> Arc<MonitorRegistry> {
700 if self.monitor_registry.is_none() {
701 self.monitor_registry = Some(Arc::new(MonitorRegistry::new()));
702 }
703 self.monitor_registry.as_ref().unwrap().clone()
704 }
705
706 pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
710 let registry = self
713 .monitor_registry
714 .clone()
715 .unwrap_or_else(|| Arc::new(MonitorRegistry::new()));
716 self.store.set_registry(registry.clone()).await;
717
718 let sources = Arc::new(SourceRegistry::new());
720 sources.add("builtin", 0, self.store.clone()).await;
721
722 for (label, order, source) in &self.extra_sources {
724 sources.add(label.clone(), *order, source.clone()).await;
725 }
726
727 for (name, period, callback) in &self.scans {
729 let store = self.store.clone();
730 let name = name.clone();
731 let period = *period;
732 let callback = callback.clone();
733 tokio::spawn(async move {
734 let mut interval = tokio::time::interval(period);
735 loop {
736 interval.tick().await;
737 let new_val = callback(&name);
738 store.set_value(&name, new_val).await;
739 }
740 });
741 }
742
743 let pv_count = self.store.pv_names().await.len();
744 info!(
745 "PvaServer starting: {} PVs on port {}",
746 pv_count, self.config.tcp_port
747 );
748
749 run_pva_server_with_registry(sources, self.config, registry).await
750 }
751}
752
753fn make_scalar_record(name: &str, record_type: RecordType, value: ScalarValue) -> RecordInstance {
756 let nt = NtScalar::from_value(value);
757 let data = match record_type {
758 RecordType::Ai => RecordData::Ai {
759 nt,
760 inp: None,
761 siml: None,
762 siol: None,
763 simm: false,
764 },
765 RecordType::Bi => RecordData::Bi {
766 nt,
767 inp: None,
768 znam: "Off".to_string(),
769 onam: "On".to_string(),
770 siml: None,
771 siol: None,
772 simm: false,
773 },
774 RecordType::StringIn => RecordData::StringIn {
775 nt,
776 inp: None,
777 siml: None,
778 siol: None,
779 simm: false,
780 },
781 _ => panic!("make_scalar_record: unsupported type {record_type:?}"),
782 };
783 RecordInstance {
784 name: name.to_string(),
785 record_type,
786 common: DbCommonState::default(),
787 data,
788 raw_fields: HashMap::new(),
789 }
790}
791
792fn make_output_record(name: &str, record_type: RecordType, value: ScalarValue) -> RecordInstance {
793 let nt = NtScalar::from_value(value);
794 let data = match record_type {
795 RecordType::Ao => RecordData::Ao {
796 nt,
797 out: None,
798 dol: None,
799 omsl: OutputMode::Supervisory,
800 drvl: None,
801 drvh: None,
802 oroc: None,
803 siml: None,
804 siol: None,
805 simm: false,
806 },
807 RecordType::Bo => RecordData::Bo {
808 nt,
809 out: None,
810 dol: None,
811 omsl: OutputMode::Supervisory,
812 znam: "Off".to_string(),
813 onam: "On".to_string(),
814 siml: None,
815 siol: None,
816 simm: false,
817 },
818 RecordType::StringOut => RecordData::StringOut {
819 nt,
820 out: None,
821 dol: None,
822 omsl: OutputMode::Supervisory,
823 siml: None,
824 siol: None,
825 simm: false,
826 },
827 _ => panic!("make_output_record: unsupported type {record_type:?}"),
828 };
829 RecordInstance {
830 name: name.to_string(),
831 record_type,
832 common: DbCommonState::default(),
833 data,
834 raw_fields: HashMap::new(),
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn builder_creates_records() {
844 let server = PvaServer::builder()
845 .ai("T:AI", 1.0)
846 .ao("T:AO", 2.0)
847 .bi("T:BI", true)
848 .bo("T:BO", false)
849 .string_in("T:SI", "hello")
850 .string_out("T:SO", "world")
851 .build();
852
853 let rt = tokio::runtime::Builder::new_current_thread()
854 .enable_all()
855 .build()
856 .unwrap();
857 let names = rt.block_on(server.store.pv_names());
858 assert_eq!(names.len(), 6);
859 }
860
861 #[test]
862 fn builder_defaults() {
863 let server = PvaServer::builder().build();
864 assert_eq!(server.config.tcp_port, 5075);
865 assert_eq!(server.config.udp_port, 5076);
866 assert!(!server.config.compute_alarms);
867 }
868
869 #[test]
870 fn builder_port_override() {
871 let server = PvaServer::builder().port(9075).udp_port(9076).build();
872 assert_eq!(server.config.tcp_port, 9075);
873 assert_eq!(server.config.udp_port, 9076);
874 }
875
876 #[test]
877 fn builder_db_string() {
878 let db = r#"
879 record(ai, "TEST:VAL") {
880 field(VAL, "3.14")
881 }
882 "#;
883 let server = PvaServer::builder().db_string(db).build();
884 let rt = tokio::runtime::Builder::new_current_thread()
885 .enable_all()
886 .build()
887 .unwrap();
888 assert!(rt.block_on(server.store.get_value("TEST:VAL")).is_some());
889 }
890
891 #[test]
892 fn builder_waveform() {
893 let data = ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]);
894 let server = PvaServer::builder().waveform("T:WF", data).build();
895 let rt = tokio::runtime::Builder::new_current_thread()
896 .enable_all()
897 .build()
898 .unwrap();
899 let names = rt.block_on(server.store.pv_names());
900 assert!(names.contains(&"T:WF".to_string()));
901 }
902
903 #[test]
904 fn builder_scan_callback() {
905 let server = PvaServer::builder()
906 .ai("SCAN:V", 0.0)
907 .scan("SCAN:V", Duration::from_secs(1), |_name| {
908 ScalarValue::F64(42.0)
909 })
910 .build();
911 assert_eq!(server.scans.len(), 1);
912 }
913
914 #[test]
915 fn builder_on_put_callback() {
916 let server = PvaServer::builder()
917 .ao("PUT:V", 0.0)
918 .on_put("PUT:V", |_name, _val| {})
919 .build();
920 let rt = tokio::runtime::Builder::new_current_thread()
923 .enable_all()
924 .build()
925 .unwrap();
926 assert!(rt.block_on(server.store.get_value("PUT:V")).is_some());
927 }
928
929 #[test]
930 fn store_runtime_get_set() {
931 let server = PvaServer::builder().ao("RT:V", 0.0).build();
932 let rt = tokio::runtime::Builder::new_current_thread()
933 .enable_all()
934 .build()
935 .unwrap();
936 let store = server.store().clone();
937 rt.block_on(async {
938 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(0.0)));
939 store.set_value("RT:V", ScalarValue::F64(99.0)).await;
940 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(99.0)));
941 });
942 }
943
944 #[test]
945 fn link_propagates_on_set_value() {
946 let server = PvaServer::builder()
947 .ao("INPUT:A", 1.0)
948 .ao("INPUT:B", 2.0)
949 .ai("CALC:SUM", 0.0)
950 .link("CALC:SUM", &["INPUT:A", "INPUT:B"], |values| {
951 let a = match &values[0] {
952 ScalarValue::F64(v) => *v,
953 _ => 0.0,
954 };
955 let b = match &values[1] {
956 ScalarValue::F64(v) => *v,
957 _ => 0.0,
958 };
959 ScalarValue::F64(a + b)
960 })
961 .build();
962
963 let rt = tokio::runtime::Builder::new_current_thread()
964 .enable_all()
965 .build()
966 .unwrap();
967 let store = server.store().clone();
968 rt.block_on(async {
969 store.set_value("INPUT:A", ScalarValue::F64(10.0)).await;
971 assert_eq!(
972 store.get_value("CALC:SUM").await,
973 Some(ScalarValue::F64(12.0))
974 );
975
976 store.set_value("INPUT:B", ScalarValue::F64(5.0)).await;
978 assert_eq!(
979 store.get_value("CALC:SUM").await,
980 Some(ScalarValue::F64(15.0))
981 );
982 });
983 }
984}