Skip to main content

rusty_tip/
buffered_tcp_reader.rs

1//! Buffered TCP Reader for continuous signal data collection
2//!
3//! This module provides a BufferedTCPReader that automatically buffers TCP logger data
4//! in the background using a lightweight time-series database approach. It leverages
5//! the existing TCPLoggerStream infrastructure while providing efficient time-windowed
6//! queries for synchronized data collection during SPM experiments.
7
8use crate::types::TimestampedSignalFrame;
9use crate::NanonisError;
10use nanonis_rs::TCPLoggerStream;
11use parking_lot::RwLock;
12use std::collections::VecDeque;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{mpsc, Arc};
15use std::thread::{self, JoinHandle};
16use std::time::{Duration, Instant};
17
18// TODO: For 2kHz sampling, consider replacing with:
19// use crossbeam::queue::ArrayQueue; // Lock-free ring buffer
20// use parking_lot::RwLock;          // Faster reader-writer lock
21
22/// Buffered TCP reader that continuously collects timestamped signal data
23///
24/// This component creates a background thread that reads lightweight SignalFrame data
25/// from TCPLoggerStream's channel and buffers it with high-resolution timestamps in a
26/// circular buffer. It provides time-windowed query methods for retrieving data before,
27/// during, and after specific time periods.
28///
29/// # High-Frequency Performance (2kHz+)
30/// **IMPORTANT**: At sampling rates above 1kHz, lock contention becomes critical:
31/// - Current implementation uses `Mutex<VecDeque>` suitable for <1kHz
32/// - For 2kHz+, consider `crossbeam::queue::ArrayQueue` (lock-free)
33/// - Alternative: `parking_lot::RwLock` for multiple concurrent readers
34/// - Query methods must complete in <0.1ms to avoid data loss
35///
36/// # Memory Efficiency
37/// Works with lightweight SignalFrame structures (just counter + data) throughout the
38/// entire pipeline, avoiding the overhead of full TCPLoggerData per frame.
39///
40/// # Architecture
41/// - TCPLoggerStream converts protocol data to SignalFrame (protocol → lightweight conversion)
42/// - BufferedTCPReader adds timestamps to SignalFrame (timing layer)
43/// - Thread-safe time-windowed queries while continuous collection runs in background
44pub struct BufferedTCPReader {
45    /// Thread-safe circular buffer of timestamped signal frames
46    buffer: Arc<RwLock<VecDeque<TimestampedSignalFrame>>>,
47    /// Background thread handle for buffering operations
48    buffering_thread: Option<JoinHandle<Result<(), NanonisError>>>,
49    /// Maximum number of frames to keep in circular buffer
50    max_buffer_size: usize,
51    /// Time when data collection started (for relative timestamps)
52    start_time: Instant,
53    /// Signal to shut down background thread
54    shutdown_signal: Arc<AtomicBool>,
55    /// Number of channels (configuration parameter)
56    num_channels: u32,
57    /// Oversampling rate (configuration parameter)
58    oversampling: f32,
59}
60
61impl BufferedTCPReader {
62    /// Create a new BufferedTCPReader with automatic background data collection
63    ///
64    /// This establishes a connection to the TCP logger stream and starts a background
65    /// thread for continuous data buffering with lightweight SignalFrame structures.
66    ///
67    /// # Arguments
68    /// * `host` - TCP server host address (e.g., "127.0.0.1")
69    /// * `port` - TCP logger data stream port (typically 6590)
70    /// * `buffer_size` - Maximum number of frames to keep in circular buffer
71    /// * `num_channels` - Number of channels being recorded by TCP logger
72    /// * `oversampling` - Oversampling rate configured for TCP logger
73    ///
74    /// # Returns
75    /// A BufferedTCPReader with active background collection, ready for queries
76    ///
77    /// # Implementation Notes
78    /// - Creates TCPLoggerStream and gets its background reader channel
79    /// - Starts buffering thread that converts SignalFrame to TimestampedSignalFrame
80    /// - Implements circular buffer behavior (drops oldest when full)
81    pub fn new(
82        host: &str,
83        port: u16,
84        buffer_size: usize,
85        num_channels: u32,
86        oversampling: f32,
87    ) -> Result<Self, NanonisError> {
88        let tcp_stream = TCPLoggerStream::new(host, port)?;
89        // 0.4.0 returns (receiver, JoinHandle); we only need the receiver here.
90        let (tcp_receiver, _stream_handle) = tcp_stream.spawn_background_reader();
91
92        let buffer = Arc::new(RwLock::new(VecDeque::with_capacity(buffer_size)));
93        let buffer_clone = buffer.clone();
94
95        let shutdown_signal = Arc::new(AtomicBool::new(false));
96        let shutdown_clone = shutdown_signal.clone();
97
98        let start_time = Instant::now();
99
100        // Don't block waiting for first frame - let background thread handle it
101        // The TCP logger might not be started yet when this constructor runs
102
103        let buffering_thread = thread::spawn(move || -> Result<(), NanonisError> {
104            log::debug!("Started buffering thread for TCP logger data");
105
106            while !shutdown_clone.load(Ordering::Relaxed) {
107                match tcp_receiver.recv_timeout(Duration::from_millis(100)) {
108                    Ok(signal_frame) => {
109                        // Skip the first frame (signal indices metadata)
110                        if signal_frame.counter == 0 {
111                            log::debug!("Skipping metadata frame (counter=0) with signal indices");
112                            continue;
113                        }
114
115                        let timestamped_frame =
116                            TimestampedSignalFrame::new(signal_frame, start_time);
117
118                        {
119                            let mut buffer = buffer_clone.write();
120                            buffer.push_back(timestamped_frame);
121
122                            if buffer.len() > buffer_size {
123                                buffer.pop_front();
124                            }
125                        }
126                    }
127                    Err(mpsc::RecvTimeoutError::Timeout) => {
128                        continue;
129                    }
130                    Err(mpsc::RecvTimeoutError::Disconnected) => {
131                        log::info!("TCP logger stream disconnected ending buffering");
132                        break;
133                    }
134                }
135            }
136            Ok(())
137        });
138
139        Ok(Self {
140            buffer,
141            buffering_thread: Some(buffering_thread),
142            max_buffer_size: buffer_size,
143            start_time,
144            shutdown_signal,
145            num_channels,
146            oversampling,
147        })
148    }
149
150    /// Check if the background buffering thread is still active
151    ///
152    /// # Returns
153    /// `true` if buffering is active, `false` if stopped or failed
154    pub fn is_buffering(&self) -> bool {
155        !self.shutdown_signal.load(Ordering::Relaxed)
156    }
157
158    /// Get current buffer utilization as a percentage
159    ///
160    /// # Returns
161    /// Value between 0.0 and 1.0 indicating how full the buffer is
162    ///
163    /// # Usage
164    /// Useful for monitoring buffer health and detecting if data collection
165    /// is faster than buffer capacity
166    pub fn buffer_utilization(&self) -> f64 {
167        let buffer = self.buffer.read();
168        buffer.len() as f64 / self.max_buffer_size as f64
169    }
170
171    /// Get the total uptime of the buffered TCP reader
172    ///
173    /// Returns the duration since the BufferedTCPReader was created and started
174    /// collecting data. This can be useful for monitoring, logging, and understanding
175    /// the data collection timespan.
176    ///
177    /// # Returns
178    /// Duration since the reader was started
179    ///
180    /// # Thread Safety
181    /// This method is very fast as it only reads the start_time field and calculates
182    /// the current duration. No locks are acquired.
183    ///
184    /// # Example
185    /// ```rust,ignore
186    /// let tcp_reader = BufferedTCPReader::new("127.0.0.1", 6590, 1000, 24, 100.0)?;
187    ///
188    /// // Later...
189    /// let uptime = tcp_reader.uptime();
190    /// println!("TCP reader has been running for {:.1}s", uptime.as_secs_f64());
191    ///
192    /// // Useful for rate calculations
193    /// let (frame_count, _, _) = tcp_reader.buffer_stats();
194    /// let avg_rate = frame_count as f64 / uptime.as_secs_f64();
195    /// println!("Average data rate: {:.1} frames/sec", avg_rate);
196    /// ```
197    pub fn uptime(&self) -> Duration {
198        self.start_time.elapsed()
199    }
200
201    /// Get all signal data since a specific timestamp
202    ///
203    /// # Arguments
204    /// * `since` - Timestamp to start collecting data from
205    ///
206    /// # Returns
207    /// Vector of timestamped signal frames from the specified time onwards
208    ///
209    /// # Thread Safety
210    /// This method acquires a lock on the buffer briefly to copy matching frames.
211    /// Lock is held for minimal time to avoid blocking the buffering thread.
212    pub fn get_data_since(&self, since: Instant) -> Vec<TimestampedSignalFrame> {
213        let buffer = self.buffer.read();
214        buffer
215            .iter()
216            .filter(|frame| frame.timestamp >= since)
217            .cloned()
218            .collect()
219    }
220
221    /// Get signal data between two timestamps (time window query)
222    ///
223    /// # Arguments
224    /// * `start` - Start of time window (inclusive)
225    /// * `end` - End of time window (inclusive)
226    ///
227    /// # Returns
228    /// Vector of timestamped signal frames within the specified time window
229    ///
230    /// # Thread Safety
231    /// Minimizes lock time to avoid blocking the buffering thread.
232    ///
233    /// # Usage
234    /// This is the core method for synchronized data collection during actions.
235    /// Typically used to get data before/during/after specific operations.
236    pub fn get_data_between(&self, start: Instant, end: Instant) -> Vec<TimestampedSignalFrame> {
237        let buffer = self.buffer.read();
238        buffer
239            .iter()
240            .filter(|frame| frame.timestamp >= start && frame.timestamp <= end)
241            .cloned()
242            .collect()
243    }
244
245    /// Get recent signal data for a specific duration
246    ///
247    /// # Arguments
248    /// * `duration` - How far back to collect data from current time
249    ///
250    /// # Returns
251    /// Vector of timestamped signal frames from the recent past
252    ///
253    /// # Thread Safety
254    /// Delegates to get_data_since() which minimizes lock time.
255    ///
256    /// # Usage
257    /// Convenient for real-time monitoring and getting recent signal history
258    /// without needing to track specific timestamps
259    pub fn get_recent_data(&self, duration: Duration) -> Vec<TimestampedSignalFrame> {
260        let since = Instant::now() - duration;
261        self.get_data_since(since)
262    }
263
264    /// Get all buffered signal data
265    ///
266    /// # Returns
267    /// Vector containing all currently buffered timestamped signal frames
268    ///
269    /// # Thread Safety
270    /// WARNING: This clones the entire buffer. For large buffers, prefer time-windowed queries.
271    /// Lock is held briefly but cloning large amounts of data may still impact performance.
272    ///
273    /// # Usage
274    /// Useful for final data collection when stopping buffering, or for
275    /// full experiment analysis
276    pub fn get_all_data(&self) -> Vec<TimestampedSignalFrame> {
277        let buffer = self.buffer.read();
278        buffer.iter().cloned().collect()
279    }
280
281    /// Get TCP logger configuration that was provided during construction
282    ///
283    /// # Returns
284    /// Tuple of (num_channels, oversampling) from the TCP logger
285    ///
286    /// # Usage
287    /// Needed when converting TimestampedSignalFrame back to TCPLoggerData
288    /// for backward compatibility
289    pub fn get_tcp_config(&self) -> (u32, f32) {
290        (self.num_channels, self.oversampling)
291    }
292
293    /// Get buffer statistics for monitoring
294    ///
295    /// # Returns
296    /// Tuple of (current_count, max_capacity, time_span_of_data)
297    ///
298    /// # Thread Safety
299    /// Very brief lock to read buffer metadata only, no cloning.
300    ///
301    /// # Usage
302    /// Useful for monitoring buffer health, detecting overruns, and
303    /// understanding the time span of collected data
304    pub fn buffer_stats(&self) -> (usize, usize, Duration) {
305        let buffer = self.buffer.read();
306        let count = buffer.len();
307        let capacity = self.max_buffer_size;
308        let time_span = if let (Some(first), Some(last)) = (buffer.front(), buffer.back()) {
309            last.timestamp.duration_since(first.timestamp)
310        } else {
311            Duration::ZERO
312        };
313        (count, capacity, time_span)
314    }
315
316    /// Get the most recent N frames from the buffer
317    ///
318    /// Returns frames in reverse chronological order (newest first).
319    /// If fewer than `count` frames are available, returns all available frames.
320    ///
321    /// # Arguments
322    /// * `count` - Maximum number of frames to retrieve
323    ///
324    /// # Returns
325    /// Vector of timestamped signal frames, newest first
326    ///
327    /// # Example
328    /// ```rust,ignore
329    /// let recent_100 = tcp_reader.get_recent_frames(100);
330    /// ```
331    pub fn get_recent_frames(&self, count: usize) -> Vec<TimestampedSignalFrame> {
332        let buffer = self.buffer.read();
333        buffer.iter().rev().take(count).cloned().collect()
334    }
335
336    /// Get the oldest N frames from the buffer
337    ///
338    /// Returns frames in chronological order (oldest first).
339    /// If fewer than `count` frames are available, returns all available frames.
340    /// Useful for FIFO processing or getting a stable baseline.
341    ///
342    /// # Arguments
343    /// * `count` - Maximum number of frames to retrieve
344    ///
345    /// # Returns
346    /// Vector of timestamped signal frames, oldest first
347    ///
348    /// # Example
349    /// ```rust,ignore
350    /// let baseline = tcp_reader.get_oldest_frames(50);
351    /// ```
352    pub fn get_oldest_frames(&self, count: usize) -> Vec<TimestampedSignalFrame> {
353        let buffer = self.buffer.read();
354        buffer.iter().take(count).cloned().collect()
355    }
356
357    /// Get the current number of frames in the buffer
358    ///
359    /// Returns the total count of frames currently stored in the circular buffer.
360    /// This can be used to check buffer fill level or validate requests.
361    ///
362    /// # Returns
363    /// Number of frames currently buffered
364    ///
365    /// # Example
366    /// ```rust,ignore
367    /// let available = tcp_reader.frame_count();
368    /// if available >= 100 {
369    ///     let data = tcp_reader.get_recent_frames(100);
370    /// }
371    /// ```
372    pub fn frame_count(&self) -> usize {
373        let buffer = self.buffer.read();
374        buffer.len()
375    }
376
377    /// Get frames from a specific range in the buffer
378    ///
379    /// Returns frames starting from `start_idx` (0 = oldest frame) for `count` frames.
380    /// If the range extends beyond available data, returns available frames only.
381    /// Useful for windowed analysis or specific time periods.
382    ///
383    /// # Arguments
384    /// * `start_idx` - Starting index (0 = oldest frame in buffer)
385    /// * `count` - Number of frames to retrieve from start_idx
386    ///
387    /// # Returns
388    /// Vector of timestamped signal frames in chronological order
389    ///
390    /// # Example
391    /// ```rust,ignore
392    /// // Get frames 50-149 (middle section of buffer)
393    /// let middle_data = tcp_reader.get_frame_range(50, 100);
394    /// ```
395    pub fn get_frame_range(&self, start_idx: usize, count: usize) -> Vec<TimestampedSignalFrame> {
396        let buffer = self.buffer.read();
397
398        buffer.iter().skip(start_idx).take(count).cloned().collect()
399    }
400
401    /// Check if the buffer has at least N frames available
402    ///
403    /// Convenience method to check data availability before requesting frames.
404    /// More efficient than getting frame_count() when you only need a threshold check.
405    ///
406    /// # Arguments
407    /// * `min_count` - Minimum number of frames required
408    ///
409    /// # Returns
410    /// True if buffer contains at least `min_count` frames
411    ///
412    /// # Example
413    /// ```rust,ignore
414    /// if tcp_reader.has_frames(100) {
415    ///     let stable_data = tcp_reader.get_recent_frames(100);
416    /// } else {
417    ///     println!("Not enough data yet, only {} frames", tcp_reader.frame_count());
418    /// }
419    /// ```
420    pub fn has_frames(&self, min_count: usize) -> bool {
421        self.frame_count() > min_count
422    }
423
424    /// Clear all buffered data
425    ///
426    /// This removes all frames from the buffer, effectively resetting it to an empty state.
427    /// The background thread continues to run and will start filling the buffer again.
428    /// This is useful when you want to discard old data and start fresh.
429    ///
430    /// # Example
431    /// ```rust,ignore
432    /// // Clear any stale data before starting a new measurement
433    /// tcp_reader.clear_buffer();
434    /// thread::sleep(Duration::from_millis(500)); // Wait for fresh data
435    /// let fresh_data = tcp_reader.get_recent_data(Duration::from_millis(100));
436    /// ```
437    pub fn clear_buffer(&self) {
438        let mut buffer = self.buffer.write();
439        buffer.clear();
440        log::debug!("Cleared TCP reader buffer");
441    }
442
443    /// Stop background buffering and clean up resources
444    ///
445    /// # Returns
446    /// Result indicating if cleanup was successful
447    ///
448    /// # Implementation Notes
449    /// - Sets shutdown signal to stop background thread
450    /// - Waits for thread to finish and returns any errors
451    /// - Called automatically when BufferedTCPReader is dropped
452    pub fn stop(&mut self) -> Result<(), NanonisError> {
453        self.shutdown_signal.store(true, Ordering::Relaxed);
454        if let Some(handle) = self.buffering_thread.take() {
455            match handle.join() {
456                Ok(result) => result,
457                Err(_) => Err(NanonisError::Protocol(
458                    "Buffering thread panicked".to_string(),
459                )),
460            }
461        } else {
462            Ok(())
463        }
464    }
465}
466
467impl Drop for BufferedTCPReader {
468    /// Automatically stop buffering when BufferedTCPReader is dropped
469    fn drop(&mut self) {
470        let _ = self.stop();
471    }
472}