diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4596a452e3..507335a20c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,3 +133,6 @@ jobs: - name: Check ravedude run: | cargo check --manifest-path ravedude/Cargo.toml + - name: Test ravedude + run: | + cargo test --manifest-path ravedude/Cargo.toml diff --git a/ravedude/Cargo.lock b/ravedude/Cargo.lock index 1e46957410..4131ee0561 100644 --- a/ravedude/Cargo.lock +++ b/ravedude/Cargo.lock @@ -122,6 +122,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "getrandom" version = "0.2.2" @@ -152,9 +164,15 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote", - "syn", + "syn 1.0.64", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.3.2" @@ -173,6 +191,16 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -285,7 +313,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.64", "version_check", ] @@ -308,18 +336,18 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -371,10 +399,13 @@ dependencies = [ "anyhow", "colored", "ctrlc", + "either", "git-version", + "serde", "serialport", "structopt", "tempfile", + "toml", ] [[package]] @@ -412,6 +443,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serialport" version = "4.0.0" @@ -450,7 +510,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.64", ] [[package]] @@ -464,6 +524,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.2.0" @@ -487,6 +558,46 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "toml" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -544,3 +655,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] diff --git a/ravedude/Cargo.toml b/ravedude/Cargo.toml index b3b133ba99..3ee61459d1 100644 --- a/ravedude/Cargo.toml +++ b/ravedude/Cargo.toml @@ -17,6 +17,9 @@ serialport = "4.0.0" anyhow = "1.0.38" git-version = "0.3.4" ctrlc = "3.2.1" +serde = { version = "1.0.197", features = ["serde_derive"] } +toml = "0.8.11" +either = "1.10.0" [dependencies.structopt] version = "0.3.21" diff --git a/ravedude/src/avrdude/mod.rs b/ravedude/src/avrdude/mod.rs index ff913da94b..65224e4ba8 100644 --- a/ravedude/src/avrdude/mod.rs +++ b/ravedude/src/avrdude/mod.rs @@ -4,13 +4,7 @@ use std::process; use std::io::Write; -#[derive(Debug)] -pub struct AvrdudeOptions<'a> { - pub programmer: &'a str, - pub partno: &'a str, - pub baudrate: Option, - pub do_chip_erase: bool, -} +use crate::config::BoardAvrdudeOptions; #[derive(Debug)] pub struct Avrdude { @@ -50,7 +44,7 @@ impl Avrdude { } pub fn run( - options: &AvrdudeOptions, + options: &BoardAvrdudeOptions, port: Option>, bin: &path::Path, debug: bool, @@ -79,15 +73,25 @@ impl Avrdude { let mut command = command .arg("-c") - .arg(options.programmer) + .arg( + options + .programmer + .as_ref() + .ok_or_else(|| anyhow::anyhow!("board has no programmer"))?, + ) .arg("-p") - .arg(options.partno); + .arg( + options + .partno + .as_ref() + .ok_or_else(|| anyhow::anyhow!("board has no part number"))?, + ); if let Some(port) = port { command = command.arg("-P").arg(port.as_ref()); } - if let Some(baudrate) = options.baudrate { + if let Some(baudrate) = options.baudrate.flatten() { command = command.arg("-b").arg(baudrate.to_string()); } @@ -96,7 +100,10 @@ impl Avrdude { flash_instruction.push(bin); flash_instruction.push(":e"); - if options.do_chip_erase { + if options + .do_chip_erase + .ok_or_else(|| anyhow::anyhow!("board doesn't specify whether to erase the chip"))? + { command = command.arg("-e"); } diff --git a/ravedude/src/board.rs b/ravedude/src/board.rs index 5bb3991327..7b7ab55e40 100644 --- a/ravedude/src/board.rs +++ b/ravedude/src/board.rs @@ -1,465 +1,113 @@ -use crate::avrdude; - -pub trait Board { - fn display_name(&self) -> &str; - fn needs_reset(&self) -> Option<&str>; - fn avrdude_options(&self) -> avrdude::AvrdudeOptions; - fn guess_port(&self) -> Option>; -} - -pub fn get_board(board: &str) -> Option> { - Some(match board { - "uno" => Box::new(ArduinoUno), - "nano" => Box::new(ArduinoNano), - "nano-new" => Box::new(ArduinoNanoNew), - "leonardo" => Box::new(ArduinoLeonardo), - "micro" => Box::new(ArduinoMicro), - "mega2560" => Box::new(ArduinoMega2560), - "mega1280" => Box::new(ArduinoMega1280), - "diecimila" => Box::new(ArduinoDiecimila), - "promicro" => Box::new(SparkFunProMicro), - "promini-3v3" => Box::new(SparkFunProMini3V), - "promini-5v" => Box::new(SparkFunProMini5V), - "trinket-pro" => Box::new(TrinketPro), - "trinket" => Box::new(Trinket), - "nano168" => Box::new(Nano168), - "duemilanove" => Box::new(ArduinoDuemilanove), - _ => return None, - }) -} - -// ---------------------------------------------------------------------------- - -fn find_port_from_vid_pid_list(list: &[(u16, u16)]) -> anyhow::Result { - for serialport::SerialPortInfo { - port_name, - port_type, - } in serialport::available_ports().unwrap() - { - if let serialport::SerialPortType::UsbPort(usb_info) = port_type { - for (vid, pid) in list.iter() { - if usb_info.vid == *vid && usb_info.pid == *pid { - return Ok(port_name.into()); - } - } - } - } - Err(anyhow::anyhow!("Serial port not found.")) -} - -// ---------------------------------------------------------------------------- - -struct ArduinoUno; - -impl Board for ArduinoUno { - fn display_name(&self) -> &str { - "Arduino Uno" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: None, - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(find_port_from_vid_pid_list(&[ - (0x2341, 0x0043), - (0x2341, 0x0001), - (0x2A03, 0x0043), - (0x2341, 0x0243), - ])) - } -} - -struct ArduinoMicro; - -impl Board for ArduinoMicro { - fn display_name(&self) -> &str { - "Arduino Micro" - } - - fn needs_reset(&self) -> Option<&str> { - Some("Reset the board by pressing the reset button once.") - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "avr109", - partno: "atmega32u4", - baudrate: Some(115200), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(find_port_from_vid_pid_list(&[ - (0x2341, 0x0037), - (0x2341, 0x8037), - (0x2A03, 0x0037), - (0x2A03, 0x8037), - (0x2341, 0x0237), - (0x2341, 0x8237), - ])) - } -} - -struct ArduinoNano; - -impl Board for ArduinoNano { - fn display_name(&self) -> &str { - "Arduino Nano" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: Some(57600), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } -} - -struct ArduinoNanoNew; - -impl Board for ArduinoNanoNew { - fn display_name(&self) -> &str { - "Arduino Nano (New Bootloader)" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: Some(115200), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } -} - -struct ArduinoLeonardo; - -impl Board for ArduinoLeonardo { - fn display_name(&self) -> &str { - "Arduino Leonardo" - } - - fn needs_reset(&self) -> Option<&str> { - let a = self.guess_port(); - match a { - Some(Ok(name)) => match serialport::new(name.to_str().unwrap(), 1200).open() { - Ok(_) => { - std::thread::sleep(core::time::Duration::from_secs(1)); - None - } - Err(_) => Some("Reset the board by pressing the reset button once."), - }, - _ => Some("Reset the board by pressing the reset button once."), - } - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "avr109", - partno: "atmega32u4", - baudrate: None, - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(find_port_from_vid_pid_list(&[ - (0x2341, 0x0036), - (0x2341, 0x8036), - (0x2A03, 0x0036), - (0x2A03, 0x8036), - ])) - } -} - -struct ArduinoMega1280; - -impl Board for ArduinoMega1280 { - fn display_name(&self) -> &str { - "Arduino Mega 1280" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega1280", - baudrate: Some(57600), - do_chip_erase: false, - } - } - - fn guess_port(&self) -> Option> { - // This board uses a generic serial interface id 0403:6001 which is too common for auto detection. - Some(Err(anyhow::anyhow!("Unable to guess port."))) - } -} - -struct ArduinoMega2560; - -impl Board for ArduinoMega2560 { - fn display_name(&self) -> &str { - "Arduino Mega 2560" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "wiring", - partno: "atmega2560", - baudrate: Some(115200), - do_chip_erase: false, +use std::{collections::HashMap, path::Path}; + +use crate::config; + +fn get_all_boards() -> anyhow::Result> { + toml::from_str(include_str!("boards.toml")).map_err(|err| { + if cfg!(test) { + anyhow::anyhow!( + "boards.toml in ravedude source is invalid.\n{}", + err.message() + ) + } else { + anyhow::anyhow!( + "boards.toml in ravedude source is invalid. This is a bug, please report it!\n{}", + err.message() + ) } - } - - fn guess_port(&self) -> Option> { - Some(find_port_from_vid_pid_list(&[ - (0x2341, 0x0010), - (0x2341, 0x0042), - (0x2A03, 0x0010), - (0x2A03, 0x0042), - (0x2341, 0x0210), - (0x2341, 0x0242), - ])) - } -} - -struct ArduinoDiecimila; - -impl Board for ArduinoDiecimila { - fn display_name(&self) -> &str { - "Arduino Diecimila" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega168", - baudrate: Some(19200), - do_chip_erase: false, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } -} - -struct SparkFunProMicro; - -impl Board for SparkFunProMicro { - fn display_name(&self) -> &str { - "SparkFun Pro Micro" - } - - fn needs_reset(&self) -> Option<&str> { - Some("Reset the board by quickly pressing the reset button **twice**.") - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "avr109", - partno: "atmega32u4", - baudrate: None, - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(find_port_from_vid_pid_list(&[ - (0x1B4F, 0x9205), //5V - (0x1B4F, 0x9206), //5V - (0x1B4F, 0x9203), //3.3V - (0x1B4F, 0x9204), //3.3V - ])) - } -} - -struct SparkFunProMini3V; - -impl Board for SparkFunProMini3V { - fn display_name(&self) -> &str { - "SparkFun Pro Mini 3.3V (8MHz)" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: Some(57600), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } -} - -struct SparkFunProMini5V; - -impl Board for SparkFunProMini5V { - fn display_name(&self) -> &str { - "SparkFun Pro Mini 5V (16MHz)" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: Some(57600), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } -} - -struct TrinketPro; - -impl Board for TrinketPro { - fn display_name(&self) -> &str { - "Trinket Pro" - } - - fn needs_reset(&self) -> Option<&str> { - Some("Reset the board by pressing the reset button once.") - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "usbtiny", - partno: "atmega328p", - baudrate: None, - do_chip_erase: false, - } - } - - fn guess_port(&self) -> Option> { - None // The TrinketPro does not have USB-to-Serial. - } + }) } -struct Trinket; +pub fn get_board_from_name(board_name: &str) -> anyhow::Result { + let mut all_boards = get_all_boards()?; -impl Board for Trinket { - fn display_name(&self) -> &str { - "Trinket" - } + Ok(config::RavedudeConfig { + board_config: Some(all_boards.remove(board_name).ok_or_else(|| { + let mut msg = format!("invalid board: {board_name}\n"); - fn needs_reset(&self) -> Option<&str> { - Some("Reset the board by pressing the reset button once.") - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "usbtiny", - partno: "attiny85", - baudrate: None, - do_chip_erase: true, - } - } + msg.push_str("valid boards:"); - fn guess_port(&self) -> Option> { - None // The Trinket does not have USB-to-Serial. - } + for board in all_boards.keys() { + msg.push('\n'); + msg.push_str(&board); + } + anyhow::anyhow!(msg) + })?), + ..Default::default() + }) } -struct Nano168; +pub fn get_board_from_manifest(manifest_path: &Path) -> anyhow::Result { + Ok({ + let file_contents = std::fs::read_to_string(manifest_path) + .map_err(|err| anyhow::anyhow!("Ravedude.toml read error:\n{}", err))?; -impl Board for Nano168 { - fn display_name(&self) -> &str { - "Nano Clone (ATmega168)" - } + let mut board: config::RavedudeConfig = toml::from_str(&file_contents) + .map_err(|err| anyhow::anyhow!("invalid Ravedude.toml:\n{}", err))?; - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega168", - baudrate: Some(19200), - do_chip_erase: false, + if let Some(board_config) = board.board_config.as_ref() { + if let Some(board_name) = board.general_options.board.as_deref() { + anyhow::bail!( + "can't both have board in [general] and [board] section; set inherit = \"{}\" under [board] to inherit its options", + board_name + ) + } + if let Some(inherit) = board_config.inherit.as_deref() { + let base_board = get_board_from_name(inherit)?.board_config.unwrap(); + board.board_config = Some(board.board_config.take().unwrap().merge(base_board)); + } + } else if let Some(board_name) = board.general_options.board.as_deref() { + let base_board = get_board_from_name(board_name)?.board_config.unwrap(); + board.board_config = Some(base_board); } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) - } + board + }) } -struct ArduinoDuemilanove; - -impl Board for ArduinoDuemilanove { - fn display_name(&self) -> &str { - "Arduino Duemilanove" - } - - fn needs_reset(&self) -> Option<&str> { - None - } - - fn avrdude_options(&self) -> avrdude::AvrdudeOptions { - avrdude::AvrdudeOptions { - programmer: "arduino", - partno: "atmega328p", - baudrate: Some(57600), - do_chip_erase: true, - } - } - - fn guess_port(&self) -> Option> { - Some(Err(anyhow::anyhow!("Not able to guess port"))) +#[cfg(test)] +mod tests { + use super::get_all_boards; + + #[test] + fn validate_board_list() -> anyhow::Result<()> { + let all_boards = get_all_boards()?; + + for (name, board) in all_boards.iter() { + assert!( + board.name.is_some(), + "Board {name:?} doesn't have a `name` key" + ); + assert!( + board.inherit.is_none(), + "Board {name:?} has illegal `inherit` key" + ); + assert!( + board.reset.is_some(), + "Board {name:?} doesn't have a `reset` key" + ); + assert!( + board.avrdude.is_some(), + "Board {name:?} doesn't have an `avrdude` key" + ); + let avrdude = board.avrdude.as_ref().unwrap(); + assert!( + avrdude.programmer.is_some(), + "Board {name:?}'s avrdude options doesn't have a `programmer` key" + ); + assert!( + avrdude.partno.is_some(), + "Board {name:?}'s avrdude options doesn't have a `partno` key" + ); + assert!( + avrdude.baudrate.is_some(), + "Board {name:?}'s avrdude options doesn't have a `baudrate` key" + ); + assert!( + avrdude.do_chip_erase.is_some(), + "Board {name:?}'s avrdude options doesn't have a `do_chip_erase` key" + ); + } + + Ok(()) } } diff --git a/ravedude/src/boards.toml b/ravedude/src/boards.toml new file mode 100644 index 0000000000..7609a18683 --- /dev/null +++ b/ravedude/src/boards.toml @@ -0,0 +1,240 @@ +[uno] + name = "Arduino Uno" + + [uno.reset] + automatic = true + + [uno.avrdude] + programmer = "arduino" + partno = "atmega328p" + baudrate = -1 + do-chip-erase = true + + [uno.usb-info] + port-ids = [ + { vid = 0x2341, pid = 0x0043 }, + { vid = 0x2341, pid = 0x0001 }, + { vid = 0x2A03, pid = 0x0043 }, + { vid = 0x2341, pid = 0x0243 }, + ] + +[nano] + name = "Arduino Nano" + + [nano.reset] + automatic = true + + [nano.avrdude] + programmer = "arduino" + partno = "atmega328p" + baudrate = 57600 + do-chip-erase = true + + [nano.usb-info] + error = "Not able to guess port" + +[nano-new] + name = "Arduino Nano (New Bootloader)" + + [nano-new.reset] + automatic = true + + [nano-new.avrdude] + programmer = "arduino" + partno = "atmega328p" + baudrate = 115200 + do-chip-erase = true + + [nano-new.usb-info] + error = "Not able to guess port" + +[leonardo] + name = "Arduino Leonardo" + + [leonardo.reset] + automatic = false + + [leonardo.avrdude] + programmer = "avr109" + partno = "atmega32u4" + baudrate = -1 + do-chip-erase = true + + [leonardo.usb-info] + port-ids = [ + { vid = 0x2341, pid = 0x0036 }, + { vid = 0x2341, pid = 0x8036 }, + { vid = 0x2A03, pid = 0x0036 }, + { vid = 0x2A03, pid = 0x8036 }, + ] + +[micro] + name = "Arduino Micro" + + [micro.reset] + automatic = false + + [micro.avrdude] + programmer = "avr109" + partno = "atmega32u4" + baudrate = 115200 + do-chip-erase = true + + [micro.usb-info] + port-ids = [ + { vid = 0x2341, pid = 0x0037 }, + { vid = 0x2341, pid = 0x8037 }, + { vid = 0x2A03, pid = 0x0037 }, + { vid = 0x2A03, pid = 0x8037 }, + { vid = 0x2341, pid = 0x0237 }, + { vid = 0x2341, pid = 0x8237 }, + ] + +[mega2560] + name = "Arduino Mega 2560" + + [mega2560.reset] + automatic = true + + [mega2560.avrdude] + programmer = "wiring" + partno = "atmega2560" + baudrate = 115200 + do-chip-erase = false + + [mega2560.usb-info] + port-ids = [ + { vid = 0x2341, pid = 0x0010 }, + { vid = 0x2341, pid = 0x0042 }, + { vid = 0x2A03, pid = 0x0010 }, + { vid = 0x2A03, pid = 0x0042 }, + { vid = 0x2341, pid = 0x0210 }, + { vid = 0x2341, pid = 0x0242 }, + ] + +[mega1280] + name = "Arduino Mega 1280" + + [mega1280.reset] + automatic = true + + [mega1280.avrdude] + programmer = "wiring" + partno = "atmega1280" + baudrate = 57600 + do-chip-erase = false + + [mega1280.usb-info] + # This board uses a generic serial interface id 0403:6001 which is too common for auto detection. + error = "Not able to guess port" + +[diecimila] + name = "Arduino Diecimila" + + [diecimila.reset] + automatic = true + + [diecimila.avrdude] + programmer = "arduino" + partno = "atmega168" + baudrate = 19200 + do-chip-erase = false + + [diecimila.usb-info] + # No IDs known. + error = "Not able to guess port" + +[promicro] + name = "SparkFun Pro Micro" + + [promicro.reset] + automatic = false + + [promicro.avrdude] + programmer = "avr109" + partno = "atmega32u4" + baudrate = -1 + do-chip-erase = true + + [promicro.usb-info] + port-ids = [ + { vid = 0x1B4F, pid = 0x9205 }, # 5V + { vid = 0x1B4F, pid = 0x9206 }, # 5V + { vid = 0x1B4F, pid = 0x9203 }, # 3.3V + { vid = 0x1B4F, pid = 0x9204 }, # 3.3V + ] + +[promini-5v] + name = "SparkFun Pro Mini 5V (16MHz)" + + [promini-5v.reset] + automatic = true + + [promini-5v.avrdude] + programmer = "arduino" + partno = "atmega328p" + baudrate = 57600 + do-chip-erase = true + + [promini-5v.usb-info] + error = "Not able to guess port" + +[trinket-pro] + name = "Trinket Pro" + + [trinket-pro.reset] + automatic = false + + [trinket-pro.avrdude] + programmer = "usbtiny" + partno = "atmega328p" + baudrate = -1 + do-chip-erase = false + + # The Trinket Pro does not have USB-Serial, thus no port is known or needed. + +[trinket] + name = "Trinket" + + [trinket.reset] + automatic = false + + [trinket.avrdude] + programmer = "usbtiny" + partno = "atmega328p" + baudrate = -1 + do-chip-erase = true + + # The Trinket does not have USB-Serial, thus no port is known or needed. + +[nano168] + name = "Nano Clone (ATmega168)" + + [nano168.reset] + automatic = true + + [nano168.avrdude] + programmer = "arduino" + partno = "atmega168" + baudrate = 19200 + do-chip-erase = false + + [nano168.usb-info] + # No IDs here because the Nano 168 uses a generic USB-Serial chip. + error = "Not able to guess port" + +[duemilanove] + name = "Arduino Duemilanove" + + [duemilanove.reset] + automatic = true + + [duemilanove.avrdude] + programmer = "arduino" + partno = "atmega328p" + baudrate = 57600 + do-chip-erase = true + + [duemilanove.usb-info] + # No IDs here because the Nano 168 uses a generic USB-Serial chip. + error = "Not able to guess port" diff --git a/ravedude/src/config.rs b/ravedude/src/config.rs new file mode 100644 index 0000000000..8e688f22ff --- /dev/null +++ b/ravedude/src/config.rs @@ -0,0 +1,183 @@ +use serde::{Deserialize, Serialize}; +use std::num::NonZeroU32; + +#[derive(serde::Serialize, serde::Deserialize, Debug, Default)] +#[serde(rename_all = "kebab-case")] +pub struct RavedudeConfig { + #[serde(rename = "general")] + pub general_options: RavedudeGeneralConfig, + + #[serde(rename = "board")] + pub board_config: Option, +} + +fn serialize_baudrate(val: &Option>, serializer: S) -> Result +where + S: serde::Serializer, +{ + let baudrate = val.as_ref().map(|val| val.map_or(-1, |x| x.get() as i32)); + + baudrate.serialize(serializer) +} + +fn deserialize_baudrate<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + Ok(match Option::::deserialize(deserializer)? { + None => None, + Some(-1) => Some(None), + Some(baudrate) => Some(Some(NonZeroU32::new(baudrate as _).ok_or_else(|| { + serde::de::Error::custom(format!("invalid baudrate: {baudrate}")) + })?)), + }) +} + +impl RavedudeConfig { + pub fn from_args(args: &crate::Args) -> anyhow::Result { + Ok(Self { + general_options: RavedudeGeneralConfig { + open_console: args.open_console, + serial_baudrate: match args.baudrate { + Some(serial_baudrate) => Some( + NonZeroU32::new(serial_baudrate) + .ok_or_else(|| anyhow::anyhow!("baudrate must not be 0"))?, + ), + None => None, + }, + port: args.port.clone(), + reset_delay: args.reset_delay, + board: args.legacy_board_name().clone(), + }, + board_config: Default::default(), + }) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Default)] +#[serde(rename_all = "kebab-case")] +pub struct RavedudeGeneralConfig { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub open_console: bool, + pub serial_baudrate: Option, + pub port: Option, + pub reset_delay: Option, + pub board: Option, +} + +impl RavedudeGeneralConfig { + /// Apply command-line overrides to this configuration. Command-line arguments take priority over Ravedude.toml + pub fn apply_overrides_from(&mut self, args: &crate::Args) -> anyhow::Result<()> { + if args.open_console { + self.open_console = true; + } + if let Some(serial_baudrate) = args.baudrate { + self.serial_baudrate = Some( + NonZeroU32::new(serial_baudrate) + .ok_or_else(|| anyhow::anyhow!("baudrate must not be 0"))?, + ); + } + if let Some(port) = args.port.clone() { + self.port = Some(port); + } + if let Some(reset_delay) = args.reset_delay { + self.reset_delay = Some(reset_delay); + } + Ok(()) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Default)] +#[serde(rename_all = "kebab-case")] +pub struct BoardConfig { + pub name: Option, + pub inherit: Option, + pub reset: Option, + pub avrdude: Option, + pub usb_info: Option, +} + +impl BoardConfig { + pub fn merge(self, base: BoardConfig) -> Self { + Self { + name: self.name.or(base.name), + // inherit is used to decide what BoardConfig to inherit and isn't used anywhere else + inherit: None, + reset: self.reset.or(base.reset), + avrdude: match self.avrdude { + Some(avrdude) => base.avrdude.map(|base_avrdude| avrdude.merge(base_avrdude)), + None => base.avrdude, + }, + usb_info: self.usb_info.or(base.usb_info), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct ResetOptions { + pub automatic: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct BoardAvrdudeOptions { + pub programmer: Option, + pub partno: Option, + #[serde( + serialize_with = "serialize_baudrate", + deserialize_with = "deserialize_baudrate" + )] + // Inner option to represent whether the baudrate exists, outer option to allow for overriding. + // Option + pub baudrate: Option>, + pub do_chip_erase: Option, +} +impl BoardAvrdudeOptions { + pub fn merge(self, base: Self) -> Self { + Self { + programmer: self.programmer.or(base.programmer), + partno: self.partno.or(base.partno), + baudrate: self.baudrate.or(base.baudrate), + do_chip_erase: self.do_chip_erase.or(base.do_chip_erase), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum BoardUSBInfo { + PortIds(Vec), + #[serde(rename = "error")] + Error(String), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct BoardPortID { + pub vid: u16, + pub pid: u16, +} + +impl BoardConfig { + pub fn guess_port(&self) -> Option> { + match &self.usb_info { + Some(BoardUSBInfo::Error(err)) => Some(Err(anyhow::anyhow!(err.clone()))), + Some(BoardUSBInfo::PortIds(ports)) => { + for serialport::SerialPortInfo { + port_name, + port_type, + } in serialport::available_ports().unwrap() + { + if let serialport::SerialPortType::UsbPort(usb_info) = port_type { + for &BoardPortID { vid, pid } in ports { + if usb_info.vid == vid && usb_info.pid == pid { + return Some(Ok(port_name.into())); + } + } + } + } + Some(Err(anyhow::anyhow!("Serial port not found."))) + } + None => None, + } + } +} diff --git a/ravedude/src/main.rs b/ravedude/src/main.rs index d91ec780f9..d34b77e44c 100644 --- a/ravedude/src/main.rs +++ b/ravedude/src/main.rs @@ -2,11 +2,13 @@ use anyhow::Context as _; use colored::Colorize as _; use structopt::clap::AppSettings; +use std::path::Path; use std::thread; use std::time::Duration; mod avrdude; mod board; +mod config; mod console; mod ui; @@ -28,6 +30,10 @@ const MIN_VERSION_AVRDUDE: (u8, u8) = (6, 3); fallback = "unknown" ))] struct Args { + /// Utility flag for dumping a config of a named board to TOML. + #[structopt(long = "dump-config")] + dump_config: bool, + /// After successfully flashing the program, open a serial console to see output sent by the /// board and possibly interact with it. #[structopt(short = "c", long = "open-console")] @@ -37,7 +43,7 @@ struct Args { #[structopt(short = "b", long = "baudrate")] baudrate: Option, - /// Overwrite which port to use. By default ravedude will try to find a connected board by + /// Overwrite which port to use. By default ravedude will try to find a connected board by /// itself. #[structopt(short = "P", long = "port", parse(from_os_str), env = "RAVEDUDE_PORT")] port: Option, @@ -52,33 +58,40 @@ struct Args { #[structopt(long = "debug-avrdude")] debug_avrdude: bool, - /// Which board to interact with. - /// - /// Must be one of the known board identifiers: - /// - /// * uno - /// * nano - /// * nano-new - /// * leonardo - /// * micro - /// * mega2560 - /// * mega1280 - /// * diecimila - /// * promicro - /// * promini-3v3 - /// * promini-5v - /// * trinket-pro - /// * trinket - /// * nano168 - /// * duemilanove - #[structopt(name = "BOARD", verbatim_doc_comment)] - board: String, - + #[structopt(name = "BINARY", parse(from_os_str))] /// The binary to be flashed. /// /// If no binary is given, flashing will be skipped. - #[structopt(name = "BINARY", parse(from_os_str))] + // (Note: this is where the board is stored in legacy configurations.) bin: Option, + + /// Deprecated binary for old configurations of ravedude without `Ravedude.toml`. + /// Should not be used in newer configurations. + #[structopt(name = "LEGACY BINARY", parse(from_os_str))] + bin_legacy: Option, +} +impl Args { + /// Get the board name for legacy configurations. + /// `None` if the configuration isn't a legacy configuration or the board name doesn't exist. + fn legacy_board_name(&self) -> Option { + if self.bin_legacy.is_none() { + None + } else { + self.bin + .as_deref() + .and_then(|board| board.to_str().map(String::from)) + } + } + + /// Get the binary argument with fallback for the legacy menchanism. + /// + /// Returns `None` if no binary argument was passed. + fn bin_or_legacy_bin(&self) -> Option<&std::path::Path> { + self.bin_legacy + .as_ref() + .map(|p| p.as_path()) + .or(self.bin.as_ref().map(|p| p.as_path())) + } } fn main() { @@ -91,45 +104,106 @@ fn main() { } } +/// Finds the location of a `Ravedude.toml` or `None` if not found. +fn find_manifest() -> anyhow::Result> { + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + let path = Path::new(&manifest_dir).join("Ravedude.toml"); + return Ok(path.exists().then_some(path)); + } + + // If `CARGO_MANIFEST_DIR` isn't set, Cargo scans the current dir and all of its parents for Cargo.toml + // so we mirror its behavior here. + let current_dir = std::env::current_dir()?; + + for dir_to_test in current_dir.ancestors() { + let path_to_test = dir_to_test.join("Ravedude.toml"); + if path_to_test.exists() { + return Ok(Some(path_to_test)); + } + } + + Ok(None) +} + fn ravedude() -> anyhow::Result<()> { let args: Args = structopt::StructOpt::from_args(); - avrdude::Avrdude::require_min_ver(MIN_VERSION_AVRDUDE)?; - let board = board::get_board(&args.board).expect("board not found"); + let manifest_path = find_manifest()?; - task_message!("Board", "{}", board.display_name()); + let mut ravedude_config = match (manifest_path.as_deref(), args.legacy_board_name()) { + (Some(_), Some(board_name)) => { + anyhow::bail!("can't pass board as command-line argument when Ravedude.toml is present; set `board = {:?}` under [general] in Ravedude.toml", board_name); + } + (Some(path), None) => board::get_board_from_manifest(path)?, + (None, Some(board_name)) => { + warning!( + "Passing the board as command-line argument is deprecated; create a Ravedude.toml in the project root instead:" + ); + eprintln!( + "\n# Ravedude.toml\n{}", + toml::to_string(&config::RavedudeConfig::from_args(&args)?)? + ); - if let Some(wait_time) = args.reset_delay { - if wait_time > 0 { - println!("Waiting {} ms before proceeding", wait_time); - let wait_time = Duration::from_millis(wait_time); - thread::sleep(wait_time); - } else { - println!("Assuming board has been reset"); + board::get_board_from_name(&board_name)? } - } else { - if let Some(msg) = board.needs_reset() { - warning!("this board cannot reset itself."); - eprintln!(""); - eprintln!(" {}", msg); - eprintln!(""); - eprint!("Once reset, press ENTER here: "); - std::io::stdin().read_line(&mut String::new())?; + (None, None) => { + anyhow::bail!("couldn't find Ravedude.toml in project"); } + }; + + ravedude_config + .general_options + .apply_overrides_from(&args)?; + + if args.dump_config { + println!("{}", toml::to_string(&ravedude_config)?); + return Ok(()); } - let port = match args.port { + avrdude::Avrdude::require_min_ver(MIN_VERSION_AVRDUDE)?; + + let Some(mut board) = ravedude_config.board_config else { + anyhow::bail!("no named board given and no board options provided"); + }; + + let board_avrdude_options = board + .avrdude + .take() + .ok_or_else(|| anyhow::anyhow!("board has no avrdude options"))?; + + task_message!( + "Board", + "{}", + &board.name.as_deref().unwrap_or("Unnamed Board") + ); + + let port = match ravedude_config.general_options.port { Some(port) => Ok(Some(port)), None => match board.guess_port() { Some(Ok(port)) => Ok(Some(port)), p @ Some(Err(_)) => p.transpose().context( - "no matching serial port found, use -P or set RAVEDUDE_PORT in your environment", + "no matching serial port found, use -P, add a serial-port entry under [general] in Ravedude.toml, or set RAVEDUDE_PORT in your environment", ), None => Ok(None), }, }?; - if let Some(bin) = args.bin.as_ref() { + if let Some(bin) = args.bin_or_legacy_bin() { + if let Some(wait_time) = args.reset_delay { + if wait_time > 0 { + println!("Waiting {} ms before proceeding", wait_time); + let wait_time = Duration::from_millis(wait_time); + thread::sleep(wait_time); + } else { + println!("Assuming board has been reset"); + } + } else if matches!(board.reset, Some(config::ResetOptions { automatic: false })) { + warning!("this board cannot reset itself."); + eprintln!(); + eprint!("Once reset, press ENTER here: "); + std::io::stdin().read_line(&mut String::new())?; + } + if let Some(port) = port.as_ref() { task_message!( "Programming", @@ -143,7 +217,7 @@ fn ravedude() -> anyhow::Result<()> { } let mut avrdude = avrdude::Avrdude::run( - &board.avrdude_options(), + &board_avrdude_options, port.as_ref(), bin, args.debug_avrdude, @@ -159,10 +233,15 @@ fn ravedude() -> anyhow::Result<()> { ); } - if args.open_console { - let baudrate = args - .baudrate - .context("-b/--baudrate is needed for the serial console")?; + if ravedude_config.general_options.open_console { + let baudrate = ravedude_config + .general_options + .serial_baudrate + .context(if manifest_path.is_some() { + "`serial-baudrate` under [general] in Ravedude.toml is needed for the serial console" + }else{ + "-b/--baudrate is needed for the serial console" + })?; let port = port.context("console can only be opened for devices with USB-to-Serial")?; @@ -170,7 +249,7 @@ fn ravedude() -> anyhow::Result<()> { task_message!("", "{}", "CTRL+C to exit.".dimmed()); // Empty line for visual consistency eprintln!(); - console::open(&port, baudrate)?; + console::open(&port, baudrate.get())?; } else if args.bin.is_none() && port.is_some() { warning!("you probably meant to add -c/--open-console?"); }