Supporting multiple boards
This commit is contained in:
505
src/commands/board.rs
Normal file
505
src/commands/board.rs
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
use anyhow::{Result, Context, bail};
|
||||||
|
use colored::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::board::presets;
|
||||||
|
use crate::project::config::{ProjectConfig, CONFIG_FILENAME};
|
||||||
|
|
||||||
|
/// Resolve a human-readable name for a board identifier by checking presets.
|
||||||
|
fn board_label(fqbn: &str) -> String {
|
||||||
|
for p in presets::PRESETS {
|
||||||
|
if p.fqbn == fqbn {
|
||||||
|
return p.description.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fqbn.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all boards configured for this project.
|
||||||
|
pub fn list_boards(project_dir: Option<&str>) -> Result<()> {
|
||||||
|
let project_path = resolve_project_dir(project_dir)?;
|
||||||
|
let config = ProjectConfig::load(&project_path)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("Boards for: {}", config.project.name)
|
||||||
|
.bright_cyan()
|
||||||
|
.bold()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let mut names: Vec<&String> = config.boards.keys().collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
if names.is_empty() {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"No boards configured.".bright_black()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for name in &names {
|
||||||
|
let profile = &config.boards[*name];
|
||||||
|
let baud_str = match profile.baud {
|
||||||
|
Some(b) => format!("baud={}", b),
|
||||||
|
None => format!("baud={}", config.monitor.baud),
|
||||||
|
};
|
||||||
|
let default_marker = if *name == &config.build.default {
|
||||||
|
" (default)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" {:9} {}{} {}",
|
||||||
|
name.bright_white().bold(),
|
||||||
|
board_label(&profile.fqbn).bright_cyan(),
|
||||||
|
default_marker.green(),
|
||||||
|
baud_str.bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if names.len() > 1 {
|
||||||
|
println!();
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Use: build.bat --board <n>".bright_black()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Use: ./build.sh --board <n>".bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", "Next steps:".bright_yellow().bold());
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" Add a board: {}",
|
||||||
|
"anvil board --add mega".bright_cyan()
|
||||||
|
);
|
||||||
|
if config.boards.len() > 1 {
|
||||||
|
println!(
|
||||||
|
" Remove a board: {}",
|
||||||
|
format!(
|
||||||
|
"anvil board --remove {}",
|
||||||
|
config.boards.keys()
|
||||||
|
.find(|k| *k != &config.build.default)
|
||||||
|
.unwrap_or(&config.build.default)
|
||||||
|
).bright_cyan()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" Browse boards: {}",
|
||||||
|
"anvil board --listall".bright_cyan()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show all available boards in a single unified list.
|
||||||
|
pub fn listall_boards(search: Option<&str>) -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Boards you can add:".bright_cyan().bold()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Filter presets by search term
|
||||||
|
let presets_to_show: Vec<_> = match search {
|
||||||
|
Some(term) => {
|
||||||
|
let lower = term.to_lowercase();
|
||||||
|
presets::PRESETS.iter().filter(|p| {
|
||||||
|
p.name.contains(&lower)
|
||||||
|
|| p.description.to_lowercase().contains(&lower)
|
||||||
|
|| p.also_known_as.to_lowercase().contains(&lower)
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
None => presets::PRESETS.iter().collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show presets
|
||||||
|
if !presets_to_show.is_empty() {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Common boards:".bright_cyan().bold()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for p in &presets_to_show {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
p.name.bright_white().bold()
|
||||||
|
);
|
||||||
|
let mut board_names = vec![p.description.to_string()];
|
||||||
|
if !p.also_known_as.is_empty() {
|
||||||
|
for alias in p.also_known_as.split(", ") {
|
||||||
|
board_names.push(alias.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" Works with: {}",
|
||||||
|
board_names.join(", ").bright_black()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Add: {}",
|
||||||
|
format!("anvil board --add {}", p.name).bright_cyan()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full catalog from arduino-cli
|
||||||
|
let cli = crate::board::find_arduino_cli();
|
||||||
|
let cli_path = match cli {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
if presets_to_show.is_empty() {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"No boards match your search.".bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Install arduino-cli to see more boards: anvil setup"
|
||||||
|
.bright_yellow()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut args = vec!["board", "listall"];
|
||||||
|
if let Some(term) = search {
|
||||||
|
args.push(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = std::process::Command::new(&cli_path)
|
||||||
|
.args(&args)
|
||||||
|
.output()
|
||||||
|
.context("Failed to run arduino-cli board listall")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("arduino-cli board listall failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let lines: Vec<&str> = stdout.lines().collect();
|
||||||
|
|
||||||
|
// Collect non-preset boards
|
||||||
|
let mut other_boards: Vec<(&str, &str)> = Vec::new();
|
||||||
|
for line in lines.iter().skip(1) {
|
||||||
|
let fqbn = line.split_whitespace().last().unwrap_or("");
|
||||||
|
if fqbn.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let board_name = line.trim_end_matches(fqbn).trim();
|
||||||
|
if board_name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let is_preset = presets::PRESETS.iter().any(|p| p.fqbn == fqbn);
|
||||||
|
if !is_preset {
|
||||||
|
other_boards.push((board_name, fqbn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !other_boards.is_empty() {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"More boards:".bright_cyan().bold()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for (board_name, fqbn) in &other_boards {
|
||||||
|
let suggested = suggest_short_name(board_name);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
board_name.bright_white()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Add: {}",
|
||||||
|
format!(
|
||||||
|
"anvil board --add {} --id {}",
|
||||||
|
suggested, fqbn
|
||||||
|
).bright_cyan()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if presets_to_show.is_empty() && other_boards.is_empty() {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"No boards match your search.".bright_black()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.is_some() {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"See all: anvil board --listall".bright_black()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Search: anvil board --listall <search>".bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Remove: anvil board --remove <n>".bright_black()
|
||||||
|
);
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Use: upload.bat --board <n>".bright_black()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Use: ./upload.sh --board <n>".bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Suggest a short project name from a board's display name.
|
||||||
|
fn suggest_short_name(board_name: &str) -> String {
|
||||||
|
let lower = board_name.to_lowercase();
|
||||||
|
|
||||||
|
let stripped = lower
|
||||||
|
.trim_start_matches("arduino ")
|
||||||
|
.trim_start_matches("adafruit ")
|
||||||
|
.trim_start_matches("sparkfun ")
|
||||||
|
.trim_start_matches("seeed ");
|
||||||
|
|
||||||
|
let meaningful = if let Some(pos) = stripped.find(" or ") {
|
||||||
|
stripped[pos + 4..].trim()
|
||||||
|
} else {
|
||||||
|
stripped.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
let words: Vec<&str> = meaningful
|
||||||
|
.split(|c: char| !c.is_alphanumeric())
|
||||||
|
.filter(|w| {
|
||||||
|
!w.is_empty()
|
||||||
|
&& !matches!(*w, "the" | "a" | "arduino" | "board")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if words.is_empty() {
|
||||||
|
let fallback: Vec<&str> = stripped
|
||||||
|
.split(|c: char| !c.is_alphanumeric())
|
||||||
|
.filter(|w| !w.is_empty() && *w != "arduino")
|
||||||
|
.take(2)
|
||||||
|
.collect();
|
||||||
|
if fallback.is_empty() {
|
||||||
|
"board".to_string()
|
||||||
|
} else {
|
||||||
|
fallback.join("-")
|
||||||
|
}
|
||||||
|
} else if words.len() == 1 {
|
||||||
|
words[0].to_string()
|
||||||
|
} else {
|
||||||
|
words.iter().take(2).copied().collect::<Vec<_>>().join("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a board to .anvil.toml.
|
||||||
|
pub fn add_board(
|
||||||
|
name: &str,
|
||||||
|
id: Option<&str>,
|
||||||
|
baud: Option<u32>,
|
||||||
|
project_dir: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let project_path = resolve_project_dir(project_dir)?;
|
||||||
|
let config = ProjectConfig::load(&project_path)?;
|
||||||
|
|
||||||
|
if config.boards.contains_key(name) {
|
||||||
|
bail!(
|
||||||
|
"Board '{}' already exists.\n \
|
||||||
|
Remove it first: anvil board --remove {}",
|
||||||
|
name, name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the board identifier: explicit --id > preset lookup > error
|
||||||
|
let resolved_id = match id {
|
||||||
|
Some(f) => f.to_string(),
|
||||||
|
None => {
|
||||||
|
match presets::find_preset(name) {
|
||||||
|
Some(preset) => {
|
||||||
|
preset.fqbn.to_string()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"'{}' is not a recognized board name.",
|
||||||
|
name
|
||||||
|
).bright_yellow()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Find your board:"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"anvil board --listall".bright_cyan()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
format!("anvil board --listall {}", name).bright_cyan()
|
||||||
|
);
|
||||||
|
bail!("Run anvil board --listall to find the right command.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve baud: explicit > preset default > None (inherit)
|
||||||
|
let resolved_baud = match baud {
|
||||||
|
Some(b) => Some(b),
|
||||||
|
None => {
|
||||||
|
if let Some(preset) = presets::find_preset(name) {
|
||||||
|
if preset.baud != config.monitor.baud {
|
||||||
|
Some(preset.baud)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Append to .anvil.toml as text to preserve formatting and comments
|
||||||
|
let config_path = project_path.join(CONFIG_FILENAME);
|
||||||
|
let mut content = fs::read_to_string(&config_path)
|
||||||
|
.context("Failed to read .anvil.toml")?;
|
||||||
|
|
||||||
|
let mut section = format!("\n[boards.{}]\n", name);
|
||||||
|
section.push_str(&format!("fqbn = \"{}\"\n", resolved_id));
|
||||||
|
if let Some(b) = resolved_baud {
|
||||||
|
section.push_str(&format!("baud = {}\n", b));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push_str(§ion);
|
||||||
|
fs::write(&config_path, content)
|
||||||
|
.context("Failed to write .anvil.toml")?;
|
||||||
|
|
||||||
|
let label = board_label(&resolved_id);
|
||||||
|
println!(
|
||||||
|
"{} Added board: {} ({})",
|
||||||
|
"ok".green(),
|
||||||
|
name.bright_white().bold(),
|
||||||
|
label.bright_cyan()
|
||||||
|
);
|
||||||
|
if let Some(b) = resolved_baud {
|
||||||
|
println!(" baud = {}", b.to_string().bright_cyan());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
format!("Use: build.bat --board {}", name).bright_black()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
format!("Use: ./build.sh --board {}", name).bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a board from .anvil.toml.
|
||||||
|
pub fn remove_board(name: &str, project_dir: Option<&str>) -> Result<()> {
|
||||||
|
let project_path = resolve_project_dir(project_dir)?;
|
||||||
|
let config = ProjectConfig::load(&project_path)?;
|
||||||
|
|
||||||
|
if !config.boards.contains_key(name) {
|
||||||
|
let available: Vec<&String> = config.boards.keys().collect();
|
||||||
|
if available.is_empty() {
|
||||||
|
bail!("No boards configured.");
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"No board '{}' found.\n Available: {}",
|
||||||
|
name,
|
||||||
|
available.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow removing the default board
|
||||||
|
if name == config.build.default {
|
||||||
|
bail!(
|
||||||
|
"Cannot remove '{}' because it is the default board.\n \
|
||||||
|
Change the default in .anvil.toml first, or remove a different board.",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = project_path.join(CONFIG_FILENAME);
|
||||||
|
let content = fs::read_to_string(&config_path)
|
||||||
|
.context("Failed to read .anvil.toml")?;
|
||||||
|
|
||||||
|
let section_header = format!("[boards.{}]", name);
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut skipping = false;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == section_header {
|
||||||
|
skipping = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if skipping && trimmed.starts_with('[') {
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
if skipping {
|
||||||
|
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.contains('=') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
output.push_str(line);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
while output.ends_with("\n\n\n") {
|
||||||
|
output.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&config_path, &output)
|
||||||
|
.context("Failed to write .anvil.toml")?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} Removed board: {}",
|
||||||
|
"ok".green(),
|
||||||
|
name.bright_white().bold()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_project_dir(project_dir: Option<&str>) -> Result<std::path::PathBuf> {
|
||||||
|
let start = match project_dir {
|
||||||
|
Some(dir) => std::path::PathBuf::from(dir),
|
||||||
|
None => std::env::current_dir()
|
||||||
|
.context("Could not determine current directory")?,
|
||||||
|
};
|
||||||
|
ProjectConfig::find_project_root(&start)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use crate::board;
|
use crate::board;
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ impl SystemHealth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_diagnostics() -> Result<()> {
|
pub fn run_diagnostics(fix: bool) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
"Checking system health...".bright_yellow().bold()
|
"Checking system health...".bright_yellow().bold()
|
||||||
@@ -40,6 +41,19 @@ pub fn run_diagnostics() -> Result<()> {
|
|||||||
.bright_green()
|
.bright_green()
|
||||||
.bold()
|
.bold()
|
||||||
);
|
);
|
||||||
|
if fix {
|
||||||
|
// Check optional tools
|
||||||
|
let optional_missing = !health.cmake_ok
|
||||||
|
|| !health.cpp_compiler_ok
|
||||||
|
|| !health.git_ok;
|
||||||
|
if optional_missing {
|
||||||
|
println!();
|
||||||
|
run_fix_optional(&health)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if fix {
|
||||||
|
println!();
|
||||||
|
run_fix(&health)?;
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -49,6 +63,11 @@ pub fn run_diagnostics() -> Result<()> {
|
|||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
print_fix_instructions(&health);
|
print_fix_instructions(&health);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Or run: anvil doctor --fix (to install missing tools automatically)"
|
||||||
|
.bright_cyan()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -155,7 +174,7 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
println!("{}", "Optional:".bright_yellow().bold());
|
println!("{}", "Optional:".bright_yellow().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// avr-size -- installed as part of the avr core, not a separate step
|
// avr-size
|
||||||
if health.avr_size_ok {
|
if health.avr_size_ok {
|
||||||
println!(" {} avr-size (binary size reporting)", "ok".green());
|
println!(" {} avr-size (binary size reporting)", "ok".green());
|
||||||
} else if !health.avr_core_ok {
|
} else if !health.avr_core_ok {
|
||||||
@@ -165,9 +184,6 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
"included with arduino:avr core (no separate install)".yellow()
|
"included with arduino:avr core (no separate install)".yellow()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Core is installed but avr-size is not on PATH --
|
|
||||||
// this can happen on Windows where the tool is buried
|
|
||||||
// inside the Arduino15 packages directory.
|
|
||||||
println!(
|
println!(
|
||||||
" {} avr-size {}",
|
" {} avr-size {}",
|
||||||
"na".yellow(),
|
"na".yellow(),
|
||||||
@@ -300,7 +316,6 @@ fn print_fix_instructions(health: &SystemHealth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !health.arduino_cli_ok {
|
if !health.arduino_cli_ok {
|
||||||
// They need to open a new terminal after installing arduino-cli
|
|
||||||
println!(
|
println!(
|
||||||
" {}. {}",
|
" {}. {}",
|
||||||
step,
|
step,
|
||||||
@@ -353,6 +368,365 @@ fn print_fix_instructions(health: &SystemHealth) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// --fix: automated installation
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Prompt the user for yes/no confirmation.
|
||||||
|
fn confirm(prompt: &str) -> bool {
|
||||||
|
print!("{} [Y/n] ", prompt);
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
let mut input = String::new();
|
||||||
|
if io::stdin().read_line(&mut input).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let trimmed = input.trim().to_lowercase();
|
||||||
|
trimmed.is_empty() || trimmed == "y" || trimmed == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a command, streaming output to the terminal.
|
||||||
|
fn run_cmd(program: &str, args: &[&str]) -> bool {
|
||||||
|
println!(
|
||||||
|
" {} {} {}",
|
||||||
|
"$".bright_black(),
|
||||||
|
program.bright_cyan(),
|
||||||
|
args.join(" ").bright_cyan()
|
||||||
|
);
|
||||||
|
std::process::Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect which package manager is available on the system.
|
||||||
|
fn detect_package_manager() -> Option<&'static str> {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
if which::which("winget").is_ok() {
|
||||||
|
Some("winget")
|
||||||
|
} else if which::which("choco").is_ok() {
|
||||||
|
Some("choco")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
if which::which("brew").is_ok() {
|
||||||
|
Some("brew")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
if which::which("apt").is_ok() {
|
||||||
|
Some("apt")
|
||||||
|
} else if which::which("dnf").is_ok() {
|
||||||
|
Some("dnf")
|
||||||
|
} else if which::which("pacman").is_ok() {
|
||||||
|
Some("pacman")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fix required items (arduino-cli, avr core).
|
||||||
|
fn run_fix(health: &SystemHealth) -> Result<()> {
|
||||||
|
let pm = detect_package_manager();
|
||||||
|
|
||||||
|
// -- arduino-cli --
|
||||||
|
if !health.arduino_cli_ok {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"arduino-cli is required but not installed.".bright_yellow()
|
||||||
|
);
|
||||||
|
|
||||||
|
match pm {
|
||||||
|
Some("winget") => {
|
||||||
|
if confirm("Install arduino-cli via winget?") {
|
||||||
|
if !run_cmd("winget", &["install", "--id", "ArduinoSA.CLI", "-e"]) {
|
||||||
|
println!("{} winget install failed. Try installing manually.", "FAIL".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{} arduino-cli installed. {} to update PATH.",
|
||||||
|
"ok".green(),
|
||||||
|
"Restart your terminal".bright_yellow()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("choco") => {
|
||||||
|
if confirm("Install arduino-cli via Chocolatey?") {
|
||||||
|
if !run_cmd("choco", &["install", "arduino-cli", "-y"]) {
|
||||||
|
println!("{} choco install failed. Try installing manually.", "FAIL".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{} arduino-cli installed. {} to update PATH.",
|
||||||
|
"ok".green(),
|
||||||
|
"Restart your terminal".bright_yellow()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("brew") => {
|
||||||
|
if confirm("Install arduino-cli via Homebrew?") {
|
||||||
|
if !run_cmd("brew", &["install", "arduino-cli"]) {
|
||||||
|
println!("{} brew install failed.", "FAIL".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("{} arduino-cli installed.", "ok".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("apt") => {
|
||||||
|
if confirm("Install arduino-cli via apt?") {
|
||||||
|
if !run_cmd("sudo", &["apt", "install", "-y", "arduino-cli"]) {
|
||||||
|
println!(
|
||||||
|
"{} apt install failed. Try the curl installer instead:",
|
||||||
|
"FAIL".red()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh"
|
||||||
|
.bright_cyan()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("{} arduino-cli installed.", "ok".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("dnf") => {
|
||||||
|
println!(
|
||||||
|
" arduino-cli is not in dnf repos. Install manually:"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh"
|
||||||
|
.bright_cyan()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Some("pacman") => {
|
||||||
|
if confirm("Install arduino-cli via pacman (AUR)?") {
|
||||||
|
if !run_cmd("yay", &["-S", "--noconfirm", "arduino-cli"]) {
|
||||||
|
println!("{} AUR install failed. Try the curl installer.", "FAIL".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("{} arduino-cli installed.", "ok".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!(
|
||||||
|
" No supported package manager found. Install manually:"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"https://arduino.github.io/arduino-cli/installation/"
|
||||||
|
.bright_cyan()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check after install
|
||||||
|
if board::find_arduino_cli().is_none() {
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{} arduino-cli not found after install. You may need to restart your terminal.",
|
||||||
|
"warn".yellow()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- AVR core --
|
||||||
|
if !health.avr_core_ok {
|
||||||
|
if health.arduino_cli_ok || board::find_arduino_cli().is_some() {
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"arduino:avr core is required but not installed.".bright_yellow()
|
||||||
|
);
|
||||||
|
if confirm("Install arduino:avr core now?") {
|
||||||
|
let cli = board::find_arduino_cli()
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from("arduino-cli"));
|
||||||
|
let cli_str = cli.to_string_lossy();
|
||||||
|
if !run_cmd(&cli_str, &["core", "install", "arduino:avr"]) {
|
||||||
|
println!("{} Core installation failed.", "FAIL".red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("{} arduino:avr core installed.", "ok".green());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offer optional tools too
|
||||||
|
run_fix_optional(health)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fix optional items (cmake, C++ compiler, git).
|
||||||
|
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
||||||
|
let pm = detect_package_manager();
|
||||||
|
|
||||||
|
let items: Vec<(&str, bool, FixSpec)> = vec![
|
||||||
|
("cmake", health.cmake_ok, fix_spec_cmake(pm)),
|
||||||
|
("C++ compiler", health.cpp_compiler_ok, fix_spec_cpp(pm)),
|
||||||
|
("git", health.git_ok, fix_spec_git(pm)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let missing: Vec<_> = items.iter().filter(|(_, ok, _)| !ok).collect();
|
||||||
|
if missing.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", "Optional tools:".bright_yellow().bold());
|
||||||
|
|
||||||
|
for (name, _, spec) in &missing {
|
||||||
|
match spec {
|
||||||
|
FixSpec::Auto { prompt, program, args } => {
|
||||||
|
if confirm(prompt) {
|
||||||
|
if run_cmd(program, args) {
|
||||||
|
println!("{} {} installed.", "ok".green(), name);
|
||||||
|
} else {
|
||||||
|
println!("{} Failed to install {}.", "FAIL".red(), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FixSpec::Manual { message } => {
|
||||||
|
println!(" {}: {}", name, message.bright_black());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FixSpec {
|
||||||
|
Auto {
|
||||||
|
prompt: &'static str,
|
||||||
|
program: &'static str,
|
||||||
|
args: &'static [&'static str],
|
||||||
|
},
|
||||||
|
Manual {
|
||||||
|
message: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_spec_cmake(pm: Option<&str>) -> FixSpec {
|
||||||
|
match pm {
|
||||||
|
Some("winget") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via winget?",
|
||||||
|
program: "winget",
|
||||||
|
args: &["install", "--id", "Kitware.CMake", "-e"],
|
||||||
|
},
|
||||||
|
Some("choco") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via Chocolatey?",
|
||||||
|
program: "choco",
|
||||||
|
args: &["install", "cmake", "-y"],
|
||||||
|
},
|
||||||
|
Some("brew") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via Homebrew?",
|
||||||
|
program: "brew",
|
||||||
|
args: &["install", "cmake"],
|
||||||
|
},
|
||||||
|
Some("apt") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via apt?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["apt", "install", "-y", "cmake"],
|
||||||
|
},
|
||||||
|
Some("dnf") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via dnf?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["dnf", "install", "-y", "cmake"],
|
||||||
|
},
|
||||||
|
Some("pacman") => FixSpec::Auto {
|
||||||
|
prompt: "Install cmake via pacman?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["pacman", "-S", "--noconfirm", "cmake"],
|
||||||
|
},
|
||||||
|
_ => FixSpec::Manual {
|
||||||
|
message: "install from https://cmake.org/download/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_spec_cpp(pm: Option<&str>) -> FixSpec {
|
||||||
|
match pm {
|
||||||
|
Some("winget") => FixSpec::Auto {
|
||||||
|
prompt: "Install Visual Studio Build Tools via winget?",
|
||||||
|
program: "winget",
|
||||||
|
args: &["install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e"],
|
||||||
|
},
|
||||||
|
Some("choco") => FixSpec::Auto {
|
||||||
|
prompt: "Install MinGW g++ via Chocolatey?",
|
||||||
|
program: "choco",
|
||||||
|
args: &["install", "mingw", "-y"],
|
||||||
|
},
|
||||||
|
Some("brew") => FixSpec::Manual {
|
||||||
|
message: "run: xcode-select --install",
|
||||||
|
},
|
||||||
|
Some("apt") => FixSpec::Auto {
|
||||||
|
prompt: "Install g++ via apt?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["apt", "install", "-y", "g++"],
|
||||||
|
},
|
||||||
|
Some("dnf") => FixSpec::Auto {
|
||||||
|
prompt: "Install g++ via dnf?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["dnf", "install", "-y", "gcc-c++"],
|
||||||
|
},
|
||||||
|
Some("pacman") => FixSpec::Auto {
|
||||||
|
prompt: "Install g++ via pacman?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["pacman", "-S", "--noconfirm", "gcc"],
|
||||||
|
},
|
||||||
|
_ => FixSpec::Manual {
|
||||||
|
message: "install a C++ compiler (g++, clang++, or MSVC)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_spec_git(pm: Option<&str>) -> FixSpec {
|
||||||
|
match pm {
|
||||||
|
Some("winget") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via winget?",
|
||||||
|
program: "winget",
|
||||||
|
args: &["install", "--id", "Git.Git", "-e"],
|
||||||
|
},
|
||||||
|
Some("choco") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via Chocolatey?",
|
||||||
|
program: "choco",
|
||||||
|
args: &["install", "git", "-y"],
|
||||||
|
},
|
||||||
|
Some("brew") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via Homebrew?",
|
||||||
|
program: "brew",
|
||||||
|
args: &["install", "git"],
|
||||||
|
},
|
||||||
|
Some("apt") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via apt?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["apt", "install", "-y", "git"],
|
||||||
|
},
|
||||||
|
Some("dnf") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via dnf?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["dnf", "install", "-y", "git"],
|
||||||
|
},
|
||||||
|
Some("pacman") => FixSpec::Auto {
|
||||||
|
prompt: "Install git via pacman?",
|
||||||
|
program: "sudo",
|
||||||
|
args: &["pacman", "-S", "--noconfirm", "git"],
|
||||||
|
},
|
||||||
|
_ => FixSpec::Manual {
|
||||||
|
message: "install from https://git-scm.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Platform-aware install hints (one-liners for the diagnostics table)
|
// Platform-aware install hints (one-liners for the diagnostics table)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ pub mod doctor;
|
|||||||
pub mod setup;
|
pub mod setup;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod refresh;
|
pub mod refresh;
|
||||||
|
pub mod board;
|
||||||
@@ -48,7 +48,6 @@ pub fn list_boards() -> Result<()> {
|
|||||||
if !preset.also_known_as.is_empty() {
|
if !preset.also_known_as.is_empty() {
|
||||||
println!(" Also: {}", preset.also_known_as.bright_black());
|
println!(" Also: {}", preset.also_known_as.bright_black());
|
||||||
}
|
}
|
||||||
println!(" FQBN: {}", preset.fqbn.bright_black());
|
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +58,11 @@ pub fn list_boards() -> Result<()> {
|
|||||||
println!();
|
println!();
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
"For boards not listed here, create a project and edit the".bright_black()
|
"For boards not listed here, create a project and then:".bright_black()
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
"fqbn value in .anvil.toml to any valid arduino-cli FQBN.".bright_black()
|
" anvil board --listall".bright_black()
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -128,10 +127,6 @@ pub fn create_project(
|
|||||||
"{}",
|
"{}",
|
||||||
format!("Board: {} ({})", preset.name, preset.description).bright_cyan()
|
format!("Board: {} ({})", preset.name, preset.description).bright_cyan()
|
||||||
);
|
);
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
format!("FQBN: {}", preset.fqbn).bright_black()
|
|
||||||
);
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
@@ -142,6 +137,7 @@ pub fn create_project(
|
|||||||
let context = TemplateContext {
|
let context = TemplateContext {
|
||||||
project_name: name.to_string(),
|
project_name: name.to_string(),
|
||||||
anvil_version: ANVIL_VERSION.to_string(),
|
anvil_version: ANVIL_VERSION.to_string(),
|
||||||
|
board_name: preset.name.to_string(),
|
||||||
fqbn: preset.fqbn.to_string(),
|
fqbn: preset.fqbn.to_string(),
|
||||||
baud: preset.baud,
|
baud: preset.baud,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
|||||||
let context = TemplateContext {
|
let context = TemplateContext {
|
||||||
project_name: config.project.name.clone(),
|
project_name: config.project.name.clone(),
|
||||||
anvil_version: ANVIL_VERSION.to_string(),
|
anvil_version: ANVIL_VERSION.to_string(),
|
||||||
fqbn: config.build.fqbn.clone(),
|
board_name: config.build.default.clone(),
|
||||||
|
fqbn: config.default_fqbn().unwrap_or_else(|_| "arduino:avr:uno".to_string()),
|
||||||
baud: config.monitor.baud,
|
baud: config.monitor.baud,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
72
src/main.rs
72
src/main.rs
@@ -45,7 +45,11 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Check system health and diagnose issues
|
/// Check system health and diagnose issues
|
||||||
Doctor,
|
Doctor {
|
||||||
|
/// Automatically install missing tools
|
||||||
|
#[arg(long)]
|
||||||
|
fix: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Install arduino-cli and required cores
|
/// Install arduino-cli and required cores
|
||||||
Setup,
|
Setup,
|
||||||
@@ -81,6 +85,36 @@ enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Manage board profiles in .anvil.toml
|
||||||
|
Board {
|
||||||
|
/// Board name (e.g. mega, nano)
|
||||||
|
name: Option<String>,
|
||||||
|
|
||||||
|
/// Add a board to the project
|
||||||
|
#[arg(long, conflicts_with_all = ["remove", "listall"])]
|
||||||
|
add: bool,
|
||||||
|
|
||||||
|
/// Remove a board from the project
|
||||||
|
#[arg(long, conflicts_with_all = ["add", "listall"])]
|
||||||
|
remove: bool,
|
||||||
|
|
||||||
|
/// Browse all available boards
|
||||||
|
#[arg(long, conflicts_with_all = ["add", "remove"])]
|
||||||
|
listall: bool,
|
||||||
|
|
||||||
|
/// Board identifier (from anvil board --listall)
|
||||||
|
#[arg(long, value_name = "ID")]
|
||||||
|
id: Option<String>,
|
||||||
|
|
||||||
|
/// Baud rate override
|
||||||
|
#[arg(long, value_name = "RATE")]
|
||||||
|
baud: Option<u32>,
|
||||||
|
|
||||||
|
/// Path to project directory (defaults to current directory)
|
||||||
|
#[arg(long, short = 'd', value_name = "DIR")]
|
||||||
|
dir: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@@ -113,8 +147,8 @@ fn main() -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Doctor => {
|
Commands::Doctor { fix } => {
|
||||||
commands::doctor::run_diagnostics()
|
commands::doctor::run_diagnostics(fix)
|
||||||
}
|
}
|
||||||
Commands::Setup => {
|
Commands::Setup => {
|
||||||
commands::setup::run_setup()
|
commands::setup::run_setup()
|
||||||
@@ -143,6 +177,38 @@ fn main() -> Result<()> {
|
|||||||
force,
|
force,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Commands::Board { name, add, remove, listall, id, baud, dir } => {
|
||||||
|
if listall {
|
||||||
|
commands::board::listall_boards(name.as_deref())
|
||||||
|
} else if add {
|
||||||
|
let board_name = name.as_deref().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Board name required.\n\
|
||||||
|
Usage: anvil board --add mega\n\
|
||||||
|
Browse available boards: anvil board --listall"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
commands::board::add_board(
|
||||||
|
board_name,
|
||||||
|
id.as_deref(),
|
||||||
|
baud,
|
||||||
|
dir.as_deref(),
|
||||||
|
)
|
||||||
|
} else if remove {
|
||||||
|
let board_name = name.as_deref().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Board name required.\n\
|
||||||
|
Usage: anvil board --remove mega"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
commands::board::remove_board(
|
||||||
|
board_name,
|
||||||
|
dir.as_deref(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
commands::board::list_boards(dir.as_deref())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use anyhow::{Result, Context, bail};
|
use anyhow::{Result, Context, bail};
|
||||||
@@ -12,6 +13,8 @@ pub struct ProjectConfig {
|
|||||||
pub project: ProjectMeta,
|
pub project: ProjectMeta,
|
||||||
pub build: BuildConfig,
|
pub build: BuildConfig,
|
||||||
pub monitor: MonitorConfig,
|
pub monitor: MonitorConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub boards: HashMap<String, BoardProfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@@ -22,10 +25,15 @@ pub struct ProjectMeta {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct BuildConfig {
|
pub struct BuildConfig {
|
||||||
pub fqbn: String,
|
/// Name of the default board from [boards.*]
|
||||||
|
#[serde(default)]
|
||||||
|
pub default: String,
|
||||||
pub warnings: String,
|
pub warnings: String,
|
||||||
pub include_dirs: Vec<String>,
|
pub include_dirs: Vec<String>,
|
||||||
pub extra_flags: Vec<String>,
|
pub extra_flags: Vec<String>,
|
||||||
|
/// Legacy: FQBN used to live here directly. Now lives in [boards.*].
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fqbn: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@@ -35,16 +43,30 @@ pub struct MonitorConfig {
|
|||||||
pub port: Option<String>,
|
pub port: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A named board with its FQBN and optional baud override.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct BoardProfile {
|
||||||
|
pub fqbn: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub baud: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ProjectConfig {
|
impl ProjectConfig {
|
||||||
/// Create a new project config with sensible defaults.
|
/// Create a new project config with sensible defaults.
|
||||||
pub fn new(name: &str) -> Self {
|
pub fn new(name: &str) -> Self {
|
||||||
|
let mut boards = HashMap::new();
|
||||||
|
boards.insert("uno".to_string(), BoardProfile {
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: None,
|
||||||
|
});
|
||||||
Self {
|
Self {
|
||||||
project: ProjectMeta {
|
project: ProjectMeta {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
anvil_version: ANVIL_VERSION.to_string(),
|
anvil_version: ANVIL_VERSION.to_string(),
|
||||||
},
|
},
|
||||||
build: BuildConfig {
|
build: BuildConfig {
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
default: "uno".to_string(),
|
||||||
|
fqbn: None,
|
||||||
warnings: "more".to_string(),
|
warnings: "more".to_string(),
|
||||||
include_dirs: vec!["lib/hal".to_string(), "lib/app".to_string()],
|
include_dirs: vec!["lib/hal".to_string(), "lib/app".to_string()],
|
||||||
extra_flags: vec!["-Werror".to_string()],
|
extra_flags: vec!["-Werror".to_string()],
|
||||||
@@ -53,6 +75,61 @@ impl ProjectConfig {
|
|||||||
baud: 115200,
|
baud: 115200,
|
||||||
port: None,
|
port: None,
|
||||||
},
|
},
|
||||||
|
boards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new project config with a specific board preset.
|
||||||
|
pub fn new_with_board(name: &str, board_name: &str, fqbn: &str, baud: u32) -> Self {
|
||||||
|
let mut config = Self::new(name);
|
||||||
|
config.build.default = board_name.to_string();
|
||||||
|
config.boards.clear();
|
||||||
|
config.boards.insert(board_name.to_string(), BoardProfile {
|
||||||
|
fqbn: fqbn.to_string(),
|
||||||
|
baud: None,
|
||||||
|
});
|
||||||
|
config.monitor.baud = baud;
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default board's FQBN and baud.
|
||||||
|
pub fn default_board(&self) -> Result<(String, u32)> {
|
||||||
|
self.resolve_board(&self.build.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default board's FQBN.
|
||||||
|
pub fn default_fqbn(&self) -> Result<String> {
|
||||||
|
let (fqbn, _) = self.default_board()?;
|
||||||
|
Ok(fqbn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the FQBN and baud for a named board.
|
||||||
|
/// Returns (fqbn, baud).
|
||||||
|
pub fn resolve_board(&self, board_name: &str) -> Result<(String, u32)> {
|
||||||
|
match self.boards.get(board_name) {
|
||||||
|
Some(profile) => {
|
||||||
|
let baud = profile.baud.unwrap_or(self.monitor.baud);
|
||||||
|
Ok((profile.fqbn.clone(), baud))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let available: Vec<&String> = self.boards.keys().collect();
|
||||||
|
if available.is_empty() {
|
||||||
|
bail!(
|
||||||
|
"No board '{}' found in .anvil.toml.\n \
|
||||||
|
No boards are defined. Add one: anvil board --add {}",
|
||||||
|
board_name, board_name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"No board '{}' found in .anvil.toml.\n \
|
||||||
|
Available: {}\n \
|
||||||
|
Add it: anvil board --add {}",
|
||||||
|
board_name,
|
||||||
|
available.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "),
|
||||||
|
board_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +145,24 @@ impl ProjectConfig {
|
|||||||
}
|
}
|
||||||
let contents = fs::read_to_string(&config_path)
|
let contents = fs::read_to_string(&config_path)
|
||||||
.context(format!("Failed to read {}", config_path.display()))?;
|
.context(format!("Failed to read {}", config_path.display()))?;
|
||||||
let config: ProjectConfig = toml::from_str(&contents)
|
let mut config: ProjectConfig = toml::from_str(&contents)
|
||||||
.context(format!("Failed to parse {}", config_path.display()))?;
|
.context(format!("Failed to parse {}", config_path.display()))?;
|
||||||
|
|
||||||
|
// Migrate old format: fqbn in [build] -> [boards.X] + default
|
||||||
|
if config.build.default.is_empty() {
|
||||||
|
if let Some(legacy_fqbn) = config.build.fqbn.take() {
|
||||||
|
let board_name = crate::board::presets::PRESETS.iter()
|
||||||
|
.find(|p| p.fqbn == legacy_fqbn)
|
||||||
|
.map(|p| p.name.to_string())
|
||||||
|
.unwrap_or_else(|| "default".to_string());
|
||||||
|
config.boards.entry(board_name.clone()).or_insert(BoardProfile {
|
||||||
|
fqbn: legacy_fqbn,
|
||||||
|
baud: None,
|
||||||
|
});
|
||||||
|
config.build.default = board_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +248,11 @@ mod tests {
|
|||||||
fn test_new_config_defaults() {
|
fn test_new_config_defaults() {
|
||||||
let config = ProjectConfig::new("test_project");
|
let config = ProjectConfig::new("test_project");
|
||||||
assert_eq!(config.project.name, "test_project");
|
assert_eq!(config.project.name, "test_project");
|
||||||
assert_eq!(config.build.fqbn, "arduino:avr:uno");
|
assert_eq!(config.build.default, "uno");
|
||||||
assert_eq!(config.monitor.baud, 115200);
|
assert_eq!(config.monitor.baud, 115200);
|
||||||
assert!(config.build.include_dirs.contains(&"lib/hal".to_string()));
|
assert!(config.build.include_dirs.contains(&"lib/hal".to_string()));
|
||||||
|
assert!(config.boards.contains_key("uno"));
|
||||||
|
assert_eq!(config.boards["uno"].fqbn, "arduino:avr:uno");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -168,8 +263,20 @@ mod tests {
|
|||||||
|
|
||||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
assert_eq!(loaded.project.name, "roundtrip");
|
assert_eq!(loaded.project.name, "roundtrip");
|
||||||
assert_eq!(loaded.build.fqbn, config.build.fqbn);
|
assert_eq!(loaded.build.default, "uno");
|
||||||
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
||||||
|
assert!(loaded.boards.contains_key("uno"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_with_board() {
|
||||||
|
let config = ProjectConfig::new_with_board(
|
||||||
|
"test", "mega", "arduino:avr:mega:cpu=atmega2560", 115200
|
||||||
|
);
|
||||||
|
assert_eq!(config.build.default, "mega");
|
||||||
|
assert!(config.boards.contains_key("mega"));
|
||||||
|
assert_eq!(config.boards["mega"].fqbn, "arduino:avr:mega:cpu=atmega2560");
|
||||||
|
assert!(!config.boards.contains_key("uno"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -178,7 +285,6 @@ mod tests {
|
|||||||
let config = ProjectConfig::new("finder");
|
let config = ProjectConfig::new("finder");
|
||||||
config.save(tmp.path()).unwrap();
|
config.save(tmp.path()).unwrap();
|
||||||
|
|
||||||
// Create a subdirectory and search from there
|
|
||||||
let sub = tmp.path().join("sketch").join("deep");
|
let sub = tmp.path().join("sketch").join("deep");
|
||||||
fs::create_dir_all(&sub).unwrap();
|
fs::create_dir_all(&sub).unwrap();
|
||||||
|
|
||||||
@@ -219,4 +325,61 @@ mod tests {
|
|||||||
assert!(flags.contains("-Werror"));
|
assert!(flags.contains("-Werror"));
|
||||||
assert!(flags.contains("-I"));
|
assert!(flags.contains("-I"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_board() {
|
||||||
|
let config = ProjectConfig::new("test");
|
||||||
|
let (fqbn, baud) = config.default_board().unwrap();
|
||||||
|
assert_eq!(fqbn, "arduino:avr:uno");
|
||||||
|
assert_eq!(baud, 115200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_board_named() {
|
||||||
|
let mut config = ProjectConfig::new("test");
|
||||||
|
config.boards.insert("mega".to_string(), BoardProfile {
|
||||||
|
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
||||||
|
baud: Some(9600),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (fqbn, baud) = config.resolve_board("mega").unwrap();
|
||||||
|
assert_eq!(fqbn, "arduino:avr:mega:cpu=atmega2560");
|
||||||
|
assert_eq!(baud, 9600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_board_inherits_baud() {
|
||||||
|
let mut config = ProjectConfig::new("test");
|
||||||
|
config.boards.insert("nano".to_string(), BoardProfile {
|
||||||
|
fqbn: "arduino:avr:nano:cpu=atmega328".to_string(),
|
||||||
|
baud: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let (fqbn, baud) = config.resolve_board("nano").unwrap();
|
||||||
|
assert_eq!(fqbn, "arduino:avr:nano:cpu=atmega328");
|
||||||
|
assert_eq!(baud, 115200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_board_unknown() {
|
||||||
|
let config = ProjectConfig::new("test");
|
||||||
|
assert!(config.resolve_board("esp32").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_board_roundtrip() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let mut config = ProjectConfig::new("multi");
|
||||||
|
config.boards.insert("mega".to_string(), BoardProfile {
|
||||||
|
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
||||||
|
baud: Some(57600),
|
||||||
|
});
|
||||||
|
config.save(tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
assert!(loaded.boards.contains_key("mega"));
|
||||||
|
let mega = &loaded.boards["mega"];
|
||||||
|
assert_eq!(mega.fqbn, "arduino:avr:mega:cpu=atmega2560");
|
||||||
|
assert_eq!(mega.baud, Some(57600));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic")
|
|||||||
pub struct TemplateContext {
|
pub struct TemplateContext {
|
||||||
pub project_name: String,
|
pub project_name: String,
|
||||||
pub anvil_version: String,
|
pub anvil_version: String,
|
||||||
|
pub board_name: String,
|
||||||
pub fqbn: String,
|
pub fqbn: String,
|
||||||
pub baud: u32,
|
pub baud: u32,
|
||||||
}
|
}
|
||||||
@@ -142,6 +143,7 @@ fn substitute_variables(text: &str, context: &TemplateContext) -> String {
|
|||||||
text.replace("{{PROJECT_NAME}}", &context.project_name)
|
text.replace("{{PROJECT_NAME}}", &context.project_name)
|
||||||
.replace("{{ANVIL_VERSION}}", &context.anvil_version)
|
.replace("{{ANVIL_VERSION}}", &context.anvil_version)
|
||||||
.replace("{{ANVIL_VERSION_CURRENT}}", ANVIL_VERSION)
|
.replace("{{ANVIL_VERSION_CURRENT}}", ANVIL_VERSION)
|
||||||
|
.replace("{{BOARD_NAME}}", &context.board_name)
|
||||||
.replace("{{FQBN}}", &context.fqbn)
|
.replace("{{FQBN}}", &context.fqbn)
|
||||||
.replace("{{BAUD}}", &context.baud.to_string())
|
.replace("{{BAUD}}", &context.baud.to_string())
|
||||||
}
|
}
|
||||||
@@ -176,14 +178,15 @@ mod tests {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "my_project".to_string(),
|
project_name: "my_project".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "mega".to_string(),
|
||||||
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
||||||
baud: 9600,
|
baud: 9600,
|
||||||
};
|
};
|
||||||
let input = "Name: {{PROJECT_NAME}}, Version: {{ANVIL_VERSION}}, FQBN: {{FQBN}}, Baud: {{BAUD}}";
|
let input = "Name: {{PROJECT_NAME}}, Board: {{BOARD_NAME}}, FQBN: {{FQBN}}, Baud: {{BAUD}}";
|
||||||
let output = substitute_variables(input, &ctx);
|
let output = substitute_variables(input, &ctx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
output,
|
output,
|
||||||
"Name: my_project, Version: 1.0.0, FQBN: arduino:avr:mega:cpu=atmega2560, Baud: 9600"
|
"Name: my_project, Board: mega, FQBN: arduino:avr:mega:cpu=atmega2560, Baud: 9600"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +202,7 @@ mod tests {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "test_proj".to_string(),
|
project_name: "test_proj".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -231,6 +235,7 @@ mod tests {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "my_sensor".to_string(),
|
project_name: "my_sensor".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,25 @@ name = "{{PROJECT_NAME}}"
|
|||||||
anvil_version = "{{ANVIL_VERSION}}"
|
anvil_version = "{{ANVIL_VERSION}}"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
fqbn = "{{FQBN}}"
|
default = "{{BOARD_NAME}}"
|
||||||
warnings = "more"
|
warnings = "more"
|
||||||
include_dirs = ["lib/hal", "lib/app"]
|
include_dirs = ["lib/hal", "lib/app"]
|
||||||
extra_flags = ["-Werror"]
|
extra_flags = ["-Werror"]
|
||||||
|
|
||||||
[monitor]
|
[monitor]
|
||||||
baud = {{BAUD}}
|
baud = {{BAUD}}
|
||||||
|
|
||||||
|
[boards.{{BOARD_NAME}}]
|
||||||
|
fqbn = "{{FQBN}}"
|
||||||
|
|
||||||
|
# -- Additional boards -----------------------------------------------------
|
||||||
|
# Add more boards here. Use --board NAME with any script:
|
||||||
|
# upload --board mega
|
||||||
|
# build.bat --board nano
|
||||||
|
#
|
||||||
|
# [boards.mega]
|
||||||
|
# fqbn = "arduino:avr:mega:cpu=atmega2560"
|
||||||
|
#
|
||||||
|
# [boards.nano]
|
||||||
|
# fqbn = "arduino:avr:nano:cpu=atmega328"
|
||||||
|
# baud = 9600
|
||||||
|
|||||||
@@ -6,20 +6,21 @@ setlocal enabledelayedexpansion
|
|||||||
:: Reads all settings from .anvil.toml. No Anvil binary required.
|
:: Reads all settings from .anvil.toml. No Anvil binary required.
|
||||||
::
|
::
|
||||||
:: Usage:
|
:: Usage:
|
||||||
:: build.bat Compile (verify only)
|
:: build.bat Compile (verify only)
|
||||||
:: build.bat --clean Delete build cache first
|
:: build.bat --board mega Use a named board
|
||||||
:: build.bat --verbose Show full compiler output
|
:: build.bat --clean Delete build cache first
|
||||||
|
:: build.bat --verbose Show full compiler output
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
set "SCRIPT_DIR=%~dp0"
|
||||||
set "CONFIG=%SCRIPT_DIR%.anvil.toml"
|
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||||
|
set "CONFIG=%SCRIPT_DIR%\.anvil.toml"
|
||||||
|
|
||||||
if not exist "%CONFIG%" (
|
if not exist "%CONFIG%" (
|
||||||
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: -- Parse .anvil.toml ----------------------------------------------------
|
:: -- Parse .anvil.toml (flat keys) ----------------------------------------
|
||||||
:: Read file directly, skip comments and section headers
|
|
||||||
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
||||||
set "_K=%%a"
|
set "_K=%%a"
|
||||||
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
||||||
@@ -30,7 +31,7 @@ for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
|||||||
set "_V=!_V:"=!"
|
set "_V=!_V:"=!"
|
||||||
)
|
)
|
||||||
if "!_K!"=="name" set "SKETCH_NAME=!_V!"
|
if "!_K!"=="name" set "SKETCH_NAME=!_V!"
|
||||||
if "!_K!"=="fqbn" set "FQBN=!_V!"
|
if "!_K!"=="default" set "DEFAULT_BOARD=!_V!"
|
||||||
if "!_K!"=="warnings" set "WARNINGS=!_V!"
|
if "!_K!"=="warnings" set "WARNINGS=!_V!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -40,15 +41,17 @@ if "%SKETCH_NAME%"=="" (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
set "SKETCH_DIR=%SCRIPT_DIR%%SKETCH_NAME%"
|
set "SKETCH_DIR=%SCRIPT_DIR%\%SKETCH_NAME%"
|
||||||
set "BUILD_DIR=%SCRIPT_DIR%.build"
|
set "BUILD_DIR=%SCRIPT_DIR%\.build"
|
||||||
|
|
||||||
:: -- Parse arguments ------------------------------------------------------
|
:: -- Parse arguments ------------------------------------------------------
|
||||||
set "DO_CLEAN=0"
|
set "DO_CLEAN=0"
|
||||||
set "VERBOSE="
|
set "VERBOSE="
|
||||||
|
set "BOARD_NAME="
|
||||||
|
|
||||||
:parse_args
|
:parse_args
|
||||||
if "%~1"=="" goto done_args
|
if "%~1"=="" goto done_args
|
||||||
|
if "%~1"=="--board" set "BOARD_NAME=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args
|
if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args
|
||||||
if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args
|
if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args
|
||||||
if "%~1"=="--help" goto show_help
|
if "%~1"=="--help" goto show_help
|
||||||
@@ -57,12 +60,51 @@ echo FAIL: Unknown option: %~1
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:show_help
|
:show_help
|
||||||
echo Usage: build.bat [--clean] [--verbose]
|
echo Usage: build.bat [--board NAME] [--clean] [--verbose]
|
||||||
echo Compiles the sketch. Settings from .anvil.toml.
|
echo Compiles the sketch. Settings from .anvil.toml.
|
||||||
|
echo --board NAME selects a board from [boards.NAME].
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:done_args
|
:done_args
|
||||||
|
|
||||||
|
:: -- Resolve board --------------------------------------------------------
|
||||||
|
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
|
||||||
|
|
||||||
|
set "BOARD_SECTION=[boards.%BOARD_NAME%]"
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
set "FQBN="
|
||||||
|
for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
|
||||||
|
set "_LINE=%%L"
|
||||||
|
if "!_LINE!"=="!BOARD_SECTION!" (
|
||||||
|
set "IN_SECTION=1"
|
||||||
|
) else if "!IN_SECTION!"=="1" (
|
||||||
|
if "!_LINE:~0,1!"=="[" (
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
) else if not "!_LINE:~0,1!"=="#" (
|
||||||
|
for /f "tokens=1,* delims==" %%a in ("!_LINE!") do (
|
||||||
|
set "_BK=%%a"
|
||||||
|
set "_BK=!_BK: =!"
|
||||||
|
set "_BV=%%b"
|
||||||
|
if defined _BV (
|
||||||
|
set "_BV=!_BV: =!"
|
||||||
|
set "_BV=!_BV:"=!"
|
||||||
|
)
|
||||||
|
if "!_BK!"=="fqbn" set "FQBN=!_BV!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "!FQBN!"=="" (
|
||||||
|
echo FAIL: No board '%BOARD_NAME%' in .anvil.toml.
|
||||||
|
echo Add it: anvil board --add %BOARD_NAME%
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not "%BOARD_NAME%"=="%DEFAULT_BOARD%" (
|
||||||
|
echo ok Using board: %BOARD_NAME% -- %FQBN%
|
||||||
|
)
|
||||||
|
|
||||||
:: -- Preflight ------------------------------------------------------------
|
:: -- Preflight ------------------------------------------------------------
|
||||||
where arduino-cli >nul 2>nul
|
where arduino-cli >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -86,8 +128,8 @@ if "%DO_CLEAN%"=="1" (
|
|||||||
:: -- Build include flags --------------------------------------------------
|
:: -- Build include flags --------------------------------------------------
|
||||||
set "BUILD_FLAGS="
|
set "BUILD_FLAGS="
|
||||||
for %%d in (lib\hal lib\app) do (
|
for %%d in (lib\hal lib\app) do (
|
||||||
if exist "%SCRIPT_DIR%%%d" (
|
if exist "%SCRIPT_DIR%\%%d" (
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%%%d"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
||||||
@@ -100,7 +142,7 @@ echo.
|
|||||||
|
|
||||||
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
||||||
|
|
||||||
arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "build.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%"
|
arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "compiler.cpp.extra_flags=%BUILD_FLAGS%" --build-property "compiler.c.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo FAIL: Compilation failed.
|
echo FAIL: Compilation failed.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./build.sh Compile (verify only)
|
# ./build.sh Compile (verify only)
|
||||||
|
# ./build.sh --board mega Use a named board
|
||||||
# ./build.sh --clean Delete build cache first
|
# ./build.sh --clean Delete build cache first
|
||||||
# ./build.sh --verbose Show full compiler output
|
# ./build.sh --verbose Show full compiler output
|
||||||
#
|
#
|
||||||
@@ -32,27 +33,34 @@ die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; }
|
|||||||
# -- Parse .anvil.toml -----------------------------------------------------
|
# -- Parse .anvil.toml -----------------------------------------------------
|
||||||
[[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR"
|
[[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR"
|
||||||
|
|
||||||
# Extract a simple string value: toml_get "key"
|
|
||||||
# Searches the whole file; for sectioned keys, grep is specific enough
|
|
||||||
# given our small, flat schema.
|
|
||||||
toml_get() {
|
toml_get() {
|
||||||
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' '
|
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract a TOML array as space-separated values: toml_array "key"
|
|
||||||
toml_array() {
|
toml_array() {
|
||||||
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \
|
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \
|
||||||
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toml_section_get() {
|
||||||
|
local section="$1" key="$2"
|
||||||
|
awk -v section="[$section]" -v key="$key" '
|
||||||
|
$0 == section { found=1; next }
|
||||||
|
/^\[/ { found=0 }
|
||||||
|
found && $1 == key && /=/ {
|
||||||
|
sub(/^[^=]*= *"?/, ""); sub(/"? *$/, ""); print; exit
|
||||||
|
}
|
||||||
|
' "$CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
SKETCH_NAME="$(toml_get 'name')"
|
SKETCH_NAME="$(toml_get 'name')"
|
||||||
FQBN="$(toml_get 'fqbn')"
|
DEFAULT_BOARD="$(toml_get 'default')"
|
||||||
WARNINGS="$(toml_get 'warnings')"
|
WARNINGS="$(toml_get 'warnings')"
|
||||||
INCLUDE_DIRS="$(toml_array 'include_dirs')"
|
INCLUDE_DIRS="$(toml_array 'include_dirs')"
|
||||||
EXTRA_FLAGS="$(toml_array 'extra_flags')"
|
EXTRA_FLAGS="$(toml_array 'extra_flags')"
|
||||||
|
|
||||||
[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml"
|
[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml"
|
||||||
[[ -n "$FQBN" ]] || die "Could not read fqbn from .anvil.toml"
|
[[ -n "$DEFAULT_BOARD" ]] || die "Could not read default board from .anvil.toml"
|
||||||
|
|
||||||
SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME"
|
SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME"
|
||||||
BUILD_DIR="$SCRIPT_DIR/.build"
|
BUILD_DIR="$SCRIPT_DIR/.build"
|
||||||
@@ -60,20 +68,35 @@ BUILD_DIR="$SCRIPT_DIR/.build"
|
|||||||
# -- Parse arguments -------------------------------------------------------
|
# -- Parse arguments -------------------------------------------------------
|
||||||
DO_CLEAN=0
|
DO_CLEAN=0
|
||||||
VERBOSE=""
|
VERBOSE=""
|
||||||
|
BOARD_NAME=""
|
||||||
|
|
||||||
for arg in "$@"; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$arg" in
|
case "$1" in
|
||||||
--clean) DO_CLEAN=1 ;;
|
--board) BOARD_NAME="$2"; shift 2 ;;
|
||||||
--verbose) VERBOSE="--verbose" ;;
|
--clean) DO_CLEAN=1; shift ;;
|
||||||
|
--verbose) VERBOSE="--verbose"; shift ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
echo "Usage: ./build.sh [--clean] [--verbose]"
|
echo "Usage: ./build.sh [--board NAME] [--clean] [--verbose]"
|
||||||
echo " Compiles the sketch. Settings from .anvil.toml."
|
echo " Compiles the sketch. Settings from .anvil.toml."
|
||||||
|
echo " --board NAME selects a board from [boards.NAME]."
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*) die "Unknown option: $arg" ;;
|
*) die "Unknown option: $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# -- Resolve board ---------------------------------------------------------
|
||||||
|
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
|
||||||
|
FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")"
|
||||||
|
|
||||||
|
if [[ -z "$FQBN" ]]; then
|
||||||
|
die "No board '$ACTIVE_BOARD' in .anvil.toml.\n Add it: anvil board --add $ACTIVE_BOARD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$BOARD_NAME" ]]; then
|
||||||
|
ok "Using board: $BOARD_NAME ($FQBN)"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- Preflight -------------------------------------------------------------
|
# -- Preflight -------------------------------------------------------------
|
||||||
command -v arduino-cli &>/dev/null \
|
command -v arduino-cli &>/dev/null \
|
||||||
|| die "arduino-cli not found in PATH. Install it first."
|
|| die "arduino-cli not found in PATH. Install it first."
|
||||||
@@ -121,7 +144,8 @@ COMPILE_ARGS=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if [[ -n "$BUILD_FLAGS" ]]; then
|
if [[ -n "$BUILD_FLAGS" ]]; then
|
||||||
COMPILE_ARGS+=(--build-property "build.extra_flags=$BUILD_FLAGS")
|
COMPILE_ARGS+=(--build-property "compiler.cpp.extra_flags=$BUILD_FLAGS")
|
||||||
|
COMPILE_ARGS+=(--build-property "compiler.c.extra_flags=$BUILD_FLAGS")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$VERBOSE" ]]; then
|
if [[ -n "$VERBOSE" ]]; then
|
||||||
|
|||||||
@@ -9,17 +9,19 @@ setlocal enabledelayedexpansion
|
|||||||
:: monitor.bat Open monitor (auto-detect port)
|
:: monitor.bat Open monitor (auto-detect port)
|
||||||
:: monitor.bat -p COM3 Specify port
|
:: monitor.bat -p COM3 Specify port
|
||||||
:: monitor.bat -b 9600 Override baud rate
|
:: monitor.bat -b 9600 Override baud rate
|
||||||
|
:: monitor.bat --board mega Use baud from a named board
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
set "SCRIPT_DIR=%~dp0"
|
||||||
set "CONFIG=%SCRIPT_DIR%.anvil.toml"
|
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||||
set "LOCAL_CONFIG=%SCRIPT_DIR%.anvil.local"
|
set "CONFIG=%SCRIPT_DIR%\.anvil.toml"
|
||||||
|
set "LOCAL_CONFIG=%SCRIPT_DIR%\.anvil.local"
|
||||||
|
|
||||||
if not exist "%CONFIG%" (
|
if not exist "%CONFIG%" (
|
||||||
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: -- Parse .anvil.toml ----------------------------------------------------
|
:: -- Parse .anvil.toml (flat keys) ----------------------------------------
|
||||||
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
||||||
set "_K=%%a"
|
set "_K=%%a"
|
||||||
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
||||||
@@ -29,11 +31,12 @@ for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
|||||||
set "_V=!_V: =!"
|
set "_V=!_V: =!"
|
||||||
set "_V=!_V:"=!"
|
set "_V=!_V:"=!"
|
||||||
)
|
)
|
||||||
|
if "!_K!"=="default" set "DEFAULT_BOARD=!_V!"
|
||||||
if "!_K!"=="baud" set "BAUD=!_V!"
|
if "!_K!"=="baud" set "BAUD=!_V!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
:: -- Parse .anvil.local (machine-specific, not in git) --------------------
|
:: -- Parse .anvil.local ---------------------------------------------------
|
||||||
set "LOCAL_PORT="
|
set "LOCAL_PORT="
|
||||||
set "LOCAL_VID_PID="
|
set "LOCAL_VID_PID="
|
||||||
if exist "%LOCAL_CONFIG%" (
|
if exist "%LOCAL_CONFIG%" (
|
||||||
@@ -56,6 +59,7 @@ if "%BAUD%"=="" set "BAUD=115200"
|
|||||||
|
|
||||||
:: -- Parse arguments ------------------------------------------------------
|
:: -- Parse arguments ------------------------------------------------------
|
||||||
set "PORT="
|
set "PORT="
|
||||||
|
set "BOARD_NAME="
|
||||||
|
|
||||||
:parse_args
|
:parse_args
|
||||||
if "%~1"=="" goto done_args
|
if "%~1"=="" goto done_args
|
||||||
@@ -63,18 +67,54 @@ if "%~1"=="-p" set "PORT=%~2" & shift & shift & goto parse_args
|
|||||||
if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args
|
if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="-b" set "BAUD=%~2" & shift & shift & goto parse_args
|
if "%~1"=="-b" set "BAUD=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="--baud" set "BAUD=%~2" & shift & shift & goto parse_args
|
if "%~1"=="--baud" set "BAUD=%~2" & shift & shift & goto parse_args
|
||||||
|
if "%~1"=="--board" set "BOARD_NAME=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="--help" goto show_help
|
if "%~1"=="--help" goto show_help
|
||||||
if "%~1"=="-h" goto show_help
|
if "%~1"=="-h" goto show_help
|
||||||
echo FAIL: Unknown option: %~1
|
echo FAIL: Unknown option: %~1
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:show_help
|
:show_help
|
||||||
echo Usage: monitor.bat [-p PORT] [-b BAUD]
|
echo Usage: monitor.bat [-p PORT] [-b BAUD] [--board NAME]
|
||||||
echo Opens serial monitor. Baud rate from .anvil.toml.
|
echo Opens serial monitor. Baud rate from .anvil.toml.
|
||||||
|
echo --board NAME selects a board from [boards.NAME].
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:done_args
|
:done_args
|
||||||
|
|
||||||
|
:: -- Resolve board --------------------------------------------------------
|
||||||
|
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
|
||||||
|
|
||||||
|
set "BOARD_SECTION=[boards.%BOARD_NAME%]"
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
set "BOARD_BAUD="
|
||||||
|
for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
|
||||||
|
set "_LINE=%%L"
|
||||||
|
if "!_LINE!"=="!BOARD_SECTION!" (
|
||||||
|
set "IN_SECTION=1"
|
||||||
|
) else if "!IN_SECTION!"=="1" (
|
||||||
|
if "!_LINE:~0,1!"=="[" (
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
) else if not "!_LINE:~0,1!"=="#" (
|
||||||
|
for /f "tokens=1,* delims==" %%a in ("!_LINE!") do (
|
||||||
|
set "_BK=%%a"
|
||||||
|
set "_BK=!_BK: =!"
|
||||||
|
set "_BV=%%b"
|
||||||
|
if defined _BV (
|
||||||
|
set "_BV=!_BV: =!"
|
||||||
|
set "_BV=!_BV:"=!"
|
||||||
|
)
|
||||||
|
if "!_BK!"=="baud" set "BOARD_BAUD=!_BV!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not "!BOARD_BAUD!"=="" set "BAUD=!BOARD_BAUD!"
|
||||||
|
|
||||||
|
if not "%BOARD_NAME%"=="%DEFAULT_BOARD%" (
|
||||||
|
echo ok Using board: %BOARD_NAME% -- baud: !BAUD!
|
||||||
|
)
|
||||||
|
|
||||||
:: -- Preflight ------------------------------------------------------------
|
:: -- Preflight ------------------------------------------------------------
|
||||||
where arduino-cli >nul 2>nul
|
where arduino-cli >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -85,7 +125,7 @@ if errorlevel 1 (
|
|||||||
:: -- Resolve port ---------------------------------------------------------
|
:: -- Resolve port ---------------------------------------------------------
|
||||||
:: Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
:: Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
||||||
if "%PORT%"=="" (
|
if "%PORT%"=="" (
|
||||||
for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%_detect_port.ps1" -VidPid "%LOCAL_VID_PID%" -SavedPort "%LOCAL_PORT%"') do (
|
for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%\_detect_port.ps1" -VidPid "%LOCAL_VID_PID%" -SavedPort "%LOCAL_PORT%"') do (
|
||||||
if "!PORT!"=="" set "PORT=%%p"
|
if "!PORT!"=="" set "PORT=%%p"
|
||||||
)
|
)
|
||||||
if "!PORT!"=="" (
|
if "!PORT!"=="" (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
# ./monitor.sh Auto-detect port
|
# ./monitor.sh Auto-detect port
|
||||||
# ./monitor.sh -p /dev/ttyUSB0 Specify port
|
# ./monitor.sh -p /dev/ttyUSB0 Specify port
|
||||||
# ./monitor.sh -b 9600 Override baud rate
|
# ./monitor.sh -b 9600 Override baud rate
|
||||||
|
# ./monitor.sh --board mega Use baud from a named board
|
||||||
# ./monitor.sh --watch Reconnect after reset/replug
|
# ./monitor.sh --watch Reconnect after reset/replug
|
||||||
#
|
#
|
||||||
# Prerequisites: arduino-cli in PATH
|
# Prerequisites: arduino-cli in PATH
|
||||||
@@ -25,6 +26,7 @@ else
|
|||||||
RED=''; GRN=''; YLW=''; CYN=''; BLD=''; RST=''
|
RED=''; GRN=''; YLW=''; CYN=''; BLD=''; RST=''
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ok() { echo "${GRN}ok${RST} $*"; }
|
||||||
warn() { echo "${YLW}warn${RST} $*"; }
|
warn() { echo "${YLW}warn${RST} $*"; }
|
||||||
die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; }
|
die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; }
|
||||||
|
|
||||||
@@ -35,6 +37,18 @@ toml_get() {
|
|||||||
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' '
|
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toml_section_get() {
|
||||||
|
local section="$1" key="$2"
|
||||||
|
awk -v section="[$section]" -v key="$key" '
|
||||||
|
$0 == section { found=1; next }
|
||||||
|
/^\[/ { found=0 }
|
||||||
|
found && $1 == key && /=/ {
|
||||||
|
sub(/^[^=]*= *"?/, ""); sub(/"? *$/, ""); print; exit
|
||||||
|
}
|
||||||
|
' "$CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_BOARD="$(toml_get 'default')"
|
||||||
BAUD="$(toml_get 'baud')"
|
BAUD="$(toml_get 'baud')"
|
||||||
BAUD="${BAUD:-115200}"
|
BAUD="${BAUD:-115200}"
|
||||||
LOCAL_CONFIG="$SCRIPT_DIR/.anvil.local"
|
LOCAL_CONFIG="$SCRIPT_DIR/.anvil.local"
|
||||||
@@ -48,28 +62,41 @@ fi
|
|||||||
# -- Parse arguments -------------------------------------------------------
|
# -- Parse arguments -------------------------------------------------------
|
||||||
PORT=""
|
PORT=""
|
||||||
DO_WATCH=0
|
DO_WATCH=0
|
||||||
|
BOARD_NAME=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-p|--port) PORT="$2"; shift 2 ;;
|
-p|--port) PORT="$2"; shift 2 ;;
|
||||||
-b|--baud) BAUD="$2"; shift 2 ;;
|
-b|--baud) BAUD="$2"; shift 2 ;;
|
||||||
|
--board) BOARD_NAME="$2"; shift 2 ;;
|
||||||
--watch) DO_WATCH=1; shift ;;
|
--watch) DO_WATCH=1; shift ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
echo "Usage: ./monitor.sh [-p PORT] [-b BAUD] [--watch]"
|
echo "Usage: ./monitor.sh [-p PORT] [-b BAUD] [--board NAME] [--watch]"
|
||||||
echo " Opens serial monitor. Baud rate from .anvil.toml."
|
echo " Opens serial monitor. Baud rate from .anvil.toml."
|
||||||
|
echo " --board NAME selects a board from [boards.NAME]."
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*) die "Unknown option: $1" ;;
|
*) die "Unknown option: $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# -- Resolve board ---------------------------------------------------------
|
||||||
|
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
|
||||||
|
BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")"
|
||||||
|
if [[ -n "$BOARD_BAUD" ]]; then
|
||||||
|
BAUD="$BOARD_BAUD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$BOARD_NAME" ]]; then
|
||||||
|
ok "Using board: $BOARD_NAME (baud: $BAUD)"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- Preflight -------------------------------------------------------------
|
# -- Preflight -------------------------------------------------------------
|
||||||
command -v arduino-cli &>/dev/null \
|
command -v arduino-cli &>/dev/null \
|
||||||
|| die "arduino-cli not found in PATH."
|
|| die "arduino-cli not found in PATH."
|
||||||
|
|
||||||
# -- Auto-detect port ------------------------------------------------------
|
# -- Auto-detect port ------------------------------------------------------
|
||||||
auto_detect() {
|
auto_detect() {
|
||||||
# Prefer ttyUSB/ttyACM (real USB devices) over ttyS (hardware UART)
|
|
||||||
local port
|
local port
|
||||||
port=$(arduino-cli board list 2>/dev/null \
|
port=$(arduino-cli board list 2>/dev/null \
|
||||||
| grep -i "serial" \
|
| grep -i "serial" \
|
||||||
@@ -77,7 +104,6 @@ auto_detect() {
|
|||||||
| grep -E 'ttyUSB|ttyACM|COM' \
|
| grep -E 'ttyUSB|ttyACM|COM' \
|
||||||
| head -1)
|
| head -1)
|
||||||
|
|
||||||
# Fallback: any serial port
|
|
||||||
if [[ -z "$port" ]]; then
|
if [[ -z "$port" ]]; then
|
||||||
port=$(arduino-cli board list 2>/dev/null \
|
port=$(arduino-cli board list 2>/dev/null \
|
||||||
| grep -i "serial" \
|
| grep -i "serial" \
|
||||||
@@ -88,7 +114,6 @@ auto_detect() {
|
|||||||
echo "$port"
|
echo "$port"
|
||||||
}
|
}
|
||||||
|
|
||||||
# resolve_vid_pid VID:PID -- search arduino-cli JSON for matching device
|
|
||||||
resolve_vid_pid() {
|
resolve_vid_pid() {
|
||||||
local target_vid target_pid json
|
local target_vid target_pid json
|
||||||
target_vid="$(echo "$1" | cut -d: -f1 | tr '[:upper:]' '[:lower:]')"
|
target_vid="$(echo "$1" | cut -d: -f1 | tr '[:upper:]' '[:lower:]')"
|
||||||
@@ -113,7 +138,6 @@ except: pass
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [[ -z "$PORT" ]]; then
|
if [[ -z "$PORT" ]]; then
|
||||||
# Try VID:PID resolution first
|
|
||||||
if [[ -n "$LOCAL_VID_PID" ]]; then
|
if [[ -n "$LOCAL_VID_PID" ]]; then
|
||||||
PORT="$(resolve_vid_pid "$LOCAL_VID_PID")"
|
PORT="$(resolve_vid_pid "$LOCAL_VID_PID")"
|
||||||
if [[ -n "$PORT" ]]; then
|
if [[ -n "$PORT" ]]; then
|
||||||
@@ -125,13 +149,11 @@ if [[ -z "$PORT" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fall back to saved port
|
|
||||||
if [[ -z "$PORT" ]] && [[ -n "$LOCAL_PORT" ]]; then
|
if [[ -z "$PORT" ]] && [[ -n "$LOCAL_PORT" ]]; then
|
||||||
PORT="$LOCAL_PORT"
|
PORT="$LOCAL_PORT"
|
||||||
warn "Using port $PORT (from .anvil.local)"
|
warn "Using port $PORT (from .anvil.local)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fall back to auto-detect
|
|
||||||
if [[ -z "$PORT" ]]; then
|
if [[ -z "$PORT" ]]; then
|
||||||
PORT="$(auto_detect)"
|
PORT="$(auto_detect)"
|
||||||
|
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ setlocal enabledelayedexpansion
|
|||||||
:: Usage:
|
:: Usage:
|
||||||
:: upload.bat Auto-detect port, compile + upload
|
:: upload.bat Auto-detect port, compile + upload
|
||||||
:: upload.bat -p COM3 Specify port
|
:: upload.bat -p COM3 Specify port
|
||||||
|
:: upload.bat --board mega Use a named board
|
||||||
:: upload.bat --monitor Open serial monitor after upload
|
:: upload.bat --monitor Open serial monitor after upload
|
||||||
:: upload.bat --clean Clean build cache first
|
:: upload.bat --clean Clean build cache first
|
||||||
:: upload.bat --verbose Full compiler + avrdude output
|
:: upload.bat --verbose Full compiler + avrdude output
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
set "SCRIPT_DIR=%~dp0"
|
||||||
set "CONFIG=%SCRIPT_DIR%.anvil.toml"
|
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||||
set "LOCAL_CONFIG=%SCRIPT_DIR%.anvil.local"
|
set "CONFIG=%SCRIPT_DIR%\.anvil.toml"
|
||||||
|
set "LOCAL_CONFIG=%SCRIPT_DIR%\.anvil.local"
|
||||||
|
|
||||||
if not exist "%CONFIG%" (
|
if not exist "%CONFIG%" (
|
||||||
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: -- Parse .anvil.toml ----------------------------------------------------
|
:: -- Parse .anvil.toml (flat keys) ----------------------------------------
|
||||||
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
||||||
set "_K=%%a"
|
set "_K=%%a"
|
||||||
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
||||||
@@ -32,13 +34,13 @@ for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
|||||||
set "_V=!_V:"=!"
|
set "_V=!_V:"=!"
|
||||||
)
|
)
|
||||||
if "!_K!"=="name" set "SKETCH_NAME=!_V!"
|
if "!_K!"=="name" set "SKETCH_NAME=!_V!"
|
||||||
if "!_K!"=="fqbn" set "FQBN=!_V!"
|
if "!_K!"=="default" set "DEFAULT_BOARD=!_V!"
|
||||||
if "!_K!"=="warnings" set "WARNINGS=!_V!"
|
if "!_K!"=="warnings" set "WARNINGS=!_V!"
|
||||||
if "!_K!"=="baud" set "BAUD=!_V!"
|
if "!_K!"=="baud" set "BAUD=!_V!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
:: -- Parse .anvil.local (machine-specific, not in git) --------------------
|
:: -- Parse .anvil.local ---------------------------------------------------
|
||||||
set "LOCAL_PORT="
|
set "LOCAL_PORT="
|
||||||
set "LOCAL_VID_PID="
|
set "LOCAL_VID_PID="
|
||||||
if exist "%LOCAL_CONFIG%" (
|
if exist "%LOCAL_CONFIG%" (
|
||||||
@@ -63,19 +65,21 @@ if "%SKETCH_NAME%"=="" (
|
|||||||
)
|
)
|
||||||
if "%BAUD%"=="" set "BAUD=115200"
|
if "%BAUD%"=="" set "BAUD=115200"
|
||||||
|
|
||||||
set "SKETCH_DIR=%SCRIPT_DIR%%SKETCH_NAME%"
|
set "SKETCH_DIR=%SCRIPT_DIR%\%SKETCH_NAME%"
|
||||||
set "BUILD_DIR=%SCRIPT_DIR%.build"
|
set "BUILD_DIR=%SCRIPT_DIR%\.build"
|
||||||
|
|
||||||
:: -- Parse arguments ------------------------------------------------------
|
:: -- Parse arguments ------------------------------------------------------
|
||||||
set "PORT="
|
set "PORT="
|
||||||
set "DO_MONITOR=0"
|
set "DO_MONITOR=0"
|
||||||
set "DO_CLEAN=0"
|
set "DO_CLEAN=0"
|
||||||
set "VERBOSE="
|
set "VERBOSE="
|
||||||
|
set "BOARD_NAME="
|
||||||
|
|
||||||
:parse_args
|
:parse_args
|
||||||
if "%~1"=="" goto done_args
|
if "%~1"=="" goto done_args
|
||||||
if "%~1"=="-p" set "PORT=%~2" & shift & shift & goto parse_args
|
if "%~1"=="-p" set "PORT=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args
|
if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args
|
||||||
|
if "%~1"=="--board" set "BOARD_NAME=%~2" & shift & shift & goto parse_args
|
||||||
if "%~1"=="--monitor" set "DO_MONITOR=1" & shift & goto parse_args
|
if "%~1"=="--monitor" set "DO_MONITOR=1" & shift & goto parse_args
|
||||||
if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args
|
if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args
|
||||||
if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args
|
if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args
|
||||||
@@ -85,12 +89,54 @@ echo FAIL: Unknown option: %~1
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:show_help
|
:show_help
|
||||||
echo Usage: upload.bat [-p PORT] [--monitor] [--clean] [--verbose]
|
echo Usage: upload.bat [-p PORT] [--board NAME] [--monitor] [--clean] [--verbose]
|
||||||
echo Compiles and uploads the sketch. Settings from .anvil.toml.
|
echo Compiles and uploads the sketch. Settings from .anvil.toml.
|
||||||
|
echo --board NAME selects a board from [boards.NAME].
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:done_args
|
:done_args
|
||||||
|
|
||||||
|
:: -- Resolve board --------------------------------------------------------
|
||||||
|
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
|
||||||
|
|
||||||
|
set "BOARD_SECTION=[boards.%BOARD_NAME%]"
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
set "FQBN="
|
||||||
|
set "BOARD_BAUD="
|
||||||
|
for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
|
||||||
|
set "_LINE=%%L"
|
||||||
|
if "!_LINE!"=="!BOARD_SECTION!" (
|
||||||
|
set "IN_SECTION=1"
|
||||||
|
) else if "!IN_SECTION!"=="1" (
|
||||||
|
if "!_LINE:~0,1!"=="[" (
|
||||||
|
set "IN_SECTION=0"
|
||||||
|
) else if not "!_LINE:~0,1!"=="#" (
|
||||||
|
for /f "tokens=1,* delims==" %%a in ("!_LINE!") do (
|
||||||
|
set "_BK=%%a"
|
||||||
|
set "_BK=!_BK: =!"
|
||||||
|
set "_BV=%%b"
|
||||||
|
if defined _BV (
|
||||||
|
set "_BV=!_BV: =!"
|
||||||
|
set "_BV=!_BV:"=!"
|
||||||
|
)
|
||||||
|
if "!_BK!"=="fqbn" set "FQBN=!_BV!"
|
||||||
|
if "!_BK!"=="baud" set "BOARD_BAUD=!_BV!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "!FQBN!"=="" (
|
||||||
|
echo FAIL: No board '%BOARD_NAME%' in .anvil.toml.
|
||||||
|
echo Add it: anvil board --add %BOARD_NAME%
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
if not "!BOARD_BAUD!"=="" set "BAUD=!BOARD_BAUD!"
|
||||||
|
|
||||||
|
if not "%BOARD_NAME%"=="%DEFAULT_BOARD%" (
|
||||||
|
echo ok Using board: %BOARD_NAME% -- !FQBN!
|
||||||
|
)
|
||||||
|
|
||||||
:: -- Preflight ------------------------------------------------------------
|
:: -- Preflight ------------------------------------------------------------
|
||||||
where arduino-cli >nul 2>nul
|
where arduino-cli >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -101,7 +147,7 @@ if errorlevel 1 (
|
|||||||
:: -- Resolve port ---------------------------------------------------------
|
:: -- Resolve port ---------------------------------------------------------
|
||||||
:: Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
:: Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
||||||
if "%PORT%"=="" (
|
if "%PORT%"=="" (
|
||||||
for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%_detect_port.ps1" -VidPid "%LOCAL_VID_PID%" -SavedPort "%LOCAL_PORT%"') do (
|
for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%\_detect_port.ps1" -VidPid "%LOCAL_VID_PID%" -SavedPort "%LOCAL_PORT%"') do (
|
||||||
if "!PORT!"=="" set "PORT=%%p"
|
if "!PORT!"=="" set "PORT=%%p"
|
||||||
)
|
)
|
||||||
if "!PORT!"=="" (
|
if "!PORT!"=="" (
|
||||||
@@ -131,8 +177,8 @@ if "%DO_CLEAN%"=="1" (
|
|||||||
:: -- Build include flags --------------------------------------------------
|
:: -- Build include flags --------------------------------------------------
|
||||||
set "BUILD_FLAGS="
|
set "BUILD_FLAGS="
|
||||||
for %%d in (lib\hal lib\app) do (
|
for %%d in (lib\hal lib\app) do (
|
||||||
if exist "%SCRIPT_DIR%%%d" (
|
if exist "%SCRIPT_DIR%\%%d" (
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%%%d"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
||||||
@@ -141,7 +187,7 @@ set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
|||||||
echo Compiling %SKETCH_NAME%...
|
echo Compiling %SKETCH_NAME%...
|
||||||
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
||||||
|
|
||||||
arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "build.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%"
|
arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "compiler.cpp.extra_flags=%BUILD_FLAGS%" --build-property "compiler.c.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo FAIL: Compilation failed.
|
echo FAIL: Compilation failed.
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# ./upload.sh Auto-detect port, compile + upload
|
# ./upload.sh Auto-detect port, compile + upload
|
||||||
# ./upload.sh -p /dev/ttyUSB0 Specify port
|
# ./upload.sh -p /dev/ttyUSB0 Specify port
|
||||||
|
# ./upload.sh --board mega Use a named board
|
||||||
# ./upload.sh --monitor Open serial monitor after upload
|
# ./upload.sh --monitor Open serial monitor after upload
|
||||||
# ./upload.sh --clean Clean build cache first
|
# ./upload.sh --clean Clean build cache first
|
||||||
# ./upload.sh --verbose Full compiler + avrdude output
|
# ./upload.sh --verbose Full compiler + avrdude output
|
||||||
@@ -42,15 +43,26 @@ toml_array() {
|
|||||||
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toml_section_get() {
|
||||||
|
local section="$1" key="$2"
|
||||||
|
awk -v section="[$section]" -v key="$key" '
|
||||||
|
$0 == section { found=1; next }
|
||||||
|
/^\[/ { found=0 }
|
||||||
|
found && $1 == key && /=/ {
|
||||||
|
sub(/^[^=]*= *"?/, ""); sub(/"? *$/, ""); print; exit
|
||||||
|
}
|
||||||
|
' "$CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
SKETCH_NAME="$(toml_get 'name')"
|
SKETCH_NAME="$(toml_get 'name')"
|
||||||
FQBN="$(toml_get 'fqbn')"
|
DEFAULT_BOARD="$(toml_get 'default')"
|
||||||
WARNINGS="$(toml_get 'warnings')"
|
WARNINGS="$(toml_get 'warnings')"
|
||||||
INCLUDE_DIRS="$(toml_array 'include_dirs')"
|
INCLUDE_DIRS="$(toml_array 'include_dirs')"
|
||||||
EXTRA_FLAGS="$(toml_array 'extra_flags')"
|
EXTRA_FLAGS="$(toml_array 'extra_flags')"
|
||||||
BAUD="$(toml_get 'baud')"
|
BAUD="$(toml_get 'baud')"
|
||||||
|
|
||||||
[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml"
|
[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml"
|
||||||
[[ -n "$FQBN" ]] || die "Could not read fqbn from .anvil.toml"
|
[[ -n "$DEFAULT_BOARD" ]] || die "Could not read default board from .anvil.toml"
|
||||||
|
|
||||||
BAUD="${BAUD:-115200}"
|
BAUD="${BAUD:-115200}"
|
||||||
LOCAL_CONFIG="$SCRIPT_DIR/.anvil.local"
|
LOCAL_CONFIG="$SCRIPT_DIR/.anvil.local"
|
||||||
@@ -68,22 +80,42 @@ PORT=""
|
|||||||
DO_MONITOR=0
|
DO_MONITOR=0
|
||||||
DO_CLEAN=0
|
DO_CLEAN=0
|
||||||
VERBOSE=""
|
VERBOSE=""
|
||||||
|
BOARD_NAME=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-p|--port) PORT="$2"; shift 2 ;;
|
-p|--port) PORT="$2"; shift 2 ;;
|
||||||
|
--board) BOARD_NAME="$2"; shift 2 ;;
|
||||||
--monitor) DO_MONITOR=1; shift ;;
|
--monitor) DO_MONITOR=1; shift ;;
|
||||||
--clean) DO_CLEAN=1; shift ;;
|
--clean) DO_CLEAN=1; shift ;;
|
||||||
--verbose) VERBOSE="--verbose"; shift ;;
|
--verbose) VERBOSE="--verbose"; shift ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
echo "Usage: ./upload.sh [-p PORT] [--monitor] [--clean] [--verbose]"
|
echo "Usage: ./upload.sh [-p PORT] [--board NAME] [--monitor] [--clean] [--verbose]"
|
||||||
echo " Compiles and uploads the sketch. Settings from .anvil.toml."
|
echo " Compiles and uploads the sketch. Settings from .anvil.toml."
|
||||||
|
echo " --board NAME selects a board from [boards.NAME]."
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*) die "Unknown option: $1" ;;
|
*) die "Unknown option: $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# -- Resolve board ---------------------------------------------------------
|
||||||
|
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
|
||||||
|
FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")"
|
||||||
|
|
||||||
|
if [[ -z "$FQBN" ]]; then
|
||||||
|
die "No board '$ACTIVE_BOARD' in .anvil.toml.\n Add it: anvil board --add $ACTIVE_BOARD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")"
|
||||||
|
if [[ -n "$BOARD_BAUD" ]]; then
|
||||||
|
BAUD="$BOARD_BAUD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$BOARD_NAME" ]]; then
|
||||||
|
ok "Using board: $BOARD_NAME ($FQBN)"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- Preflight -------------------------------------------------------------
|
# -- Preflight -------------------------------------------------------------
|
||||||
command -v arduino-cli &>/dev/null \
|
command -v arduino-cli &>/dev/null \
|
||||||
|| die "arduino-cli not found in PATH."
|
|| die "arduino-cli not found in PATH."
|
||||||
@@ -94,13 +126,11 @@ command -v arduino-cli &>/dev/null \
|
|||||||
# -- Resolve port ----------------------------------------------------------
|
# -- Resolve port ----------------------------------------------------------
|
||||||
# Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
# Priority: -p flag > VID:PID resolve > saved port > auto-detect
|
||||||
|
|
||||||
# resolve_vid_pid VID:PID -- search arduino-cli JSON for matching device
|
|
||||||
resolve_vid_pid() {
|
resolve_vid_pid() {
|
||||||
local target_vid target_pid json
|
local target_vid target_pid json
|
||||||
target_vid="$(echo "$1" | cut -d: -f1 | tr '[:upper:]' '[:lower:]')"
|
target_vid="$(echo "$1" | cut -d: -f1 | tr '[:upper:]' '[:lower:]')"
|
||||||
target_pid="$(echo "$1" | cut -d: -f2 | tr '[:upper:]' '[:lower:]')"
|
target_pid="$(echo "$1" | cut -d: -f2 | tr '[:upper:]' '[:lower:]')"
|
||||||
json="$(arduino-cli board list --format json 2>/dev/null)" || return
|
json="$(arduino-cli board list --format json 2>/dev/null)" || return
|
||||||
# Walk through JSON looking for matching vid/pid on serial ports
|
|
||||||
echo "$json" | python3 -c "
|
echo "$json" | python3 -c "
|
||||||
import sys, json
|
import sys, json
|
||||||
try:
|
try:
|
||||||
@@ -120,7 +150,6 @@ except: pass
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [[ -z "$PORT" ]]; then
|
if [[ -z "$PORT" ]]; then
|
||||||
# Try VID:PID resolution first
|
|
||||||
if [[ -n "$LOCAL_VID_PID" ]]; then
|
if [[ -n "$LOCAL_VID_PID" ]]; then
|
||||||
PORT="$(resolve_vid_pid "$LOCAL_VID_PID")"
|
PORT="$(resolve_vid_pid "$LOCAL_VID_PID")"
|
||||||
if [[ -n "$PORT" ]]; then
|
if [[ -n "$PORT" ]]; then
|
||||||
@@ -132,13 +161,11 @@ if [[ -z "$PORT" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fall back to saved port
|
|
||||||
if [[ -z "$PORT" ]] && [[ -n "$LOCAL_PORT" ]]; then
|
if [[ -z "$PORT" ]] && [[ -n "$LOCAL_PORT" ]]; then
|
||||||
PORT="$LOCAL_PORT"
|
PORT="$LOCAL_PORT"
|
||||||
warn "Using port $PORT (from .anvil.local)"
|
warn "Using port $PORT (from .anvil.local)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fall back to auto-detect
|
|
||||||
if [[ -z "$PORT" ]]; then
|
if [[ -z "$PORT" ]]; then
|
||||||
PORT=$(arduino-cli board list 2>/dev/null \
|
PORT=$(arduino-cli board list 2>/dev/null \
|
||||||
| grep -i "serial" \
|
| grep -i "serial" \
|
||||||
@@ -191,7 +218,8 @@ COMPILE_ARGS=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if [[ -n "$BUILD_FLAGS" ]]; then
|
if [[ -n "$BUILD_FLAGS" ]]; then
|
||||||
COMPILE_ARGS+=(--build-property "build.extra_flags=$BUILD_FLAGS")
|
COMPILE_ARGS+=(--build-property "compiler.cpp.extra_flags=$BUILD_FLAGS")
|
||||||
|
COMPILE_ARGS+=(--build-property "compiler.c.extra_flags=$BUILD_FLAGS")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -n "$VERBOSE" ]] && COMPILE_ARGS+=("$VERBOSE")
|
[[ -n "$VERBOSE" ]] && COMPILE_ARGS+=("$VERBOSE")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ fn test_basic_template_extracts_all_expected_files() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "test_proj".to_string(),
|
project_name: "test_proj".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -29,6 +30,7 @@ fn test_template_creates_sketch_directory() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "blink".to_string(),
|
project_name: "blink".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -59,6 +61,7 @@ fn test_template_creates_hal_files() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "sensor".to_string(),
|
project_name: "sensor".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -92,6 +95,7 @@ fn test_template_creates_app_header() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "my_sensor".to_string(),
|
project_name: "my_sensor".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -118,6 +122,7 @@ fn test_template_creates_test_infrastructure() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "blink".to_string(),
|
project_name: "blink".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -156,6 +161,7 @@ fn test_template_test_file_references_correct_app() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "motor_ctrl".to_string(),
|
project_name: "motor_ctrl".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -177,6 +183,7 @@ fn test_template_cmake_references_correct_project() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "my_bot".to_string(),
|
project_name: "my_bot".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -198,6 +205,7 @@ fn test_template_creates_dot_files() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "blink".to_string(),
|
project_name: "blink".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -228,6 +236,7 @@ fn test_template_creates_readme() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "blink".to_string(),
|
project_name: "blink".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -251,6 +260,7 @@ fn test_template_creates_valid_config() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "blink".to_string(),
|
project_name: "blink".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -260,7 +270,7 @@ fn test_template_creates_valid_config() {
|
|||||||
// Should be loadable by ProjectConfig
|
// Should be loadable by ProjectConfig
|
||||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
assert_eq!(config.project.name, "blink");
|
assert_eq!(config.project.name, "blink");
|
||||||
assert_eq!(config.build.fqbn, "arduino:avr:uno");
|
assert_eq!(config.build.default, "uno");
|
||||||
assert_eq!(config.monitor.baud, 115200);
|
assert_eq!(config.monitor.baud, 115200);
|
||||||
assert!(config.build.extra_flags.contains(&"-Werror".to_string()));
|
assert!(config.build.extra_flags.contains(&"-Werror".to_string()));
|
||||||
}
|
}
|
||||||
@@ -273,7 +283,7 @@ fn test_config_roundtrip() {
|
|||||||
|
|
||||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
assert_eq!(loaded.project.name, "roundtrip_test");
|
assert_eq!(loaded.project.name, "roundtrip_test");
|
||||||
assert_eq!(loaded.build.fqbn, config.build.fqbn);
|
assert_eq!(loaded.build.default, config.build.default);
|
||||||
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
||||||
assert_eq!(loaded.build.include_dirs, config.build.include_dirs);
|
assert_eq!(loaded.build.include_dirs, config.build.include_dirs);
|
||||||
}
|
}
|
||||||
@@ -332,6 +342,7 @@ fn test_full_project_structure() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "full_test".to_string(),
|
project_name: "full_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -383,6 +394,7 @@ fn test_no_unicode_in_template_output() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "ascii_test".to_string(),
|
project_name: "ascii_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -432,6 +444,7 @@ fn test_unknown_template_fails() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "test".to_string(),
|
project_name: "test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -457,6 +470,7 @@ fn test_template_creates_self_contained_scripts() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "standalone".to_string(),
|
project_name: "standalone".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -481,6 +495,7 @@ fn test_build_sh_reads_anvil_toml() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "toml_reader".to_string(),
|
project_name: "toml_reader".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -508,6 +523,7 @@ fn test_upload_sh_reads_anvil_toml() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "uploader".to_string(),
|
project_name: "uploader".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -543,6 +559,7 @@ fn test_monitor_sh_reads_anvil_toml() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "serial_mon".to_string(),
|
project_name: "serial_mon".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -570,6 +587,7 @@ fn test_scripts_have_shebangs() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "shebangs".to_string(),
|
project_name: "shebangs".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -594,6 +612,7 @@ fn test_scripts_no_anvil_binary_dependency() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "no_anvil_dep".to_string(),
|
project_name: "no_anvil_dep".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -643,6 +662,7 @@ fn test_gitignore_excludes_build_cache() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "gitcheck".to_string(),
|
project_name: "gitcheck".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -670,6 +690,7 @@ fn test_readme_documents_self_contained_workflow() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "docs_check".to_string(),
|
project_name: "docs_check".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -705,6 +726,7 @@ fn test_scripts_tolerate_missing_toml_keys() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "grep_safe".to_string(),
|
project_name: "grep_safe".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -742,6 +764,7 @@ fn test_bat_scripts_no_unescaped_parens_in_echo() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "parens_test".to_string(),
|
project_name: "parens_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -803,6 +826,7 @@ fn test_scripts_read_anvil_local_for_port() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "local_test".to_string(),
|
project_name: "local_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -825,6 +849,7 @@ fn test_anvil_toml_template_has_no_port() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "no_port".to_string(),
|
project_name: "no_port".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -856,6 +881,7 @@ fn test_bat_scripts_call_detect_port_ps1() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "ps1_test".to_string(),
|
project_name: "ps1_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -878,6 +904,7 @@ fn test_detect_port_ps1_is_valid() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "ps1_valid".to_string(),
|
project_name: "ps1_valid".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -910,6 +937,7 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "fresh_proj".to_string(),
|
project_name: "fresh_proj".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -940,6 +968,7 @@ fn test_refresh_detects_modified_script() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "mod_proj".to_string(),
|
project_name: "mod_proj".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -1009,6 +1038,7 @@ fn test_scripts_read_vid_pid_from_anvil_local() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "vidpid_test".to_string(),
|
project_name: "vidpid_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
fqbn: "arduino:avr:uno".to_string(),
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -1035,6 +1065,7 @@ fn test_board_preset_fqbn_in_config() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "mega_test".to_string(),
|
project_name: "mega_test".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "mega".to_string(),
|
||||||
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
|
||||||
baud: 115200,
|
baud: 115200,
|
||||||
};
|
};
|
||||||
@@ -1042,7 +1073,7 @@ fn test_board_preset_fqbn_in_config() {
|
|||||||
|
|
||||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.build.fqbn, "arduino:avr:mega:cpu=atmega2560",
|
config.build.default, "mega",
|
||||||
".anvil.toml should contain mega FQBN"
|
".anvil.toml should contain mega FQBN"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1054,12 +1085,113 @@ fn test_board_preset_custom_fqbn_in_config() {
|
|||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "custom_board".to_string(),
|
project_name: "custom_board".to_string(),
|
||||||
anvil_version: "1.0.0".to_string(),
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "esp".to_string(),
|
||||||
fqbn: "esp32:esp32:esp32".to_string(),
|
fqbn: "esp32:esp32:esp32".to_string(),
|
||||||
baud: 9600,
|
baud: 9600,
|
||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
assert_eq!(config.build.fqbn, "esp32:esp32:esp32");
|
assert_eq!(config.build.default, "esp");
|
||||||
assert_eq!(config.monitor.baud, 9600);
|
assert_eq!(config.monitor.baud, 9600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Multi-board profile tests
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scripts_accept_board_flag() {
|
||||||
|
// All build/upload/monitor scripts should accept --board
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: "multi_test".to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
let scripts = vec![
|
||||||
|
"build.sh", "build.bat",
|
||||||
|
"upload.sh", "upload.bat",
|
||||||
|
"monitor.sh", "monitor.bat",
|
||||||
|
];
|
||||||
|
|
||||||
|
for script in &scripts {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("--board"),
|
||||||
|
"{} should accept --board flag",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sh_scripts_have_toml_section_get() {
|
||||||
|
// Shell scripts need section-aware TOML parsing for board profiles
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: "section_test".to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
for script in &["build.sh", "upload.sh", "monitor.sh"] {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("toml_section_get"),
|
||||||
|
"{} should have toml_section_get function for board profiles",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bat_scripts_have_section_parser() {
|
||||||
|
// Batch scripts need section-aware TOML parsing for board profiles
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: "bat_section".to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
for bat in &["build.bat", "upload.bat", "monitor.bat"] {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("BOARD_SECTION") || content.contains("IN_SECTION"),
|
||||||
|
"{} should have section parser for board profiles",
|
||||||
|
bat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_toml_template_has_board_profile_comments() {
|
||||||
|
// The generated .anvil.toml should include commented examples
|
||||||
|
// showing how to add board profiles
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: "comment_test".to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("[boards.mega]") || content.contains("boards.mega"),
|
||||||
|
".anvil.toml should show board profile examples in comments"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user