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") {
|
if self.port_name.contains("ttyUSB") || self.port_name.contains("ttyACM") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// FreeBSD: cuaU* are USB serial ports
|
||||||
|
if self.port_name.contains("cuaU") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +175,19 @@ fn list_ports_fallback() -> Vec<PortInfo> {
|
|||||||
if let Ok(entries) = fs::read_dir("/dev") {
|
if let Ok(entries) = fs::read_dir("/dev") {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
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 path = format!("/dev/{}", name);
|
||||||
let board = if name.starts_with("ttyUSB") {
|
let board = if name.starts_with("ttyUSB") {
|
||||||
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
|
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
|
||||||
} else {
|
} else if name.starts_with("ttyACM") {
|
||||||
"Likely Arduino (run 'anvil setup' for full detection)"
|
"Likely Arduino (run 'anvil setup' for full detection)"
|
||||||
|
} else {
|
||||||
|
"USB serial device (run 'anvil setup' for full detection)"
|
||||||
};
|
};
|
||||||
result.push(PortInfo {
|
result.push(PortInfo {
|
||||||
port_name: path,
|
port_name: path,
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ pub fn scan_devices() -> Result<()> {
|
|||||||
println!(" - Check kernel log: dmesg | tail -20");
|
println!(" - Check kernel log: dmesg | tail -20");
|
||||||
println!(" - Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
println!(" - Open Device Manager and check Ports (COM & LPT)");
|
println!(" - Open Device Manager and check Ports (COM & LPT)");
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ pub struct SystemHealth {
|
|||||||
pub arduino_cli_path: Option<String>,
|
pub arduino_cli_path: Option<String>,
|
||||||
pub avr_core_ok: bool,
|
pub avr_core_ok: bool,
|
||||||
pub avr_size_ok: bool,
|
pub avr_size_ok: bool,
|
||||||
|
pub avr_size_path: Option<String>,
|
||||||
pub dialout_ok: bool,
|
pub dialout_ok: bool,
|
||||||
|
pub operator_ok: bool,
|
||||||
pub cmake_ok: bool,
|
pub cmake_ok: bool,
|
||||||
pub cpp_compiler_ok: bool,
|
pub cpp_compiler_ok: bool,
|
||||||
pub cpp_on_path: bool,
|
pub cpp_on_path: bool,
|
||||||
@@ -87,8 +89,9 @@ pub fn check_system_health() -> SystemHealth {
|
|||||||
false
|
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 dialout_ok = check_dialout();
|
||||||
|
let operator_ok = check_operator_group();
|
||||||
let cmake_ok = which::which("cmake").is_ok();
|
let cmake_ok = which::which("cmake").is_ok();
|
||||||
|
|
||||||
let cpp_on_path = which::which("g++").is_ok()
|
let cpp_on_path = which::which("g++").is_ok()
|
||||||
@@ -108,7 +111,9 @@ pub fn check_system_health() -> SystemHealth {
|
|||||||
arduino_cli_path,
|
arduino_cli_path,
|
||||||
avr_core_ok,
|
avr_core_ok,
|
||||||
avr_size_ok,
|
avr_size_ok,
|
||||||
|
avr_size_path,
|
||||||
dialout_ok,
|
dialout_ok,
|
||||||
|
operator_ok,
|
||||||
cmake_ok,
|
cmake_ok,
|
||||||
cpp_compiler_ok,
|
cpp_compiler_ok,
|
||||||
cpp_on_path,
|
cpp_on_path,
|
||||||
@@ -189,10 +194,11 @@ 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 {
|
||||||
|
let hint = hint_avr_size_not_on_path(&health.avr_size_path);
|
||||||
println!(
|
println!(
|
||||||
" {} avr-size {}",
|
" {} avr-size {}",
|
||||||
"na".yellow(),
|
"na".yellow(),
|
||||||
hint_avr_size_not_on_path().yellow()
|
hint.as_str().yellow()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +213,20 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
" {} {} group {}",
|
" {} {} group {}",
|
||||||
"WARN".yellow(),
|
"WARN".yellow(),
|
||||||
group_name,
|
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 {
|
if health.ports_found > 0 {
|
||||||
println!(
|
println!(
|
||||||
" {} {} serial port(s) detected",
|
" {} {} Arduino board(s) detected",
|
||||||
"ok".green(),
|
"ok".green(),
|
||||||
health.ports_found
|
health.ports_found
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let port_hint = if is_freebsd() {
|
// On FreeBSD, missing group membership prevents port detection entirely --
|
||||||
"(plug in a board to detect -- FreeBSD ports appear as /dev/cuaU0)"
|
// make that clear so the user doesn't chase a phantom hardware problem.
|
||||||
} else {
|
#[cfg(target_os = "freebsd")]
|
||||||
"(plug in a board to detect)"
|
{
|
||||||
};
|
if !health.dialout_ok || !health.operator_ok {
|
||||||
println!(
|
println!(
|
||||||
" {} no serial ports {}",
|
" {} no board detected {}",
|
||||||
"na".yellow(),
|
"na".yellow(),
|
||||||
port_hint.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!();
|
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 {
|
if !health.git_ok {
|
||||||
println!(
|
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)?;
|
run_fix_optional(health)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -625,6 +707,52 @@ fn run_fix(health: &SystemHealth) -> Result<()> {
|
|||||||
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
||||||
let pm = detect_package_manager();
|
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![
|
let items: Vec<(&str, bool, FixSpec)> = vec![
|
||||||
("cmake", health.cmake_ok, fix_spec_cmake(pm)),
|
("cmake", health.cmake_ok, fix_spec_cmake(pm)),
|
||||||
("C++ compiler", health.cpp_compiler_ok, fix_spec_cpp(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();
|
let missing: Vec<_> = items.iter().filter(|(_, ok, _)| !ok).collect();
|
||||||
|
if missing.is_empty() && health.avr_size_ok {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if missing.is_empty() {
|
if missing.is_empty() {
|
||||||
return Ok(());
|
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") {
|
if cfg!(target_os = "windows") {
|
||||||
"installed but not on PATH (binary size reports will be skipped)"
|
format!("installed but not on PATH ({})", location)
|
||||||
} else {
|
} 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 {
|
fn hint_cmake() -> String {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
"install: winget install Kitware.CMake (or choco install cmake)".into()
|
"install: winget install Kitware.CMake (or choco install cmake)".into()
|
||||||
@@ -889,3 +1051,46 @@ fn check_dialout() -> bool {
|
|||||||
true
|
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