Doctor and devices update
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled

- Doctor command indicates that a log out/in may be needed
- Devices works (I think) on FreeBSD
This commit is contained in:
Eric Ratliff
2026-03-15 17:52:32 -05:00
parent fedb304b7c
commit 1b00a98702
3 changed files with 243 additions and 19 deletions

View File

@@ -35,6 +35,10 @@ impl PortInfo {
if self.port_name.contains("ttyUSB") || self.port_name.contains("ttyACM") {
return true;
}
// FreeBSD: cuaU* are USB serial ports
if self.port_name.contains("cuaU") {
return true;
}
false
}
@@ -171,12 +175,19 @@ fn list_ports_fallback() -> Vec<PortInfo> {
if let Ok(entries) = fs::read_dir("/dev") {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("ttyUSB") || name.starts_with("ttyACM") {
// Linux: ttyUSB* (CH340/FTDI), ttyACM* (Arduino native USB)
// FreeBSD: cuaU* (USB serial)
let is_usb_port = name.starts_with("ttyUSB")
|| name.starts_with("ttyACM")
|| name.starts_with("cuaU");
if is_usb_port {
let path = format!("/dev/{}", name);
let board = if name.starts_with("ttyUSB") {
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
} else {
} else if name.starts_with("ttyACM") {
"Likely Arduino (run 'anvil setup' for full detection)"
} else {
"USB serial device (run 'anvil setup' for full detection)"
};
result.push(PortInfo {
port_name: path,

View File

@@ -62,6 +62,14 @@ pub fn scan_devices() -> Result<()> {
println!(" - Check kernel log: dmesg | tail -20");
println!(" - Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
}
#[cfg(target_os = "freebsd")]
{
println!(" - Check kernel log: dmesg | grep -i usb");
println!(" - List USB devices: usbconfig list");
println!(" - Board should appear as /dev/cuaU0 (or cuaU1, cuaU2...)");
println!(" - Make sure you are in the dialer group: sudo pw groupmod dialer -m $USER");
println!(" - Then log out and back in for group change to take effect");
}
#[cfg(target_os = "windows")]
{
println!(" - Open Device Manager and check Ports (COM & LPT)");

View File

@@ -10,7 +10,9 @@ pub struct SystemHealth {
pub arduino_cli_path: Option<String>,
pub avr_core_ok: bool,
pub avr_size_ok: bool,
pub avr_size_path: Option<String>,
pub dialout_ok: bool,
pub operator_ok: bool,
pub cmake_ok: bool,
pub cpp_compiler_ok: bool,
pub cpp_on_path: bool,
@@ -87,8 +89,9 @@ pub fn check_system_health() -> SystemHealth {
false
};
let avr_size_ok = which::which("avr-size").is_ok();
let (avr_size_ok, avr_size_path) = find_avr_size();
let dialout_ok = check_dialout();
let operator_ok = check_operator_group();
let cmake_ok = which::which("cmake").is_ok();
let cpp_on_path = which::which("g++").is_ok()
@@ -108,7 +111,9 @@ pub fn check_system_health() -> SystemHealth {
arduino_cli_path,
avr_core_ok,
avr_size_ok,
avr_size_path,
dialout_ok,
operator_ok,
cmake_ok,
cpp_compiler_ok,
cpp_on_path,
@@ -189,10 +194,11 @@ fn print_diagnostics(health: &SystemHealth) {
"included with arduino:avr core (no separate install)".yellow()
);
} else {
let hint = hint_avr_size_not_on_path(&health.avr_size_path);
println!(
" {} avr-size {}",
"na".yellow(),
hint_avr_size_not_on_path().yellow()
hint.as_str().yellow()
);
}
@@ -207,7 +213,20 @@ fn print_diagnostics(health: &SystemHealth) {
" {} {} group {}",
"WARN".yellow(),
group_name,
format!("run: {}", fix_cmd).yellow()
format!("run: {} (then log out and back in)", fix_cmd).yellow()
);
}
}
#[cfg(target_os = "freebsd")]
{
if health.operator_ok {
println!(" {} user in operator group (USB device access)", "ok".green());
} else {
println!(
" {} operator group {}",
"WARN".yellow(),
"run: sudo pw groupmod operator -m $USER (then log out and back in)".yellow()
);
}
}
@@ -254,21 +273,37 @@ fn print_diagnostics(health: &SystemHealth) {
if health.ports_found > 0 {
println!(
" {} {} serial port(s) detected",
" {} {} Arduino board(s) detected",
"ok".green(),
health.ports_found
);
} else {
let port_hint = if is_freebsd() {
"(plug in a board to detect -- FreeBSD ports appear as /dev/cuaU0)"
} else {
"(plug in a board to detect)"
};
println!(
" {} no serial ports {}",
"na".yellow(),
port_hint.yellow()
);
// On FreeBSD, missing group membership prevents port detection entirely --
// make that clear so the user doesn't chase a phantom hardware problem.
#[cfg(target_os = "freebsd")]
{
if !health.dialout_ok || !health.operator_ok {
println!(
" {} no board detected {}",
"na".yellow(),
"(cannot scan for boards until group access is fixed -- resolve the WARNs above and log out and back in)".yellow()
);
} else {
println!(
" {} no board detected {}",
"na".yellow(),
"(plug in your Arduino -- on FreeBSD boards appear as /dev/cuaU0)".yellow()
);
}
}
#[cfg(not(target_os = "freebsd"))]
{
println!(
" {} no board detected {}",
"na".yellow(),
"(plug in your Arduino to continue)".yellow()
);
}
}
}
@@ -388,6 +423,16 @@ fn print_fix_instructions(health: &SystemHealth) {
println!();
}
#[cfg(target_os = "freebsd")]
if !health.operator_ok {
println!(
" {}",
"Tip: add yourself to the operator group for USB device access: sudo pw groupmod operator -m $USER"
.bright_black()
);
println!();
}
if !health.git_ok {
println!(
" {}",
@@ -618,6 +663,43 @@ fn run_fix(health: &SystemHealth) -> Result<()> {
}
}
// -- serial group (dialout / dialer) --
#[cfg(unix)]
if !health.dialout_ok {
let group = serial_group_name();
let fix_cmd = serial_group_fix_command();
println!();
println!(
"{}",
format!("You are not in the {} group (needed for board upload).", group).bright_yellow()
);
if confirm(&format!("Add yourself to {} now?", group)) {
let parts: Vec<&str> = fix_cmd.split_whitespace().collect();
if run_cmd(parts[0], &parts[1..]) {
println!("{} Added to {} group. Log out and back in to apply.", "ok".green(), group);
} else {
println!("{} Failed. Run manually: {}", "FAIL".red(), fix_cmd);
}
}
}
// -- operator group (FreeBSD USB device access) --
#[cfg(target_os = "freebsd")]
if !health.operator_ok {
println!();
println!(
"{}",
"You are not in the operator group (needed for USB device access on FreeBSD).".bright_yellow()
);
if confirm("Add yourself to operator group now?") {
if run_cmd("sudo", &["pw", "groupmod", "operator", "-m", &whoami()]) {
println!("{} Added to operator group. Log out and back in to apply.", "ok".green());
} else {
println!("{} Failed. Run manually: sudo pw groupmod operator -m $USER", "FAIL".red());
}
}
}
run_fix_optional(health)?;
Ok(())
}
@@ -625,6 +707,52 @@ fn run_fix(health: &SystemHealth) -> Result<()> {
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
let pm = detect_package_manager();
// -- avr-size PATH fix --
if !health.avr_size_ok {
if let Some(ref bin_path) = health.avr_size_path {
let bin_dir = std::path::Path::new(bin_path)
.parent()
.map(|p| p.display().to_string())
.unwrap_or_default();
println!();
println!(
"{}",
"avr-size is installed but not on PATH.".bright_yellow()
);
println!(
" Found at: {}",
bin_path.bright_cyan()
);
if !bin_dir.is_empty() {
let shell_rc = detect_shell_rc();
let export_line = format!(r#"export PATH="{}:$PATH""#, bin_dir);
if confirm(&format!("Add {} to PATH in {}?", bin_dir, shell_rc)) {
let rc_path = std::path::Path::new(&shell_rc);
let existing = std::fs::read_to_string(rc_path).unwrap_or_default();
if existing.contains(&export_line) {
println!("{} Already in {} -- log out and back in to apply.", "ok".green(), shell_rc);
} else {
let line = format!("
# Added by anvil doctor --fix
{}
", export_line);
if std::fs::OpenOptions::new()
.append(true)
.open(rc_path)
.and_then(|mut f| { use std::io::Write; f.write_all(line.as_bytes()) })
.is_ok()
{
println!("{} Added to {}. Log out and back in to apply.", "ok".green(), shell_rc);
} else {
println!("{} Could not write to {}. Add manually:", "FAIL".red(), shell_rc);
println!(" {}", export_line.bright_cyan());
}
}
}
}
}
}
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)),
@@ -632,6 +760,9 @@ fn run_fix_optional(health: &SystemHealth) -> Result<()> {
];
let missing: Vec<_> = items.iter().filter(|(_, ok, _)| !ok).collect();
if missing.is_empty() && health.avr_size_ok {
return Ok(());
}
if missing.is_empty() {
return Ok(());
}
@@ -795,14 +926,45 @@ fn fix_spec_git(pm: Option<&str>) -> FixSpec {
}
}
fn hint_avr_size_not_on_path() -> &'static str {
fn hint_avr_size_not_on_path(found_path: &Option<String>) -> String {
let location = match found_path {
Some(p) => format!("found at {} -- add its directory to PATH, or log out and back in", p),
None => "not found -- install the AVR core first: anvil setup".to_string(),
};
if cfg!(target_os = "windows") {
"installed but not on PATH (binary size reports will be skipped)"
format!("installed but not on PATH ({})", location)
} else {
"installed but not on PATH"
format!("not on PATH ({})", location)
}
}
/// Search common locations for avr-size and return (on_path, found_path).
fn find_avr_size() -> (bool, Option<String>) {
if which::which("avr-size").is_ok() {
return (true, None);
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let search_roots = vec![
format!("{}/.arduino15/packages/arduino/tools/avr-gcc", home),
format!("{}/.local/share/arduino15/packages/arduino/tools/avr-gcc", home),
format!("{}\\AppData\\Local\\Arduino15\\packages\\arduino\\tools\\avr-gcc", home),
];
for root in &search_roots {
let root_path = std::path::Path::new(root);
if !root_path.exists() { continue; }
if let Ok(entries) = std::fs::read_dir(root_path) {
for version_entry in entries.flatten() {
let bin_name = if cfg!(target_os = "windows") { "avr-size.exe" } else { "avr-size" };
let bin = version_entry.path().join("bin").join(bin_name);
if bin.exists() {
return (false, Some(bin.display().to_string()));
}
}
}
}
(false, None)
}
fn hint_cmake() -> String {
if cfg!(target_os = "windows") {
"install: winget install Kitware.CMake (or choco install cmake)".into()
@@ -889,3 +1051,46 @@ fn check_dialout() -> bool {
true
}
}
fn check_operator_group() -> bool {
#[cfg(target_os = "freebsd")]
{
match std::process::Command::new("groups").output() {
Ok(out) => {
let groups = String::from_utf8_lossy(&out.stdout);
groups.contains("operator")
}
Err(_) => false,
}
}
#[cfg(not(target_os = "freebsd"))]
{
true // not applicable on other platforms
}
}
fn whoami() -> String {
std::process::Command::new("whoami")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "$USER".to_string())
}
/// Detect the user's shell RC file for PATH exports.
fn detect_shell_rc() -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
// Check SHELL env var first
let shell = std::env::var("SHELL").unwrap_or_default();
if shell.contains("zsh") {
return format!("{}/.zshrc", home);
}
if shell.contains("fish") {
return format!("{}/.config/fish/config.fish", home);
}
// Fall back to checking which RC files exist
let zshrc = format!("{}/.zshrc", home);
if std::path::Path::new(&zshrc).exists() {
return zshrc;
}
format!("{}/.bashrc", home)
}