Skip to main content

instrument_core/scpi/
session.rs

1use super::framing::extract_response;
2use super::protocol::{
3    is_no_error_reply, is_opc_supported_reply, is_syst_err_supported_reply, max_write_attempts,
4    normalize_command, parse_f64, SessionCapabilities,
5};
6use crate::connect::ConnectOptions;
7use crate::diagnostics::{CommsEventKind, Diagnostics};
8use crate::error::{Error, Result};
9use crate::ieee4882::Ieee4882;
10use crate::transport::{DynTransport, Transport};
11use std::thread;
12use std::time::{Duration, Instant};
13
14/// SCPI session over a boxed transport.
15pub struct ScpiSession {
16    transport: DynTransport,
17    opts: ConnectOptions,
18    capabilities: SessionCapabilities,
19    read_buffer: Vec<u8>,
20    diagnostics: Option<Diagnostics>,
21    pending_command: Option<String>,
22    pending_error_reply: Option<String>,
23    skip_block_terminator: bool,
24}
25
26impl ScpiSession {
27    pub fn new(mut transport: DynTransport, opts: ConnectOptions) -> Result<Self> {
28        transport.configure(&opts)?;
29        let mut session = Self {
30            transport,
31            opts,
32            capabilities: SessionCapabilities::default(),
33            read_buffer: Vec::with_capacity(4096),
34            diagnostics: None,
35            pending_command: None,
36            pending_error_reply: None,
37            skip_block_terminator: false,
38        };
39        if session.opts.reset_on_connect {
40            let _ = Ieee4882::new(&mut session).clear_status();
41            let _ = Ieee4882::new(&mut session).reset();
42            let _ = session.restore_io_timeout();
43        }
44        Ok(session)
45    }
46
47    pub fn with_diagnostics(mut self, diagnostics: Diagnostics) -> Self {
48        self.diagnostics = Some(diagnostics);
49        self
50    }
51
52    pub fn transport(&self) -> &dyn Transport {
53        self.transport.as_ref()
54    }
55
56    pub fn options(&self) -> &ConnectOptions {
57        &self.opts
58    }
59
60    /// Drains pending bytes from the transport read buffer.
61    pub fn flush(&mut self) -> Result<()> {
62        let short = Duration::from_millis(50);
63        self.transport.set_read_timeout(short)?;
64        let result = self.drain_read_buffer();
65        let _ = self.restore_io_timeout();
66        result
67    }
68
69    fn drain_read_buffer(&mut self) -> Result<()> {
70        let mut chunk = [0u8; 256];
71        loop {
72            match self.transport.read(&mut chunk) {
73                Ok(0) => break,
74                Ok(_) => continue,
75                Err(Error::Timeout) => break,
76                Err(e) => return Err(e),
77            }
78        }
79        self.read_buffer.clear();
80        Ok(())
81    }
82
83    /// Writes a command without expecting a response.
84    pub fn write(&mut self, command: &str) -> Result<()> {
85        self.write_with_retry(command, false)
86    }
87
88    /// Sends a query and returns the response string.
89    pub fn query(&mut self, command: &str) -> Result<String> {
90        self.query_with_timeout(command, self.effective_read_timeout())
91    }
92
93    pub fn query_with_timeout(&mut self, command: &str, timeout: Duration) -> Result<String> {
94        let max_attempts = max_write_attempts(true, self.opts.retries);
95        let mut attempts = 0;
96        loop {
97            attempts += 1;
98            self.write_with_retry(command, true)?;
99            match self.read_response(timeout) {
100                Ok(bytes) => {
101                    return Ok(String::from_utf8_lossy(&bytes).trim().to_string());
102                }
103                Err(Error::Timeout) if attempts < max_attempts => {
104                    let _ = self.flush();
105                    if self.opts.reconnect_on_failure {
106                        self.try_reconnect();
107                    }
108                    thread::sleep(self.opts.retry_backoff * attempts);
109                }
110                Err(Error::Timeout) => {
111                    let _ = self.flush();
112                    return Err(Error::Timeout);
113                }
114                Err(e) => return Err(e),
115            }
116        }
117    }
118
119    fn write_with_retry(&mut self, command: &str, idempotent: bool) -> Result<()> {
120        let payload = normalize_command(command, &self.opts.terminator);
121        let data = payload.as_bytes();
122
123        let mut attempts = 0;
124        let max_attempts = max_write_attempts(idempotent, self.opts.retries);
125
126        loop {
127            attempts += 1;
128            let started = Instant::now();
129            self.pending_command = Some(command.to_string());
130            match self.transport.write(data) {
131                Ok(()) => {
132                    self.record_success(CommsEventKind::WriteOk, Some(command), attempts, started);
133                    return Ok(());
134                }
135                Err(Error::Timeout) if attempts < max_attempts => {
136                    self.record_failure(
137                        CommsEventKind::Timeout,
138                        Some(command),
139                        attempts,
140                        started,
141                        "write timeout",
142                    );
143                    if self.opts.reconnect_on_failure {
144                        self.try_reconnect();
145                    }
146                    thread::sleep(self.opts.retry_backoff);
147                }
148                Err(Error::Timeout) => {
149                    self.record_failure(
150                        CommsEventKind::Timeout,
151                        Some(command),
152                        attempts,
153                        started,
154                        "write timeout",
155                    );
156                    return Err(Error::Timeout);
157                }
158                Err(e) => {
159                    self.record_failure(
160                        CommsEventKind::WriteFailed,
161                        Some(command),
162                        attempts,
163                        started,
164                        &e.to_string(),
165                    );
166                    return Err(e);
167                }
168            }
169        }
170    }
171
172    fn read_response(&mut self, timeout: Duration) -> Result<Vec<u8>> {
173        self.transport.set_read_timeout(timeout)?;
174        let result = self.read_framed_response(timeout);
175        let _ = self.restore_io_timeout();
176        result
177    }
178
179    fn read_framed_response(&mut self, timeout: Duration) -> Result<Vec<u8>> {
180        let deadline = Instant::now() + timeout;
181        self.read_buffer.clear();
182        let command = self.pending_command.clone();
183
184        let mut chunk = [0u8; 1024];
185        loop {
186            let started = Instant::now();
187            let remaining = deadline.saturating_duration_since(Instant::now());
188            if remaining.is_zero() {
189                return Err(Error::Timeout);
190            }
191            self.transport.set_read_timeout(remaining)?;
192            match self.transport.read(&mut chunk) {
193                Ok(0) => {
194                    if !self.read_buffer.is_empty() {
195                        if let Ok((payload, _)) =
196                            extract_response(&self.read_buffer, &self.opts.terminator)
197                        {
198                            self.record_success(
199                                CommsEventKind::ReadOk,
200                                command.as_deref(),
201                                1,
202                                started,
203                            );
204                            self.skip_block_terminator = self.read_buffer.starts_with(b"#")
205                                && self.read_buffer.get(1) != Some(&b'0');
206                            return Ok(payload);
207                        }
208                    }
209                    self.record_failure(
210                        CommsEventKind::Timeout,
211                        command.as_deref(),
212                        1,
213                        started,
214                        "zero-byte read",
215                    );
216                    return Err(Error::Timeout);
217                }
218                Ok(n) => {
219                    for &byte in &chunk[..n] {
220                        if self.skip_block_terminator && matches!(byte, b'\r' | b'\n') {
221                            continue;
222                        }
223                        self.skip_block_terminator = false;
224                        self.read_buffer.push(byte);
225                    }
226                    if let Ok((payload, _)) =
227                        extract_response(&self.read_buffer, &self.opts.terminator)
228                    {
229                        self.record_success(CommsEventKind::ReadOk, command.as_deref(), 1, started);
230                        self.skip_block_terminator = self.read_buffer.starts_with(b"#")
231                            && self.read_buffer.get(1) != Some(&b'0');
232                        return Ok(payload);
233                    }
234                }
235                Err(Error::Timeout) => {
236                    if !self.read_buffer.is_empty() {
237                        if let Ok((payload, _)) =
238                            extract_response(&self.read_buffer, &self.opts.terminator)
239                        {
240                            self.record_success(
241                                CommsEventKind::ReadOk,
242                                command.as_deref(),
243                                1,
244                                started,
245                            );
246                            self.skip_block_terminator = self.read_buffer.starts_with(b"#")
247                                && self.read_buffer.get(1) != Some(&b'0');
248                            return Ok(payload);
249                        }
250                    }
251                    self.record_failure(
252                        CommsEventKind::Timeout,
253                        command.as_deref(),
254                        1,
255                        started,
256                        "read timeout",
257                    );
258                    return Err(Error::Timeout);
259                }
260                Err(e) => {
261                    self.record_failure(
262                        CommsEventKind::ReadFailed,
263                        command.as_deref(),
264                        1,
265                        started,
266                        &e.to_string(),
267                    );
268                    return Err(e);
269                }
270            }
271        }
272    }
273
274    fn record_success(
275        &self,
276        kind: CommsEventKind,
277        command: Option<&str>,
278        attempt: u32,
279        started: Instant,
280    ) {
281        if let Some(diag) = &self.diagnostics {
282            diag.record_success(kind, command, attempt, started.elapsed());
283        }
284    }
285
286    fn record_failure(
287        &self,
288        kind: CommsEventKind,
289        command: Option<&str>,
290        attempt: u32,
291        started: Instant,
292        detail: &str,
293    ) {
294        if let Some(diag) = &self.diagnostics {
295            diag.record_failure(kind, command, attempt, started.elapsed(), detail);
296        }
297    }
298
299    fn record_reconnect(&self) {
300        if let Some(diag) = &self.diagnostics {
301            diag.record_success(CommsEventKind::Reconnect, None, 1, Duration::ZERO);
302        }
303    }
304
305    fn try_reconnect(&mut self) {
306        if self.transport.reconnect().is_ok() {
307            self.record_reconnect();
308        }
309    }
310
311    fn effective_read_timeout(&self) -> Duration {
312        self.opts.per_op_timeout.unwrap_or(self.opts.read_timeout)
313    }
314
315    /// Restores the session I/O timeout after a short probe or flush.
316    fn restore_io_timeout(&mut self) -> Result<()> {
317        self.transport.set_read_timeout(self.opts.io_timeout())
318    }
319
320    /// Probes and caches whether SYST:ERR? is supported.
321    pub fn probe_syst_err(&mut self) -> bool {
322        if let Some(v) = self.capabilities.syst_err {
323            return v;
324        }
325        match self.query_with_timeout("SYST:ERR?", Duration::from_millis(500)) {
326            Ok(resp) => {
327                let supported = is_syst_err_supported_reply(&resp);
328                self.capabilities.syst_err = Some(supported);
329                // A zero reply describes the queue at probe time, not at the next check.
330                if supported && !is_no_error_reply(&resp) {
331                    self.pending_error_reply = Some(resp);
332                }
333                supported
334            }
335            Err(_) => false,
336        }
337    }
338
339    /// Timeout is inconclusive and must not be cached as unsupported.
340    pub fn probe_opc(&mut self) -> bool {
341        if let Some(v) = self.capabilities.opc {
342            return v;
343        }
344        match self.query_with_timeout("*OPC?", Duration::from_millis(500)) {
345            Ok(resp) => {
346                let supported = is_opc_supported_reply(&resp);
347                self.capabilities.opc = Some(supported);
348                supported
349            }
350            Err(_) => false,
351        }
352    }
353
354    /// Drains the error queue, including any entry consumed by an earlier probe.
355    pub fn check_errors(&mut self) -> Result<Vec<String>> {
356        if self.capabilities.syst_err == Some(false) {
357            return Ok(Vec::new());
358        }
359        let mut errors = Vec::new();
360        loop {
361            let resp = match self.pending_error_reply.take() {
362                Some(reply) => reply,
363                None => self.query("SYST:ERR?")?,
364            };
365            if !is_syst_err_supported_reply(&resp) {
366                if self.capabilities.syst_err.is_none() {
367                    self.capabilities.syst_err = Some(false);
368                    return Ok(errors);
369                }
370                return Err(Error::Parse(format!("invalid error queue reply '{resp}'")));
371            }
372            self.capabilities.syst_err = Some(true);
373            if is_no_error_reply(&resp) {
374                break;
375            }
376            errors.push(resp);
377            if errors.len() >= 50 {
378                break;
379            }
380        }
381        Ok(errors)
382    }
383
384    pub(crate) fn query_completion(&mut self, timeout: Duration) -> Result<String> {
385        self.ensure_opc_available()?;
386        self.write_with_retry("*OPC?", false)?;
387        match self.read_response(timeout) {
388            Ok(bytes) => Ok(String::from_utf8_lossy(&bytes).trim().into()),
389            Err(Error::Timeout) => {
390                let _ = self.flush();
391                Err(Error::Timeout)
392            }
393            Err(err) => Err(err),
394        }
395    }
396
397    pub(crate) fn ensure_opc_available(&self) -> Result<()> {
398        if self.capabilities.opc == Some(false) {
399            return Err(Error::Unsupported(
400                "operation completion requires *OPC? support",
401            ));
402        }
403        Ok(())
404    }
405
406    /// Parses a numeric SCPI response.
407    pub fn parse_f64(response: &str) -> Result<f64> {
408        parse_f64(response)
409    }
410}