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 _ = ad_core_rs::params::ndarray_driver::NDArrayDriverParams::create(&mut base);
315 let _ = ad_core_rs::plugin::params::PluginBaseParams::create(&mut base);
316
317 let ts_acquire = base.create_param("TS_ACQUIRE", ParamType::Int32).unwrap();
319 let _ = base.set_int32_param(ts_acquire, 0, 0);
320 let ts_read = base.create_param("TS_READ", ParamType::Int32).unwrap();
321 let ts_num_points = base
322 .create_param("TS_NUM_POINTS", ParamType::Int32)
323 .unwrap();
324 let _ = base.set_int32_param(ts_num_points, 0, num_points as i32);
325 let ts_current_point = base
326 .create_param("TS_CURRENT_POINT", ParamType::Int32)
327 .unwrap();
328 let _ = base.set_int32_param(ts_current_point, 0, 0);
329 let ts_time_per_point = base
330 .create_param("TS_TIME_PER_POINT", ParamType::Float64)
331 .unwrap();
332 let ts_averaging_time = base
333 .create_param("TS_AVERAGING_TIME", ParamType::Float64)
334 .unwrap();
335 let ts_num_average = base
336 .create_param("TS_NUM_AVERAGE", ParamType::Int32)
337 .unwrap();
338 let _ = base.set_int32_param(ts_num_average, 0, 1);
339 let ts_elapsed_time = base
340 .create_param("TS_ELAPSED_TIME", ParamType::Float64)
341 .unwrap();
342 let ts_acquire_mode = base
343 .create_param("TS_ACQUIRE_MODE", ParamType::Int32)
344 .unwrap();
345 let _ = base.set_int32_param(ts_acquire_mode, 0, 0);
346 let ts_time_axis = base
347 .create_param("TS_TIME_AXIS", ParamType::Float64Array)
348 .unwrap();
349
350 let time_per_point = 1.0;
352 let time_axis: Vec<f64> = (0..num_points).map(|i| i as f64 * time_per_point).collect();
353 let _ = base.params.set_float64_array(ts_time_axis, 0, time_axis);
354
355 let mut ts_channels = Vec::with_capacity(num_channels);
357 for name in channel_names {
358 let param_name = format!("TS_CHAN_{name}");
359 let idx = base
360 .create_param(¶m_name, ParamType::Float64Array)
361 .unwrap();
362 let _ = base.params.set_float64_array(idx, 0, vec![0.0; num_points]);
363 ts_channels.push(idx);
364 }
365
366 let ts_time_series = base
368 .create_param("TS_TIME_SERIES", ParamType::Float64Array)
369 .unwrap();
370 let ts_timestamp = base
371 .create_param("TS_TIMESTAMP", ParamType::Float64Array)
372 .unwrap();
373
374 let params = TSParams {
375 ts_acquire,
376 ts_read,
377 ts_num_points,
378 ts_current_point,
379 ts_time_per_point,
380 ts_averaging_time,
381 ts_num_average,
382 ts_elapsed_time,
383 ts_acquire_mode,
384 ts_time_axis,
385 ts_channels,
386 channel_names: channel_names.iter().map(|s| s.to_string()).collect(),
387 ts_time_series,
388 ts_timestamp,
389 };
390
391 Self {
392 base,
393 params,
394 shared,
395 num_channels,
396 time_per_point,
397 }
398 }
399
400 fn build_time_axis(&self, num_points: usize, mode: TimeSeriesMode) -> Vec<f64> {
407 (0..num_points)
408 .map(|i| match mode {
409 TimeSeriesMode::OneShot => i as f64 * self.time_per_point,
410 TimeSeriesMode::RingBuffer => {
411 -((num_points.saturating_sub(1) - i) as f64) * self.time_per_point
412 }
413 })
414 .collect()
415 }
416
417 fn refresh_time_axis(&mut self) {
419 let (num_points, mode) = {
420 let s = self.shared.lock();
421 (s.num_points, s.mode)
422 };
423 let axis = self.build_time_axis(num_points, mode);
424 let _ = self
425 .base
426 .params
427 .set_float64_array(self.params.ts_time_axis, 0, axis);
428 }
429
430 fn update_waveform_params(&mut self) {
432 let state = self.shared.lock();
433 let num_points = state.num_points;
434
435 for (i, buf) in state.buffers.iter().enumerate() {
437 let mut values = buf.values();
438 values.resize(num_points, 0.0);
439 let _ = self
440 .base
441 .params
442 .set_float64_array(self.params.ts_channels[i], 0, values);
443 }
444
445 let current_point = state.buffers[0].count();
447 let _ = self
448 .base
449 .set_int32_param(self.params.ts_current_point, 0, current_point as i32);
450
451 if let Some(start) = state.start_time {
453 let elapsed = start.elapsed().as_secs_f64();
454 let _ = self
455 .base
456 .set_float64_param(self.params.ts_elapsed_time, 0, elapsed);
457 }
458
459 let acquiring = state.acquiring;
461 drop(state);
462
463 let _ = self
464 .base
465 .set_int32_param(self.params.ts_acquire, 0, if acquiring { 1 } else { 0 });
466
467 let _ = self.base.call_param_callbacks(0);
469 }
470}
471
472impl PortDriver for TimeSeriesPortDriver {
473 fn base(&self) -> &PortDriverBase {
474 &self.base
475 }
476
477 fn base_mut(&mut self) -> &mut PortDriverBase {
478 &mut self.base
479 }
480
481 fn write_int32(&mut self, user: &mut AsynUser, value: i32) -> asyn_rs::error::AsynResult<()> {
482 let reason = user.reason;
483
484 if reason == self.params.ts_acquire {
485 let mut state = self.shared.lock();
486 if value != 0 {
487 if !state.acquiring {
489 if state.buffers[0].count() == 0 {
491 for buf in state.buffers.iter_mut() {
492 buf.reset();
493 }
494 }
495 state.reset_average();
498 state.acquiring = true;
499 state.start_time = Some(Instant::now());
500 }
501 } else {
502 state.acquiring = false;
504 }
505 drop(state);
506 self.base.set_int32_param(reason, 0, value)?;
507 self.base.call_param_callbacks(0)?;
508 } else if reason == self.params.ts_read {
509 self.update_waveform_params();
511 } else if reason == self.params.ts_num_points {
512 let new_size = value.max(1) as usize;
513 {
514 let mut state = self.shared.lock();
515 state.num_points = new_size;
516 for buf in state.buffers.iter_mut() {
517 buf.resize(new_size);
518 }
519 state.reset_average();
520 state.acquiring = false;
521 }
522
523 self.refresh_time_axis();
525
526 for i in 0..self.num_channels {
528 let _ = self.base.params.set_float64_array(
529 self.params.ts_channels[i],
530 0,
531 vec![0.0; new_size],
532 );
533 }
534
535 self.base.set_int32_param(reason, 0, value)?;
536 self.base
537 .set_int32_param(self.params.ts_current_point, 0, 0)?;
538 self.base.set_int32_param(self.params.ts_acquire, 0, 0)?;
539 self.base.call_param_callbacks(0)?;
540 } else if reason == self.params.ts_num_average {
541 let n = value.max(1) as usize;
544 {
545 let mut state = self.shared.lock();
546 state.num_average = n;
547 state.reset_average();
548 }
549 self.base.set_int32_param(reason, 0, n as i32)?;
550 self.base.call_param_callbacks(0)?;
551 } else if reason == self.params.ts_acquire_mode {
552 let mode = if value == 0 {
554 TimeSeriesMode::OneShot
555 } else {
556 TimeSeriesMode::RingBuffer
557 };
558 {
559 let mut state = self.shared.lock();
560 state.mode = mode;
561 for buf in state.buffers.iter_mut() {
562 buf.set_mode(mode);
563 }
564 state.reset_average();
565 state.acquiring = false;
566 }
567 self.refresh_time_axis();
569
570 self.base.set_int32_param(reason, 0, value)?;
571 self.base.set_int32_param(self.params.ts_acquire, 0, 0)?;
572 self.base.call_param_callbacks(0)?;
573 } else {
574 self.base.set_int32_param(reason, user.addr, value)?;
576 self.base.call_param_callbacks(user.addr)?;
577 }
578
579 Ok(())
580 }
581
582 fn write_float64(&mut self, user: &mut AsynUser, value: f64) -> asyn_rs::error::AsynResult<()> {
583 let reason = user.reason;
584 if reason == self.params.ts_time_per_point {
585 self.time_per_point = value;
586 self.base.set_float64_param(reason, user.addr, value)?;
587 self.refresh_time_axis();
589 self.base.call_param_callbacks(user.addr)?;
590 } else {
591 self.base.set_float64_param(reason, user.addr, value)?;
592 self.base.call_param_callbacks(user.addr)?;
593 }
594 Ok(())
595 }
596
597 fn read_float64_array(
598 &mut self,
599 user: &AsynUser,
600 buf: &mut [f64],
601 ) -> asyn_rs::error::AsynResult<usize> {
602 let data = self.base.params.get_float64_array(user.reason, user.addr)?;
603 let n = data.len().min(buf.len());
604 buf[..n].copy_from_slice(&data[..n]);
605 Ok(n)
606 }
607}
608
609fn ts_data_thread(shared: Arc<Mutex<SharedTsState>>, mut data_rx: TimeSeriesReceiver) {
618 while let Some(data) = data_rx.blocking_recv() {
619 let mut state = shared.lock();
620 if !state.acquiring {
621 continue;
622 }
623 let emitted = state.accumulate(&data.values);
624 if emitted
627 && state.mode == TimeSeriesMode::OneShot
628 && state.buffers[0].count() >= state.num_points
629 {
630 state.acquiring = false;
631 }
632 }
633}
634
635pub fn create_ts_port_runtime(
641 port_name: &str,
642 channel_names: &[&str],
643 num_points: usize,
644 data_rx: TimeSeriesReceiver,
645) -> (
646 PortRuntimeHandle,
647 TSParams,
648 std::thread::JoinHandle<()>,
649 std::thread::JoinHandle<()>,
650) {
651 let num_channels = channel_names.len();
652 let shared = Arc::new(Mutex::new(SharedTsState::new(num_channels, num_points)));
653
654 let driver = TimeSeriesPortDriver::new(port_name, channel_names, num_points, shared.clone());
655
656 let ts_params = TSParams {
658 ts_acquire: driver.params.ts_acquire,
659 ts_read: driver.params.ts_read,
660 ts_num_points: driver.params.ts_num_points,
661 ts_current_point: driver.params.ts_current_point,
662 ts_time_per_point: driver.params.ts_time_per_point,
663 ts_averaging_time: driver.params.ts_averaging_time,
664 ts_num_average: driver.params.ts_num_average,
665 ts_elapsed_time: driver.params.ts_elapsed_time,
666 ts_acquire_mode: driver.params.ts_acquire_mode,
667 ts_time_axis: driver.params.ts_time_axis,
668 ts_channels: driver.params.ts_channels.clone(),
669 channel_names: driver.params.channel_names.clone(),
670 ts_time_series: driver.params.ts_time_series,
671 ts_timestamp: driver.params.ts_timestamp,
672 };
673
674 let (runtime_handle, actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
681 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
682
683 let data_jh = MandatoryThread::new(
690 format!("ts-data-{port_name}"),
691 ThreadPriority::Medium,
692 StackSizeClass::Medium,
693 )
694 .spawn(move || {
695 ts_data_thread(shared, data_rx);
696 });
697
698 (runtime_handle, ts_params, actor_jh, data_jh)
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 #[test]
720 fn the_ts_data_thread_is_mandatory() {
721 let src = include_str!("time_series.rs");
722 let prod = match src.find("\n#[cfg(test)]") {
723 Some(i) => &src[..i],
724 None => src,
725 };
726 assert_eq!(prod.matches("MandatoryThread::new(").count(), 1);
727 let strays: Vec<&str> = prod
728 .lines()
729 .map(str::trim)
730 .filter(|l| !l.starts_with("//"))
731 .filter(|l| {
732 l.contains(concat!("thread", "::Builder::new()"))
733 || l.contains(concat!("thread", "::spawn("))
734 })
735 .collect();
736 assert!(
737 strays.is_empty(),
738 "a data thread created outside `MandatoryThread` resolves its own \
739 spawn failure locally: {strays:?}"
740 );
741 }
742
743 #[test]
744 fn test_one_shot() {
745 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
746 for i in 0..5 {
747 ts.add_value(i as f64);
748 }
749 assert_eq!(ts.count(), 5);
750 assert_eq!(ts.values(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
751
752 ts.add_value(99.0);
754 assert_eq!(ts.count(), 5);
755 }
756
757 #[test]
758 fn test_ring_buffer() {
759 let mut ts = TimeSeries::new(4, TimeSeriesMode::RingBuffer);
760 for i in 0..6 {
761 ts.add_value(i as f64);
762 }
763 assert_eq!(ts.count(), 4);
764 assert_eq!(ts.values(), vec![2.0, 3.0, 4.0, 5.0]);
766 }
767
768 #[test]
769 fn test_ring_buffer_partial() {
770 let mut ts = TimeSeries::new(4, TimeSeriesMode::RingBuffer);
771 ts.add_value(10.0);
772 ts.add_value(20.0);
773 assert_eq!(ts.count(), 2);
774 assert_eq!(ts.values(), vec![10.0, 20.0]);
775 }
776
777 #[test]
778 fn test_reset() {
779 let mut ts = TimeSeries::new(3, TimeSeriesMode::OneShot);
780 ts.add_value(1.0);
781 ts.add_value(2.0);
782 ts.reset();
783 assert_eq!(ts.count(), 0);
784 assert!(ts.values().is_empty());
785 }
786
787 #[test]
788 fn test_resize() {
789 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
790 ts.add_value(1.0);
791 ts.add_value(2.0);
792 ts.resize(3);
793 assert_eq!(ts.num_points, 3);
794 assert_eq!(ts.count(), 0);
795 assert!(ts.values().is_empty());
796 }
797
798 #[test]
799 fn test_set_mode() {
800 let mut ts = TimeSeries::new(5, TimeSeriesMode::OneShot);
801 ts.add_value(1.0);
802 ts.set_mode(TimeSeriesMode::RingBuffer);
803 assert_eq!(ts.mode, TimeSeriesMode::RingBuffer);
804 assert_eq!(ts.count(), 0);
805 }
806
807 const TEST_CHANNELS: [&str; 3] = ["ChA", "ChB", "ChC"];
810
811 #[test]
812 fn test_shared_ts_state_init() {
813 let state = SharedTsState::new(3, 100);
814 assert_eq!(state.buffers.len(), 3);
815 assert_eq!(state.num_points, 100);
816 assert!(!state.acquiring);
817 assert_eq!(state.mode, TimeSeriesMode::OneShot);
818 }
819
820 #[test]
821 fn test_ts_port_driver_create() {
822 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
823 let driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared);
824 assert_eq!(driver.base().port_name, "TEST_TS");
825 assert_eq!(driver.num_channels, 3);
826 assert!(!driver.base().flags.multi_device);
827 }
828
829 #[test]
830 fn test_ts_port_driver_write_acquire() {
831 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
832 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
833
834 let mut user = AsynUser::new(driver.params.ts_acquire);
836 driver.write_int32(&mut user, 1).unwrap();
837 assert!(shared.lock().acquiring);
838
839 driver.write_int32(&mut user, 0).unwrap();
841 assert!(!shared.lock().acquiring);
842 }
843
844 #[test]
845 fn test_ts_port_driver_write_num_points() {
846 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
847 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
848
849 let mut user = AsynUser::new(driver.params.ts_num_points);
850 driver.write_int32(&mut user, 50).unwrap();
851
852 let state = shared.lock();
853 assert_eq!(state.num_points, 50);
854 for buf in &state.buffers {
855 assert_eq!(buf.num_points, 50);
856 }
857 }
858
859 #[test]
860 fn test_ts_port_driver_write_mode() {
861 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 100)));
862 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 100, shared.clone());
863
864 let mut user = AsynUser::new(driver.params.ts_acquire_mode);
865 driver.write_int32(&mut user, 1).unwrap();
866
867 let state = shared.lock();
868 assert_eq!(state.mode, TimeSeriesMode::RingBuffer);
869 for buf in &state.buffers {
870 assert_eq!(buf.mode, TimeSeriesMode::RingBuffer);
871 }
872 }
873
874 #[test]
875 fn test_ts_port_driver_update_waveforms() {
876 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 10)));
877 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 10, shared.clone());
878
879 {
881 let mut state = shared.lock();
882 state.acquiring = true;
883 state.start_time = Some(Instant::now());
884 for buf in state.buffers.iter_mut() {
885 buf.add_value(42.0);
886 buf.add_value(43.0);
887 }
888 }
889
890 driver.update_waveform_params();
892
893 let cp = driver
895 .base
896 .get_int32_param(driver.params.ts_current_point, 0)
897 .unwrap();
898 assert_eq!(cp, 2);
899
900 let data = driver
902 .base
903 .params
904 .get_float64_array(driver.params.ts_channels[0], 0)
905 .unwrap();
906 assert_eq!(data[0], 42.0);
907 assert_eq!(data[1], 43.0);
908 }
909
910 #[test]
911 fn test_ts_port_driver_read_array() {
912 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 5)));
913 let mut driver = TimeSeriesPortDriver::new("TEST_TS", &TEST_CHANNELS, 5, shared);
914
915 let user = AsynUser::new(driver.params.ts_time_axis);
916 let mut buf = vec![0.0; 5];
917 let n = driver.read_float64_array(&user, &mut buf).unwrap();
918 assert_eq!(n, 5);
919 assert_eq!(buf, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
920 }
921
922 #[test]
923 fn test_ts_data_ingestion_oneshot() {
924 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 3)));
925 let (tx, rx) = tokio::sync::mpsc::channel(16);
926
927 shared.lock().acquiring = true;
929
930 let shared_clone = shared.clone();
931 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
932
933 tx.blocking_send(TimeSeriesData {
935 values: vec![1.0, 10.0, 100.0],
936 })
937 .unwrap();
938 tx.blocking_send(TimeSeriesData {
939 values: vec![2.0, 20.0, 200.0],
940 })
941 .unwrap();
942 tx.blocking_send(TimeSeriesData {
943 values: vec![3.0, 30.0, 300.0],
944 })
945 .unwrap();
946 tx.blocking_send(TimeSeriesData {
947 values: vec![4.0, 40.0, 400.0],
948 })
949 .unwrap(); drop(tx);
953 jh.join().unwrap();
954
955 let state = shared.lock();
956 assert_eq!(state.buffers[0].count(), 3);
957 assert_eq!(state.buffers[0].values(), vec![1.0, 2.0, 3.0]);
958 assert_eq!(state.buffers[1].values(), vec![10.0, 20.0, 30.0]);
959 assert_eq!(state.buffers[2].values(), vec![100.0, 200.0, 300.0]);
960 assert!(!state.acquiring); }
962
963 #[test]
964 fn test_ts_data_ingestion_not_acquiring() {
965 let shared = Arc::new(Mutex::new(SharedTsState::new(3, 10)));
966 let (tx, rx) = tokio::sync::mpsc::channel(16);
967
968 let shared_clone = shared.clone();
970 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
971
972 tx.blocking_send(TimeSeriesData {
973 values: vec![1.0, 2.0, 3.0],
974 })
975 .unwrap();
976
977 drop(tx);
978 jh.join().unwrap();
979
980 let state = shared.lock();
981 assert_eq!(state.buffers[0].count(), 0);
982 }
983
984 #[test]
985 fn test_num_average_averages_input_samples() {
986 let mut state = SharedTsState::new(1, 10);
989 state.num_average = 3;
990 assert!(!state.accumulate(&[0.0]));
991 assert!(!state.accumulate(&[1.0]));
992 assert!(state.accumulate(&[2.0])); assert!(!state.accumulate(&[3.0]));
994 assert!(!state.accumulate(&[4.0]));
995 assert!(state.accumulate(&[5.0])); let vals = state.buffers[0].values();
997 assert_eq!(vals.len(), 2);
998 assert!((vals[0] - 1.0).abs() < 1e-10);
999 assert!((vals[1] - 4.0).abs() < 1e-10);
1000 }
1001
1002 #[test]
1003 fn test_num_average_one_is_passthrough() {
1004 let mut state = SharedTsState::new(2, 10);
1006 state.num_average = 1;
1007 assert!(state.accumulate(&[5.0, 50.0]));
1008 assert!(state.accumulate(&[6.0, 60.0]));
1009 assert_eq!(state.buffers[0].values(), vec![5.0, 6.0]);
1010 assert_eq!(state.buffers[1].values(), vec![50.0, 60.0]);
1011 }
1012
1013 #[test]
1014 fn test_num_average_drives_ingestion_thread() {
1015 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 5)));
1017 {
1018 let mut s = shared.lock();
1019 s.num_average = 2;
1020 s.acquiring = true;
1021 }
1022 let (tx, rx) = tokio::sync::mpsc::channel(16);
1023 let shared_clone = shared.clone();
1024 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1025
1026 for v in [10.0, 20.0, 30.0, 40.0] {
1027 tx.blocking_send(TimeSeriesData { values: vec![v] })
1028 .unwrap();
1029 }
1030 drop(tx);
1031 jh.join().unwrap();
1032
1033 let state = shared.lock();
1035 let vals = state.buffers[0].values();
1036 assert_eq!(vals.len(), 2);
1037 assert!((vals[0] - 15.0).abs() < 1e-10);
1038 assert!((vals[1] - 35.0).abs() < 1e-10);
1039 }
1040
1041 #[test]
1042 fn test_fixed_mode_stops_at_num_points() {
1043 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 3)));
1046 {
1047 let mut s = shared.lock();
1048 s.num_average = 1;
1049 s.mode = TimeSeriesMode::OneShot;
1050 s.acquiring = true;
1051 }
1052 let (tx, rx) = tokio::sync::mpsc::channel(16);
1053 let shared_clone = shared.clone();
1054 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1055 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
1056 tx.blocking_send(TimeSeriesData { values: vec![v] })
1057 .unwrap();
1058 }
1059 drop(tx);
1060 jh.join().unwrap();
1061
1062 let state = shared.lock();
1063 assert!(!state.acquiring, "Fixed mode must auto-stop");
1064 assert_eq!(state.buffers[0].count(), 3);
1065 assert_eq!(state.buffers[0].values(), vec![1.0, 2.0, 3.0]);
1066 }
1067
1068 #[test]
1069 fn test_circular_mode_wraps_and_keeps_acquiring() {
1070 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 3)));
1073 {
1074 let mut s = shared.lock();
1075 s.num_average = 1;
1076 s.mode = TimeSeriesMode::RingBuffer;
1077 for buf in s.buffers.iter_mut() {
1078 buf.set_mode(TimeSeriesMode::RingBuffer);
1079 }
1080 s.acquiring = true;
1081 }
1082 let (tx, rx) = tokio::sync::mpsc::channel(16);
1083 let shared_clone = shared.clone();
1084 let jh = std::thread::spawn(move || ts_data_thread(shared_clone, rx));
1085 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
1086 tx.blocking_send(TimeSeriesData { values: vec![v] })
1087 .unwrap();
1088 }
1089 drop(tx);
1090 jh.join().unwrap();
1091
1092 let state = shared.lock();
1093 assert!(state.acquiring, "Circular mode must keep acquiring");
1094 assert_eq!(state.buffers[0].values(), vec![3.0, 4.0, 5.0]);
1096 }
1097
1098 #[test]
1099 fn test_acquire_mode_param_drives_behavior_and_axis() {
1100 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 4)));
1103 let mut driver = TimeSeriesPortDriver::new("TEST_TS_MODE", &["Ch0"], 4, shared.clone());
1104
1105 let axis = driver
1107 .base
1108 .params
1109 .get_float64_array(driver.params.ts_time_axis, 0)
1110 .unwrap();
1111 assert_eq!(&*axis, &[0.0, 1.0, 2.0, 3.0]);
1112
1113 let mut user = AsynUser::new(driver.params.ts_acquire_mode);
1115 driver.write_int32(&mut user, 1).unwrap();
1116 assert_eq!(shared.lock().mode, TimeSeriesMode::RingBuffer);
1117
1118 let axis = driver
1120 .base
1121 .params
1122 .get_float64_array(driver.params.ts_time_axis, 0)
1123 .unwrap();
1124 assert_eq!(&*axis, &[-3.0, -2.0, -1.0, 0.0]);
1125 }
1126
1127 #[test]
1128 fn test_num_average_param_drives_state() {
1129 let shared = Arc::new(Mutex::new(SharedTsState::new(1, 10)));
1131 let mut driver = TimeSeriesPortDriver::new("TEST_TS_NAVG", &["Ch0"], 10, shared.clone());
1132 let mut user = AsynUser::new(driver.params.ts_num_average);
1133 driver.write_int32(&mut user, 5).unwrap();
1134 assert_eq!(shared.lock().num_average, 5);
1135 driver.write_int32(&mut user, 0).unwrap();
1137 assert_eq!(shared.lock().num_average, 1);
1138 }
1139
1140 #[test]
1141 fn test_create_ts_port_runtime() {
1142 let (_tx, rx) = tokio::sync::mpsc::channel(16);
1143 let (handle, params, _actor_jh, _data_jh) =
1144 create_ts_port_runtime("TEST_TS_RT", &TEST_CHANNELS, 100, rx);
1145 assert_eq!(handle.port_name(), "TEST_TS_RT");
1146 assert_eq!(params.ts_channels.len(), 3);
1147 handle.shutdown();
1148 }
1149}