use anyhow::{Context, Result, bail};
use inquire::{Select, Text};
use std::{
fmt, fs,
path::PathBuf,
process::{Command, Stdio},
};
#[derive(Debug, Clone)]
struct CanBitrate {
bitrate: u32,
}
impl fmt::Display for CanBitrate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.bitrate {
1_000_000 => "1 Mbit".to_string(),
_ if self.bitrate >= 1_000 => format!("{} kbit", self.bitrate / 1_000),
_ => self.bitrate.to_string(),
};
write!(f, "{:<10} ({})", label, self.bitrate)
}
}
fn can_bitrates() -> Vec<CanBitrate> {
let mut bitrates = vec![
CanBitrate { bitrate: 10_000 },
CanBitrate { bitrate: 20_000 },
CanBitrate { bitrate: 50_000 },
CanBitrate { bitrate: 125_000 },
CanBitrate { bitrate: 250_000 },
CanBitrate { bitrate: 500_000 },
CanBitrate { bitrate: 1_000_000 },
];
bitrates.sort_by(|a, b| b.bitrate.cmp(&a.bitrate));
bitrates
}
#[derive(Debug, Clone)]
struct SlcanSpeed {
bitrate: u32,
flag: &'static str,
}
impl fmt::Display for SlcanSpeed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.bitrate {
1_000_000 => "1 Mbit".to_string(),
_ if self.bitrate >= 1_000 => format!("{} kbit", self.bitrate / 1_000),
_ => self.bitrate.to_string(),
};
write!(f, "{:<10} ({}, {})", label, self.bitrate, self.flag)
}
}
fn slcan_speeds() -> Vec<SlcanSpeed> {
let mut speeds = vec![
SlcanSpeed {
bitrate: 10_000,
flag: "s0",
},
SlcanSpeed {
bitrate: 20_000,
flag: "s1",
},
SlcanSpeed {
bitrate: 50_000,
flag: "s2",
},
SlcanSpeed {
bitrate: 100_000,
flag: "s3",
},
SlcanSpeed {
bitrate: 125_000,
flag: "s4",
},
SlcanSpeed {
bitrate: 250_000,
flag: "s5",
},
SlcanSpeed {
bitrate: 500_000,
flag: "s6",
},
SlcanSpeed {
bitrate: 800_000,
flag: "s7",
},
SlcanSpeed {
bitrate: 1_000_000,
flag: "s8",
},
];
speeds.sort_by(|a, b| b.bitrate.cmp(&a.bitrate));
speeds
}
fn list_serial_candidates() -> Result<Vec<String>> {
let mut devices = Vec::new();
for entry in fs::read_dir("/dev").context("failed to read /dev")? {
let entry = entry?;
let path: PathBuf = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if name.starts_with("ttyUSB") || name.starts_with("ttyACM") {
devices.push(format!("/dev/{name}"));
}
}
devices.sort();
Ok(devices)
}
fn prompt_serial_device() -> Result<String> {
let mut options = list_serial_candidates()?;
if options.is_empty() {
return Ok(
Text::new("No serial interfaces detected. Enter device manually:")
.with_default("/dev/ttyUSB0")
.prompt()?,
);
}
options.push("Enter manually".to_string());
let selected = Select::new("Select serial device:", options).prompt()?;
if selected == "Enter manually" {
Ok(Text::new("Serial device:")
.with_default("/dev/ttyUSB0")
.prompt()?)
} else {
Ok(selected)
}
}
#[derive(Debug, Clone)]
enum CanMode {
Native,
Slcan,
Virtual,
}
impl fmt::Display for CanMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CanMode::Native => write!(f, "Native CAN bus"),
CanMode::Slcan => write!(f, "Non-native CAN bus (slcand)"),
CanMode::Virtual => write!(f, "Virtual CAN bus (vcan)"),
}
}
}
#[derive(Debug, Clone)]
struct NativeConfig {
iface: String,
bitrate: CanBitrate,
}
#[derive(Debug, Clone)]
struct SlcanConfig {
tty: String,
iface: String,
speed: SlcanSpeed,
uart_baud: u32,
}
#[derive(Debug, Clone)]
struct VirtualConfig {
iface: String,
}
#[derive(Debug, Clone)]
enum AppConfig {
Native(NativeConfig),
Slcan(SlcanConfig),
Virtual(VirtualConfig),
}
impl AppConfig {
fn iface(&self) -> &str {
match self {
AppConfig::Native(cfg) => &cfg.iface,
AppConfig::Slcan(cfg) => &cfg.iface,
AppConfig::Virtual(cfg) => &cfg.iface,
}
}
fn set_iface(&mut self, new_iface: String) {
match self {
AppConfig::Native(cfg) => cfg.iface = new_iface,
AppConfig::Slcan(cfg) => cfg.iface = new_iface,
AppConfig::Virtual(cfg) => cfg.iface = new_iface,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExistingIfaceAction {
Replace,
Rename,
Skip,
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InterfaceResolution {
Proceed,
SkipSetup,
}
pub fn run_interactive() -> Result<()> {
let mode = Select::new(
"Select CAN connection type:",
vec![CanMode::Native, CanMode::Slcan, CanMode::Virtual],
)
.prompt()
.context("failed to read CAN mode")?;
let mut config = match mode {
CanMode::Native => AppConfig::Native(prompt_native()?),
CanMode::Slcan => AppConfig::Slcan(prompt_slcan()?),
CanMode::Virtual => AppConfig::Virtual(prompt_virtual()?),
};
if ensure_interface_name_is_available(&mut config)? == InterfaceResolution::SkipSetup {
println!(
"Keeping existing interface '{}' and skipping setup.",
config.iface()
);
return Ok(());
}
print_plan(&config);
let execute = Select::new(
"What do you want to do?",
vec!["Execute now", "Only print commands"],
)
.prompt()
.context("failed to read execution mode")?;
if execute == "Execute now" {
execute_config(&config)?;
println!("Done.");
}
Ok(())
}
fn prompt_native() -> Result<NativeConfig> {
let iface = Text::new("CAN interface name:")
.with_default("can0")
.prompt()?;
let bitrate = Select::new("Select CAN bitrate:", can_bitrates()).prompt()?;
Ok(NativeConfig { iface, bitrate })
}
fn prompt_slcan() -> Result<SlcanConfig> {
let tty = prompt_serial_device()?;
let iface = Text::new("SLCAN interface name:")
.with_default("slcan0")
.prompt()?;
let speed = Select::new("Select CAN bitrate:", slcan_speeds()).prompt()?;
let uart_baud_str = Select::new(
"Select UART baud rate to adapter:",
vec!["115200", "230400", "460800", "921600", "3000000"],
)
.with_starting_cursor(4)
.prompt()?;
let uart_baud = uart_baud_str.parse::<u32>()?;
Ok(SlcanConfig {
tty,
iface,
speed,
uart_baud,
})
}
fn prompt_virtual() -> Result<VirtualConfig> {
let iface = Text::new("Virtual CAN interface name:")
.with_default("vcan0")
.prompt()?;
Ok(VirtualConfig { iface })
}
fn interface_exists(iface: &str) -> bool {
Command::new("ip")
.args(["link", "show", iface])
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
fn prompt_existing_interface_action(iface: &str) -> Result<ExistingIfaceAction> {
let choice = Select::new(
&format!(
"Interface '{}' already exists. What do you want to do?",
iface
),
vec![
"Replace existing interface",
"Enter another interface name",
"Keep existing and skip setup",
"Cancel",
],
)
.prompt()?;
Ok(match choice {
"Replace existing interface" => ExistingIfaceAction::Replace,
"Enter another interface name" => ExistingIfaceAction::Rename,
"Keep existing and skip setup" => ExistingIfaceAction::Skip,
_ => ExistingIfaceAction::Cancel,
})
}
fn remove_existing_interface(config: &AppConfig) -> Result<()> {
let iface = config.iface();
let _ = run_sudo(&["ip", "link", "set", iface, "down"]);
match config {
AppConfig::Native(_) => {
}
AppConfig::Slcan(_) => {
let _ = run_sudo(&["slcand", "-k", iface]);
}
AppConfig::Virtual(_) => {
run_sudo(&["ip", "link", "delete", iface])?;
}
}
Ok(())
}
fn ensure_interface_name_is_available(config: &mut AppConfig) -> Result<InterfaceResolution> {
loop {
let iface = config.iface().to_string();
if !interface_exists(&iface) {
return Ok(InterfaceResolution::Proceed);
}
match prompt_existing_interface_action(&iface)? {
ExistingIfaceAction::Replace => {
remove_existing_interface(config)?;
return Ok(InterfaceResolution::Proceed);
}
ExistingIfaceAction::Rename => {
let new_iface = Text::new("Enter a new interface name:")
.with_initial_value(&iface)
.prompt()?;
config.set_iface(new_iface);
}
ExistingIfaceAction::Skip => {
return Ok(InterfaceResolution::SkipSetup);
}
ExistingIfaceAction::Cancel => {
bail!("operation cancelled by user");
}
}
}
}
fn print_plan(config: &AppConfig) {
println!("\nPlanned commands:\n");
match config {
AppConfig::Native(cfg) => {
println!(
"sudo ip link set {} up type can bitrate {}",
cfg.iface, cfg.bitrate.bitrate
);
}
AppConfig::Slcan(cfg) => {
println!(
"sudo slcand -c -o -f -{} -t hw -S {} {} {}",
cfg.speed.flag, cfg.uart_baud, cfg.tty, cfg.iface
);
println!("sudo ip link set up {}", cfg.iface);
}
AppConfig::Virtual(cfg) => {
println!("sudo ip link add dev {} type vcan", cfg.iface);
println!("sudo ip link set up {}", cfg.iface);
println!("ip link show {}", cfg.iface);
}
}
println!();
}
fn execute_config(config: &AppConfig) -> Result<()> {
match config {
AppConfig::Native(cfg) => {
run_sudo(&[
"ip",
"link",
"set",
cfg.iface.as_str(),
"up",
"type",
"can",
"bitrate",
&cfg.bitrate.bitrate.to_string(),
])?;
}
AppConfig::Slcan(cfg) => {
let speed_arg = format!("-{}", cfg.speed.flag);
let baud_arg = cfg.uart_baud.to_string();
run_sudo(&[
"slcand",
"-c",
"-o",
"-f",
speed_arg.as_str(),
"-t",
"hw",
"-S",
baud_arg.as_str(),
cfg.tty.as_str(),
cfg.iface.as_str(),
])?;
run_sudo(&["ip", "link", "set", "up", cfg.iface.as_str()])?;
}
AppConfig::Virtual(cfg) => {
run_sudo(&[
"ip",
"link",
"add",
"dev",
cfg.iface.as_str(),
"type",
"vcan",
])?;
run_sudo(&["ip", "link", "set", "up", cfg.iface.as_str()])?;
run(&["ip", "link", "show", cfg.iface.as_str()])?;
}
}
Ok(())
}
fn run_sudo(args: &[&str]) -> Result<()> {
let status = Command::new("sudo")
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.with_context(|| format!("failed to start sudo {:?}", args))?;
if !status.success() {
bail!("command failed: sudo {:?}", args);
}
Ok(())
}
fn run(args: &[&str]) -> Result<()> {
let (program, rest) = args.split_first().context("empty command provided")?;
let status = Command::new(program)
.args(rest)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.with_context(|| format!("failed to start {:?}", args))?;
if !status.success() {
bail!("command failed: {:?}", args);
}
Ok(())
}