mod link;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use serialport::{SerialPortInfo, SerialPortType};
use link::{Link, BASE_BAUD, DEFAULT_BAUD};
const EXIT_INTERRUPTED: u8 = 130;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, global = true, value_name = "DEVICE")]
device: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Boot {
image: PathBuf,
#[arg(long, value_parser = parse_u32)]
load_addr: u32,
#[arg(long, value_parser = parse_u32, default_value_t = DEFAULT_BAUD)]
baud: u32,
},
MemWrite {
#[arg(value_parser = parse_u32)]
addr: u32,
file: PathBuf,
#[arg(long, value_parser = parse_u32, default_value_t = DEFAULT_BAUD)]
baud: u32,
},
Exec {
#[arg(value_parser = parse_u32)]
addr: u32,
#[arg(long)]
terminal: bool,
},
SdList {
#[arg(default_value = "/")]
path: String,
},
SdRead {
remote: String,
local: PathBuf,
#[arg(long, value_parser = parse_u32, default_value_t = DEFAULT_BAUD)]
baud: u32,
},
SdWrite {
local: PathBuf,
remote: String,
#[arg(long, value_parser = parse_u32, default_value_t = DEFAULT_BAUD)]
baud: u32,
},
SdDelete {
remote: String,
},
SdMkdir {
remote: String,
},
Terminal,
List {
#[arg(long)]
all: bool,
},
}
impl Command {
fn needs_handshake(&self) -> bool {
!matches!(self, Command::Terminal | Command::List { .. })
}
}
fn parse_u32(s: &str) -> Result<u32, String> {
let text = s.trim().replace('_', "");
let (digits, radix) = match text.get(..2).map(str::to_ascii_lowercase).as_deref() {
Some("0x") => (&text[2..], 16),
Some("0b") => (&text[2..], 2),
Some("0o") => (&text[2..], 8),
_ => (&text[..], 10),
};
u32::from_str_radix(digits, radix).map_err(|e| format!("{s:?} is not a number: {e}"))
}
fn read_file(path: &Path) -> Result<Vec<u8>> {
fs::read(path).with_context(|| format!("reading {}", path.display()))
}
fn describe(port_type: &SerialPortType) -> String {
match port_type {
SerialPortType::UsbPort(info) => {
let mut parts = Vec::new();
let name = [info.manufacturer.as_deref(), info.product.as_deref()]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
if !name.is_empty() {
parts.push(name);
}
parts.push(format!("{:04x}:{:04x}", info.vid, info.pid));
if let Some(serial) = &info.serial_number {
parts.push(format!("serial {serial}"));
}
parts.join(" ")
}
SerialPortType::PciPort => "PCI serial".into(),
SerialPortType::BluetoothPort => "Bluetooth serial".into(),
_ => "unknown".into(),
}
}
fn list_ports(all: bool) -> Result<()> {
let mut ports = serialport::available_ports().context("enumerating serial ports")?;
if !all {
ports.retain(|port| matches!(port.port_type, SerialPortType::UsbPort(_)));
}
ports.sort_by(|a, b| {
let rank = |port: &SerialPortInfo| !matches!(port.port_type, SerialPortType::UsbPort(_));
(rank(a), a.port_name.clone()).cmp(&(rank(b), b.port_name.clone()))
});
if ports.is_empty() {
eprintln!(
"{}",
if all {
"No serial ports found."
} else {
"No USB serial ports found; pass --all to list every port."
}
);
return Ok(());
}
let width = ports
.iter()
.map(|port| port.port_name.len())
.max()
.unwrap_or(0);
for port in &ports {
println!(
"{:width$} {}",
port.port_name,
describe(&port.port_type),
width = width
);
}
Ok(())
}
fn main() -> ExitCode {
let cli = Cli::parse();
let interrupted = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&interrupted);
if let Err(e) = ctrlc::set_handler(move || flag.store(true, Ordering::SeqCst)) {
eprintln!("Warning: could not install the Ctrl-C handler: {e}");
}
match run(cli, Arc::clone(&interrupted)) {
Ok(()) if interrupted.load(Ordering::SeqCst) => ExitCode::from(EXIT_INTERRUPTED),
Ok(()) => ExitCode::SUCCESS,
Err(_) if interrupted.load(Ordering::SeqCst) => {
eprintln!("\nInterrupted.");
ExitCode::from(EXIT_INTERRUPTED)
}
Err(e) => {
eprintln!("Error: {e:#}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli, interrupted: Arc<AtomicBool>) -> Result<()> {
if let Command::List { all } = &cli.command {
return list_ports(*all);
}
let device = cli.device.as_deref().ok_or_else(|| {
anyhow!("no serial device given; pass --device (e.g. --device /dev/ttyUSB0)")
})?;
let mut link = Link::open(device, interrupted)?;
if cli.command.needs_handshake() {
link.handshake()?;
}
match cli.command {
Command::Boot {
image,
load_addr,
baud,
} => {
let data = read_file(&image)?;
link.negotiate_baud(baud)?;
link.mem_write(load_addr, &data)?;
link.negotiate_baud(BASE_BAUD)?;
link.exec(load_addr)?;
eprintln!("Jumped to {load_addr:#x}.");
link.terminal()?;
}
Command::MemWrite { addr, file, baud } => {
let data = read_file(&file)?;
link.negotiate_baud(baud)?;
link.mem_write(addr, &data)?;
link.negotiate_baud(BASE_BAUD)?;
eprintln!("Wrote {} bytes to {addr:#x}.", data.len());
}
Command::Exec { addr, terminal } => {
link.exec(addr)?;
eprintln!("Jumped to {addr:#x}.");
if terminal {
link.terminal()?;
}
}
Command::SdList { path } => {
for entry in link.sd_list(&path)? {
let marker = if entry.is_dir { "/" } else { "" };
println!("{:>12} {}{marker}", entry.size, entry.name);
}
}
Command::SdRead {
remote,
local,
baud,
} => {
link.negotiate_baud(baud)?;
let data = link.sd_read(&remote)?;
link.negotiate_baud(BASE_BAUD)?;
fs::write(&local, &data).with_context(|| format!("writing {}", local.display()))?;
eprintln!(
"Read {} bytes from {remote} -> {}",
data.len(),
local.display()
);
}
Command::SdWrite {
local,
remote,
baud,
} => {
let data = read_file(&local)?;
link.negotiate_baud(baud)?;
link.sd_write(&remote, &data)?;
link.negotiate_baud(BASE_BAUD)?;
}
Command::SdDelete { remote } => {
link.sd_delete(&remote)?;
eprintln!("Deleted {remote}");
}
Command::SdMkdir { remote } => {
link.sd_mkdir(&remote)?;
eprintln!("Created directory {remote}");
}
Command::Terminal => link.terminal()?,
Command::List { .. } => unreachable!(),
}
Ok(())
}