Supporting multiple boards
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use anyhow::{Result, Context, bail};
|
||||
@@ -12,6 +13,8 @@ pub struct ProjectConfig {
|
||||
pub project: ProjectMeta,
|
||||
pub build: BuildConfig,
|
||||
pub monitor: MonitorConfig,
|
||||
#[serde(default)]
|
||||
pub boards: HashMap<String, BoardProfile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -22,10 +25,15 @@ pub struct ProjectMeta {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BuildConfig {
|
||||
pub fqbn: String,
|
||||
/// Name of the default board from [boards.*]
|
||||
#[serde(default)]
|
||||
pub default: String,
|
||||
pub warnings: String,
|
||||
pub include_dirs: 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)]
|
||||
@@ -35,16 +43,30 @@ pub struct MonitorConfig {
|
||||
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 {
|
||||
/// Create a new project config with sensible defaults.
|
||||
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 {
|
||||
project: ProjectMeta {
|
||||
name: name.to_string(),
|
||||
anvil_version: ANVIL_VERSION.to_string(),
|
||||
},
|
||||
build: BuildConfig {
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
default: "uno".to_string(),
|
||||
fqbn: None,
|
||||
warnings: "more".to_string(),
|
||||
include_dirs: vec!["lib/hal".to_string(), "lib/app".to_string()],
|
||||
extra_flags: vec!["-Werror".to_string()],
|
||||
@@ -53,6 +75,61 @@ impl ProjectConfig {
|
||||
baud: 115200,
|
||||
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)
|
||||
.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()))?;
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -155,9 +248,11 @@ mod tests {
|
||||
fn test_new_config_defaults() {
|
||||
let config = ProjectConfig::new("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!(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]
|
||||
@@ -168,8 +263,20 @@ mod tests {
|
||||
|
||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||
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!(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]
|
||||
@@ -178,7 +285,6 @@ mod tests {
|
||||
let config = ProjectConfig::new("finder");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Create a subdirectory and search from there
|
||||
let sub = tmp.path().join("sketch").join("deep");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
|
||||
@@ -219,4 +325,61 @@ mod tests {
|
||||
assert!(flags.contains("-Werror"));
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user