Skip to main content

instrument/classes/
scope.rs

1use super::dialect_io;
2use instrument_core::error::Result;
3use instrument_core::kind::InstrumentKind;
4use instrument_core::scpi::{parse_f64_csv, ScpiSession};
5use instrument_core::scpi_commands;
6use instrument_core::InstrumentSession;
7
8/// Captured voltage waveform samples in SI units.
9#[derive(Debug, Clone, PartialEq)]
10pub struct VoltageTrace {
11    /// Sample values in volts.
12    pub samples: Vec<f64>,
13    /// Time between samples in seconds (0.0 when preamble unavailable).
14    pub sample_interval_s: f64,
15}
16
17/// Oscilloscope session view (IVI-inspired / SCPI :TIMebase, :CHANnel, :WAVeform).
18pub struct Oscilloscope {
19    session: InstrumentSession,
20}
21
22impl Oscilloscope {
23    pub fn new(session: InstrumentSession) -> Self {
24        Self { session }
25    }
26
27    pub fn session(&self) -> &InstrumentSession {
28        &self.session
29    }
30
31    pub fn session_mut(&mut self) -> &mut InstrumentSession {
32        &mut self.session
33    }
34
35    fn dialect(&self) -> &'static instrument_core::DialectProfile {
36        self.session.dialect_for(InstrumentKind::Oscilloscope)
37    }
38
39    fn cmd(&self, key: &str, vars: &[(&str, String)], fallback: String) -> String {
40        dialect_io::try_formatted(self.dialect(), key, vars, fallback)
41    }
42
43    /// Sets horizontal timebase scale in seconds per division (SI).
44    pub fn set_timebase_scale(&mut self, seconds_per_div: f64) -> Result<()> {
45        let cmd = self.cmd(
46            "set_timebase_scale",
47            &[("seconds_per_div", dialect_io::f64_text(seconds_per_div))],
48            scpi_commands::scope_set_timebase_scale(seconds_per_div),
49        );
50        self.session.scpi_mut().write(&cmd)
51    }
52
53    /// Reads horizontal timebase scale in seconds per division (SI).
54    pub fn read_timebase_scale(&mut self) -> Result<f64> {
55        let cmd = dialect_io::try_command(
56            self.dialect(),
57            "read_timebase_scale",
58            scpi_commands::SCOPE_READ_TIMEBASE_SCALE,
59        );
60        let resp = self.session.scpi_mut().query(cmd)?;
61        ScpiSession::parse_f64(&resp)
62    }
63
64    /// Sets vertical scale for a channel in volts per division (SI).
65    pub fn set_channel_scale(&mut self, channel: u32, volts_per_div: f64) -> Result<()> {
66        let cmd = self.cmd(
67            "set_channel_scale",
68            &[
69                ("channel", channel.to_string()),
70                ("volts_per_div", dialect_io::f64_text(volts_per_div)),
71            ],
72            scpi_commands::scope_set_channel_scale(channel, volts_per_div),
73        );
74        self.session.scpi_mut().write(&cmd)
75    }
76
77    /// Enables or disables channel display.
78    pub fn set_channel_display(&mut self, channel: u32, enabled: bool) -> Result<()> {
79        let state = if enabled { "ON" } else { "OFF" };
80        let cmd = self.cmd(
81            "channel_display",
82            &[("channel", channel.to_string()), ("state", state.into())],
83            scpi_commands::scope_channel_display(channel, state),
84        );
85        self.session.scpi_mut().write(&cmd)
86    }
87
88    /// Sets channel coupling (`DC`, `AC`, or `GND`).
89    pub fn set_channel_coupling(&mut self, channel: u32, coupling: &str) -> Result<()> {
90        let cmd = self.cmd(
91            "channel_coupling",
92            &[
93                ("channel", channel.to_string()),
94                ("coupling", coupling.into()),
95            ],
96            scpi_commands::scope_channel_coupling(channel, coupling),
97        );
98        self.session.scpi_mut().write(&cmd)
99    }
100
101    /// Sets edge trigger source (e.g. `CHAN1`, `EXT`).
102    pub fn set_trigger_source(&mut self, source: &str) -> Result<()> {
103        let cmd = self.cmd(
104            "trigger_source",
105            &[("source", source.into())],
106            scpi_commands::scope_trigger_source(source),
107        );
108        self.session.scpi_mut().write(&cmd)
109    }
110
111    /// Sets edge trigger level in volts (SI).
112    pub fn set_trigger_level(&mut self, volts: f64) -> Result<()> {
113        let cmd = self.cmd(
114            "trigger_level",
115            &[("volts", dialect_io::f64_text(volts))],
116            scpi_commands::scope_trigger_level(volts),
117        );
118        self.session.scpi_mut().write(&cmd)
119    }
120
121    /// Sets edge trigger slope (`POS` or `NEG`).
122    pub fn set_trigger_slope(&mut self, slope: &str) -> Result<()> {
123        let cmd = self.cmd(
124            "trigger_slope",
125            &[("slope", slope.into())],
126            scpi_commands::scope_trigger_slope(slope),
127        );
128        self.session.scpi_mut().write(&cmd)
129    }
130
131    /// Starts acquisition.
132    pub fn run(&mut self) -> Result<()> {
133        let cmd = dialect_io::try_command(self.dialect(), "run", scpi_commands::SCOPE_RUN);
134        self.session.scpi_mut().write(cmd)
135    }
136
137    /// Stops acquisition.
138    pub fn stop(&mut self) -> Result<()> {
139        let cmd = dialect_io::try_command(self.dialect(), "stop", scpi_commands::SCOPE_STOP);
140        self.session.scpi_mut().write(cmd)
141    }
142
143    /// Arms a single acquisition.
144    pub fn single(&mut self) -> Result<()> {
145        let cmd = dialect_io::try_command(self.dialect(), "single", scpi_commands::SCOPE_SINGLE);
146        self.session.scpi_mut().write(cmd)
147    }
148
149    /// Measures peak-to-peak voltage on a channel (SI volts).
150    pub fn measure_vpp(&mut self, channel: u32) -> Result<f64> {
151        let cmd = self.cmd(
152            "measure_vpp",
153            &[("channel", channel.to_string())],
154            scpi_commands::scope_measure_vpp(channel),
155        );
156        let resp = self.session.scpi_mut().query(&cmd)?;
157        ScpiSession::parse_f64(&resp)
158    }
159
160    /// Measures frequency on a channel (SI hertz).
161    pub fn measure_frequency(&mut self, channel: u32) -> Result<f64> {
162        let cmd = self.cmd(
163            "measure_frequency",
164            &[("channel", channel.to_string())],
165            scpi_commands::scope_measure_frequency(channel),
166        );
167        let resp = self.session.scpi_mut().query(&cmd)?;
168        ScpiSession::parse_f64(&resp)
169    }
170
171    /// Captures a voltage trace from a channel (1-based). ASCII only; binary `#N` is deferred.
172    pub fn capture_voltage_trace(&mut self, channel: u32) -> Result<VoltageTrace> {
173        let source = self.cmd(
174            "waveform_source",
175            &[("channel", channel.to_string())],
176            scpi_commands::scope_set_waveform_source(channel),
177        );
178        let format = dialect_io::try_command(
179            self.dialect(),
180            "waveform_format_ascii",
181            scpi_commands::SCOPE_WAVEFORM_FORMAT_ASCII,
182        );
183        let preamble_cmd = dialect_io::try_command(
184            self.dialect(),
185            "waveform_preamble",
186            scpi_commands::SCOPE_WAVEFORM_PREAMBLE,
187        );
188        let data_cmd = dialect_io::try_command(
189            self.dialect(),
190            "waveform_data",
191            scpi_commands::SCOPE_WAVEFORM_DATA,
192        );
193        let scpi = self.session.scpi_mut();
194        scpi.write(&source)?;
195        scpi.write(format)?;
196        let sample_interval_s = scpi
197            .query(preamble_cmd)
198            .ok()
199            .and_then(|preamble| parse_preamble_x_increment(&preamble))
200            .unwrap_or(0.0);
201        let data = scpi.query(data_cmd)?;
202        let samples = parse_f64_csv(&data)?;
203        Ok(VoltageTrace {
204            samples,
205            sample_interval_s,
206        })
207    }
208}
209
210pub(crate) fn parse_preamble_x_increment(preamble: &str) -> Option<f64> {
211    let fields: Vec<&str> = preamble.split(',').map(str::trim).collect();
212    fields.get(4).and_then(|v| v.parse().ok())
213}