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 self.records.insert(
163 name.clone(),
164 make_array_record(&name, RecordType::Waveform, data),
165 );
166 self
167 }
168
169 pub fn aai(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
171 let name = name.into();
172 self.records.insert(
173 name.clone(),
174 make_array_record(&name, RecordType::Aai, data),
175 );
176 self
177 }
178
179 pub fn aao(mut self, name: impl Into<String>, data: ScalarArrayValue) -> Self {
181 let name = name.into();
182 self.records.insert(
183 name.clone(),
184 make_array_record(&name, RecordType::Aao, data),
185 );
186 self
187 }
188
189 pub fn sub_array(
191 mut self,
192 name: impl Into<String>,
193 data: ScalarArrayValue,
194 indx: usize,
195 nelm: usize,
196 ) -> Self {
197 let name = name.into();
198 let ftvl = data.type_label().trim_end_matches("[]").to_string();
199 let malm = data.len();
200 let nord = nelm.min(malm.saturating_sub(indx));
201 self.records.insert(
202 name.clone(),
203 RecordInstance {
204 name: name.clone(),
205 record_type: RecordType::SubArray,
206 common: DbCommonState::default(),
207 data: RecordData::SubArray {
208 nt: NtScalarArray::from_value(data),
209 inp: None,
210 ftvl,
211 malm,
212 nelm,
213 nord,
214 indx,
215 },
216 raw_fields: HashMap::new(),
217 },
218 );
219 self
220 }
221
222 pub fn nt_table(
224 mut self,
225 name: impl Into<String>,
226 columns: Vec<(String, ScalarArrayValue)>,
227 ) -> Self {
228 let name = name.into();
229 let labels: Vec<String> = columns.iter().map(|(n, _)| n.clone()).collect();
230 let cols: Vec<NtTableColumn> = columns
231 .into_iter()
232 .map(|(n, v)| NtTableColumn { name: n, values: v })
233 .collect();
234 self.records.insert(
235 name.clone(),
236 RecordInstance {
237 name: name.clone(),
238 record_type: RecordType::NtTable,
239 common: DbCommonState::default(),
240 data: RecordData::NtTable {
241 nt: NtTableType {
242 labels,
243 columns: cols,
244 descriptor: None,
245 alarm: None,
246 time_stamp: None,
247 },
248 inp: None,
249 out: None,
250 omsl: OutputMode::Supervisory,
251 },
252 raw_fields: HashMap::new(),
253 },
254 );
255 self
256 }
257
258 pub fn nt_ndarray(
260 mut self,
261 name: impl Into<String>,
262 data: ScalarArrayValue,
263 dims: Vec<(i32, i32)>,
264 ) -> Self {
265 let name = name.into();
266 let dimension: Vec<NdDimension> = dims
267 .into_iter()
268 .map(|(size, offset)| NdDimension {
269 size,
270 offset,
271 full_size: size,
272 binning: 1,
273 reverse: false,
274 })
275 .collect();
276 let uncompressed_size = (data.len() * data.element_size_bytes().max(1)) as i64;
277 self.records.insert(
278 name.clone(),
279 RecordInstance {
280 name: name.clone(),
281 record_type: RecordType::NtNdArray,
282 common: DbCommonState::default(),
283 data: RecordData::NtNdArray {
284 nt: NtNdArrayType {
285 value: data,
286 codec: NdCodec {
287 name: String::new(),
288 parameters: Default::default(),
289 },
290 compressed_size: uncompressed_size,
291 uncompressed_size,
292 dimension,
293 unique_id: 0,
294 data_time_stamp: NtTimeStamp {
295 seconds_past_epoch: 0,
296 nanoseconds: 0,
297 user_tag: 0,
298 },
299 attribute: vec![],
300 descriptor: None,
301 alarm: None,
302 time_stamp: None,
303 display: None,
304 },
305 inp: None,
306 out: None,
307 omsl: OutputMode::Supervisory,
308 },
309 raw_fields: HashMap::new(),
310 },
311 );
312 self
313 }
314
315 pub fn mbbi(mut self, name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
317 let name = name.into();
318 self.records.insert(
319 name.clone(),
320 RecordInstance {
321 name: name.clone(),
322 record_type: RecordType::Mbbi,
323 common: DbCommonState::default(),
324 data: RecordData::NtEnum {
325 nt: NtEnum::new(initial, choices),
326 inp: None,
327 out: None,
328 omsl: OutputMode::Supervisory,
329 },
330 raw_fields: HashMap::new(),
331 },
332 );
333 self
334 }
335
336 pub fn mbbo(mut self, name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
338 let name = name.into();
339 self.records.insert(
340 name.clone(),
341 RecordInstance {
342 name: name.clone(),
343 record_type: RecordType::Mbbo,
344 common: DbCommonState::default(),
345 data: RecordData::NtEnum {
346 nt: NtEnum::new(initial, choices),
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 generic(
359 mut self,
360 name: impl Into<String>,
361 struct_id: impl Into<String>,
362 fields: Vec<(String, PvValue)>,
363 ) -> Self {
364 let name = name.into();
365 self.records.insert(
366 name.clone(),
367 RecordInstance {
368 name: name.clone(),
369 record_type: RecordType::Generic,
370 common: DbCommonState::default(),
371 data: RecordData::Generic {
372 struct_id: struct_id.into(),
373 fields,
374 inp: None,
375 out: None,
376 omsl: OutputMode::Supervisory,
377 },
378 raw_fields: HashMap::new(),
379 },
380 );
381 self
382 }
383
384 pub fn db_file(mut self, path: impl AsRef<str>) -> Self {
388 match load_db(path.as_ref()) {
389 Ok(records) => {
390 self.records.extend(records);
391 }
392 Err(e) => {
393 tracing::error!("Failed to load db file '{}': {}", path.as_ref(), e);
394 }
395 }
396 self
397 }
398
399 pub fn db_string(mut self, content: &str) -> Self {
401 match parse_db(content) {
402 Ok(records) => {
403 self.records.extend(records);
404 }
405 Err(e) => {
406 tracing::error!("Failed to parse db string: {}", e);
407 }
408 }
409 self
410 }
411
412 pub fn on_put<F>(mut self, name: impl Into<String>, callback: F) -> Self
416 where
417 F: Fn(&str, &spvirit_codec::spvd_decode::DecodedValue) + Send + Sync + 'static,
418 {
419 self.on_put.insert(name.into(), Arc::new(callback));
420 self
421 }
422
423 pub fn scan<F>(mut self, name: impl Into<String>, period: Duration, callback: F) -> Self
425 where
426 F: Fn(&str) -> ScalarValue + Send + Sync + 'static,
427 {
428 self.scans.push((name.into(), period, Arc::new(callback)));
429 self
430 }
431
432 pub fn link<F>(mut self, output: impl Into<String>, inputs: &[&str], compute: F) -> Self
447 where
448 F: Fn(&[ScalarValue]) -> ScalarValue + Send + Sync + 'static,
449 {
450 self.links.push(LinkDef {
451 output: output.into(),
452 inputs: inputs.iter().map(|s| s.to_string()).collect(),
453 compute: Arc::new(compute),
454 });
455 self
456 }
457
458 pub fn source(mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) -> Self {
470 self.extra_sources.push((label.into(), order, source));
471 self
472 }
473
474 pub fn port(mut self, port: u16) -> Self {
478 self.tcp_port = port;
479 self
480 }
481
482 pub fn udp_port(mut self, port: u16) -> Self {
484 self.udp_port = port;
485 self
486 }
487
488 pub fn listen_ip(mut self, ip: IpAddr) -> Self {
490 self.listen_ip = Some(ip);
491 self
492 }
493
494 pub fn advertise_ip(mut self, ip: IpAddr) -> Self {
496 self.advertise_ip = Some(ip);
497 self
498 }
499
500 pub fn compute_alarms(mut self, enabled: bool) -> Self {
502 self.compute_alarms = enabled;
503 self
504 }
505
506 pub fn beacon_period(mut self, secs: u64) -> Self {
508 self.beacon_period_secs = secs;
509 self
510 }
511
512 pub fn conn_timeout(mut self, timeout: Duration) -> Self {
514 self.conn_timeout = timeout;
515 self
516 }
517
518 pub fn pvlist_mode(mut self, mode: PvListMode) -> Self {
520 self.pvlist_mode = mode;
521 self
522 }
523
524 pub fn pvlist_max(mut self, max: usize) -> Self {
526 self.pvlist_max = max;
527 self
528 }
529
530 pub fn pvlist_allow_pattern(mut self, pattern: Regex) -> Self {
532 self.pvlist_allow_pattern = Some(pattern);
533 self
534 }
535
536 pub fn build(self) -> PvaServer {
538 let store = Arc::new(SimplePvStore::new(
539 self.records,
540 self.on_put,
541 self.links,
542 self.compute_alarms,
543 ));
544
545 let mut config = PvaServerConfig::default();
546 config.tcp_port = self.tcp_port;
547 config.udp_port = self.udp_port;
548 config.compute_alarms = self.compute_alarms;
549 if let Some(ip) = self.listen_ip {
550 config.listen_ip = ip;
551 }
552 config.advertise_ip = self.advertise_ip;
553 config.beacon_period_secs = self.beacon_period_secs;
554 config.conn_timeout = self.conn_timeout;
555 config.pvlist_mode = self.pvlist_mode;
556 config.pvlist_max = self.pvlist_max;
557 config.pvlist_allow_pattern = self.pvlist_allow_pattern;
558
559 PvaServer {
560 store,
561 extra_sources: self.extra_sources,
562 config,
563 scans: self.scans,
564 monitor_registry: None,
565 }
566 }
567}
568
569pub struct PvaServer {
590 store: Arc<SimplePvStore>,
591 extra_sources: Vec<(String, i32, Arc<dyn Source>)>,
592 config: PvaServerConfig,
593 scans: Vec<(String, Duration, ScanCallback)>,
594 monitor_registry: Option<Arc<MonitorRegistry>>,
597}
598
599impl PvaServer {
600 pub fn builder() -> PvaServerBuilder {
602 PvaServerBuilder::new()
603 }
604
605 pub fn store(&self) -> &Arc<SimplePvStore> {
607 &self.store
608 }
609
610 pub async fn pv<T: crate::pv::PvScalar>(
613 &self,
614 name: &str,
615 ) -> Result<crate::pv::Pv<T>, crate::pv::PvError> {
616 crate::pv::Pv::attach(&self.store, name).await
617 }
618
619 pub async fn array_pv(&self, name: &str) -> Result<crate::pv::PvArray, crate::pv::PvError> {
622 crate::pv::PvArray::attach(&self.store, name).await
623 }
624
625 pub fn add_source(&mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) {
637 self.extra_sources.push((label.into(), order, source));
638 }
639
640 pub fn set_monitor_registry(&mut self, registry: Arc<MonitorRegistry>) {
646 self.monitor_registry = Some(registry);
647 }
648
649 pub fn monitor_registry(&mut self) -> Arc<MonitorRegistry> {
653 if self.monitor_registry.is_none() {
654 self.monitor_registry = Some(Arc::new(MonitorRegistry::new()));
655 }
656 self.monitor_registry.as_ref().unwrap().clone()
657 }
658
659 pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
663 let registry = self
666 .monitor_registry
667 .clone()
668 .unwrap_or_else(|| Arc::new(MonitorRegistry::new()));
669 self.store.set_registry(registry.clone()).await;
670
671 let sources = Arc::new(SourceRegistry::new());
673 sources.add("builtin", 0, self.store.clone()).await;
674
675 sources
678 .add(
679 "record-fields",
680 10,
681 Arc::new(crate::record_fields::RecordFieldSource::new(
682 self.store.clone(),
683 )),
684 )
685 .await;
686
687 for (label, order, source) in &self.extra_sources {
689 sources.add(label.clone(), *order, source.clone()).await;
690 }
691
692 for (name, period, callback) in &self.scans {
694 let store = self.store.clone();
695 let name = name.clone();
696 let period = *period;
697 let callback = callback.clone();
698 tokio::spawn(async move {
699 let mut interval = tokio::time::interval(period);
700 loop {
701 interval.tick().await;
702 let new_val = callback(&name);
703 store.set_value(&name, new_val).await;
704 }
705 });
706 }
707
708 let pv_count = self.store.pv_names().await.len();
709 info!(
710 "PvaServer starting: {} PVs on port {}",
711 pv_count, self.config.tcp_port
712 );
713
714 run_pva_server_with_registry(sources, self.config, registry).await
715 }
716}
717
718impl PvaServer {
721 pub fn serve(pvs: impl IntoIterator<Item = impl Into<crate::pv::AnyPv>>) -> ServeBuilder {
724 ServeBuilder {
725 inner: PvaServerBuilder::new(),
726 handles: Vec::new(),
727 }
728 .pvs(pvs)
729 }
730}
731
732pub struct ServeBuilder {
735 inner: PvaServerBuilder,
736 handles: Vec<crate::pv::AnyPv>,
737}
738
739impl ServeBuilder {
740 pub fn pvs(mut self, pvs: impl IntoIterator<Item = impl Into<crate::pv::AnyPv>>) -> Self {
741 self.handles.extend(pvs.into_iter().map(Into::into));
742 self
743 }
744 pub fn db_file(mut self, path: impl AsRef<str>) -> Self {
745 self.inner = self.inner.db_file(path);
746 self
747 }
748 pub fn db_string(mut self, content: &str) -> Self {
749 self.inner = self.inner.db_string(content);
750 self
751 }
752 pub fn source(mut self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) -> Self {
753 self.inner = self.inner.source(label, order, source);
754 self
755 }
756 pub fn port(mut self, port: u16) -> Self {
757 self.inner = self.inner.port(port);
758 self
759 }
760 pub fn udp_port(mut self, port: u16) -> Self {
761 self.inner = self.inner.udp_port(port);
762 self
763 }
764 pub fn listen_ip(mut self, ip: IpAddr) -> Self {
765 self.inner = self.inner.listen_ip(ip);
766 self
767 }
768 pub fn advertise_ip(mut self, ip: IpAddr) -> Self {
769 self.inner = self.inner.advertise_ip(ip);
770 self
771 }
772 pub fn compute_alarms(mut self, enabled: bool) -> Self {
773 self.inner = self.inner.compute_alarms(enabled);
774 self
775 }
776 pub fn beacon_period(mut self, secs: u64) -> Self {
777 self.inner = self.inner.beacon_period(secs);
778 self
779 }
780
781 pub async fn build(mut self) -> PvaServer {
789 let mut validators: Vec<(String, crate::simple_store::PutValidator)> = Vec::new();
790 for h in &self.handles {
791 let name = h.name().to_string();
792 if let Some(rec) = h.take_record() {
793 self.inner.records.insert(name.clone(), rec);
794 }
795 if let Some(v) = h.take_validator() {
796 validators.push((name.clone(), v));
797 }
798 if let Some((period, cb)) = h.take_scan() {
799 self.inner.scans.push((name.clone(), period, cb));
800 }
801 if let Some((inputs, compute)) = h.take_calc() {
802 self.inner.links.push(LinkDef {
803 output: name.clone(),
804 inputs,
805 compute,
806 });
807 }
808 }
809 let server = self.inner.build();
810 let store = server.store().clone();
811 for h in &self.handles {
812 h.bind(&store);
813 }
814 for (name, v) in validators {
815 store.set_validator(name, v).await;
816 }
817 server
818 }
819
820 pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
822 self.build().await.run().await
823 }
824
825 pub async fn start(self) -> RunningServer {
827 let server = self.build().await;
828 let store = server.store().clone();
829 let handle = tokio::spawn(async move {
830 if let Err(e) = server.run().await {
831 tracing::error!("PvaServer exited with error: {e}");
832 }
833 });
834 RunningServer { store, handle }
835 }
836}
837
838pub struct RunningServer {
840 store: Arc<SimplePvStore>,
841 handle: tokio::task::JoinHandle<()>,
842}
843
844impl RunningServer {
845 pub async fn pv<T: crate::pv::PvScalar>(
847 &self,
848 name: &str,
849 ) -> Result<crate::pv::Pv<T>, crate::pv::PvError> {
850 crate::pv::Pv::attach(&self.store, name).await
851 }
852
853 pub async fn array_pv(&self, name: &str) -> Result<crate::pv::PvArray, crate::pv::PvError> {
855 crate::pv::PvArray::attach(&self.store, name).await
856 }
857
858 pub fn store(&self) -> &Arc<SimplePvStore> {
859 &self.store
860 }
861
862 pub fn abort(&self) {
863 self.handle.abort();
864 }
865}
866
867pub(crate) fn make_scalar_record(
870 name: &str,
871 record_type: RecordType,
872 value: ScalarValue,
873) -> RecordInstance {
874 let nt = NtScalar::from_value(value);
875 let data = match record_type {
876 RecordType::Ai => RecordData::Ai {
877 nt,
878 inp: None,
879 siml: None,
880 siol: None,
881 simm: false,
882 },
883 RecordType::Bi => RecordData::Bi {
884 nt,
885 inp: None,
886 znam: "Off".to_string(),
887 onam: "On".to_string(),
888 siml: None,
889 siol: None,
890 simm: false,
891 },
892 RecordType::StringIn => RecordData::StringIn {
893 nt,
894 inp: None,
895 siml: None,
896 siol: None,
897 simm: false,
898 },
899 RecordType::LongIn => RecordData::Ai {
901 nt,
902 inp: None,
903 siml: None,
904 siol: None,
905 simm: false,
906 },
907 _ => panic!("make_scalar_record: unsupported type {record_type:?}"),
908 };
909 RecordInstance {
910 name: name.to_string(),
911 record_type,
912 common: DbCommonState::default(),
913 data,
914 raw_fields: HashMap::new(),
915 }
916}
917
918pub(crate) fn make_output_record(
919 name: &str,
920 record_type: RecordType,
921 value: ScalarValue,
922) -> RecordInstance {
923 let nt = NtScalar::from_value(value);
924 let data = match record_type {
925 RecordType::Ao => RecordData::Ao {
926 nt,
927 out: None,
928 dol: None,
929 omsl: OutputMode::Supervisory,
930 drvl: None,
931 drvh: None,
932 oroc: None,
933 siml: None,
934 siol: None,
935 simm: false,
936 },
937 RecordType::Bo => RecordData::Bo {
938 nt,
939 out: None,
940 dol: None,
941 omsl: OutputMode::Supervisory,
942 znam: "Off".to_string(),
943 onam: "On".to_string(),
944 siml: None,
945 siol: None,
946 simm: false,
947 },
948 RecordType::StringOut => RecordData::StringOut {
949 nt,
950 out: None,
951 dol: None,
952 omsl: OutputMode::Supervisory,
953 siml: None,
954 siol: None,
955 simm: false,
956 },
957 RecordType::LongOut => RecordData::Ao {
959 nt,
960 out: None,
961 dol: None,
962 omsl: OutputMode::Supervisory,
963 drvl: None,
964 drvh: None,
965 oroc: None,
966 siml: None,
967 siol: None,
968 simm: false,
969 },
970 _ => panic!("make_output_record: unsupported type {record_type:?}"),
971 };
972 RecordInstance {
973 name: name.to_string(),
974 record_type,
975 common: DbCommonState::default(),
976 data,
977 raw_fields: HashMap::new(),
978 }
979}
980
981pub(crate) fn make_array_record(
985 name: &str,
986 record_type: RecordType,
987 data: ScalarArrayValue,
988) -> RecordInstance {
989 let ftvl = data.type_label().trim_end_matches("[]").to_string();
990 let nelm = data.len();
991 let nt = NtScalarArray::from_value(data);
992 let record_data = match record_type {
993 RecordType::Waveform => RecordData::Waveform {
994 nt,
995 inp: None,
996 ftvl,
997 nelm,
998 nord: nelm,
999 },
1000 RecordType::Aai => RecordData::Aai {
1001 nt,
1002 inp: None,
1003 ftvl,
1004 nelm,
1005 nord: nelm,
1006 },
1007 RecordType::Aao => RecordData::Aao {
1008 nt,
1009 out: None,
1010 dol: None,
1011 omsl: OutputMode::Supervisory,
1012 ftvl,
1013 nelm,
1014 nord: nelm,
1015 },
1016 _ => panic!("make_array_record: unsupported type {record_type:?}"),
1017 };
1018 RecordInstance {
1019 name: name.to_string(),
1020 record_type,
1021 common: DbCommonState::default(),
1022 data: record_data,
1023 raw_fields: HashMap::new(),
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 #[test]
1032 fn builder_creates_records() {
1033 let server = PvaServer::builder()
1034 .ai("T:AI", 1.0)
1035 .ao("T:AO", 2.0)
1036 .bi("T:BI", true)
1037 .bo("T:BO", false)
1038 .string_in("T:SI", "hello")
1039 .string_out("T:SO", "world")
1040 .build();
1041
1042 let rt = tokio::runtime::Builder::new_current_thread()
1043 .enable_all()
1044 .build()
1045 .unwrap();
1046 let names = rt.block_on(server.store.pv_names());
1047 assert_eq!(names.len(), 6);
1048 }
1049
1050 #[test]
1051 fn builder_defaults() {
1052 let server = PvaServer::builder().build();
1053 assert_eq!(server.config.tcp_port, 5075);
1054 assert_eq!(server.config.udp_port, 5076);
1055 assert!(!server.config.compute_alarms);
1056 }
1057
1058 #[test]
1059 fn builder_port_override() {
1060 let server = PvaServer::builder().port(9075).udp_port(9076).build();
1061 assert_eq!(server.config.tcp_port, 9075);
1062 assert_eq!(server.config.udp_port, 9076);
1063 }
1064
1065 #[test]
1066 fn builder_db_string() {
1067 let db = r#"
1068 record(ai, "TEST:VAL") {
1069 field(VAL, "3.14")
1070 }
1071 "#;
1072 let server = PvaServer::builder().db_string(db).build();
1073 let rt = tokio::runtime::Builder::new_current_thread()
1074 .enable_all()
1075 .build()
1076 .unwrap();
1077 assert!(rt.block_on(server.store.get_value("TEST:VAL")).is_some());
1078 }
1079
1080 #[test]
1081 fn builder_waveform() {
1082 let data = ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]);
1083 let server = PvaServer::builder().waveform("T:WF", data).build();
1084 let rt = tokio::runtime::Builder::new_current_thread()
1085 .enable_all()
1086 .build()
1087 .unwrap();
1088 let names = rt.block_on(server.store.pv_names());
1089 assert!(names.contains(&"T:WF".to_string()));
1090 }
1091
1092 #[test]
1093 fn builder_scan_callback() {
1094 let server = PvaServer::builder()
1095 .ai("SCAN:V", 0.0)
1096 .scan("SCAN:V", Duration::from_secs(1), |_name| {
1097 ScalarValue::F64(42.0)
1098 })
1099 .build();
1100 assert_eq!(server.scans.len(), 1);
1101 }
1102
1103 #[test]
1104 fn builder_on_put_callback() {
1105 let server = PvaServer::builder()
1106 .ao("PUT:V", 0.0)
1107 .on_put("PUT:V", |_name, _val| {})
1108 .build();
1109 let rt = tokio::runtime::Builder::new_current_thread()
1112 .enable_all()
1113 .build()
1114 .unwrap();
1115 assert!(rt.block_on(server.store.get_value("PUT:V")).is_some());
1116 }
1117
1118 #[test]
1119 fn store_runtime_get_set() {
1120 let server = PvaServer::builder().ao("RT:V", 0.0).build();
1121 let rt = tokio::runtime::Builder::new_current_thread()
1122 .enable_all()
1123 .build()
1124 .unwrap();
1125 let store = server.store().clone();
1126 rt.block_on(async {
1127 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(0.0)));
1128 store.set_value("RT:V", ScalarValue::F64(99.0)).await;
1129 assert_eq!(store.get_value("RT:V").await, Some(ScalarValue::F64(99.0)));
1130 });
1131 }
1132
1133 #[test]
1134 fn link_propagates_on_set_value() {
1135 let server = PvaServer::builder()
1136 .ao("INPUT:A", 1.0)
1137 .ao("INPUT:B", 2.0)
1138 .ai("CALC:SUM", 0.0)
1139 .link("CALC:SUM", &["INPUT:A", "INPUT:B"], |values| {
1140 let a = match &values[0] {
1141 ScalarValue::F64(v) => *v,
1142 _ => 0.0,
1143 };
1144 let b = match &values[1] {
1145 ScalarValue::F64(v) => *v,
1146 _ => 0.0,
1147 };
1148 ScalarValue::F64(a + b)
1149 })
1150 .build();
1151
1152 let rt = tokio::runtime::Builder::new_current_thread()
1153 .enable_all()
1154 .build()
1155 .unwrap();
1156 let store = server.store().clone();
1157 rt.block_on(async {
1158 store.set_value("INPUT:A", ScalarValue::F64(10.0)).await;
1160 assert_eq!(
1161 store.get_value("CALC:SUM").await,
1162 Some(ScalarValue::F64(12.0))
1163 );
1164
1165 store.set_value("INPUT:B", ScalarValue::F64(5.0)).await;
1167 assert_eq!(
1168 store.get_value("CALC:SUM").await,
1169 Some(ScalarValue::F64(15.0))
1170 );
1171 });
1172 }
1173
1174 use crate::pv::{AnyPv, Pv};
1175
1176 #[tokio::test]
1177 async fn serve_builder_binds_handles_and_registers_everything() {
1178 let temp = Pv::ai("S:T", 22.5).mdel(0.1);
1179 let sp = Pv::ao("S:SP", 25.0).on_put(|_pv, _v: f64| Ok(()));
1180 let a = Pv::ai("S:A", 1.0);
1181 let b = Pv::ai("S:B", 2.0);
1182 let sum = Pv::calc("S:SUM", &[&a, &b], |vals| vals.iter().sum());
1183
1184 let server = PvaServer::serve([AnyPv::from(temp.clone()), AnyPv::from(sp)])
1188 .pvs([
1189 AnyPv::from(a.clone()),
1190 AnyPv::from(b),
1191 AnyPv::from(sum.clone()),
1192 ])
1193 .build()
1194 .await;
1195
1196 temp.set(23.0).await.unwrap();
1198 assert_eq!(temp.get().await, Ok(23.0));
1199
1200 a.set(10.0).await.unwrap();
1202 assert_eq!(sum.get().await, Ok(12.0));
1203
1204 let rec = server.store().get_record("S:T").await.unwrap();
1206 assert_eq!(rec.raw_fields.get("MDEL").map(String::as_str), Some("0.1"));
1207 }
1208
1209 #[tokio::test]
1210 async fn running_server_mints_handles_to_db_records() {
1211 let server = PvaServer::serve(Vec::<AnyPv>::new())
1216 .db_string("record(ao, \"DB:X\") {\n field(VAL, \"2.5\")\n}")
1217 .build()
1218 .await;
1219 let store = server.store().clone();
1220 let h: crate::pv::Pv<f64> = crate::pv::Pv::attach(&store, "DB:X").await.unwrap();
1221 assert_eq!(h.get().await, Ok(2.5));
1222 }
1223
1224 #[tokio::test]
1225 async fn homogeneous_iterator_feeds_serve_without_manual_erasure() {
1226 let bpms: Vec<Pv<f64>> = (0..100)
1227 .map(|i| Pv::ai(format!("BPM:{i:03}:X"), 0.0))
1228 .collect();
1229 let server = PvaServer::serve(bpms.iter().cloned()).build().await;
1230 assert_eq!(server.store().pv_names().await.len(), 100);
1231 bpms[42].set(1.23).await.unwrap();
1232 assert_eq!(bpms[42].get().await, Ok(1.23));
1233 }
1234
1235 #[tokio::test]
1236 async fn pva_server_mints_typed_handles_pre_run() {
1237 let server = PvaServer::serve([AnyPv::from(Pv::ai("PRE:X", 5.0))])
1238 .build()
1239 .await;
1240 let h: crate::pv::Pv<f64> = server.pv("PRE:X").await.unwrap();
1241 assert_eq!(h.get().await, Ok(5.0));
1242 assert!(matches!(
1243 server.pv::<bool>("PRE:X").await,
1244 Err(crate::pv::PvError::TypeMismatch { .. })
1245 ));
1246 }
1247}