1use std::sync::Arc;
2use std::time::Instant;
3
4use asyn_rs::param::ParamType;
5use asyn_rs::port::{PortDriver, PortDriverBase, PortFlags};
6use asyn_rs::runtime::config::RuntimeConfig;
7use asyn_rs::runtime::port::{PortRuntimeHandle, create_port_runtime, port_runtime_unavailable};
8use asyn_rs::user::AsynUser;
9use epics_base_rs::runtime::task::{MandatoryThread, StackSizeClass, ThreadPriority};
12use parking_lot::Mutex;
13
14pub const NUM_STATS_TS_CHANNELS: usize = 23;
18
19pub const STATS_TS_CHANNEL_NAMES: [&str; NUM_STATS_TS_CHANNELS] = [
21 "TSMinValue",
22 "TSMinX",
23 "TSMinY",
24 "TSMaxValue",
25 "TSMaxX",
26 "TSMaxY",
27 "TSMeanValue",
28 "TSSigma",
29 "TSTotal",
30 "TSNet",
31 "TSCentroidTotal",
32 "TSCentroidX",
33 "TSCentroidY",
34 "TSSigmaX",
35 "TSSigmaY",
36 "TSSigmaXY",
37 "TSSkewX",
38 "TSSkewY",
39 "TSKurtosisX",
40 "TSKurtosisY",
41 "TSEccentricity",
42 "TSOrientation",
43 "TSTimestamp",
44];
45
46pub struct TimeSeriesData {
51 pub values: Vec<f64>,
52}
53
54pub type TimeSeriesSender = tokio::sync::mpsc::Sender<TimeSeriesData>;
56pub type TimeSeriesReceiver = tokio::sync::mpsc::Receiver<TimeSeriesData>;
58
59pub struct TsReceiverRegistry {
62 inner: std::sync::Mutex<std::collections::HashMap<String, (TimeSeriesReceiver, Vec<String>)>>,
63}
64
65impl TsReceiverRegistry {
66 pub fn new() -> Self {
67 Self {
68 inner: std::sync::Mutex::new(std::collections::HashMap::new()),
69 }
70 }
71
72 pub fn store(
74 &self,
75 upstream_port: &str,
76 receiver: TimeSeriesReceiver,
77 channel_names: Vec<String>,
78 ) {
79 let mut map = self.inner.lock().unwrap();
80 map.insert(upstream_port.to_string(), (receiver, channel_names));
81 }
82
83 pub fn take(&self, upstream_port: &str) -> Option<(TimeSeriesReceiver, Vec<String>)> {
85 let mut map = self.inner.lock().unwrap();
86 map.remove(upstream_port)
87 }
88}
89
90impl Default for TsReceiverRegistry {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum TimeSeriesMode {
103 OneShot,
105 RingBuffer,
107}
108
109pub struct TimeSeries {
111 pub num_points: usize,
112 pub mode: TimeSeriesMode,
113 buffer: Vec<f64>,
114 write_pos: usize,
115 count: usize,
116}
117
118impl TimeSeries {
119 pub fn new(num_points: usize, mode: TimeSeriesMode) -> Self {
120 Self {
121 num_points,
122 mode,
123 buffer: vec![0.0; num_points],
124 write_pos: 0,
125 count: 0,
126 }
127 }
128
129 pub fn add_value(&mut self, value: f64) {
131 match self.mode {
132 TimeSeriesMode::OneShot => {
133 if self.write_pos < self.num_points {
134 self.buffer[self.write_pos] = value;
135 self.write_pos += 1;
136 self.count = self.write_pos;
137 }
138 }
139 TimeSeriesMode::RingBuffer => {
140 self.buffer[self.write_pos % self.num_points] = value;
141 self.write_pos += 1;
142 self.count = self.count.max(self.write_pos.min(self.num_points));
143 }
144 }
145 }
146
147 pub fn values(&self) -> Vec<f64> {
149 match self.mode {
150 TimeSeriesMode::OneShot => self.buffer[..self.count].to_vec(),
151 TimeSeriesMode::RingBuffer => {
152 if self.write_pos <= self.num_points {
153 self.buffer[..self.count].to_vec()
154 } else {
155 let start = self.write_pos % self.num_points;
156 let mut result = Vec::with_capacity(self.num_points);
157 result.extend_from_slice(&self.buffer[start..]);
158 result.extend_from_slice(&self.buffer[..start]);
159 result
160 }
161 }
162 }
163 }
164
165 pub fn count(&self) -> usize {
166 self.count
167 }
168
169 pub fn reset(&mut self) {
170 self.buffer.fill(0.0);
171 self.write_pos = 0;
172 self.count = 0;
173 }
174
175 pub fn resize(&mut self, num_points: usize) {
177 self.num_points = num_points;
178 self.buffer = vec![0.0; num_points];
179 self.write_pos = 0;
180 self.count = 0;
181 }
182
183 pub fn set_mode(&mut self, mode: TimeSeriesMode) {
185 self.mode = mode;
186 self.reset();
187 }
188}
189
190pub struct TSParams {
194 pub ts_acquire: usize,
195 pub ts_read: usize,
196 pub ts_num_points: usize,
197 pub ts_current_point: usize,
198 pub ts_time_per_point: usize,
199 pub ts_averaging_time: usize,
200 pub ts_num_average: usize,
201 pub ts_elapsed_time: usize,
202 pub ts_acquire_mode: usize,
203 pub ts_time_axis: usize,
204 pub ts_channels: Vec<usize>,
206 pub channel_names: Vec<String>,
208 pub ts_time_series: usize,
210 pub ts_timestamp: usize,
212}
213
214pub struct SharedTsState {
216 pub buffers: Vec<TimeSeries>,
217 pub acquiring: bool,
218 pub start_time: Option<Instant>,
219 pub num_points: usize,
220 pub mode: TimeSeriesMode,
221 pub num_average: usize,
224 average_store: Vec<f64>,
227 num_averaged: usize,
230}
231
232impl SharedTsState {
233 fn new(num_channels: usize, num_points: usize) -> Self {
234 let buffers = (0..num_channels)
235 .map(|_| TimeSeries::new(num_points, TimeSeriesMode::OneShot))
236 .collect();
237 Self {
238 buffers,
239 acquiring: false,
240 start_time: None,
241 num_points,
242 mode: TimeSeriesMode::OneShot,
243 num_average: 1,
244 average_store: vec![0.0; num_channels],
245 num_averaged: 0,
246 }
247 }
248
249 fn reset_average(&mut self) {
252 for v in &mut self.average_store {
253 *v = 0.0;
254 }
255 self.num_averaged = 0;
256 }
257
258 fn accumulate(&mut self, values: &[f64]) -> bool {
265 let n = values.len().min(self.average_store.len());
266 for i in 0..n {
267 self.average_store[i] += values[i];
268 }
269 self.num_averaged += 1;
270 if self.num_averaged < self.num_average.max(1) {
271 return false;
272 }
273 let divisor = self.num_averaged as f64;
274 let nb = n.min(self.buffers.len());
275 for i in 0..nb {
276 self.buffers[i].add_value(self.average_store[i] / divisor);
277 }
278 self.reset_average();
279 true
280 }
281}
282
283pub struct TimeSeriesPortDriver {
288 base: PortDriverBase,
289 params: TSParams,
290 shared: Arc<Mutex<SharedTsState>>,
291 num_channels: usize,
292 time_per_point: f64,
293}
294
295impl TimeSeriesPortDriver {
296 fn new(
297 port_name: &str,
298 channel_names: &[&str],
299 num_points: usize,
300 shared: Arc<Mutex<SharedTsState>>,
301 ) -> Self {
302 let num_channels = channel_names.len();
303 let mut base = PortDriverBase::new(
304 port_name,
305 1,
306 PortFlags {
307 multi_device: false,
308 can_block: false,
309 destructible: true,
310 },
311 );
312
313 let nd_params =
315 ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base).unwrap();
316 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
317 let _ = ad_core_rs::driver::ndarray_driver::init_read_only_params(
324 &mut base, &nd_params, port_name,
325 );
326
327 let ts_acquire = base.create_param("TS_ACQUIRE", ParamType::Int32).unwrap();
329 let _ = base.set_int32_param(ts_acquire, 0, 0);
330 let ts_read = base.create_param("TS_READ", ParamType::Int32).unwrap();
331 let ts_num_points = base
332 .create_param("TS_NUM_POINTS", ParamType::Int32)
333 .unwrap();
334 let _ = base.set_int32_param(ts_num_points, 0, num_points as i32);
335 let ts_current_point = base
336 .create_param("TS_CURRENT_POINT", ParamType::Int32)
337 .unwrap();
338 let _ = base.set_int32_param(ts_current_point, 0, 0);
339 let ts_time_per_point = base
340 .create_param("TS_TIME_PER_POINT", ParamType::Float64)
341 .unwrap();
342 let ts_averaging_time = base
343 .create_param("TS_AVERAGING_TIME", ParamType::Float64)
344 .unwrap();
345 let ts_num_average = base
346 .create_param("TS_NUM_AVERAGE", ParamType::Int32)
347 .unwrap();
348 let _ = base.set_int32_param(ts_num_average, 0, 1);
349 let ts_elapsed_time = base
350 .create_param("TS_ELAPSED_TIME", ParamType::Float64)
351 .unwrap();
352 let ts_acquire_mode = base
353 .create_param("TS_ACQUIRE_MODE", ParamType::Int32)
354 .unwrap();
355 let _ = base.set_int32_param(ts_acquire_mode, 0, 0);
356 let ts_time_axis = base
357 .create_param("TS_TIME_AXIS", ParamType::Float64Array)
358 .unwrap();
359
360 let time_per_point = 1.0;
362 let time_axis: Vec<f64> = (0..num_points).map(|i| i as f64 * time_per_point).collect();
363 let _ = base.params.set_float64_array(ts_time_axis, 0, time_axis);
364
365 let mut ts_channels = Vec::with_capacity(num_channels);
367 for name in channel_names {
368 let param_name = format!("TS_CHAN_{name}");
369 let idx = base
370 .create_param(¶m_name, ParamType::Float64Array)
371 .unwrap();
372 let _ = base.params.set_float64_array(idx, 0, vec![0.0; num_points]);
373 ts_channels.push(idx);
374 }
375
376 let ts_time_series = base
378 .create_param("TS_TIME_SERIES", ParamType::Float64Array)
379 .unwrap();
380 let ts_timestamp = base
381 .create_param("TS_TIMESTAMP", ParamType::Float64Array)
382 .unwrap();
383
384 let params = TSParams {
385 ts_acquire,
386 ts_read,
387 ts_num_points,
388 ts_current_point,
389 ts_time_per_point,
390 ts_averaging_time,
391 ts_num_average,
392 ts_elapsed_time,
393 ts_acquire_mode,
394 ts_time_axis,
395 ts_channels,
396 channel_names: channel_names.iter().map(|s| s.to_string()).collect(),
397 ts_time_series,
398 ts_timestamp,
399 };
400
401 Self {
402 base,
403 params,
404 shared,
405 num_channels,
406 time_per_point,
407 }
408 }
409
410 fn build_time_axis(&self, num_points: usize, mode: TimeSeriesMode) -> Vec<f64> {
417 (0..num_points)
418 .map(|i| match mode {
419 TimeSeriesMode::OneShot => i as f64 * self.time_per_point,
420 TimeSeriesMode::RingBuffer => {
421 -((num_points.saturating_sub(1) - i) as f64) * self.time_per_point
422 }
423 })
424 .collect()
425 }
426
427 fn refresh_time_axis(&mut self) {
429 let (num_points, mode) = {
430 let s = self.shared.lock();
431 (s.num_points, s.mode)
432 };
433 let axis = self.build_time_axis(num_points, mode);
434 let _ = self
435 .base
436 .params
437 .set_float64_array(self.params.ts_time_axis, 0, axis);
438 }
439
440 fn update_waveform_params(&mut self) {
442 let state = self.shared.lock();
443 let num_points = state.num_points;
444
445 for (i, buf) in state.buffers.iter().enumerate() {
447 let mut values = buf.values();
448 values.resize(num_points, 0.0);
449 let _ = self
450 .base
451 .params
452 .set_float64_array(self.params.ts_channels[i], 0, values);
453 }
454
455 let current_point = state.buffers[0].count();
457 let _ = self
458 .base
459 .set_int32_param(self.params.ts_current_point, 0, current_point as i32);
460
461 if let Some(start) = state.start_time {
463 let elapsed = start.elapsed().as_secs_f64();
464 let _ = self
465 .base
466 .set_float64_param(self.params.ts_elapsed_time, 0, elapsed);
467 }
468
469 let acquiring = state.acquiring;
471 drop(state);
472
473 let _ = self
474 .base
475 .set_int32_param(self.params.ts_acquire, 0, if acquiring { 1 } else { 0 });
476
477 let _ = self.base.call_param_callbacks(0);
479 }
480}
481
482impl PortDriver for TimeSeriesPortDriver {
483 fn base(&self) -> &PortDriverBase {
484 &self.base
485 }
486
487 fn base_mut(&mut self) -> &mut PortDriverBase {
488 &mut self.base
489 }
490
491 fn write_int32(&mut self, user: &mut AsynUser, value: i32) -> asyn_rs::error::AsynResult<()> {
492 let reason = user.reason;
493
494 if reason == self.params.ts_acquire {
495 let mut state = self.shared.lock();
496 if value != 0 {
497 if !state.acquiring {
499 if state.buffers[0].count() == 0 {
501 for buf in state.buffers.iter_mut() {
502 buf.reset();
503 }
504 }
505 state.reset_average();
508 state.acquiring = true;
509 state.start_time = Some(Instant::now());
510 }
511 } else {
512 state.acquiring = false;
514 }
515 drop(state);
516 self.base.set_int32_param(reason, 0, value)?;
517 self.base.call_param_callbacks(0)?;
518 } else if reason == self.params.ts_read {
519 self.update_waveform_params();
521 } else if reason == self.params.ts_num_points {
522 let new_size = value.max(1) as usize;
523 {
524 let mut state = self.shared.lock();
525 state.num_points = new_size;
526 for buf in state.buffers.iter_mut() {
527 buf.resize(new_size);
528 }
529 state.reset_average();
530 state.acquiring = false;
531 }
532
533 self.refresh_time_axis();
535
536 for i in 0..self.num_channels {
538 let _ = self.base.params.set_float64_array(
539 self.params.ts_channels[i],
540 0,
541 vec![0.0; new_size],
542 );
543 }
544
545 self.base.set_int32_param(reason, 0, value)?;
546 self.base
547 .set_int32_param(self.params.ts_current_point, 0, 0)?;
548 self.base.set_int32_param(self.params.ts_acquire, 0, 0)?;
549 self.base.call_param_callbacks(0)?;
550 } else if reason == self.params.ts_num_average {
551 let n = value.max(1) as usize;
554 {
555 let mut state = self.shared.lock();
556 state.num_average = n;
557 state.reset_average();
558 }
559 self.base.set_int32_param(reason, 0, n as i32)?;
560 self.base.call_param_callbacks(0)?;
561 } else if reason == self.params.ts_acquire_mode {
562 let mode = if value == 0 {
564 TimeSeriesMode::OneShot
565 } else {
566 TimeSeriesMode::RingBuffer
567 };
568 {
569 let mut state = self.shared.lock();
570 state.mode = mode;
571 for buf in state.buffers.iter_mut() {
572 buf.set_mode(mode);
573 }
574 state.reset_average();
575 state.acquiring = false;
576 }
577 self.refresh_time_axis();
579
580 self.base.set_int32_param(reason, 0, value)?;
581 self.base.set_int32_param(self.params.ts_acquire, 0, 0)?;
582 self.base.call_param_callbacks(0)?;
583 } else {
584 self.base.set_int32_param(reason, user.addr, value)?;
586 self.base.call_param_callbacks(user.addr)?;
587 }
588
589 Ok(())
590 }
591
592 fn write_float64(&mut self, user: &mut AsynUser, value: f64) -> asyn_rs::error::AsynResult<()> {
593 let reason = user.reason;
594 if reason == self.params.ts_time_per_point {
595 self.time_per_point = value;
596 self.base.set_float64_param(reason, user.addr, value)?;
597 self.refresh_time_axis();
599 self.base.call_param_callbacks(user.addr)?;
600 } else {
601 self.base.set_float64_param(reason, user.addr, value)?;
602 self.base.call_param_callbacks(user.addr)?;
603 }
604 Ok(())
605 }
606
607 fn read_float64_array(
608 &mut self,
609 user: &AsynUser,
610 buf: &mut [f64],
611 ) -> asyn_rs::error::AsynResult<usize> {
612 let data = self.base.params.get_float64_array(user.reason, user.addr)?;
613 let n = data.len().min(buf.len());
614 buf[..n].copy_from_slice(&data[..n]);
615 Ok(n)
616 }
617}
618
619fn ts_data_thread(shared: Arc<Mutex<SharedTsState>>, mut data_rx: TimeSeriesReceiver) {
628 while let Some(data) = data_rx.blocking_recv() {
629 let mut state = shared.lock();
630 if !state.acquiring {
631 continue;
632 }
633 let emitted = state.accumulate(&data.values);
634 if emitted
637 && state.mode == TimeSeriesMode::OneShot
638 && state.buffers[0].count() >= state.num_points
639 {
640 state.acquiring = false;
641 }
642 }
643}
644
645pub fn create_ts_port_runtime(
651 port_name: &str,
652 channel_names: &[&str],
653 num_points: usize,
654 data_rx: TimeSeriesReceiver,
655) -> (
656 PortRuntimeHandle,
657 TSParams,
658 std::thread::JoinHandle<()>,
659 std::thread::JoinHandle<()>,
660) {
661 let num_channels = channel_names.len();
662 let shared = Arc::new(Mutex::new(SharedTsState::new(num_channels, num_points)));
663
664 let driver = TimeSeriesPortDriver::new(port_name, channel_names, num_points, shared.clone());
665
666 let ts_params = TSParams {
668 ts_acquire: driver.params.ts_acquire,
669 ts_read: driver.params.ts_read,
670 ts_num_points: driver.params.ts_num_points,
671 ts_current_point: driver.params.ts_current_point,
672 ts_time_per_point: driver.params.ts_time_per_point,
673 ts_averaging_time: driver.params.ts_averaging_time,
674 ts_num_average: driver.params.ts_num_average,
675 ts_elapsed_time: driver.params.ts_elapsed_time,
676 ts_acquire_mode: driver.params.ts_acquire_mode,
677 ts_time_axis: driver.params.ts_time_axis,
678 ts_channels: driver.params.ts_channels.clone(),
679 channel_names: driver.params.channel_names.clone(),
680 ts_time_series: driver.params.ts_time_series,
681 ts_timestamp: driver.params.ts_timestamp,
682 };
683
684 let (runtime_handle, actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
691 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
692
693 let data_jh = MandatoryThread::new(
700 format!("ts-data-{port_name}"),
701 ThreadPriority::Medium,
702 StackSizeClass::Medium,
703 )
704 .spawn(move || {
705 ts_data_thread(shared, data_rx);
706 });
707
708 (runtime_handle, ts_params, actor_jh, data_jh)
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use source_guard::{Comments, production};
715
716 #[test]
731 fn the_ts_data_thread_is_mandatory() {
732 let src = include_str!("time_series.rs");
733 let prod = production(src, Comments::Strip);
734 assert_eq!(prod.matches("MandatoryThread::new(").count(), 1);
735 let strays: Vec<&str> = prod
736 .lines()
737 .map(str::trim)
738 .filter(|l| {
739 l.contains(concat!("thread", "::Builder::new()"))
740 || l.contains(concat!("thread", "::spawn("))
741 })
742 .collect();
743 assert!(
744 strays.is_empty(),
745 "a data thread created outside `MandatoryThread` resolves its own \
746 spawn failure locally: {strays:?}"
747 );
748 }
749
750 #[test]
751 fn test_one_shot() {
752 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
753 for i in 0..5 {
754 ts.add_value(i as f64);
755 }
756 assert_eq!(ts.count(), 5);
757 assert_eq!(ts.values(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
758
759 ts.add_value(99.0);
761 assert_eq!(ts.count(), 5);
762 }
763
764 #[test]
765 fn test_ring_buffer() {
766 let mut ts = TimeSeries::new(4, TimeSeriesMode::RingBuffer);
767 for i in 0..6 {
768 ts.add_value(i as f64);
769 }
770 assert_eq!(ts.count(), 4);
771 assert_eq!(ts.values(), vec![2.0, 3.0, 4.0, 5.0]);
773 }
774
775 #[test]
776 fn test_ring_buffer_partial() {
777 let mut ts = TimeSeries::new(4, TimeSeriesMode::RingBuffer);
778 ts.add_value(10.0);
779 ts.add_value(20.0);
780 assert_eq!(ts.count(), 2);
781 assert_eq!(ts.values(), vec![10.0, 20.0]);
782 }
783
784 #[test]
785 fn test_reset() {
786 let mut ts = TimeSeries::new(3, TimeSeriesMode::OneShot);
787 ts.add_value(1.0);
788 ts.add_value(2.0);
789 ts.reset();
790 assert_eq!(ts.count(), 0);
791 assert!(ts.values().is_empty());
792 }
793
794 #[test]
795 fn test_resize() {
796 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
797 ts.add_value(1.0);
798 ts.add_value(2.0);
799 ts.resize(3);
800 assert_eq!(ts.num_points, 3);
801 assert_eq!(ts.count(), 0);
802 assert!(ts.values().is_empty());
803 }
804
805 #[test]
806 fn test_set_mode() {
807 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
808 ts.add_value(1.0);
809 ts.set_mode(TimeSeriesMode::RingBuffer);
810 assert_eq!(ts.mode, TimeSeriesMode::RingBuffer);
811 assert_eq!(ts.count(), 0);
812 }
813
814 const TEST_CHANNELS: [&str; 3] = ["ChA", "ChB", "ChC"];
817
818 #[test]
819 fn test_shared_ts_state_init() {
820 let state = SharedTsState::new(3, 100);
821 assert_eq!(state.buffers.len(), 3);
822 assert_eq!(state.num_points, 100);
823 assert!(!state.acquiring);
824 assert_eq!(state.mode, TimeSeriesMode::OneShot);
825 }
826
827 #[test]
828 fn test_ts_port_driver_create() {
829 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
830 let driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared);
831 assert_eq!(driver.base().port_name, "TEST_TS");
832 assert_eq!(driver.num_channels, 3);
833 assert!(!driver.base().flags.multi_device);
834 }
835
836 #[test]
837 fn test_ts_port_driver_seeds_the_c_read_only_block() {
838 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
845 let driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared);
846 let base = driver.base();
847 for (name, want) in [
848 ("ARRAY_SIZE_X", 0),
849 ("ARRAY_COUNTER", 0),
850 ("COLOR_MODE", ad_core_rs::color::NDColorMode::Mono as i32),
851 (
852 "ND_ATTRIBUTES_STATUS",
853 ad_core_rs::driver::ndarray_driver::ATTR_STATUS_FILE_NOT_FOUND,
854 ),
855 ("NUM_QUEUED_ARRAYS", 0),
856 ] {
857 let index = base
858 .find_param(name)
859 .unwrap_or_else(|| panic!("{name} missing"));
860 assert_eq!(
861 base.get_int32_param_strict(index, 0)
862 .unwrap_or_else(|e| panic!("{name} unset after construction: {e:?}")),
863 want,
864 "{name}"
865 );
866 }
867 let template = base.find_param("FILE_TEMPLATE").unwrap();
868 assert_eq!(
869 base.get_string_param_strict(template, 0).unwrap(),
870 b"%s%s_%3.3d.dat"
871 );
872 let self_name = base.find_param("PORT_NAME_SELF").unwrap();
873 assert_eq!(
874 base.get_string_param_strict(self_name, 0).unwrap(),
875 b"TEST_TS"
876 );
877 }
878
879 #[test]
880 fn test_ts_port_driver_write_acquire() {
881 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
882 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
883
884 let mut user = AsynUser::new(driver.params.ts_acquire);
886 driver.write_int32(&mut user, 1).unwrap();
887 assert!(shared.lock().acquiring);
888
889 driver.write_int32(&mut user, 0).unwrap();
891 assert!(!shared.lock().acquiring);
892 }
893
894 #[test]
895 fn test_ts_port_driver_write_num_points() {
896 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
897 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
898
899 let mut user = AsynUser::new(driver.params.ts_num_points);
900 driver.write_int32(&mut user, 50).unwrap();
901
902 let state = shared.lock();
903 assert_eq!(state.num_points, 50);
904 for buf in &state.buffers {
905 assert_eq!(buf.num_points, 50);
906 }
907 }
908
909 #[test]
910 fn test_ts_port_driver_write_mode() {
911 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
912 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
913
914 let mut user = AsynUser::new(driver.params.ts_acquire_mode);
915 driver.write_int32(&mut user, 1).unwrap();
916
917 let state = shared.lock();
918 assert_eq!(state.mode, TimeSeriesMode::RingBuffer);
919 for buf in &state.buffers {
920 assert_eq!(buf.mode, TimeSeriesMode::RingBuffer);
921 }
922 }
923
924 #[test]
925 fn test_ts_port_driver_update_waveforms() {
926 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 10)));
927 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 10, shared.clone());
928
929 {
931 let mut state = shared.lock();
932 state.acquiring = true;
933 state.start_time = Some(Instant::now());
934 for buf in state.buffers.iter_mut() {
935 buf.add_value(42.0);
936 buf.add_value(43.0);
937 }
938 }
939
940 driver.update_waveform_params();
942
943 let cp = driver
945 .base
946 .get_int32_param(driver.params.ts_current_point, 0)
947 .unwrap();
948 assert_eq!(cp, 2);
949
950 let data = driver
952 .base
953 .params
954 .get_float64_array(driver.params.ts_channels[0], 0)
955 .unwrap();
956 assert_eq!(data[0], 42.0);
957 assert_eq!(data[1], 43.0);
958 }
959
960 #[test]
961 fn test_ts_port_driver_read_array() {
962 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 5)));
963 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 5, shared);
964
965 let user = AsynUser::new(driver.params.ts_time_axis);
966 let mut buf = vec![0.0; 5];
967 let n = driver.read_float64_array(&user, &mut buf).unwrap();
968 assert_eq!(n, 5);
969 assert_eq!(buf, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
970 }
971
972 #[test]
973 fn test_ts_data_ingestion_oneshot() {
974 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 3)));
975 let (tx, rx) = tokio::sync::mpsc::channel(16);
976
977 shared.lock().acquiring = true;
979
980 let shared_clone = shared.clone();
981 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
982
983 tx.blocking_send(TimeSeriesData {
985 values: vec![1.0, 10.0, 100.0],
986 })
987 .unwrap();
988 tx.blocking_send(TimeSeriesData {
989 values: vec![2.0, 20.0, 200.0],
990 })
991 .unwrap();
992 tx.blocking_send(TimeSeriesData {
993 values: vec![3.0, 30.0, 300.0],
994 })
995 .unwrap();
996 tx.blocking_send(TimeSeriesData {
997 values: vec![4.0, 40.0, 400.0],
998 })
999 .unwrap(); drop(tx);
1003 jh.join().unwrap();
1004
1005 let state = shared.lock();
1006 assert_eq!(state.buffers[0].count(), 3);
1007 assert_eq!(state.buffers[0].values(), vec![1.0, 2.0, 3.0]);
1008 assert_eq!(state.buffers[1].values(), vec![10.0, 20.0, 30.0]);
1009 assert_eq!(state.buffers[2].values(), vec![100.0, 200.0, 300.0]);
1010 assert!(!state.acquiring); }
1012
1013 #[test]
1014 fn test_ts_data_ingestion_not_acquiring() {
1015 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 10)));
1016 let (tx, rx) = tokio::sync::mpsc::channel(16);
1017
1018 let shared_clone = shared.clone();
1020 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1021
1022 tx.blocking_send(TimeSeriesData {
1023 values: vec![1.0, 2.0, 3.0],
1024 })
1025 .unwrap();
1026
1027 drop(tx);
1028 jh.join().unwrap();
1029
1030 let state = shared.lock();
1031 assert_eq!(state.buffers[0].count(), 0);
1032 }
1033
1034 #[test]
1035 fn test_num_average_averages_input_samples() {
1036 let mut state = SharedTsState::new(1, 10);
1039 state.num_average = 3;
1040 assert!(!state.accumulate(&[0.0]));
1041 assert!(!state.accumulate(&[1.0]));
1042 assert!(state.accumulate(&[2.0])); assert!(!state.accumulate(&[3.0]));
1044 assert!(!state.accumulate(&[4.0]));
1045 assert!(state.accumulate(&[5.0])); let vals = state.buffers[0].values();
1047 assert_eq!(vals.len(), 2);
1048 assert!((vals[0] - 1.0).abs() < 1e-10);
1049 assert!((vals[1] - 4.0).abs() < 1e-10);
1050 }
1051
1052 #[test]
1053 fn test_num_average_one_is_passthrough() {
1054 let mut state = SharedTsState::new(2, 10);
1056 state.num_average = 1;
1057 assert!(state.accumulate(&[5.0, 50.0]));
1058 assert!(state.accumulate(&[6.0, 60.0]));
1059 assert_eq!(state.buffers[0].values(), vec![5.0, 6.0]);
1060 assert_eq!(state.buffers[1].values(), vec![50.0, 60.0]);
1061 }
1062
1063 #[test]
1064 fn test_num_average_drives_ingestion_thread() {
1065 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 5)));
1067 {
1068 let mut s = shared.lock();
1069 s.num_average = 2;
1070 s.acquiring = true;
1071 }
1072 let (tx, rx) = tokio::sync::mpsc::channel(16);
1073 let shared_clone = shared.clone();
1074 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1075
1076 for v in [10.0, 20.0, 30.0, 40.0] {
1077 tx.blocking_send(TimeSeriesData { values: vec![v] })
1078 .unwrap();
1079 }
1080 drop(tx);
1081 jh.join().unwrap();
1082
1083 let state = shared.lock();
1085 let vals = state.buffers[0].values();
1086 assert_eq!(vals.len(), 2);
1087 assert!((vals[0] - 15.0).abs() < 1e-10);
1088 assert!((vals[1] - 35.0).abs() < 1e-10);
1089 }
1090
1091 #[test]
1092 fn test_fixed_mode_stops_at_num_points() {
1093 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 3)));
1096 {
1097 let mut s = shared.lock();
1098 s.num_average = 1;
1099 s.mode = TimeSeriesMode::OneShot;
1100 s.acquiring = true;
1101 }
1102 let (tx, rx) = tokio::sync::mpsc::channel(16);
1103 let shared_clone = shared.clone();
1104 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1105 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
1106 tx.blocking_send(TimeSeriesData { values: vec![v] })
1107 .unwrap();
1108 }
1109 drop(tx);
1110 jh.join().unwrap();
1111
1112 let state = shared.lock();
1113 assert!(!state.acquiring, "Fixed mode must auto-stop");
1114 assert_eq!(state.buffers[0].count(), 3);
1115 assert_eq!(state.buffers[0].values(), vec![1.0, 2.0, 3.0]);
1116 }
1117
1118 #[test]
1119 fn test_circular_mode_wraps_and_keeps_acquiring() {
1120 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 3)));
1123 {
1124 let mut s = shared.lock();
1125 s.num_average = 1;
1126 s.mode = TimeSeriesMode::RingBuffer;
1127 for buf in s.buffers.iter_mut() {
1128 buf.set_mode(TimeSeriesMode::RingBuffer);
1129 }
1130 s.acquiring = true;
1131 }
1132 let (tx, rx) = tokio::sync::mpsc::channel(16);
1133 let shared_clone = shared.clone();
1134 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1135 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
1136 tx.blocking_send(TimeSeriesData { values: vec![v] })
1137 .unwrap();
1138 }
1139 drop(tx);
1140 jh.join().unwrap();
1141
1142 let state = shared.lock();
1143 assert!(state.acquiring, "Circular mode must keep acquiring");
1144 assert_eq!(state.buffers[0].values(), vec![3.0, 4.0, 5.0]);
1146 }
1147
1148 #[test]
1149 fn test_acquire_mode_param_drives_behavior_and_axis() {
1150 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 4)));
1153 let mut driver = TimeSeriesPortDriver::new("TEST_TS_MODE", &["Ch0"], 4, shared.clone());
1154
1155 let axis = driver
1157 .base
1158 .params
1159 .get_float64_array(driver.params.ts_time_axis, 0)
1160 .unwrap();
1161 assert_eq!(&*axis, &[0.0, 1.0, 2.0, 3.0]);
1162
1163 let mut user = AsynUser::new(driver.params.ts_acquire_mode);
1165 driver.write_int32(&mut user, 1).unwrap();
1166 assert_eq!(shared.lock().mode, TimeSeriesMode::RingBuffer);
1167
1168 let axis = driver
1170 .base
1171 .params
1172 .get_float64_array(driver.params.ts_time_axis, 0)
1173 .unwrap();
1174 assert_eq!(&*axis, &[-3.0, -2.0, -1.0, 0.0]);
1175 }
1176
1177 #[test]
1178 fn test_num_average_param_drives_state() {
1179 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 10)));
1181 let mut driver = TimeSeriesPortDriver::new("TEST_TS_NAVG", &["Ch0"], 10, shared.clone());
1182 let mut user = AsynUser::new(driver.params.ts_num_average);
1183 driver.write_int32(&mut user, 5).unwrap();
1184 assert_eq!(shared.lock().num_average, 5);
1185 driver.write_int32(&mut user, 0).unwrap();
1187 assert_eq!(shared.lock().num_average, 1);
1188 }
1189
1190 #[test]
1191 fn test_create_ts_port_runtime() {
1192 let (_tx, rx) = tokio::sync::mpsc::channel(16);
1193 let (handle, params, _actor_jh, _data_jh) =
1194 create_ts_port_runtime("TEST_TS_RT", &TEST_CHANNELS, 100, rx);
1195 assert_eq!(handle.port_name(), "TEST_TS_RT");
1196 assert_eq!(params.ts_channels.len(), 3);
1197 handle.shutdown();
1198 }
1199}