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}