Doctor and devices update
- Doctor command indicates that a log out/in may be needed - Devices works (I think) on FreeBSD
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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,
|
||||
@@ -45,7 +47,8 @@ pub fn run_diagnostics(fix: bool) -> Result<()> {
|
||||
if fix {
|
||||
let optional_missing = !health.cmake_ok
|
||||
|| !health.cpp_compiler_ok
|
||||
|| !health.git_ok;
|
||||
|| !health.git_ok
|
||||
|| !health.avr_size_ok;
|
||||
if optional_missing {
|
||||
println!();
|
||||
run_fix_optional(&health)?;
|
||||
@@ -87,8 +90,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 +112,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 +195,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 +214,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 +274,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 +424,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 +664,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 +708,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 +761,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 +927,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()
|
||||
@@ -888,4 +1051,47 @@ 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)
|
||||
}
|
||||
Reference in New Issue
Block a user