Skip to main content

nanonis_rs/
tcplogger_stream.rs

1use crate::error::NanonisError;
2use crate::signals::SignalFrame;
3use crate::tcplog::TCPLogStatus;
4use byteorder::{BigEndian, ReadBytesExt};
5use std::io::{Cursor, Read};
6use std::net::{SocketAddr, TcpStream};
7use std::sync::mpsc;
8use std::thread::{self, JoinHandle};
9use std::time::Duration;
10
11/// Simple TCP Logger Stream - connects to data stream only, no control
12pub struct TCPLoggerStream {
13    stream: TcpStream,
14    buffer: Vec<u8>,
15}
16
17impl TCPLoggerStream {
18    /// Connect to TCP Logger data stream only
19    ///
20    /// Creates a simple connection to the TCP data stream without any control operations.
21    /// All control (start/stop/configure) should be handled externally.
22    ///
23    /// # Arguments
24    /// * `addr` - Server address (e.g., "127.0.0.1")
25    /// * `stream_port` - TCP Logger data stream port (typically 6590)
26    ///
27    /// # Returns
28    /// Connected `TCPLoggerStream` ready to read data frames.
29    pub fn new(addr: &str, stream_port: u16) -> Result<Self, NanonisError> {
30        let socket_addr: SocketAddr = format!("{addr}:{stream_port}")
31            .parse()
32            .map_err(|_| NanonisError::Protocol(format!("Invalid address: {addr}")))?;
33
34        let connect_timeout = Duration::from_secs(10);
35        let stream = TcpStream::connect_timeout(&socket_addr, connect_timeout).map_err(|e| {
36            NanonisError::from_io(
37                e,
38                format!("Failed to connect to TCP stream at {socket_addr}"),
39            )
40        })?;
41
42        // Set read timeout for continuous reading
43        stream
44            .set_read_timeout(Some(Duration::from_secs(30)))
45            .map_err(|e| NanonisError::Io {
46                source: e,
47                context: "Setting TCP stream read timeout".to_string(),
48            })?;
49
50        Ok(Self {
51            stream,
52            buffer: Vec::with_capacity(1024),
53        })
54    }
55
56    /// Spawn background reader thread.
57    ///
58    /// Creates a named background thread that continuously reads TCP Logger
59    /// data frames and sends them through a channel. Returns both the
60    /// receiver and a `JoinHandle` so the caller can detect *why* the
61    /// thread exited (clean shutdown vs. I/O error).
62    ///
63    /// The thread exits when:
64    /// - The receiver is dropped (clean shutdown, returns `Ok(())`)
65    /// - A `read_frame` call fails (returns `Err(NanonisError)`)
66    pub fn spawn_background_reader(
67        mut self,
68    ) -> (
69        mpsc::Receiver<SignalFrame>,
70        JoinHandle<Result<(), NanonisError>>,
71    ) {
72        let (sender, receiver) = mpsc::channel();
73
74        let handle = thread::Builder::new()
75            .name("tcp-logger-reader".into())
76            .spawn(move || {
77                loop {
78                    match self.read_frame() {
79                        Ok(frame) => {
80                            if sender.send(frame).is_err() {
81                                return Ok(()); // receiver dropped, clean shutdown
82                            }
83                        }
84                        Err(e) => return Err(e),
85                    }
86                }
87            })
88            .expect("failed to spawn tcp-logger-reader thread");
89
90        (receiver, handle)
91    }
92
93    /// Read a single data frame from the stream.
94    ///
95    /// Automatically skips the initial metadata frame (counter == 0) that
96    /// the Nanonis TCP logger sends when first started. This frame contains
97    /// signal index information rather than measurement data and is not
98    /// useful to callers.
99    ///
100    /// # Returns
101    /// A `SignalFrame` containing the frame counter and channel data.
102    ///
103    /// # Frame Format
104    /// Each frame is 18 bytes header + (num_channels * 4) bytes of f32 data.
105    pub fn read_frame(&mut self) -> Result<SignalFrame, NanonisError> {
106        loop {
107            let frame = self.read_frame_raw()?;
108            // The first frame after logger start has counter == 0 and carries
109            // signal index metadata, not measurement data. Skip it.
110            if frame.counter == 0 {
111                continue;
112            }
113            return Ok(frame);
114        }
115    }
116
117    /// Read a single raw frame from the stream without filtering.
118    ///
119    /// Unlike [`read_frame`], this returns every frame including the
120    /// counter-0 metadata frame. Use this only if you need to inspect
121    /// the signal index metadata.
122    pub fn read_frame_raw(&mut self) -> Result<SignalFrame, NanonisError> {
123        // First read header to determine frame size
124        let header_size = 18;
125        self.buffer.resize(header_size, 0);
126
127        // Read header into buffer
128        self.stream
129            .read_exact(&mut self.buffer[..header_size])
130            .map_err(|e| NanonisError::Io {
131                source: e,
132                context: "Reading TCP Logger frame header".to_string(),
133            })?;
134
135        // Parse header from buffer
136        let mut cursor = Cursor::new(&self.buffer[..header_size]);
137        let num_channels = cursor.read_u32::<BigEndian>()?;
138        let _oversampling = cursor.read_f32::<BigEndian>()?;
139        let counter = cursor.read_u64::<BigEndian>()?;
140        let state_val = cursor.read_u16::<BigEndian>()?;
141        let _state = TCPLogStatus::try_from(state_val as i32)?;
142
143        // Calculate total frame size and read data portion
144        let data_size = (num_channels as usize)
145            .checked_mul(4) // 4 bytes per f32
146            .ok_or_else(|| {
147                NanonisError::Protocol(format!("Channel count overflow: {num_channels} channels"))
148            })?;
149        self.buffer.resize(data_size, 0);
150
151        self.stream
152            .read_exact(&mut self.buffer[..data_size])
153            .map_err(|e| NanonisError::Io {
154                source: e,
155                context: "Reading TCP Logger frame data".to_string(),
156            })?;
157
158        // Parse data values from buffer
159        let mut cursor = Cursor::new(&self.buffer[..data_size]);
160        let mut data = Vec::with_capacity(num_channels as usize);
161        for _ in 0..num_channels {
162            data.push(cursor.read_f32::<BigEndian>()?);
163        }
164
165        Ok(SignalFrame { counter, data })
166    }
167}