Skip to main content

pico_streaming/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Streams gap-less data from Pico Technology oscilloscope drivers.
4//!
5//! This is a sub crate that you probably don't want to use directly. Try the top level
6//! [`pico-sdk`](https://crates.io/crates/pico-sdk) crate which exposes everything from here.
7//!
8//! Once streaming is started, a `PicoStreamingDevice` returns `StreamingEvent`s. The possible events
9//! and `Connected`, `Disconnected` and `Data`. The `Data` event contains raw `Vec<i16>` samples for
10//! each enabled channel that can easily be scaled to the channel units (ie. Volts, Amps, etc).
11//!
12//!
13//! # Example
14//! ```no_run
15//! # fn run() -> Result<(),Box<dyn std::error::Error>> {
16//! # use std::sync::Arc;
17//! # use pico_common::{Driver, PicoChannel, PicoCoupling, PicoRange};
18//! # use pico_driver::LibraryResolution;
19//! # use pico_device::PicoDevice;
20//! # use pico_streaming::{NewDataHandler, StreamingEvent, ToStreamDevice};
21//! # // Load the required driver
22//! # let driver = LibraryResolution::Default.try_load(Driver::PS2000)?;
23//! # // Try and load the first available ps2000 device
24//! # let device = PicoDevice::try_open(&driver, None)?;
25//! // Get a streaming device from a PicoDevice
26//! let stream_device = device.into_streaming_device();
27//!
28//! // Enable and configure 2 channels
29//! stream_device.enable_channel(PicoChannel::A, PicoRange::X1_PROBE_2V, PicoCoupling::DC);
30//! stream_device.enable_channel(PicoChannel::B, PicoRange::X1_PROBE_1V, PicoCoupling::AC);
31//!
32//! struct StdoutHandler;
33//!
34//! impl NewDataHandler for StdoutHandler {
35//!     fn handle_event(&self, event: &StreamingEvent) {
36//!         println!("Sample count: {}", event.length);
37//!     }
38//! }
39//!
40//! let handler = Arc::new(StdoutHandler);
41//!
42//! // Subscribe to streaming events
43//! stream_device.new_data.subscribe(handler);
44//!
45//! // Start streaming with a sample rate of 1k
46//! stream_device.start(1_000)?;
47//! # Ok(())
48//! # }
49//! ```
50
51use crossbeam::channel::{bounded, Sender};
52use events::StreamingEvents;
53pub use events::{NewDataHandler, RawChannelDataBlock, StreamingEvent};
54use parking_lot::RwLock;
55use pico_common::{
56    ChannelConfig, PicoChannel, PicoCoupling, PicoRange, PicoResult, PicoStatus, SampleConfig,
57};
58use pico_device::PicoDevice;
59use std::{
60    collections::HashMap,
61    fmt,
62    sync::Arc,
63    thread::{self, JoinHandle},
64    time::Duration,
65};
66use tracing::*;
67
68mod events;
69
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71#[derive(Debug, Clone, Copy)]
72enum Target {
73    Closed,
74    Open,
75    Streaming { requested_sample_rate: u32 },
76}
77
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79#[derive(Clone)]
80struct LockedTarget(Arc<RwLock<Target>>);
81
82impl LockedTarget {
83    pub fn new(target: Target) -> Self {
84        LockedTarget(Arc::new(RwLock::new(target)))
85    }
86
87    pub fn set(&self, new: Target) {
88        *self.0.write() = new;
89    }
90
91    pub fn get(&self) -> Target {
92        *self.0.read()
93    }
94}
95
96impl fmt::Debug for LockedTarget {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.write_fmt(format_args!("{:?}", self.0.try_read()))
99    }
100}
101
102type BufferMap = HashMap<PicoChannel, Arc<RwLock<Vec<i16>>>>;
103
104#[cfg_attr(feature = "serde", derive(serde::Serialize))]
105#[derive(Clone)]
106enum State {
107    Closed,
108    Open {
109        handle: i16,
110    },
111    Streaming {
112        handle: i16,
113        actual_sample_rate: u32,
114        #[cfg_attr(feature = "serde", serde(skip))]
115        buffers: BufferMap,
116    },
117}
118
119impl PartialEq for State {
120    fn eq(&self, other: &Self) -> bool {
121        matches!(
122            (self, other),
123            (State::Closed, State::Closed)
124                | (State::Open { .. }, State::Open { .. })
125                | (State::Streaming { .. }, State::Streaming { .. })
126        )
127    }
128}
129
130impl fmt::Debug for State {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            State::Closed => f.debug_struct("Closed").finish(),
134            State::Open { handle } => f.debug_struct("Open").field("handle", handle).finish(),
135            State::Streaming {
136                handle,
137                actual_sample_rate,
138                ..
139            } => f
140                .debug_struct("Streaming")
141                .field("handle", handle)
142                .field("actual_sample_rate", actual_sample_rate)
143                .finish(),
144        }
145    }
146}
147
148/// Encapsulates a `PicoDevice` and adds streaming functionality
149///
150/// Automatically reconfigures and restarts streaming if the device connection
151/// is lost.
152#[cfg_attr(feature = "serde", derive(serde::Serialize))]
153#[derive(Clone)]
154pub struct PicoStreamingDevice {
155    device: PicoDevice,
156    target_state: LockedTarget,
157    current_state: Arc<RwLock<State>>,
158    enabled_channels: Arc<RwLock<HashMap<PicoChannel, ChannelConfig>>>,
159    #[cfg_attr(feature = "serde", serde(skip))]
160    background_handle: Option<Arc<BackgroundThreadHandle>>,
161    #[cfg_attr(feature = "serde", serde(skip))]
162    pub new_data: StreamingEvents,
163}
164
165impl fmt::Debug for PicoStreamingDevice {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.debug_struct("PicoStreamingDevice")
168            .field("device", &self.device)
169            .field("target_state", &self.target_state)
170            .field("current_state", &self.current_state.try_read())
171            .finish()
172    }
173}
174
175impl PartialEq for PicoStreamingDevice {
176    fn eq(&self, other: &Self) -> bool {
177        self.get_serial() == other.get_serial()
178    }
179}
180
181impl Eq for PicoStreamingDevice {}
182
183impl std::hash::Hash for PicoStreamingDevice {
184    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
185        self.get_serial().hash(state);
186    }
187}
188
189impl From<PicoDevice> for PicoStreamingDevice {
190    fn from(d: PicoDevice) -> Self {
191        PicoStreamingDevice::new(d)
192    }
193}
194
195impl PicoStreamingDevice {
196    fn new(device: PicoDevice) -> Self {
197        let (current_state, target_state) = match device.handle.lock().take() {
198            Some(handle) => (State::Open { handle }, Target::Open),
199            None => (State::Closed, Target::Closed),
200        };
201
202        let mut device = PicoStreamingDevice {
203            device,
204            target_state: LockedTarget::new(target_state),
205            current_state: Arc::new(RwLock::new(current_state)),
206            new_data: Default::default(),
207            enabled_channels: Default::default(),
208            background_handle: Default::default(),
209        };
210
211        device.start_background_thread();
212
213        device
214    }
215
216    pub fn get_serial(&self) -> String {
217        self.device.serial.to_string()
218    }
219
220    pub fn get_variant(&self) -> String {
221        self.device.variant.to_string()
222    }
223
224    pub fn enable_channel(&self, channel: PicoChannel, range: PicoRange, coupling: PicoCoupling) {
225        self.enabled_channels.write().insert(
226            channel,
227            ChannelConfig {
228                range,
229                coupling,
230                offset: 0.0,
231            },
232        );
233    }
234
235    pub fn disable_channel(&self, channel: PicoChannel) {
236        self.enabled_channels.write().remove(&channel);
237    }
238
239    pub fn get_channels(&self) -> Vec<PicoChannel> {
240        self.device.get_channels()
241    }
242
243    pub fn get_valid_ranges(&self, channel: PicoChannel) -> Option<Vec<PicoRange>> {
244        self.device.channel_ranges.get(&channel).cloned()
245    }
246
247    pub fn get_channel_config(&self, channel: PicoChannel) -> Option<ChannelConfig> {
248        self.enabled_channels.read().get(&channel).cloned()
249    }
250
251    /// Start streaming
252    #[tracing::instrument(level = "info")]
253    pub fn start(&self, requested_sample_rate: u32) -> PicoResult<u32> {
254        // Set the target state
255
256        self.target_state.set(Target::Streaming {
257            requested_sample_rate,
258        });
259
260        // Drive the state until we get the correct state or an error we can return
261        let mut count = 0;
262        loop {
263            if let Err(e) = self.run_state() {
264                self.target_state.set(Target::Open);
265                return Err(e);
266            }
267
268            let current = self.current_state.read();
269            if let State::Streaming {
270                actual_sample_rate, ..
271            } = *current
272            {
273                return Ok(actual_sample_rate);
274            }
275
276            count += 1;
277
278            if count > 5 {
279                return Err(PicoStatus::TIMEOUT.into());
280            }
281        }
282    }
283
284    /// Stop streaming
285    #[tracing::instrument(level = "info")]
286    pub fn stop(&self) {
287        self.target_state.set(Target::Open);
288    }
289
290    /// Close device
291    #[tracing::instrument(level = "info")]
292    pub fn close(&self) {
293        self.target_state.set(Target::Closed);
294    }
295
296    fn start_background_thread(&mut self) {
297        let (tx_terminate, rx_terminate) = bounded::<()>(0);
298
299        let handle = thread::Builder::new()
300            .name("Streaming background task".to_string())
301            .spawn({
302                let device = self.clone();
303                let mut wait_for_closed = false;
304
305                move || loop {
306                    let next_wait = device
307                        .run_state()
308                        .unwrap_or_else(|_| Duration::from_millis(500));
309
310                    if !wait_for_closed && rx_terminate.recv_timeout(next_wait).is_ok() {
311                        device.close();
312                        wait_for_closed = true;
313                    }
314
315                    if wait_for_closed {
316                        if let State::Closed = *device.current_state.read() {
317                            return;
318                        }
319                    }
320                }
321            })
322            .expect("Could not start thread");
323
324        self.background_handle = Some(BackgroundThreadHandle::new(tx_terminate, handle));
325    }
326
327    #[tracing::instrument(skip(self), level = "debug", err(Display))]
328    fn run_state(&self) -> PicoResult<Duration> {
329        let mut current_state = self.current_state.write();
330        let initial_state = current_state.clone();
331
332        let target = self.target_state.get();
333
334        let (next_state, next_duration) = match current_state.clone() {
335            State::Closed => match target {
336                Target::Closed => (State::Closed, Duration::from_millis(500)),
337                Target::Open | Target::Streaming { .. } => {
338                    let handle = self.device.driver.open_unit(Some(&self.device.serial))?;
339                    (State::Open { handle }, Duration::from_millis(1))
340                }
341            },
342            State::Open { handle } => match target {
343                Target::Closed => {
344                    self.device.driver.close(handle)?;
345                    (State::Closed, Duration::from_millis(500))
346                }
347                Target::Open => self.ping(handle),
348                Target::Streaming {
349                    requested_sample_rate,
350                } => self.configure_and_start(handle, requested_sample_rate)?,
351            },
352            State::Streaming {
353                handle,
354                actual_sample_rate,
355                buffers,
356            } => match target {
357                Target::Closed | Target::Open => {
358                    self.device.driver.stop(handle)?;
359                    (State::Open { handle }, Duration::from_millis(1))
360                }
361                Target::Streaming { .. } => self.stream(handle, buffers, actual_sample_rate),
362            },
363        };
364
365        if initial_state != next_state {
366            info!("State changed '{:?}' > '{:?}'", initial_state, next_state);
367        }
368
369        *current_state = next_state;
370
371        Ok(next_duration)
372    }
373
374    fn ping(&self, handle: i16) -> (State, Duration) {
375        if self.device.driver.ping_unit(handle).is_err() {
376            let _ = self.device.driver.stop(handle);
377            let _ = self.device.driver.close(handle);
378
379            (State::Closed, Duration::from_millis(500))
380        } else {
381            (State::Open { handle }, Duration::from_millis(500))
382        }
383    }
384
385    #[tracing::instrument(skip(self), level = "debug")]
386    fn configure_and_start(
387        &self,
388        handle: i16,
389        samples_per_second: u32,
390    ) -> PicoResult<(State, Duration)> {
391        let mut buffers = HashMap::new();
392
393        let enabled_channels = self.enabled_channels.read();
394
395        let mut enabled_channel_count = 0;
396
397        for (channel, ranges) in &self.device.channel_ranges {
398            // If there are no valid ranges, skip configuring this channel
399            if ranges.is_empty() {
400                continue;
401            }
402
403            // is this channel enabled?
404            if let Some(config) = enabled_channels.get(channel) {
405                let buffer_size = samples_per_second as usize;
406
407                self.device
408                    .driver
409                    .enable_channel(handle, *channel, config)?;
410
411                let ch_buf = buffers
412                    .entry(*channel)
413                    .or_insert_with(|| Arc::new(RwLock::new(vec![0i16; buffer_size])));
414
415                self.device.driver.set_data_buffer(
416                    handle,
417                    *channel,
418                    ch_buf.clone(),
419                    buffer_size,
420                )?;
421
422                enabled_channel_count += 1;
423            } else {
424                self.device.driver.disable_channel(handle, *channel)?;
425            }
426        }
427
428        let target_config = SampleConfig::from_samples_per_second(samples_per_second);
429        let actual_sample_rate = self
430            .device
431            .driver
432            .start_streaming(handle, &target_config, enabled_channel_count)
433            .map(|sc| sc.samples_per_second())?;
434
435        Ok((
436            State::Streaming {
437                handle,
438                actual_sample_rate,
439                buffers,
440            },
441            Duration::from_millis(100),
442        ))
443    }
444
445    #[tracing::instrument(skip(self, buffers), level = "trace")]
446    fn stream(
447        &self,
448        handle: i16,
449        buffers: BufferMap,
450        actual_sample_rate: u32,
451    ) -> (State, Duration) {
452        let callback = |start_index, sample_count| {
453            let channels = self.enabled_channels.read();
454
455            let channels = channels
456                .iter()
457                .map(|(ch, config)| {
458                    let ch_buf = buffers
459                        .get(ch)
460                        .expect("Channel is enabled but has no buffer")
461                        .read();
462
463                    (
464                        *ch,
465                        RawChannelDataBlock {
466                            multiplier: config.range.get_max_scaled_value()
467                                / self.device.max_adc_value as f64,
468                            samples: ch_buf[start_index..(start_index + sample_count)].to_vec(),
469                        },
470                    )
471                })
472                .collect::<HashMap<_, _>>();
473
474            self.new_data.emit(StreamingEvent {
475                samples_per_second: actual_sample_rate,
476                length: sample_count,
477                channels,
478            });
479        };
480
481        let channels = buffers.keys().copied().collect::<Vec<_>>();
482
483        if let Err(error) =
484            self.device
485                .driver
486                .get_latest_streaming_values(handle, &channels, Box::new(callback))
487        {
488            if error.status == PicoStatus::WAITING_FOR_DATA_BUFFERS {
489                for (channel, buffer) in &buffers {
490                    let len = { buffer.read().len() };
491                    self.device
492                        .driver
493                        .set_data_buffer(handle, *channel, buffer.clone(), len)
494                        .unwrap();
495                }
496
497                (
498                    State::Streaming {
499                        handle,
500                        buffers,
501                        actual_sample_rate,
502                    },
503                    Duration::from_millis(5),
504                )
505            } else {
506                warn!("Streaming stopped: '{:?}'", error);
507
508                let _ = self.device.driver.stop(handle);
509                let _ = self.device.driver.close(handle);
510
511                (State::Closed, Duration::from_millis(200))
512            }
513        } else {
514            (
515                State::Streaming {
516                    handle,
517                    actual_sample_rate,
518                    buffers,
519                },
520                Duration::from_millis(50),
521            )
522        }
523    }
524}
525
526/// Converts `PicoDevice` into `PicoStreamingDevice`
527pub trait ToStreamDevice {
528    fn into_streaming_device(self) -> PicoStreamingDevice;
529}
530
531impl ToStreamDevice for PicoDevice {
532    fn into_streaming_device(self) -> PicoStreamingDevice {
533        PicoStreamingDevice::new(self)
534    }
535}
536
537pub struct BackgroundThreadHandle {
538    tx_terminate: Sender<()>,
539    handle: Option<JoinHandle<()>>,
540}
541
542impl BackgroundThreadHandle {
543    pub fn new(tx_terminate: Sender<()>, handle: JoinHandle<()>) -> Arc<Self> {
544        Arc::new(BackgroundThreadHandle {
545            tx_terminate,
546            handle: Some(handle),
547        })
548    }
549}
550
551impl Drop for BackgroundThreadHandle {
552    #[tracing::instrument(skip(self), level = "debug")]
553    fn drop(&mut self) {
554        self.tx_terminate.send(()).unwrap();
555
556        self.handle.take().unwrap().join().unwrap();
557    }
558}