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