pico-sdk 0.2.1

Unofficial Rust bindings and wrappers for Pico Technology oscilloscope drivers
Documentation
#![forbid(unsafe_code)]

use anyhow::{anyhow, Result};
use console::{style, Style, Term};
use dialoguer::{theme::ColorfulTheme, Select};
use pico_sdk::prelude::*;
use signifix::metric;
use std::{
    collections::{HashMap, VecDeque},
    convert::TryFrom,
    sync::{Arc, Mutex},
};

fn better_theme() -> ColorfulTheme {
    ColorfulTheme {
        defaults_style: Style::new(),
        inactive_item_style: Style::new(),
        active_item_style: Style::new().bold(),
        active_item_prefix: style(">".to_string()).for_stderr().bold().green(),
        ..ColorfulTheme::default()
    }
}

#[derive(Clone)]
struct RateCalc {
    queue: Arc<Mutex<VecDeque<u64>>>,
}

impl RateCalc {
    pub fn new() -> Self {
        RateCalc {
            queue: Default::default(),
        }
    }

    pub fn get_value(&self, latest: usize) -> u64 {
        let mut calc = self.queue.lock().unwrap();
        calc.push_back(latest as u64);

        while calc.len() > 20 {
            calc.pop_front();
        }

        (calc.iter().sum::<u64>() * 20) / calc.len() as u64
    }
}

fn main() -> Result<()> {
    // tracing_subscriber::fmt()
    //     .with_max_level(Level::TRACE)
    //     .with_span_events(FmtSpan::ACTIVE)
    //     .init();

    let enumerator = DeviceEnumerator::with_resolution(cache_resolution());
    let device = select_device(&enumerator)?;
    let streaming_device = device.into_streaming_device();
    let ch_units = configure_channels(&streaming_device);
    let samples_per_second = get_capture_rate();
    let capture_stats: Arc<dyn NewDataHandler> = CaptureStats::new(ch_units);
    streaming_device.new_data.subscribe(capture_stats.clone());

    println!("Press Enter to stop streaming");
    streaming_device.start(samples_per_second).unwrap();

    Term::stdout().read_line().unwrap();

    streaming_device.stop();

    Ok(())
}

fn select_device(enumerator: &DeviceEnumerator) -> Result<PicoDevice> {
    loop {
        println!("Searching for devices...",);

        let devices = enumerator.enumerate();

        if devices.is_empty() {
            return Err(anyhow!("{}", style("No Pico devices found").red()));
        }

        let device_options = devices
            .iter()
            .map(|result| match result {
                Ok(d) => format!("PicoScope {} ({})", d.variant, d.serial),
                Err(EnumerationError::DriverLoadError { driver, .. }) => {
                    format!("PicoScope {} (Missing Driver)", driver)
                }
                Err(EnumerationError::DriverError { driver, error }) => {
                    format!("PicoScope {} (Driver Error - {})", driver, error)
                }
                Err(EnumerationError::KernelDriverError { driver }) => {
                    format!("PicoScope {} (Kernel Driver Missing)", driver)
                }
                Err(EnumerationError::VersionError { driver, .. }) => {
                    format!("PicoScope {} (Driver Version Error)", driver)
                }
            })
            .collect::<Vec<String>>();

        let device_selection = Select::with_theme(&better_theme())
            .with_prompt(&format!(
                "{}",
                style("Select a device").blue().underlined().bold()
            ))
            .default(0)
            .items(&device_options[..])
            .interact()
            .unwrap();

        println!();

        match &devices[device_selection] {
            Ok(d) => return Ok(d.open().unwrap()),
            Err(error) => match error {
                EnumerationError::DriverLoadError { driver, .. }
                | EnumerationError::VersionError {
                    driver,
                    found: _,
                    required: _,
                } => {
                    println!("Downloading {} driver", driver);
                    let _ = download_drivers_to_cache(&[*driver]);
                    println!("Download complete");
                }
                _ => {}
            },
        }
    }
}

fn configure_channels(device: &PicoStreamingDevice) -> HashMap<PicoChannel, String> {
    loop {
        let mut channels = device
            .get_channels()
            .iter()
            .map(|c| {
                (
                    *c,
                    device.get_valid_ranges(*c),
                    device.get_channel_config(*c),
                )
            })
            .collect::<Vec<_>>();

        channels.sort_by(|(a, _, _), (b, _, _)| a.cmp(b));

        let mut channel_options = channels
            .iter()
            .map(|(ch, ranges, config)| {
                if let Some(ranges) = ranges {
                    if let Some(config) = config {
                        format!(
                            "Channel {} - {}",
                            ch,
                            style(format!("{} / {:?}", config.range, config.coupling)).green()
                        )
                    } else if ranges.is_empty() {
                        format!(
                            "Channel {} - {}",
                            ch,
                            style("No Probe connected").red().bold()
                        )
                    } else {
                        format!("Channel {} - {}", ch, style("Disabled"))
                    }
                } else {
                    format!(
                        "Channel {} - {}",
                        ch,
                        style("Disabled due to power constraints").red().bold()
                    )
                }
            })
            .collect::<Vec<String>>();

        if channels.iter().any(|(_, _, config)| config.is_some()) {
            channel_options.push("Channel configuration complete".to_string());
        }

        let ch_selection = Select::with_theme(&better_theme())
            .with_prompt(&format!(
                "{}",
                style("Configure channels").blue().underlined().bold()
            ))
            .default(0)
            .items(&channel_options[..])
            .interact()
            .unwrap();

        if ch_selection >= channels.len() {
            return channels
                .iter()
                .map(|(ch, _, config)| config.map(|c| (*ch, c.range.get_units().short)))
                .flatten()
                .collect();
        }

        let (edit_channel, ranges, _) = channels[ch_selection].clone();

        if let Some(ranges) = ranges {
            if ranges.is_empty() {
                println!(
                    "{} cannot be configured with no probe connected",
                    edit_channel
                );

                continue;
            }

            if let Some(range) = select_range(&ranges) {
                device.enable_channel(edit_channel, range, PicoCoupling::DC);
            } else {
                device.disable_channel(edit_channel);
            }
        } else {
            println!(
                "{} cannot be configured as it's disabled due to power constraints",
                edit_channel
            );
        }
    }
}

fn get_colour(ch: PicoChannel) -> Style {
    match ch {
        PicoChannel::A => Style::new().blue(),
        PicoChannel::B => Style::new().red(),
        PicoChannel::C => Style::new().green(),
        PicoChannel::D => Style::new().yellow(),
        PicoChannel::E => Style::new().magenta(),
        PicoChannel::F => Style::new().white(),
        PicoChannel::G => Style::new().cyan(),
        _ => Style::new().white(),
    }
}

struct CaptureStats {
    term: Term,
    rate_calc: RateCalc,
    ch_units: HashMap<PicoChannel, String>,
}

impl CaptureStats {
    pub fn new(ch_units: HashMap<PicoChannel, String>) -> Arc<Self> {
        Arc::new(CaptureStats {
            term: Term::stdout(),
            rate_calc: RateCalc::new(),
            ch_units,
        })
    }
}

impl NewDataHandler for CaptureStats {
    #[tracing::instrument(level = "trace", skip(self, event))]
    fn handle_event(&self, event: &StreamingEvent) {
        let mut data: Vec<(PicoChannel, usize, f64, String)> = event
            .channels
            .iter()
            .map(|(ch, v)| {
                (
                    *ch,
                    v.samples.len(),
                    v.scale_sample(0),
                    self.ch_units
                        .get(&ch)
                        .unwrap_or(&"".to_string())
                        .to_string(),
                )
            })
            .collect();

        let (_, samples, _, _) = data[0];

        data.sort_by(|a, b| a.0.cmp(&b.0));

        self.term.clear_last_lines(data.len() + 1).unwrap();

        println!(
            "{} @ {}",
            format!("{}", style("Streaming").bold()),
            format!(
                "{}",
                style(format!(
                    "{}S/s",
                    metric::Signifix::try_from(self.rate_calc.get_value(samples)).unwrap()
                ))
                .bold()
            )
        );

        for (ch, _, first, unit) in data {
            let ch_col = get_colour(ch);

            let value = match metric::Signifix::try_from(first) {
                Ok(v) => format!("{}", v),
                Err(metric::Error::OutOfLowerBound(_)) => "0".to_string(),
                _ => panic!("unknown error"),
            };

            println!(
                "  {} - {}",
                format!("{}", ch_col.apply_to(ch).bold()),
                format!("{}", style(format!("{} {}", value, unit)).bold())
            );
        }
    }
}

fn get_capture_rate() -> u32 {
    let rates: Vec<u32> = vec![
        1_000,
        10_000,
        100_000,
        1_000_000,
        5_000_000,
        10_000_000,
        20_000_000,
        50_000_000,
        100_000_000,
    ];

    let rate_options: Vec<String> = rates
        .iter()
        .map(|r| {
            let sig = metric::Signifix::try_from(*r).unwrap();
            format!(
                "{:>8}",
                format!("{} {}S/s", sig.integer(), sig.symbol().unwrap_or(""))
            )
        })
        .collect();

    let rate_selection = Select::with_theme(&better_theme())
        .with_prompt(&format!(
            "{}",
            style("Select capture rate").blue().underlined().bold()
        ))
        .default(0)
        .items(&rate_options[..])
        .interact()
        .unwrap();

    rates[rate_selection]
}

fn select_range(ranges: &[PicoRange]) -> Option<PicoRange> {
    let mut range_options: Vec<String> = ranges.iter().map(|r| format!("{}", r)).collect();
    range_options.push("Disabled".to_string());

    let range_selection = Select::with_theme(&better_theme())
        .with_prompt(&format!(
            "{}",
            style("Select Range").blue().underlined().bold()
        ))
        .default(0)
        .items(&range_options[..])
        .interact()
        .unwrap();

    if range_selection >= ranges.len() {
        None
    } else {
        Some(ranges[range_selection])
    }
}