Skip to main content

lager/nets/
scope.rs

1//! Oscilloscope nets (Rigol MSO5000, PicoScope): channel setup, timebase,
2//! trigger, acquisition control and measurements (box >= 0.55.0).
3//!
4//! A scope is saved on the box as one `scope` net for the instrument and one
5//! `scope-channel` net per input. Channel settings (enable, V/div, offset,
6//! coupling, probe) and measurements go to a channel net. Everything else
7//! (timebase, horizontal position, trigger, run/stop/single) belongs to the
8//! instrument, and works through either kind of net. A channel action sent
9//! to the instrument's `scope` net fails with [`crate::Error::Box`] (HTTP
10//! 400) naming the channel nets to use instead.
11//!
12//! An older box answers HTTP 501 for scope nets, which surfaces as
13//! [`crate::Error::UnsupportedByBox`].
14//!
15//! Trace capture (the waveform stream behind the box's `/scope` page) is not
16//! on the HTTP API; see `MISSING_ENDPOINTS.md` in this crate's repository.
17//!
18//! ```no_run
19//! # #[cfg(feature = "blocking")]
20//! # fn demo() -> lager::Result<()> {
21//! use lager::{EdgeTrigger, LagerBox, Measurement, TriggerMode};
22//!
23//! let lager = LagerBox::from_env()?;
24//! let ch1 = lager.scope("scope-ch1");
25//! ch1.enable()?;
26//! ch1.set_scale(0.5)?;
27//! ch1.set_timebase(1e-3)?;
28//! ch1.trigger_edge(&EdgeTrigger {
29//!     source: Some("CHAN1".into()),
30//!     level: Some(1.2),
31//!     mode: Some(TriggerMode::Normal),
32//!     ..Default::default()
33//! })?;
34//! if let Some(hz) = ch1.measure(Measurement::Frequency)? {
35//!     println!("{hz} Hz");
36//! }
37//! # Ok(())
38//! # }
39//! # fn main() {}
40//! ```
41
42use serde::Deserialize;
43
44use super::net_handle;
45
46/// One quantity [`Scope::measure`] can read from a channel.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48#[non_exhaustive]
49pub enum Measurement {
50    /// Peak-to-peak voltage (V).
51    Vpp,
52    /// Maximum voltage (V).
53    Vmax,
54    /// Minimum voltage (V).
55    Vmin,
56    /// RMS voltage (V).
57    Vrms,
58    /// Average voltage (V).
59    Vavg,
60    /// Period (s).
61    Period,
62    /// Frequency (Hz).
63    Frequency,
64    /// Positive duty cycle (%).
65    DutyCyclePositive,
66    /// Negative duty cycle (%).
67    DutyCycleNegative,
68    /// Positive pulse width (s).
69    PulseWidthPositive,
70    /// Negative pulse width (s).
71    PulseWidthNegative,
72    /// Rise time (s).
73    RiseTime,
74    /// Fall time (s).
75    FallTime,
76    /// Overshoot (%).
77    Overshoot,
78}
79
80impl Measurement {
81    /// The box action that reads this quantity.
82    fn action(self) -> &'static str {
83        match self {
84            Measurement::Vpp => "measure_vpp",
85            Measurement::Vmax => "measure_vmax",
86            Measurement::Vmin => "measure_vmin",
87            Measurement::Vrms => "measure_vrms",
88            Measurement::Vavg => "measure_vavg",
89            Measurement::Period => "measure_period",
90            Measurement::Frequency => "measure_freq",
91            Measurement::DutyCyclePositive => "measure_dc_pos",
92            Measurement::DutyCycleNegative => "measure_dc_neg",
93            Measurement::PulseWidthPositive => "measure_pulse_width_pos",
94            Measurement::PulseWidthNegative => "measure_pulse_width_neg",
95            Measurement::RiseTime => "measure_rise_time",
96            Measurement::FallTime => "measure_fall_time",
97            Measurement::Overshoot => "measure_overshoot",
98        }
99    }
100}
101
102/// Every quantity [`Scope::measure_all`] read from one capture. A quantity
103/// the capture does not support (a period on a DC level) is `None`.
104#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize)]
105#[non_exhaustive]
106pub struct Measurements {
107    /// Peak-to-peak voltage (V).
108    pub vpp: Option<f64>,
109    /// Maximum voltage (V).
110    pub vmax: Option<f64>,
111    /// Minimum voltage (V).
112    pub vmin: Option<f64>,
113    /// RMS voltage (V).
114    pub vrms: Option<f64>,
115    /// Average voltage (V).
116    pub vavg: Option<f64>,
117    /// Period (s).
118    pub period: Option<f64>,
119    /// Frequency (Hz).
120    pub frequency: Option<f64>,
121    /// Positive duty cycle (%).
122    pub duty_cycle_positive: Option<f64>,
123    /// Negative duty cycle (%).
124    pub duty_cycle_negative: Option<f64>,
125    /// Positive pulse width (s).
126    pub pulse_width_positive: Option<f64>,
127    /// Negative pulse width (s).
128    pub pulse_width_negative: Option<f64>,
129    /// Rise time (s).
130    pub rise_time: Option<f64>,
131    /// Fall time (s).
132    pub fall_time: Option<f64>,
133    /// Overshoot (%).
134    pub overshoot: Option<f64>,
135}
136
137impl Measurements {
138    /// The value for one [`Measurement`].
139    pub fn get(&self, m: Measurement) -> Option<f64> {
140        match m {
141            Measurement::Vpp => self.vpp,
142            Measurement::Vmax => self.vmax,
143            Measurement::Vmin => self.vmin,
144            Measurement::Vrms => self.vrms,
145            Measurement::Vavg => self.vavg,
146            Measurement::Period => self.period,
147            Measurement::Frequency => self.frequency,
148            Measurement::DutyCyclePositive => self.duty_cycle_positive,
149            Measurement::DutyCycleNegative => self.duty_cycle_negative,
150            Measurement::PulseWidthPositive => self.pulse_width_positive,
151            Measurement::PulseWidthNegative => self.pulse_width_negative,
152            Measurement::RiseTime => self.rise_time,
153            Measurement::FallTime => self.fall_time,
154            Measurement::Overshoot => self.overshoot,
155        }
156    }
157}
158
159/// How the scope re-arms after a trigger.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum TriggerMode {
162    /// Trigger on the edge, or free-run when none arrives.
163    Auto,
164    /// Trigger only on the edge.
165    Normal,
166    /// Arm for one acquisition. Setting this mode arms the scope, as on a
167    /// bench scope.
168    Single,
169}
170
171impl TriggerMode {
172    fn as_str(self) -> &'static str {
173        match self {
174            TriggerMode::Auto => "auto",
175            TriggerMode::Normal => "normal",
176            TriggerMode::Single => "single",
177        }
178    }
179}
180
181/// Edge-trigger settings for [`Scope::trigger_edge`]. Only the fields that
182/// are set are applied, so `level` alone moves the level and leaves the
183/// source and slope as they are. At least one must be set.
184#[derive(Debug, Clone, Default, PartialEq)]
185pub struct EdgeTrigger {
186    /// Trigger source as the instrument names it (`"CHAN1"` on a Rigol,
187    /// `"A"` on a PicoScope).
188    pub source: Option<String>,
189    /// Edge to trigger on, as the instrument names it (`"rising"`,
190    /// `"falling"`, ...).
191    pub slope: Option<String>,
192    /// The trigger path's filter (`"dc"`, `"ac"`, `"lfreject"`,
193    /// `"hfreject"`). This is not the channel's input coupling; see
194    /// [`Scope::set_coupling`] for that.
195    pub coupling: Option<String>,
196    /// Trigger level (V).
197    pub level: Option<f64>,
198    /// Re-arm mode.
199    pub mode: Option<TriggerMode>,
200}
201
202/// The trigger as the instrument has it, from [`Scope::trigger`]. Fields are
203/// the instrument's own spellings.
204#[derive(Debug, Clone, PartialEq, Deserialize)]
205#[non_exhaustive]
206pub struct TriggerSettings {
207    /// Capture mode (`auto`, `normal`, `single`, ...).
208    #[serde(default)]
209    pub mode: Option<String>,
210    /// Trigger source.
211    #[serde(default)]
212    pub source: Option<String>,
213    /// Trigger slope.
214    #[serde(default)]
215    pub slope: Option<String>,
216    /// Trigger level (V).
217    #[serde(default)]
218    pub level: Option<f64>,
219    /// Trigger holdoff (s), when the scope's daemon reports one.
220    #[serde(default)]
221    pub holdoff: Option<f64>,
222}
223
224pub(crate) mod ops {
225    use std::time::Duration;
226
227    use serde_json::{json, Value};
228
229    use super::{EdgeTrigger, Measurement, Measurements, TriggerSettings};
230    use crate::error::{Error, Result};
231    use crate::wire::{
232        as_f64, net_command, unit, value_as, value_f64, CommandResponse, Op, Timeout,
233    };
234
235    /// The box accepts `scope` for both scope roles and returns the saved
236    /// one, which its per-channel check reads.
237    const ROLE: &str = "scope";
238
239    /// The box allows its scope adapter 30 s per command.
240    fn budget() -> Timeout {
241        Timeout::After(Duration::from_secs(45))
242    }
243
244    fn cmd<T>(
245        name: &str,
246        action: &str,
247        params: Value,
248        parse: fn(CommandResponse) -> Result<T>,
249    ) -> Op<T> {
250        Op {
251            req: net_command(name, ROLE, action, params, budget()),
252            parse,
253        }
254    }
255
256    fn parse_string(resp: CommandResponse) -> Result<String> {
257        match resp.value {
258            Some(Value::String(s)) => Ok(s),
259            other => Err(Error::Decode(format!(
260                "expected string 'value', got {other:?}"
261            ))),
262        }
263    }
264
265    fn parse_bool(resp: CommandResponse) -> Result<bool> {
266        match resp.value {
267            Some(Value::Bool(b)) => Ok(b),
268            other => Err(Error::Decode(format!(
269                "expected boolean 'value', got {other:?}"
270            ))),
271        }
272    }
273
274    /// A quantity the capture does not contain comes back as success with
275    /// no `value`.
276    fn parse_measurement(resp: CommandResponse) -> Result<Option<f64>> {
277        match resp.value {
278            None | Some(Value::Null) => Ok(None),
279            Some(v) => as_f64(&v)
280                .map(Some)
281                .ok_or_else(|| Error::Decode(format!("expected numeric 'value', got {v:?}"))),
282        }
283    }
284
285    fn parse_value(resp: CommandResponse) -> Result<Value> {
286        Ok(resp.value.unwrap_or(Value::Null))
287    }
288
289    pub(crate) fn enable(name: &str) -> Op<()> {
290        cmd(name, "enable_net", json!({}), unit)
291    }
292
293    pub(crate) fn disable(name: &str) -> Op<()> {
294        cmd(name, "disable_net", json!({}), unit)
295    }
296
297    pub(crate) fn is_enabled(name: &str) -> Op<bool> {
298        cmd(name, "get_net_enabled", json!({}), parse_bool)
299    }
300
301    pub(crate) fn run(name: &str) -> Op<()> {
302        cmd(name, "start_capture", json!({}), unit)
303    }
304
305    pub(crate) fn single(name: &str) -> Op<()> {
306        cmd(name, "start_single", json!({}), unit)
307    }
308
309    pub(crate) fn stop(name: &str) -> Op<()> {
310        cmd(name, "stop_capture", json!({}), unit)
311    }
312
313    pub(crate) fn force_trigger(name: &str) -> Op<()> {
314        cmd(name, "force_trigger", json!({}), unit)
315    }
316
317    pub(crate) fn autoscale(name: &str) -> Op<()> {
318        cmd(name, "autoscale", json!({}), unit)
319    }
320
321    pub(crate) fn set_scale(name: &str, volts_per_div: f64) -> Op<()> {
322        cmd(
323            name,
324            "set_scale",
325            json!({ "volts_per_div": volts_per_div }),
326            unit,
327        )
328    }
329
330    pub(crate) fn scale(name: &str) -> Op<f64> {
331        cmd(name, "get_scale", json!({}), value_f64)
332    }
333
334    pub(crate) fn set_timebase(name: &str, seconds_per_div: f64) -> Op<()> {
335        cmd(
336            name,
337            "set_timebase",
338            json!({ "seconds_per_div": seconds_per_div }),
339            unit,
340        )
341    }
342
343    pub(crate) fn timebase(name: &str) -> Op<f64> {
344        cmd(name, "get_timebase", json!({}), value_f64)
345    }
346
347    pub(crate) fn set_coupling(name: &str, mode: &str) -> Op<()> {
348        cmd(name, "set_coupling", json!({ "mode": mode }), unit)
349    }
350
351    pub(crate) fn coupling(name: &str) -> Op<String> {
352        cmd(name, "get_coupling", json!({}), parse_string)
353    }
354
355    pub(crate) fn set_probe(name: &str, ratio: f64) -> Op<()> {
356        cmd(name, "set_probe", json!({ "ratio": ratio }), unit)
357    }
358
359    pub(crate) fn probe(name: &str) -> Op<f64> {
360        cmd(name, "get_probe", json!({}), value_f64)
361    }
362
363    pub(crate) fn set_offset(name: &str, volts: f64) -> Op<()> {
364        cmd(name, "set_offset", json!({ "offset": volts }), unit)
365    }
366
367    pub(crate) fn offset(name: &str) -> Op<f64> {
368        cmd(name, "get_offset", json!({}), value_f64)
369    }
370
371    pub(crate) fn set_time_offset(name: &str, seconds: f64) -> Op<()> {
372        cmd(name, "set_time_offset", json!({ "offset": seconds }), unit)
373    }
374
375    pub(crate) fn time_offset(name: &str) -> Op<f64> {
376        cmd(name, "get_time_offset", json!({}), value_f64)
377    }
378
379    pub(crate) fn trigger_edge(name: &str, trigger: &EdgeTrigger) -> Op<()> {
380        let mut params = json!({});
381        if let Some(source) = &trigger.source {
382            params["source"] = json!(source);
383        }
384        if let Some(slope) = &trigger.slope {
385            params["slope"] = json!(slope);
386        }
387        if let Some(coupling) = &trigger.coupling {
388            params["coupling"] = json!(coupling);
389        }
390        if let Some(level) = trigger.level {
391            params["level"] = json!(level);
392        }
393        if let Some(mode) = trigger.mode {
394            params["mode"] = json!(mode.as_str());
395        }
396        cmd(name, "trigger_edge", params, unit)
397    }
398
399    pub(crate) fn trigger(name: &str) -> Op<TriggerSettings> {
400        cmd(name, "get_trigger", json!({}), value_as::<TriggerSettings>)
401    }
402
403    pub(crate) fn measure(name: &str, m: Measurement) -> Op<Option<f64>> {
404        cmd(name, m.action(), json!({}), parse_measurement)
405    }
406
407    pub(crate) fn measure_all(name: &str) -> Op<Measurements> {
408        cmd(name, "measure_all", json!({}), value_as::<Measurements>)
409    }
410
411    pub(crate) fn command(name: &str, action: &str, params: Value) -> Op<Value> {
412        cmd(name, action, params, parse_value)
413    }
414}
415
416net_handle! {
417    /// Handle for an oscilloscope net: an instrument (`scope`) or one of
418    /// its channels (`scope-channel`). See the [module docs](self) for which
419    /// actions need a channel net.
420    sync: Scope,
421    async: AsyncScope,
422    methods: {
423        /// Turn this net's channel on.
424        fn enable() -> () = ops::enable;
425        /// Turn this net's channel off.
426        fn disable() -> () = ops::disable;
427        /// Whether this net's channel is on, as the instrument reports it.
428        fn is_enabled() -> bool = ops::is_enabled;
429        /// Start continuous acquisition.
430        fn run() -> () = ops::run;
431        /// Arm for a single acquisition.
432        fn single() -> () = ops::single;
433        /// Stop acquisition.
434        fn stop() -> () = ops::stop;
435        /// Force a trigger now.
436        fn force_trigger() -> () = ops::force_trigger;
437        /// Let the instrument pick scale, timebase and trigger.
438        fn autoscale() -> () = ops::autoscale;
439        /// Set this channel's vertical scale (V/div).
440        fn set_scale(volts_per_div: f64) -> () = ops::set_scale;
441        /// This channel's vertical scale (V/div).
442        fn scale() -> f64 = ops::scale;
443        /// Set the timebase (s/div).
444        fn set_timebase(seconds_per_div: f64) -> () = ops::set_timebase;
445        /// The timebase (s/div).
446        fn timebase() -> f64 = ops::timebase;
447        /// Set this channel's input coupling (`"dc"`, `"ac"`; a Rigol also
448        /// takes `"gnd"`).
449        fn set_coupling(mode: &str) -> () = ops::set_coupling;
450        /// This channel's input coupling.
451        fn coupling() -> String = ops::coupling;
452        /// Set this channel's probe attenuation (1 for 1x, 10 for 10x).
453        fn set_probe(ratio: f64) -> () = ops::set_probe;
454        /// This channel's probe attenuation.
455        fn probe() -> f64 = ops::probe;
456        /// Set this channel's vertical offset (V).
457        fn set_offset(volts: f64) -> () = ops::set_offset;
458        /// This channel's vertical offset (V).
459        fn offset() -> f64 = ops::offset;
460        /// Move the capture window in time relative to the trigger (s).
461        fn set_time_offset(seconds: f64) -> () = ops::set_time_offset;
462        /// The capture window's time offset from the trigger (s).
463        fn time_offset() -> f64 = ops::time_offset;
464        /// Configure the edge trigger, applying only the fields that are
465        /// set.
466        fn trigger_edge(trigger: &EdgeTrigger) -> () = ops::trigger_edge;
467        /// The trigger as the instrument has it.
468        fn trigger() -> TriggerSettings = ops::trigger;
469        /// Read one quantity from this channel. `None` when the capture does
470        /// not contain it (a period on a DC level).
471        fn measure(m: Measurement) -> Option<f64> = ops::measure;
472        /// Every quantity this channel's capture supports, read from one
473        /// capture so the values agree with each other.
474        fn measure_all() -> Measurements = ops::measure_all;
475        /// Run any other scope action by name with raw JSON params, and
476        /// return its `value` (`null` when it has none): `capabilities`,
477        /// `get_state`, `set_acquire`, `set_trigger_holdoff`, `set_roll`,
478        /// `fft`, the cursor actions, and so on.
479        fn command(action: &str, params: serde_json::Value) -> serde_json::Value = ops::command;
480    }
481}