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
14pub 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 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 pub fn write(&mut self, command: &str) -> Result<()> {
85 self.write_with_retry(command, false)
86 }
87
88 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 fn restore_io_timeout(&mut self) -> Result<()> {
317 self.transport.set_read_timeout(self.opts.io_timeout())
318 }
319
320 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 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 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 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 pub fn parse_f64(response: &str) -> Result<f64> {
408 parse_f64(response)
409 }
410}