can-utils-rs 0.4.1

A pure Rust CLI utility for managing and manipulating CAN interfaces and sending or printing pretty CAN data.
Documentation
use anyhow::{Context, Result};
use inquire::{Select, Text};
use std::fmt;
use std::process::Command;
use std::time::Duration;

use super::parse::{SendFrame, parse_can_data, parse_can_id};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SendMode {
    Once,
    ManualRepeat,
    CyclicInterval,
    CyclicFrequency,
}

impl fmt::Display for SendMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SendMode::Once => write!(f, "Send once then exit"),
            SendMode::ManualRepeat => write!(f, "Send once each time manually"),
            SendMode::CyclicInterval => write!(f, "Send cyclically with interval"),
            SendMode::CyclicFrequency => write!(f, "Send cyclically with frequency"),
        }
    }
}

pub fn prompt_can_interface() -> Result<String> {
    let mut ifaces = detect_can_interfaces()?;
    ifaces.push("Enter manually".into());

    let choice = Select::new("Select CAN interface", ifaces).prompt()?;

    if choice == "Enter manually" {
        Ok(Text::new("Enter interface name").prompt()?)
    } else {
        Ok(choice)
    }
}

pub fn prompt_frame() -> Result<SendFrame> {
    let id_input = Text::new("Enter CAN ID (hex, e.g. 123):")
        .prompt()
        .context("failed to read CAN ID")?;

    let id = parse_can_id(&id_input)?;

    let data_input = Text::new("Enter data bytes (e.g. 11 22 33 44):")
        .with_default("")
        .prompt()
        .context("failed to read CAN data")?;

    let data = parse_can_data(&data_input)?;

    Ok(SendFrame { id, data })
}

pub fn prompt_send_mode() -> Result<SendMode> {
    Select::new(
        "How do you want to send it?",
        vec![
            SendMode::Once,
            SendMode::ManualRepeat,
            SendMode::CyclicInterval,
            SendMode::CyclicFrequency,
        ],
    )
    .prompt()
    .context("failed to read send mode")
}

pub fn prompt_interval() -> Result<Duration> {
    let input = Text::new("Interval in milliseconds:")
        .with_default("100")
        .prompt()
        .context("failed to read interval")?;

    let millis: u64 = input
        .trim()
        .parse()
        .context("interval must be a valid positive integer")?;

    Ok(Duration::from_millis(millis))
}

pub fn prompt_frequency() -> Result<Duration> {
    let input = Text::new("Frequency in Hz:")
        .with_default("10")
        .prompt()
        .context("failed to read frequency")?;

    let hz: u64 = input
        .trim()
        .parse()
        .context("frequency must be a valid positive integer")?;

    Ok(Duration::from_millis(1000 / hz))
}

fn detect_can_interfaces() -> Result<Vec<String>> {
    let output = Command::new("ip")
        .args(["-brief", "link"])
        .output()
        .context("failed to run 'ip -brief link'")?;

    let stdout = String::from_utf8_lossy(&output.stdout);

    let mut interfaces = Vec::new();

    for line in stdout.lines() {
        let iface = line.split_whitespace().next().unwrap_or("");

        if iface.starts_with("can") || iface.starts_with("slcan") || iface.starts_with("vcan") {
            interfaces.push(iface.to_string());
        }
    }

    interfaces.sort();
    Ok(interfaces)
}