From 2739d83b9957c38fe0c0b1936c6b4a8da091cf76 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Thu, 19 Feb 2026 07:41:12 -0600 Subject: [PATCH] Add board presets, devices --clear, and test/UX fixes Board presets: - anvil new --board mega (uno, mega, nano, nano-old, leonardo, micro) - anvil new --list-boards shows presets with compatible clones - FQBN and baud rate flow into .anvil.toml via template variables - Defaults to uno when --board is omitted Devices --clear: - anvil devices --clear deletes .anvil.local, reverts to auto-detect --- src/board/mod.rs | 2 + src/board/presets.rs | 134 +++++++++++++++++++++++++++ src/commands/devices.rs | 25 +++++ src/commands/new.rs | 77 ++++++++++++++- src/commands/refresh.rs | 2 + src/main.rs | 33 +++++-- src/templates/mod.rs | 19 +++- templates/basic/_dot_anvil.toml.tmpl | 4 +- tests/integration_test.rs | 100 ++++++++++++++++++++ 9 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 src/board/presets.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index 98f7b73..2280172 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -4,6 +4,8 @@ use serde::Deserialize; use std::path::{Path, PathBuf}; use std::process::Command; +pub mod presets; + /// Information about a detected serial port. #[derive(Debug, Clone)] pub struct PortInfo { diff --git a/src/board/presets.rs b/src/board/presets.rs new file mode 100644 index 0000000..67b68b9 --- /dev/null +++ b/src/board/presets.rs @@ -0,0 +1,134 @@ +/// Board presets for common Arduino boards. +/// +/// Each preset provides a known-good FQBN and default baud rate. +/// Users can always override the FQBN in .anvil.toml after creation. + +#[derive(Debug, Clone)] +pub struct BoardPreset { + /// Short name used on the command line (e.g. "uno", "mega") + pub name: &'static str, + /// Fully Qualified Board Name for arduino-cli + pub fqbn: &'static str, + /// Default serial baud rate + pub baud: u32, + /// Human-readable description + pub description: &'static str, + /// Common clones and compatible boards + pub also_known_as: &'static str, + /// Required core (for display in help text) + pub core: &'static str, +} + +/// Built-in board presets. +/// +/// Only includes boards whose core is installed by `anvil setup` +/// (arduino:avr). Users targeting other platforms can set the FQBN +/// manually in .anvil.toml. +pub const PRESETS: &[BoardPreset] = &[ + BoardPreset { + name: "uno", + fqbn: "arduino:avr:uno", + baud: 115200, + description: "Arduino Uno (ATmega328P)", + also_known_as: "SparkFun RedBoard, Elegoo Uno R3, DFRobot DFRduino, SunFounder Uno", + core: "arduino:avr", + }, + BoardPreset { + name: "mega", + fqbn: "arduino:avr:mega:cpu=atmega2560", + baud: 115200, + description: "Arduino Mega 2560", + also_known_as: "Elegoo Mega R3, Robotdyn Mega, SunFounder Mega", + core: "arduino:avr", + }, + BoardPreset { + name: "nano", + fqbn: "arduino:avr:nano:cpu=atmega328", + baud: 115200, + description: "Arduino Nano (new bootloader)", + also_known_as: "Elegoo Nano, Makerfire Nano V3", + core: "arduino:avr", + }, + BoardPreset { + name: "nano-old", + fqbn: "arduino:avr:nano:cpu=atmega328old", + baud: 115200, + description: "Arduino Nano (old bootloader)", + also_known_as: "pre-2018 Nano clones, most cheap Nano boards", + core: "arduino:avr", + }, + BoardPreset { + name: "leonardo", + fqbn: "arduino:avr:leonardo", + baud: 115200, + description: "Arduino Leonardo (ATmega32U4)", + also_known_as: "SparkFun Pro Micro (Leonardo mode), Freetronics LeoStick", + core: "arduino:avr", + }, + BoardPreset { + name: "micro", + fqbn: "arduino:avr:micro", + baud: 115200, + description: "Arduino Micro (ATmega32U4)", + also_known_as: "SparkFun Pro Micro, Adafruit ItsyBitsy 32u4", + core: "arduino:avr", + }, +]; + +/// Default board used when --board is not specified. +pub const DEFAULT_PRESET: &str = "uno"; + +/// Look up a board preset by short name (case-insensitive). +pub fn find_preset(name: &str) -> Option<&'static BoardPreset> { + let lower = name.to_lowercase(); + PRESETS.iter().find(|p| p.name == lower) +} + +/// List all preset names for display. +pub fn preset_names() -> Vec<&'static str> { + PRESETS.iter().map(|p| p.name).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_preset_uno() { + let p = find_preset("uno").unwrap(); + assert_eq!(p.fqbn, "arduino:avr:uno"); + assert_eq!(p.baud, 115200); + } + + #[test] + fn test_find_preset_case_insensitive() { + assert!(find_preset("Mega").is_some()); + assert!(find_preset("NANO").is_some()); + } + + #[test] + fn test_find_preset_unknown() { + assert!(find_preset("esp32").is_none()); + assert!(find_preset("stm32").is_none()); + } + + #[test] + fn test_default_preset_exists() { + assert!(find_preset(DEFAULT_PRESET).is_some()); + } + + #[test] + fn test_all_presets_have_avr_core() { + for p in PRESETS { + assert_eq!(p.core, "arduino:avr", "{} should use arduino:avr core", p.name); + } + } + + #[test] + fn test_preset_names() { + let names = preset_names(); + assert!(names.contains(&"uno")); + assert!(names.contains(&"mega")); + assert!(names.contains(&"nano")); + } +} \ No newline at end of file diff --git a/src/commands/devices.rs b/src/commands/devices.rs index 1186a19..d879545 100644 --- a/src/commands/devices.rs +++ b/src/commands/devices.rs @@ -274,6 +274,31 @@ pub fn set_port(port: Option<&str>, project_dir: Option<&str>) -> Result<()> { // -- Helpers -------------------------------------------------------------- +/// Delete .anvil.local from the given project directory. +pub fn clear_port(project_dir: Option<&str>) -> Result<()> { + let project_path = resolve_project_dir(project_dir)?; + require_anvil_project(&project_path)?; + + let local_file = project_path.join(".anvil.local"); + if !local_file.exists() { + println!( + "{} No .anvil.local file found -- nothing to clear.", + "--".bright_black() + ); + return Ok(()); + } + + fs::remove_file(&local_file) + .context("Failed to delete .anvil.local")?; + + println!( + "{} Removed .anvil.local -- port will be auto-detected.", + "ok".green() + ); + + Ok(()) +} + fn resolve_project_dir(project_dir: Option<&str>) -> Result { match project_dir { Some(dir) => Ok(PathBuf::from(dir)), diff --git a/src/commands/new.rs b/src/commands/new.rs index aa05a42..104e5d3 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -2,6 +2,7 @@ use anyhow::{Result, bail}; use colored::*; use std::path::PathBuf; +use crate::board::presets::{self, BoardPreset}; use crate::templates::{TemplateManager, TemplateContext}; use crate::version::ANVIL_VERSION; @@ -28,7 +29,52 @@ pub fn list_templates() -> Result<()> { Ok(()) } -pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { +pub fn list_boards() -> Result<()> { + println!("{}", "Available board presets:".bright_cyan().bold()); + println!(); + + for preset in presets::PRESETS { + let marker = if preset.name == presets::DEFAULT_PRESET { + " (default)" + } else { + "" + }; + println!( + " {}{}", + preset.name.bright_white().bold(), + marker.bright_cyan() + ); + println!(" {}", preset.description); + if !preset.also_known_as.is_empty() { + println!(" Also: {}", preset.also_known_as.bright_black()); + } + println!(" FQBN: {}", preset.fqbn.bright_black()); + println!(); + } + + println!( + " {}", + "Usage: anvil new --board mega".bright_yellow() + ); + println!(); + println!( + " {}", + "For boards not listed here, create a project and edit the".bright_black() + ); + println!( + " {}", + "fqbn value in .anvil.toml to any valid arduino-cli FQBN.".bright_black() + ); + println!(); + + Ok(()) +} + +pub fn create_project( + name: &str, + template: Option<&str>, + board: Option<&str>, +) -> Result<()> { // Validate project name validate_project_name(name)?; @@ -52,6 +98,25 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { bail!("Invalid template"); } + // Resolve board preset + let preset: &BoardPreset = match board { + Some(b) => { + match presets::find_preset(b) { + Some(p) => p, + None => { + println!( + "{}", + format!("Unknown board preset: '{}'", b).red().bold() + ); + println!(); + list_boards()?; + bail!("Invalid board preset"); + } + } + } + None => presets::find_preset(presets::DEFAULT_PRESET).unwrap(), + }; + println!( "{}", format!("Creating Arduino project: {}", name) @@ -59,6 +124,14 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { .bold() ); println!("{}", format!("Template: {}", template_name).bright_cyan()); + println!( + "{}", + format!("Board: {} ({})", preset.name, preset.description).bright_cyan() + ); + println!( + "{}", + format!("FQBN: {}", preset.fqbn).bright_black() + ); println!(); // Create project directory @@ -69,6 +142,8 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { let context = TemplateContext { project_name: name.to_string(), anvil_version: ANVIL_VERSION.to_string(), + fqbn: preset.fqbn.to_string(), + baud: preset.baud, }; let file_count = TemplateManager::extract(template_name, &project_path, &context)?; diff --git a/src/commands/refresh.rs b/src/commands/refresh.rs index e3e25ff..0c07819 100644 --- a/src/commands/refresh.rs +++ b/src/commands/refresh.rs @@ -47,6 +47,8 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> { let context = TemplateContext { project_name: config.project.name.clone(), anvil_version: ANVIL_VERSION.to_string(), + fqbn: config.build.fqbn.clone(), + baud: config.monitor.baud, }; // Extract template into a temp directory so we can compare diff --git a/src/main.rs b/src/main.rs index 9875859..d6e233e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,9 +31,17 @@ enum Commands { #[arg(long, short = 't', value_name = "TEMPLATE")] template: Option, + /// Board preset (uno, mega, nano, leonardo, micro) + #[arg(long, short = 'b', value_name = "BOARD")] + board: Option, + /// List available templates #[arg(long, conflicts_with = "name")] list_templates: bool, + + /// List available board presets + #[arg(long, conflicts_with = "name")] + list_boards: bool, }, /// Check system health and diagnose issues @@ -45,13 +53,17 @@ enum Commands { /// List connected boards and serial ports Devices { /// Save a port to .anvil.local for this project - #[arg(long, conflicts_with = "get")] + #[arg(long, conflicts_with_all = ["get", "clear"])] set: bool, /// Show the saved port for this project - #[arg(long, conflicts_with = "set")] + #[arg(long, conflicts_with_all = ["set", "clear"])] get: bool, + /// Remove .anvil.local (revert to auto-detect) + #[arg(long, conflicts_with_all = ["set", "get"])] + clear: bool, + /// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set. port_or_dir: Option, @@ -80,18 +92,23 @@ fn main() -> Result<()> { print_banner(); match cli.command { - Commands::New { name, template, list_templates } => { - if list_templates { + Commands::New { name, template, board, list_templates, list_boards } => { + if list_boards { + commands::new::list_boards() + } else if list_templates { commands::new::list_templates() } else if let Some(project_name) = name { commands::new::create_project( &project_name, template.as_deref(), + board.as_deref(), ) } else { anyhow::bail!( "Project name required.\n\ - Usage: anvil new \n\ + Usage: anvil new \n\ + Usage: anvil new --board mega\n\ + List boards: anvil new --list-boards\n\ List templates: anvil new --list-templates" ); } @@ -102,7 +119,7 @@ fn main() -> Result<()> { Commands::Setup => { commands::setup::run_setup() } - Commands::Devices { set, get, port_or_dir, dir } => { + Commands::Devices { set, get, clear, port_or_dir, dir } => { if set { commands::devices::set_port( port_or_dir.as_deref(), @@ -112,6 +129,10 @@ fn main() -> Result<()> { commands::devices::get_port( dir.as_deref().or(port_or_dir.as_deref()), ) + } else if clear { + commands::devices::clear_port( + dir.as_deref().or(port_or_dir.as_deref()), + ) } else { commands::devices::scan_devices() } diff --git a/src/templates/mod.rs b/src/templates/mod.rs index 3eef411..036827a 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -10,6 +10,8 @@ static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic") pub struct TemplateContext { pub project_name: String, pub anvil_version: String, + pub fqbn: String, + pub baud: u32, } pub struct TemplateManager; @@ -140,6 +142,8 @@ fn substitute_variables(text: &str, context: &TemplateContext) -> String { text.replace("{{PROJECT_NAME}}", &context.project_name) .replace("{{ANVIL_VERSION}}", &context.anvil_version) .replace("{{ANVIL_VERSION_CURRENT}}", ANVIL_VERSION) + .replace("{{FQBN}}", &context.fqbn) + .replace("{{BAUD}}", &context.baud.to_string()) } #[cfg(test)] @@ -172,10 +176,15 @@ mod tests { let ctx = TemplateContext { project_name: "my_project".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: 9600, }; - let input = "Name: {{PROJECT_NAME}}, Version: {{ANVIL_VERSION}}"; + let input = "Name: {{PROJECT_NAME}}, Version: {{ANVIL_VERSION}}, FQBN: {{FQBN}}, Baud: {{BAUD}}"; let output = substitute_variables(input, &ctx); - assert_eq!(output, "Name: my_project, Version: 1.0.0"); + assert_eq!( + output, + "Name: my_project, Version: 1.0.0, FQBN: arduino:avr:mega:cpu=atmega2560, Baud: 9600" + ); } #[test] @@ -190,6 +199,8 @@ mod tests { let ctx = TemplateContext { project_name: "test_proj".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -220,6 +231,8 @@ mod tests { let ctx = TemplateContext { project_name: "my_sensor".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -231,4 +244,4 @@ mod tests { ".anvil.toml should contain project name" ); } -} +} \ No newline at end of file diff --git a/templates/basic/_dot_anvil.toml.tmpl b/templates/basic/_dot_anvil.toml.tmpl index c8afc54..98324c0 100644 --- a/templates/basic/_dot_anvil.toml.tmpl +++ b/templates/basic/_dot_anvil.toml.tmpl @@ -3,10 +3,10 @@ name = "{{PROJECT_NAME}}" anvil_version = "{{ANVIL_VERSION}}" [build] -fqbn = "arduino:avr:uno" +fqbn = "{{FQBN}}" warnings = "more" include_dirs = ["lib/hal", "lib/app"] extra_flags = ["-Werror"] [monitor] -baud = 115200 +baud = {{BAUD}} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 1aaf0ec..0d7c9cc 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -15,6 +15,8 @@ fn test_basic_template_extracts_all_expected_files() { let ctx = TemplateContext { project_name: "test_proj".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -27,6 +29,8 @@ fn test_template_creates_sketch_directory() { let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -55,6 +59,8 @@ fn test_template_creates_hal_files() { let ctx = TemplateContext { project_name: "sensor".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -86,6 +92,8 @@ fn test_template_creates_app_header() { let ctx = TemplateContext { project_name: "my_sensor".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -110,6 +118,8 @@ fn test_template_creates_test_infrastructure() { let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -146,6 +156,8 @@ fn test_template_test_file_references_correct_app() { let ctx = TemplateContext { project_name: "motor_ctrl".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -165,6 +177,8 @@ fn test_template_cmake_references_correct_project() { let ctx = TemplateContext { project_name: "my_bot".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -184,6 +198,8 @@ fn test_template_creates_dot_files() { let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -212,6 +228,8 @@ fn test_template_creates_readme() { let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -233,6 +251,8 @@ fn test_template_creates_valid_config() { let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -312,6 +332,8 @@ fn test_full_project_structure() { let ctx = TemplateContext { project_name: "full_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -361,6 +383,8 @@ fn test_no_unicode_in_template_output() { let ctx = TemplateContext { project_name: "ascii_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -408,6 +432,8 @@ fn test_unknown_template_fails() { let ctx = TemplateContext { project_name: "test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx); @@ -431,6 +457,8 @@ fn test_template_creates_self_contained_scripts() { let ctx = TemplateContext { project_name: "standalone".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -453,6 +481,8 @@ fn test_build_sh_reads_anvil_toml() { let ctx = TemplateContext { project_name: "toml_reader".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -478,6 +508,8 @@ fn test_upload_sh_reads_anvil_toml() { let ctx = TemplateContext { project_name: "uploader".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -511,6 +543,8 @@ fn test_monitor_sh_reads_anvil_toml() { let ctx = TemplateContext { project_name: "serial_mon".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -536,6 +570,8 @@ fn test_scripts_have_shebangs() { let ctx = TemplateContext { project_name: "shebangs".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -558,6 +594,8 @@ fn test_scripts_no_anvil_binary_dependency() { let ctx = TemplateContext { project_name: "no_anvil_dep".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -605,6 +643,8 @@ fn test_gitignore_excludes_build_cache() { let ctx = TemplateContext { project_name: "gitcheck".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -630,6 +670,8 @@ fn test_readme_documents_self_contained_workflow() { let ctx = TemplateContext { project_name: "docs_check".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -663,6 +705,8 @@ fn test_scripts_tolerate_missing_toml_keys() { let ctx = TemplateContext { project_name: "grep_safe".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -698,6 +742,8 @@ fn test_bat_scripts_no_unescaped_parens_in_echo() { let ctx = TemplateContext { project_name: "parens_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -757,6 +803,8 @@ fn test_scripts_read_anvil_local_for_port() { let ctx = TemplateContext { project_name: "local_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -777,6 +825,8 @@ fn test_anvil_toml_template_has_no_port() { let ctx = TemplateContext { project_name: "no_port".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -806,6 +856,8 @@ fn test_bat_scripts_call_detect_port_ps1() { let ctx = TemplateContext { project_name: "ps1_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -826,6 +878,8 @@ fn test_detect_port_ps1_is_valid() { let ctx = TemplateContext { project_name: "ps1_valid".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -856,6 +910,8 @@ fn test_refresh_freshly_extracted_is_up_to_date() { let ctx = TemplateContext { project_name: "fresh_proj".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -884,6 +940,8 @@ fn test_refresh_detects_modified_script() { let ctx = TemplateContext { project_name: "mod_proj".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -951,6 +1009,8 @@ fn test_scripts_read_vid_pid_from_anvil_local() { let ctx = TemplateContext { project_name: "vidpid_test".to_string(), anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); @@ -962,4 +1022,44 @@ fn test_scripts_read_vid_pid_from_anvil_local() { script ); } +} + +// ========================================================================== +// Board preset tests +// ========================================================================== + +#[test] +fn test_board_preset_fqbn_in_config() { + // Creating a project with --board mega should set the FQBN in .anvil.toml + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "mega_test".to_string(), + anvil_version: "1.0.0".to_string(), + fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), + baud: 115200, + }; + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let config = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!( + config.build.fqbn, "arduino:avr:mega:cpu=atmega2560", + ".anvil.toml should contain mega FQBN" + ); +} + +#[test] +fn test_board_preset_custom_fqbn_in_config() { + // Even arbitrary FQBNs should work through the template + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "custom_board".to_string(), + anvil_version: "1.0.0".to_string(), + fqbn: "esp32:esp32:esp32".to_string(), + baud: 9600, + }; + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let config = ProjectConfig::load(tmp.path()).unwrap(); + assert_eq!(config.build.fqbn, "esp32:esp32:esp32"); + assert_eq!(config.monitor.baud, 9600); } \ No newline at end of file