Included pin subcommand with auditing and assignment

This commit is contained in:
Eric Ratliff
2026-02-19 16:58:41 -06:00
parent 9bda9123ea
commit 68e618feef
7 changed files with 2111 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
pub mod presets; pub mod presets;
pub mod pinmap;
/// Information about a detected serial port. /// Information about a detected serial port.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

618
src/board/pinmap.rs Normal file
View File

@@ -0,0 +1,618 @@
/// Pin maps for supported Arduino boards.
///
/// Each board has a static pin map describing:
/// - Available pins and their capabilities
/// - Hardware bus groups (SPI, I2C, UART)
/// - Supported capability categories for filtering
// -- Capability names (used as filter keys) -------------------------------
/// All recognized pin capabilities. Displayed by `anvil pin --capabilities`.
pub const ALL_CAPABILITIES: &[CapabilityInfo] = &[
CapabilityInfo { name: "digital", description: "Digital I/O (HIGH/LOW)" },
CapabilityInfo { name: "analog", description: "Analog input (ADC)" },
CapabilityInfo { name: "pwm", description: "PWM output (analogWrite)" },
CapabilityInfo { name: "interrupt", description: "External interrupt (attachInterrupt)" },
CapabilityInfo { name: "spi", description: "SPI bus pins" },
CapabilityInfo { name: "i2c", description: "I2C / TWI bus pins" },
CapabilityInfo { name: "uart", description: "UART / Serial pins" },
CapabilityInfo { name: "led", description: "Built-in LED" },
];
/// All recognized pin modes for assignments.
pub const ALL_MODES: &[&str] = &[
"input", "output", "input_pullup", "pwm", "analog",
];
// -- Data types -----------------------------------------------------------
#[derive(Debug, Clone)]
pub struct CapabilityInfo {
pub name: &'static str,
pub description: &'static str,
}
#[derive(Debug, Clone)]
pub struct PinInfo {
/// Arduino digital pin number
pub number: u8,
/// Alternate names (e.g. "A0", "LED_BUILTIN", "SDA")
pub aliases: &'static [&'static str],
/// Capabilities this pin supports
pub capabilities: &'static [&'static str],
}
#[derive(Debug, Clone)]
pub struct BusGroup {
/// Bus name: "spi", "i2c", "uart", "uart1", etc.
pub name: &'static str,
/// Human description
pub description: &'static str,
/// Fixed pins: (role, pin_number)
pub fixed_pins: &'static [(&'static str, u8)],
/// Roles the user must select a pin for (e.g. "cs" for SPI)
pub user_selectable: &'static [&'static str],
}
#[derive(Debug, Clone)]
pub struct BoardPinMap {
/// Preset board name (matches presets.rs)
pub board: &'static str,
/// Total digital pin count
pub total_digital: u8,
/// Total analog-capable pin count
pub total_analog: u8,
/// All pins
pub pins: &'static [PinInfo],
/// Hardware bus groups
pub groups: &'static [BusGroup],
}
// -- Pin maps -------------------------------------------------------------
pub static PINMAPS: &[&BoardPinMap] = &[
&PIN_MAP_UNO,
&PIN_MAP_MEGA,
&PIN_MAP_NANO,
&PIN_MAP_LEONARDO,
&PIN_MAP_MICRO,
];
// Arduino Uno (ATmega328P)
static PIN_MAP_UNO: BoardPinMap = BoardPinMap {
board: "uno",
total_digital: 14,
total_analog: 6,
pins: &[
PinInfo { number: 0, aliases: &["RX"], capabilities: &["digital", "uart"] },
PinInfo { number: 1, aliases: &["TX"], capabilities: &["digital", "uart"] },
PinInfo { number: 2, aliases: &["INT0"], capabilities: &["digital", "interrupt"] },
PinInfo { number: 3, aliases: &["INT1"], capabilities: &["digital", "pwm", "interrupt"] },
PinInfo { number: 4, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 5, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 6, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 7, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 8, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 9, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 10, aliases: &["SPI_SS"], capabilities: &["digital", "pwm", "spi"] },
PinInfo { number: 11, aliases: &["SPI_MOSI"], capabilities: &["digital", "pwm", "spi"] },
PinInfo { number: 12, aliases: &["SPI_MISO"], capabilities: &["digital", "spi"] },
PinInfo { number: 13, aliases: &["SPI_SCK", "LED_BUILTIN"], capabilities: &["digital", "spi", "led"] },
PinInfo { number: 14, aliases: &["A0"], capabilities: &["digital", "analog"] },
PinInfo { number: 15, aliases: &["A1"], capabilities: &["digital", "analog"] },
PinInfo { number: 16, aliases: &["A2"], capabilities: &["digital", "analog"] },
PinInfo { number: 17, aliases: &["A3"], capabilities: &["digital", "analog"] },
PinInfo { number: 18, aliases: &["A4", "SDA"], capabilities: &["digital", "analog", "i2c"] },
PinInfo { number: 19, aliases: &["A5", "SCL"], capabilities: &["digital", "analog", "i2c"] },
],
groups: &[
BusGroup {
name: "spi",
description: "SPI bus (hardware)",
fixed_pins: &[("sck", 13), ("mosi", 11), ("miso", 12)],
user_selectable: &["cs"],
},
BusGroup {
name: "i2c",
description: "I2C / TWI bus",
fixed_pins: &[("sda", 18), ("scl", 19)],
user_selectable: &[],
},
BusGroup {
name: "uart",
description: "Hardware serial (Serial)",
fixed_pins: &[("rx", 0), ("tx", 1)],
user_selectable: &[],
},
],
};
// Arduino Mega 2560
static PIN_MAP_MEGA: BoardPinMap = BoardPinMap {
board: "mega",
total_digital: 54,
total_analog: 16,
pins: &[
PinInfo { number: 0, aliases: &["RX0"], capabilities: &["digital", "uart"] },
PinInfo { number: 1, aliases: &["TX0"], capabilities: &["digital", "uart"] },
PinInfo { number: 2, aliases: &["INT0"], capabilities: &["digital", "pwm", "interrupt"] },
PinInfo { number: 3, aliases: &["INT1"], capabilities: &["digital", "pwm", "interrupt"] },
PinInfo { number: 4, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 5, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 6, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 7, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 8, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 9, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 10, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 11, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 12, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 13, aliases: &["LED_BUILTIN"], capabilities: &["digital", "pwm", "led"] },
PinInfo { number: 14, aliases: &["TX3"], capabilities: &["digital", "uart"] },
PinInfo { number: 15, aliases: &["RX3"], capabilities: &["digital", "uart"] },
PinInfo { number: 16, aliases: &["TX2"], capabilities: &["digital", "uart"] },
PinInfo { number: 17, aliases: &["RX2"], capabilities: &["digital", "uart"] },
PinInfo { number: 18, aliases: &["TX1", "INT5"], capabilities: &["digital", "uart", "interrupt"] },
PinInfo { number: 19, aliases: &["RX1", "INT4"], capabilities: &["digital", "uart", "interrupt"] },
PinInfo { number: 20, aliases: &["SDA", "INT3"], capabilities: &["digital", "i2c", "interrupt"] },
PinInfo { number: 21, aliases: &["SCL", "INT2"], capabilities: &["digital", "i2c", "interrupt"] },
// 22-43: general digital I/O
PinInfo { number: 22, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 23, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 24, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 25, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 26, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 27, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 28, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 29, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 30, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 31, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 32, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 33, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 34, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 35, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 36, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 37, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 38, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 39, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 40, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 41, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 42, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 43, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 44, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 45, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 46, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 47, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 48, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 49, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 50, aliases: &["SPI_MISO"], capabilities: &["digital", "spi"] },
PinInfo { number: 51, aliases: &["SPI_MOSI"], capabilities: &["digital", "spi"] },
PinInfo { number: 52, aliases: &["SPI_SCK"], capabilities: &["digital", "spi"] },
PinInfo { number: 53, aliases: &["SPI_SS"], capabilities: &["digital", "spi"] },
// Analog pins
PinInfo { number: 54, aliases: &["A0"], capabilities: &["digital", "analog"] },
PinInfo { number: 55, aliases: &["A1"], capabilities: &["digital", "analog"] },
PinInfo { number: 56, aliases: &["A2"], capabilities: &["digital", "analog"] },
PinInfo { number: 57, aliases: &["A3"], capabilities: &["digital", "analog"] },
PinInfo { number: 58, aliases: &["A4"], capabilities: &["digital", "analog"] },
PinInfo { number: 59, aliases: &["A5"], capabilities: &["digital", "analog"] },
PinInfo { number: 60, aliases: &["A6"], capabilities: &["digital", "analog"] },
PinInfo { number: 61, aliases: &["A7"], capabilities: &["digital", "analog"] },
PinInfo { number: 62, aliases: &["A8"], capabilities: &["digital", "analog"] },
PinInfo { number: 63, aliases: &["A9"], capabilities: &["digital", "analog"] },
PinInfo { number: 64, aliases: &["A10"], capabilities: &["digital", "analog"] },
PinInfo { number: 65, aliases: &["A11"], capabilities: &["digital", "analog"] },
PinInfo { number: 66, aliases: &["A12"], capabilities: &["digital", "analog"] },
PinInfo { number: 67, aliases: &["A13"], capabilities: &["digital", "analog"] },
PinInfo { number: 68, aliases: &["A14"], capabilities: &["digital", "analog"] },
PinInfo { number: 69, aliases: &["A15"], capabilities: &["digital", "analog"] },
],
groups: &[
BusGroup {
name: "spi",
description: "SPI bus (hardware)",
fixed_pins: &[("sck", 52), ("mosi", 51), ("miso", 50)],
user_selectable: &["cs"],
},
BusGroup {
name: "i2c",
description: "I2C / TWI bus",
fixed_pins: &[("sda", 20), ("scl", 21)],
user_selectable: &[],
},
BusGroup {
name: "uart",
description: "Hardware serial (Serial)",
fixed_pins: &[("rx", 0), ("tx", 1)],
user_selectable: &[],
},
BusGroup {
name: "uart1",
description: "Serial1",
fixed_pins: &[("rx", 19), ("tx", 18)],
user_selectable: &[],
},
BusGroup {
name: "uart2",
description: "Serial2",
fixed_pins: &[("rx", 17), ("tx", 16)],
user_selectable: &[],
},
BusGroup {
name: "uart3",
description: "Serial3",
fixed_pins: &[("rx", 15), ("tx", 14)],
user_selectable: &[],
},
],
};
// Arduino Nano (ATmega328P) -- same silicon as Uno, A6/A7 are analog-only
static PIN_MAP_NANO: BoardPinMap = BoardPinMap {
board: "nano",
total_digital: 14,
total_analog: 8,
pins: &[
PinInfo { number: 0, aliases: &["RX"], capabilities: &["digital", "uart"] },
PinInfo { number: 1, aliases: &["TX"], capabilities: &["digital", "uart"] },
PinInfo { number: 2, aliases: &["INT0"], capabilities: &["digital", "interrupt"] },
PinInfo { number: 3, aliases: &["INT1"], capabilities: &["digital", "pwm", "interrupt"] },
PinInfo { number: 4, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 5, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 6, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 7, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 8, aliases: &[], capabilities: &["digital"] },
PinInfo { number: 9, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 10, aliases: &["SPI_SS"], capabilities: &["digital", "pwm", "spi"] },
PinInfo { number: 11, aliases: &["SPI_MOSI"], capabilities: &["digital", "pwm", "spi"] },
PinInfo { number: 12, aliases: &["SPI_MISO"], capabilities: &["digital", "spi"] },
PinInfo { number: 13, aliases: &["SPI_SCK", "LED_BUILTIN"], capabilities: &["digital", "spi", "led"] },
PinInfo { number: 14, aliases: &["A0"], capabilities: &["digital", "analog"] },
PinInfo { number: 15, aliases: &["A1"], capabilities: &["digital", "analog"] },
PinInfo { number: 16, aliases: &["A2"], capabilities: &["digital", "analog"] },
PinInfo { number: 17, aliases: &["A3"], capabilities: &["digital", "analog"] },
PinInfo { number: 18, aliases: &["A4", "SDA"], capabilities: &["digital", "analog", "i2c"] },
PinInfo { number: 19, aliases: &["A5", "SCL"], capabilities: &["digital", "analog", "i2c"] },
// A6 and A7 are analog-only on Nano (no digital I/O)
PinInfo { number: 20, aliases: &["A6"], capabilities: &["analog"] },
PinInfo { number: 21, aliases: &["A7"], capabilities: &["analog"] },
],
groups: &[
BusGroup {
name: "spi",
description: "SPI bus (hardware)",
fixed_pins: &[("sck", 13), ("mosi", 11), ("miso", 12)],
user_selectable: &["cs"],
},
BusGroup {
name: "i2c",
description: "I2C / TWI bus",
fixed_pins: &[("sda", 18), ("scl", 19)],
user_selectable: &[],
},
BusGroup {
name: "uart",
description: "Hardware serial (Serial)",
fixed_pins: &[("rx", 0), ("tx", 1)],
user_selectable: &[],
},
],
};
// Shared pins/groups for ATmega32U4 boards (Leonardo, Micro)
static ATM32U4_PINS: &[PinInfo] = &[
PinInfo { number: 0, aliases: &["RX", "INT2"], capabilities: &["digital", "uart", "interrupt"] },
PinInfo { number: 1, aliases: &["TX", "INT3"], capabilities: &["digital", "uart", "interrupt"] },
PinInfo { number: 2, aliases: &["SDA", "INT1"], capabilities: &["digital", "i2c", "interrupt"] },
PinInfo { number: 3, aliases: &["SCL", "INT0"], capabilities: &["digital", "pwm", "i2c", "interrupt"] },
PinInfo { number: 4, aliases: &["A6"], capabilities: &["digital", "analog"] },
PinInfo { number: 5, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 6, aliases: &["A7"], capabilities: &["digital", "pwm", "analog"] },
PinInfo { number: 7, aliases: &["INT6"], capabilities: &["digital", "interrupt"] },
PinInfo { number: 8, aliases: &["A8"], capabilities: &["digital", "analog"] },
PinInfo { number: 9, aliases: &["A9"], capabilities: &["digital", "pwm", "analog"] },
PinInfo { number: 10, aliases: &["A10"], capabilities: &["digital", "pwm", "analog"] },
PinInfo { number: 11, aliases: &[], capabilities: &["digital", "pwm"] },
PinInfo { number: 12, aliases: &["A11"], capabilities: &["digital", "analog"] },
PinInfo { number: 13, aliases: &["LED_BUILTIN"], capabilities: &["digital", "pwm", "led"] },
// ICSP-routed SPI: MISO=14, SCK=15, MOSI=16, SS=17
PinInfo { number: 14, aliases: &["SPI_MISO"], capabilities: &["digital", "spi"] },
PinInfo { number: 15, aliases: &["SPI_SCK"], capabilities: &["digital", "spi"] },
PinInfo { number: 16, aliases: &["SPI_MOSI"], capabilities: &["digital", "spi"] },
PinInfo { number: 17, aliases: &["SPI_SS"], capabilities: &["digital", "spi"] },
PinInfo { number: 18, aliases: &["A0"], capabilities: &["digital", "analog"] },
PinInfo { number: 19, aliases: &["A1"], capabilities: &["digital", "analog"] },
PinInfo { number: 20, aliases: &["A2"], capabilities: &["digital", "analog"] },
PinInfo { number: 21, aliases: &["A3"], capabilities: &["digital", "analog"] },
PinInfo { number: 22, aliases: &["A4"], capabilities: &["digital", "analog"] },
PinInfo { number: 23, aliases: &["A5"], capabilities: &["digital", "analog"] },
];
static ATM32U4_GROUPS: &[BusGroup] = &[
BusGroup {
name: "spi",
description: "SPI bus (ICSP header)",
fixed_pins: &[("sck", 15), ("mosi", 16), ("miso", 14)],
user_selectable: &["cs"],
},
BusGroup {
name: "i2c",
description: "I2C / TWI bus",
fixed_pins: &[("sda", 2), ("scl", 3)],
user_selectable: &[],
},
BusGroup {
name: "uart",
description: "Hardware serial (Serial1)",
fixed_pins: &[("rx", 0), ("tx", 1)],
user_selectable: &[],
},
];
// Arduino Leonardo (ATmega32U4)
static PIN_MAP_LEONARDO: BoardPinMap = BoardPinMap {
board: "leonardo",
total_digital: 20,
total_analog: 12,
pins: ATM32U4_PINS,
groups: ATM32U4_GROUPS,
};
// Arduino Micro (ATmega32U4) -- same silicon as Leonardo
static PIN_MAP_MICRO: BoardPinMap = BoardPinMap {
board: "micro",
total_digital: 20,
total_analog: 12,
pins: ATM32U4_PINS,
groups: ATM32U4_GROUPS,
};
// -- Lookup ---------------------------------------------------------------
/// Find the pin map for a board by preset name.
pub fn find_pinmap(board: &str) -> Option<&'static BoardPinMap> {
let lower = board.to_lowercase();
PINMAPS.iter().find(|m| m.board == lower).copied()
}
/// Find the pin map for a board, also checking for nano-old -> nano alias.
pub fn find_pinmap_fuzzy(board: &str) -> Option<&'static BoardPinMap> {
find_pinmap(board).or_else(|| {
// nano-old uses the same pin map as nano
if board == "nano-old" {
find_pinmap("nano")
} else {
None
}
})
}
/// Check if a pin number is valid for a given board.
pub fn is_valid_pin(pinmap: &BoardPinMap, pin: u8) -> bool {
pinmap.pins.iter().any(|p| p.number == pin)
}
/// Get info about a specific pin.
pub fn get_pin(pinmap: &BoardPinMap, pin: u8) -> Option<&PinInfo> {
pinmap.pins.iter().find(|p| p.number == pin)
}
/// Resolve an alias like "A0", "SDA", "LED_BUILTIN" to a pin number.
pub fn resolve_alias(pinmap: &BoardPinMap, name: &str) -> Option<u8> {
let upper = name.to_uppercase();
// Try as a number first
if let Ok(n) = name.parse::<u8>() {
if is_valid_pin(pinmap, n) {
return Some(n);
}
}
// Try aliases
for p in pinmap.pins {
for alias in p.aliases {
if alias.to_uppercase() == upper {
return Some(p.number);
}
}
}
None
}
/// Get all pins that have a given capability.
pub fn pins_with_capability<'a>(pinmap: &'a BoardPinMap, capability: &str) -> Vec<&'a PinInfo> {
pinmap.pins.iter()
.filter(|p| p.capabilities.contains(&capability))
.collect()
}
/// Get all capabilities present on this board (deduplicated).
pub fn board_capabilities(pinmap: &BoardPinMap) -> Vec<&'static str> {
let mut caps: Vec<&str> = Vec::new();
for p in pinmap.pins {
for &c in p.capabilities {
if !caps.contains(&c) {
caps.push(c);
}
}
}
caps.sort();
caps
}
/// Get all pins reserved by a bus group (fixed + any user-provided).
pub fn bus_fixed_pins(group: &BusGroup) -> Vec<(String, u8)> {
group.fixed_pins.iter()
.map(|&(role, pin)| (role.to_string(), pin))
.collect()
}
/// List all board names that have pin maps.
pub fn available_boards() -> Vec<&'static str> {
PINMAPS.iter().map(|m| m.board).collect()
}
// -- Tests ----------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_pinmap_uno() {
let m = find_pinmap("uno").unwrap();
assert_eq!(m.board, "uno");
assert_eq!(m.total_digital, 14);
assert_eq!(m.total_analog, 6);
}
#[test]
fn test_find_pinmap_mega() {
let m = find_pinmap("mega").unwrap();
assert_eq!(m.board, "mega");
assert_eq!(m.total_digital, 54);
assert_eq!(m.total_analog, 16);
}
#[test]
fn test_find_pinmap_case_insensitive() {
// find_pinmap lowercases internally
assert!(find_pinmap("UNO").is_some());
}
#[test]
fn test_find_pinmap_unknown() {
assert!(find_pinmap("esp32").is_none());
}
#[test]
fn test_fuzzy_nano_old() {
assert!(find_pinmap_fuzzy("nano-old").is_some());
}
#[test]
fn test_resolve_alias_number() {
let m = find_pinmap("uno").unwrap();
assert_eq!(resolve_alias(m, "13"), Some(13));
}
#[test]
fn test_resolve_alias_a0() {
let m = find_pinmap("uno").unwrap();
assert_eq!(resolve_alias(m, "A0"), Some(14));
assert_eq!(resolve_alias(m, "a0"), Some(14));
}
#[test]
fn test_resolve_alias_sda() {
let m = find_pinmap("uno").unwrap();
assert_eq!(resolve_alias(m, "SDA"), Some(18));
}
#[test]
fn test_resolve_alias_led_builtin() {
let m = find_pinmap("uno").unwrap();
assert_eq!(resolve_alias(m, "LED_BUILTIN"), Some(13));
}
#[test]
fn test_resolve_alias_invalid() {
let m = find_pinmap("uno").unwrap();
assert_eq!(resolve_alias(m, "NOPE"), None);
assert_eq!(resolve_alias(m, "99"), None);
}
#[test]
fn test_pins_with_capability_pwm() {
let m = find_pinmap("uno").unwrap();
let pwm = pins_with_capability(m, "pwm");
let nums: Vec<u8> = pwm.iter().map(|p| p.number).collect();
assert_eq!(nums, vec![3, 5, 6, 9, 10, 11]);
}
#[test]
fn test_pins_with_capability_analog() {
let m = find_pinmap("uno").unwrap();
let analog = pins_with_capability(m, "analog");
assert_eq!(analog.len(), 6); // A0-A5
}
#[test]
fn test_board_capabilities() {
let m = find_pinmap("uno").unwrap();
let caps = board_capabilities(m);
assert!(caps.contains(&"digital"));
assert!(caps.contains(&"pwm"));
assert!(caps.contains(&"analog"));
assert!(caps.contains(&"spi"));
assert!(caps.contains(&"i2c"));
}
#[test]
fn test_mega_spi_different_from_uno() {
let uno = find_pinmap("uno").unwrap();
let mega = find_pinmap("mega").unwrap();
let uno_spi = uno.groups.iter().find(|g| g.name == "spi").unwrap();
let mega_spi = mega.groups.iter().find(|g| g.name == "spi").unwrap();
// Uno SPI SCK = 13, Mega SPI SCK = 52
let uno_sck = uno_spi.fixed_pins.iter().find(|p| p.0 == "sck").unwrap().1;
let mega_sck = mega_spi.fixed_pins.iter().find(|p| p.0 == "sck").unwrap().1;
assert_ne!(uno_sck, mega_sck);
}
#[test]
fn test_mega_has_multiple_uarts() {
let m = find_pinmap("mega").unwrap();
let uarts: Vec<_> = m.groups.iter()
.filter(|g| g.name.starts_with("uart"))
.collect();
assert_eq!(uarts.len(), 4); // uart, uart1, uart2, uart3
}
#[test]
fn test_leonardo_i2c_on_2_3() {
let m = find_pinmap("leonardo").unwrap();
let i2c = m.groups.iter().find(|g| g.name == "i2c").unwrap();
let sda = i2c.fixed_pins.iter().find(|p| p.0 == "sda").unwrap().1;
let scl = i2c.fixed_pins.iter().find(|p| p.0 == "scl").unwrap().1;
assert_eq!(sda, 2);
assert_eq!(scl, 3);
}
#[test]
fn test_micro_shares_leonardo_pins() {
let leo = find_pinmap("leonardo").unwrap();
let mic = find_pinmap("micro").unwrap();
assert_eq!(leo.pins.len(), mic.pins.len());
}
#[test]
fn test_nano_has_analog_only_pins() {
let m = find_pinmap("nano").unwrap();
let a6 = get_pin(m, 20).unwrap();
assert!(a6.aliases.contains(&"A6"));
assert!(a6.capabilities.contains(&"analog"));
assert!(!a6.capabilities.contains(&"digital"));
}
#[test]
fn test_spi_has_user_selectable_cs() {
let m = find_pinmap("uno").unwrap();
let spi = m.groups.iter().find(|g| g.name == "spi").unwrap();
assert!(spi.user_selectable.contains(&"cs"));
}
#[test]
fn test_i2c_has_no_user_selectable() {
let m = find_pinmap("uno").unwrap();
let i2c = m.groups.iter().find(|g| g.name == "i2c").unwrap();
assert!(i2c.user_selectable.is_empty());
}
#[test]
fn test_available_boards() {
let boards = available_boards();
assert!(boards.contains(&"uno"));
assert!(boards.contains(&"mega"));
assert!(boards.contains(&"nano"));
assert!(boards.contains(&"leonardo"));
assert!(boards.contains(&"micro"));
}
}

View File

@@ -3,4 +3,5 @@ pub mod doctor;
pub mod setup; pub mod setup;
pub mod devices; pub mod devices;
pub mod refresh; pub mod refresh;
pub mod board; pub mod board;
pub mod pin;

1034
src/commands/pin.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -119,6 +119,60 @@ enum Commands {
#[arg(long, short = 'd', value_name = "DIR")] #[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>, dir: Option<String>,
}, },
/// View pin maps, assign pins, and audit wiring
Pin {
/// Capability filter (pwm, analog, spi, i2c, uart, interrupt)
/// or pin/bus name for --assign / --remove
name: Option<String>,
/// Pin number or alias for --assign (e.g. 13, A0, SDA)
pin: Option<String>,
/// Assign a pin or bus group
#[arg(long, conflicts_with_all = ["remove", "audit", "generate", "capabilities", "init_from"])]
assign: bool,
/// Remove a pin or bus assignment
#[arg(long, conflicts_with_all = ["assign", "audit", "generate", "capabilities", "init_from"])]
remove: bool,
/// Show wiring audit report
#[arg(long, conflicts_with_all = ["assign", "remove", "generate", "capabilities", "init_from"])]
audit: bool,
/// Show only the wiring checklist (with --audit)
#[arg(long, requires = "audit")]
brief: bool,
/// Generate lib/hal/pins.h
#[arg(long, conflicts_with_all = ["assign", "remove", "audit", "capabilities", "init_from"])]
generate: bool,
/// List capabilities supported by the board
#[arg(long, conflicts_with_all = ["assign", "remove", "audit", "generate", "init_from"])]
capabilities: bool,
/// Copy pin assignments from another board
#[arg(long, value_name = "BOARD", conflicts_with_all = ["assign", "remove", "audit", "generate", "capabilities"])]
init_from: Option<String>,
/// Pin mode (input, output, input_pullup, pwm, analog)
#[arg(long, value_name = "MODE")]
mode: Option<String>,
/// SPI chip-select pin (with --assign spi)
#[arg(long, value_name = "PIN")]
cs: Option<String>,
/// Target board (defaults to project default)
#[arg(long, short = 'b', value_name = "BOARD")]
board: Option<String>,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@@ -225,6 +279,108 @@ fn main() -> Result<()> {
commands::board::list_boards(dir.as_deref()) commands::board::list_boards(dir.as_deref())
} }
} }
Commands::Pin {
name, pin, assign, remove, audit, brief,
generate, capabilities, init_from, mode, cs, board, dir,
} => {
if capabilities {
commands::pin::show_capabilities(
board.as_deref(),
dir.as_deref(),
)
} else if audit {
commands::pin::audit_pins(
board.as_deref(),
brief,
dir.as_deref(),
)
} else if generate {
commands::pin::generate_pins_header(
board.as_deref(),
dir.as_deref(),
)
} else if remove {
let pin_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Name required.\n\
Usage: anvil pin --remove red_led\n\
Audit current: anvil pin --audit"
)
})?;
commands::pin::remove_assignment(
pin_name,
board.as_deref(),
dir.as_deref(),
)
} else if let Some(ref source) = init_from {
let target = board.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Target board required.\n\
Usage: anvil pin --init-from uno --board mega"
)
})?;
commands::pin::init_from(
source,
target,
dir.as_deref(),
)
} else if assign {
let assign_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Name required.\n\
Usage: anvil pin --assign red_led 13\n\
Usage: anvil pin --assign spi --cs 10\n\
See pins: anvil pin"
)
})?;
// Check if this is a bus group assignment
let is_bus = {
let board_name = board.as_deref().unwrap_or("uno");
anvil::board::pinmap::find_pinmap_fuzzy(board_name)
.map(|pm| pm.groups.iter().any(|g| {
g.name == assign_name.to_lowercase()
}))
.unwrap_or(false)
};
if is_bus {
let mut user_pins: Vec<(&str, &str)> = Vec::new();
if let Some(ref cs_pin) = cs {
user_pins.push(("cs", cs_pin.as_str()));
}
commands::pin::assign_bus(
assign_name,
&user_pins,
board.as_deref(),
dir.as_deref(),
)
} else {
let pin_str = pin.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Pin number required.\n\
Usage: anvil pin --assign {} 13\n\
See pins: anvil pin",
assign_name
)
})?;
commands::pin::assign_pin(
assign_name,
pin_str,
mode.as_deref(),
board.as_deref(),
dir.as_deref(),
)
}
} else {
// Default: show pin map, optionally filtered
commands::pin::show_pin_map(
board.as_deref(),
name.as_deref(),
dir.as_deref(),
)
}
}
} }
} }

View File

@@ -15,6 +15,8 @@ pub struct ProjectConfig {
pub monitor: MonitorConfig, pub monitor: MonitorConfig,
#[serde(default)] #[serde(default)]
pub boards: HashMap<String, BoardProfile>, pub boards: HashMap<String, BoardProfile>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub pins: HashMap<String, BoardPinConfig>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -51,6 +53,32 @@ pub struct BoardProfile {
pub baud: Option<u32>, pub baud: Option<u32>,
} }
/// Pin configuration for a specific board.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BoardPinConfig {
/// Individual pin assignments: name -> { pin, mode }
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub assignments: HashMap<String, PinAssignment>,
/// Bus group reservations: "spi" -> { cs = 10 }, "i2c" -> {}
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub buses: HashMap<String, BusConfig>,
}
/// A single pin assignment with a friendly name.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PinAssignment {
pub pin: u8,
pub mode: String,
}
/// Configuration for a claimed bus group.
/// User-selectable pins (like CS for SPI) are stored as flattened keys.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BusConfig {
#[serde(flatten)]
pub user_pins: HashMap<String, u8>,
}
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 {
@@ -76,6 +104,7 @@ impl ProjectConfig {
port: None, port: None,
}, },
boards, boards,
pins: HashMap::new(),
} }
} }

View File

@@ -1970,4 +1970,275 @@ fn test_monitor_sh_timestamps_work_in_watch_mode() {
"monitor_filter should be defined and used in both watch and normal mode (found {} refs)", "monitor_filter should be defined and used in both watch and normal mode (found {} refs)",
filter_count filter_count
); );
}
// ==========================================================================
// Pin map data
// ==========================================================================
#[test]
fn test_pinmap_exists_for_all_presets() {
use anvil::board::presets;
use anvil::board::pinmap;
// Every board preset (except nano-old) should have a pin map
for preset in presets::PRESETS {
let found = pinmap::find_pinmap_fuzzy(preset.name);
assert!(
found.is_some(),
"Board preset '{}' has no pin map. Add one to pinmap.rs.",
preset.name
);
}
}
#[test]
fn test_pinmap_uno_basics() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("uno").unwrap();
assert_eq!(m.total_digital, 14);
assert_eq!(m.total_analog, 6);
// Uno has 20 pins total (D0-D13 + A0-A5 mapped as D14-D19)
assert_eq!(m.pins.len(), 20);
}
#[test]
fn test_pinmap_mega_basics() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("mega").unwrap();
assert_eq!(m.total_digital, 54);
assert_eq!(m.total_analog, 16);
// Mega has 70 pins: D0-D53 + A0-A15
assert_eq!(m.pins.len(), 70);
}
#[test]
fn test_pinmap_resolve_alias_a0() {
use anvil::board::pinmap;
let uno = pinmap::find_pinmap("uno").unwrap();
assert_eq!(pinmap::resolve_alias(uno, "A0"), Some(14));
assert_eq!(pinmap::resolve_alias(uno, "a0"), Some(14));
let mega = pinmap::find_pinmap("mega").unwrap();
assert_eq!(pinmap::resolve_alias(mega, "A0"), Some(54));
}
#[test]
fn test_pinmap_spi_pins_differ_by_board() {
use anvil::board::pinmap;
let uno = pinmap::find_pinmap("uno").unwrap();
let mega = pinmap::find_pinmap("mega").unwrap();
let uno_spi = uno.groups.iter().find(|g| g.name == "spi").unwrap();
let mega_spi = mega.groups.iter().find(|g| g.name == "spi").unwrap();
let uno_sck = uno_spi.fixed_pins.iter().find(|p| p.0 == "sck").unwrap().1;
let mega_sck = mega_spi.fixed_pins.iter().find(|p| p.0 == "sck").unwrap().1;
// Uno SCK=13, Mega SCK=52
assert_ne!(uno_sck, mega_sck, "SPI SCK should differ between uno and mega");
}
#[test]
fn test_pinmap_capabilities_filter() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("uno").unwrap();
let pwm = pinmap::pins_with_capability(m, "pwm");
// Uno has PWM on 3, 5, 6, 9, 10, 11
assert_eq!(pwm.len(), 6);
let analog = pinmap::pins_with_capability(m, "analog");
assert_eq!(analog.len(), 6); // A0-A5
}
#[test]
fn test_pinmap_board_capabilities_list() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("uno").unwrap();
let caps = pinmap::board_capabilities(m);
assert!(caps.contains(&"digital"));
assert!(caps.contains(&"analog"));
assert!(caps.contains(&"pwm"));
assert!(caps.contains(&"spi"));
assert!(caps.contains(&"i2c"));
assert!(caps.contains(&"uart"));
assert!(caps.contains(&"interrupt"));
assert!(caps.contains(&"led"));
}
#[test]
fn test_pinmap_mega_has_four_uarts() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("mega").unwrap();
let uart_count = m.groups.iter()
.filter(|g| g.name.starts_with("uart"))
.count();
assert_eq!(uart_count, 4, "Mega should have uart, uart1, uart2, uart3");
}
#[test]
fn test_pinmap_leonardo_i2c_different_from_uno() {
use anvil::board::pinmap;
let uno = pinmap::find_pinmap("uno").unwrap();
let leo = pinmap::find_pinmap("leonardo").unwrap();
let uno_i2c = uno.groups.iter().find(|g| g.name == "i2c").unwrap();
let leo_i2c = leo.groups.iter().find(|g| g.name == "i2c").unwrap();
let uno_sda = uno_i2c.fixed_pins.iter().find(|p| p.0 == "sda").unwrap().1;
let leo_sda = leo_i2c.fixed_pins.iter().find(|p| p.0 == "sda").unwrap().1;
// Uno SDA=18 (A4), Leonardo SDA=2
assert_ne!(uno_sda, leo_sda, "I2C SDA should differ between uno and leonardo");
}
#[test]
fn test_pinmap_spi_has_user_selectable_cs() {
use anvil::board::pinmap;
// All boards with SPI should require user to select CS
for board in pinmap::available_boards() {
let m = pinmap::find_pinmap(board).unwrap();
if let Some(spi) = m.groups.iter().find(|g| g.name == "spi") {
assert!(
spi.user_selectable.contains(&"cs"),
"Board '{}' SPI should have user-selectable CS pin",
board
);
}
}
}
#[test]
fn test_pinmap_nano_has_analog_only_pins() {
use anvil::board::pinmap;
let m = pinmap::find_pinmap("nano").unwrap();
let a6 = pinmap::get_pin(m, 20).unwrap(); // A6
assert!(a6.capabilities.contains(&"analog"));
assert!(!a6.capabilities.contains(&"digital"),
"Nano A6 should be analog-only");
}
// ==========================================================================
// Pin config in .anvil.toml
// ==========================================================================
#[test]
fn test_config_with_pins_roundtrips() {
use anvil::project::config::{BoardPinConfig, PinAssignment, BusConfig};
use std::collections::HashMap;
let tmp = TempDir::new().unwrap();
let mut config = ProjectConfig::new("pin_test");
// Add pin assignments
let mut assignments = HashMap::new();
assignments.insert("red_led".to_string(), PinAssignment {
pin: 13,
mode: "output".to_string(),
});
assignments.insert("limit_switch".to_string(), PinAssignment {
pin: 7,
mode: "input".to_string(),
});
let mut buses = HashMap::new();
let mut spi_pins = HashMap::new();
spi_pins.insert("cs".to_string(), 10u8);
buses.insert("spi".to_string(), BusConfig { user_pins: spi_pins });
buses.insert("i2c".to_string(), BusConfig { user_pins: HashMap::new() });
config.pins.insert("uno".to_string(), BoardPinConfig {
assignments,
buses,
});
config.save(tmp.path()).unwrap();
let loaded = ProjectConfig::load(tmp.path()).unwrap();
let pc = loaded.pins.get("uno").unwrap();
assert_eq!(pc.assignments.len(), 2);
assert_eq!(pc.assignments["red_led"].pin, 13);
assert_eq!(pc.assignments["red_led"].mode, "output");
assert_eq!(pc.assignments["limit_switch"].pin, 7);
assert_eq!(pc.buses.len(), 2);
assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8);
assert!(pc.buses["i2c"].user_pins.is_empty());
}
#[test]
fn test_config_without_pins_loads_empty() {
let tmp = TempDir::new().unwrap();
let config = ProjectConfig::new("no_pins");
config.save(tmp.path()).unwrap();
let loaded = ProjectConfig::load(tmp.path()).unwrap();
assert!(loaded.pins.is_empty(), "Config without pins section should load with empty pins");
}
#[test]
fn test_config_pins_skip_serializing_when_empty() {
let tmp = TempDir::new().unwrap();
let config = ProjectConfig::new("empty_pins");
config.save(tmp.path()).unwrap();
let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap();
assert!(
!content.contains("[pins"),
"Empty pins should not appear in serialized config"
);
}
#[test]
fn test_pins_per_board_independence() {
use anvil::project::config::{BoardPinConfig, PinAssignment};
use std::collections::HashMap;
let tmp = TempDir::new().unwrap();
let mut config = ProjectConfig::new("multi_board_pins");
config.boards.insert("mega".to_string(), BoardProfile {
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
baud: None,
});
// Different pin numbers for same friendly name on different boards
let mut uno_assigns = HashMap::new();
uno_assigns.insert("status_led".to_string(), PinAssignment {
pin: 13, mode: "output".to_string(),
});
let mut mega_assigns = HashMap::new();
mega_assigns.insert("status_led".to_string(), PinAssignment {
pin: 13, mode: "output".to_string(),
});
mega_assigns.insert("extra_led".to_string(), PinAssignment {
pin: 22, mode: "output".to_string(),
});
config.pins.insert("uno".to_string(), BoardPinConfig {
assignments: uno_assigns, buses: HashMap::new(),
});
config.pins.insert("mega".to_string(), BoardPinConfig {
assignments: mega_assigns, buses: HashMap::new(),
});
config.save(tmp.path()).unwrap();
let loaded = ProjectConfig::load(tmp.path()).unwrap();
let uno_pins = loaded.pins.get("uno").unwrap();
let mega_pins = loaded.pins.get("mega").unwrap();
assert_eq!(uno_pins.assignments.len(), 1);
assert_eq!(mega_pins.assignments.len(), 2);
assert!(mega_pins.assignments.contains_key("extra_led"));
assert!(!uno_pins.assignments.contains_key("extra_led"));
} }