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}