Refactor CLI, add refresh command, fix port detection, add device tracking
- Remove build/upload/monitor subcommands (projects are self-contained) - Remove ctrlc dependency (only used by removed monitor watch mode) - Update next-steps messaging to reference project scripts directly - Add 'anvil refresh [DIR] [--force]' to update project scripts to latest templates without touching user code - Fix Windows port detection: replace fragile findstr/batch TOML parsing with proper comment-skipping logic; add _detect_port.ps1 helper for reliable JSON-based port detection via PowerShell - Add .anvil.local for machine-specific config (gitignored) - 'anvil devices --set [PORT] [-d DIR]' saves port + VID:PID - 'anvil devices --get [-d DIR]' shows saved port status - VID:PID tracks USB devices across COM port reassignment - Port resolution: -p flag > VID:PID > saved port > auto-detect - Uppercase normalization for Windows COM port names - Update all .bat/.sh templates to read from .anvil.local - Remove port entries from .anvil.toml (no machine-specific config in git) - Add .anvil.local to .gitignore template - Expand 'anvil devices' output with VID:PID, serial number, and usage instructions
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
|
||||
use crate::board;
|
||||
|
||||
@@ -60,8 +62,261 @@ 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 = "windows")]
|
||||
{
|
||||
println!(" - Open Device Manager and check Ports (COM & LPT)");
|
||||
println!(" - Install CH340 driver if needed: https://www.wch-ic.com/downloads/CH341SER_EXE.html");
|
||||
println!(" - Check if the board appears under \"Other devices\" with a warning icon");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read and display the saved port from .anvil.local.
|
||||
pub fn get_port(project_dir: Option<&str>) -> Result<()> {
|
||||
let project_path = resolve_project_dir(project_dir)?;
|
||||
require_anvil_project(&project_path)?;
|
||||
|
||||
let local_file = project_path.join(".anvil.local");
|
||||
if !local_file.exists() {
|
||||
println!(
|
||||
"{} No saved port (no .anvil.local file).",
|
||||
"--".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!(" To save a default port for this machine, run:");
|
||||
println!();
|
||||
println!(" {} {}", "anvil devices --set".bright_cyan(),
|
||||
"auto-detect and save".bright_black());
|
||||
println!(" {} {}",
|
||||
"anvil devices --set COM3".bright_cyan(),
|
||||
"save a specific port".bright_black());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (saved_port, saved_vid_pid) = read_anvil_local(&local_file)?;
|
||||
|
||||
if saved_port.is_empty() && saved_vid_pid.is_empty() {
|
||||
println!(
|
||||
"{} .anvil.local exists but no port is set.",
|
||||
"--".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!(" To save a default port, run:");
|
||||
println!();
|
||||
println!(" {}", "anvil devices --set COM3".bright_cyan());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to resolve VID:PID to current port
|
||||
if !saved_vid_pid.is_empty() {
|
||||
match board::resolve_vid_pid(&saved_vid_pid) {
|
||||
Some(current_port) => {
|
||||
println!(
|
||||
"{} Device {} is on {}",
|
||||
"ok".green(),
|
||||
saved_vid_pid.bright_cyan(),
|
||||
current_port.bright_white().bold()
|
||||
);
|
||||
if !saved_port.is_empty() && saved_port != current_port {
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"Note: saved port was {}, device has moved",
|
||||
saved_port
|
||||
).bright_yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!(
|
||||
"{} Device {} is not connected",
|
||||
"!!".bright_red(),
|
||||
saved_vid_pid.bright_cyan()
|
||||
);
|
||||
if !saved_port.is_empty() {
|
||||
println!(
|
||||
" Last known port: {}",
|
||||
saved_port.bright_black()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
println!(" Is the board plugged in?");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{} Saved port: {}",
|
||||
"ok".green(),
|
||||
saved_port.bright_white().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"No VID:PID saved -- port won't track if reassigned.".bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"Re-run 'anvil devices --set' to save the device identity.".bright_black()
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
format!("Source: {}", local_file.display()).bright_black()
|
||||
);
|
||||
println!(" To change: {}", "anvil devices --set <PORT>".bright_cyan());
|
||||
println!(" To remove: {}", "delete .anvil.local".bright_cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a port to .anvil.local in the given project directory.
|
||||
pub fn set_port(port: Option<&str>, project_dir: Option<&str>) -> Result<()> {
|
||||
let project_path = resolve_project_dir(project_dir)?;
|
||||
require_anvil_project(&project_path)?;
|
||||
|
||||
// Resolve the port and find its VID:PID
|
||||
let ports = board::list_ports();
|
||||
|
||||
let (resolved_port, vid_pid) = match port {
|
||||
Some(p) => {
|
||||
// User specified a port -- find it in the list to get VID:PID
|
||||
let port_name = if cfg!(target_os = "windows") {
|
||||
p.to_uppercase()
|
||||
} else {
|
||||
p.to_string()
|
||||
};
|
||||
|
||||
let vp = ports.iter()
|
||||
.find(|pi| pi.port_name.eq_ignore_ascii_case(&port_name))
|
||||
.map(|pi| pi.vid_pid())
|
||||
.unwrap_or_default();
|
||||
|
||||
(port_name, vp)
|
||||
}
|
||||
None => {
|
||||
// Auto-detect the best port
|
||||
println!("Detecting best port...");
|
||||
println!();
|
||||
|
||||
if ports.is_empty() {
|
||||
anyhow::bail!(
|
||||
"No serial ports detected. Is the board plugged in?\n \
|
||||
Specify a port explicitly: anvil devices --set COM3"
|
||||
);
|
||||
}
|
||||
|
||||
let idx = board::pick_default_port(&ports).unwrap_or(0);
|
||||
let selected = &ports[idx];
|
||||
|
||||
println!(
|
||||
" Found {} port(s), best match: {}",
|
||||
ports.len(),
|
||||
selected.port_name.bright_white().bold()
|
||||
);
|
||||
if !selected.board_name.is_empty() && selected.board_name != "Unknown" {
|
||||
println!(" Board: {}", selected.board_name);
|
||||
}
|
||||
if selected.is_usb() {
|
||||
println!(" Type: USB serial");
|
||||
}
|
||||
let vp = selected.vid_pid();
|
||||
if !vp.is_empty() {
|
||||
println!(" ID: {}", vp.bright_cyan());
|
||||
}
|
||||
println!();
|
||||
|
||||
(selected.port_name.clone(), vp)
|
||||
}
|
||||
};
|
||||
|
||||
// Write .anvil.local
|
||||
let local_file = project_path.join(".anvil.local");
|
||||
let mut content = String::new();
|
||||
content.push_str("# Machine-specific Anvil config (not tracked by git)\n");
|
||||
content.push_str("# Created by: anvil devices --set\n");
|
||||
content.push_str("# To change: anvil devices --set <PORT>\n");
|
||||
content.push_str("# To remove: delete this file\n");
|
||||
content.push_str(&format!("port = \"{}\"\n", resolved_port));
|
||||
|
||||
if !vid_pid.is_empty() {
|
||||
content.push_str(&format!("vid_pid = \"{}\"\n", vid_pid));
|
||||
}
|
||||
|
||||
fs::write(&local_file, &content)
|
||||
.context(format!("Failed to write {}", local_file.display()))?;
|
||||
|
||||
println!(
|
||||
"{} Saved port {} to {}",
|
||||
"ok".green(),
|
||||
resolved_port.bright_white().bold(),
|
||||
".anvil.local".bright_cyan()
|
||||
);
|
||||
|
||||
if !vid_pid.is_empty() {
|
||||
println!(
|
||||
" Device ID: {} -- port will be tracked even if COM number changes.",
|
||||
vid_pid.bright_cyan()
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
" {}",
|
||||
"This file is gitignored -- each machine keeps its own."
|
||||
.bright_black()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -- Helpers --------------------------------------------------------------
|
||||
|
||||
fn resolve_project_dir(project_dir: Option<&str>) -> Result<PathBuf> {
|
||||
match project_dir {
|
||||
Some(dir) => Ok(PathBuf::from(dir)),
|
||||
None => std::env::current_dir()
|
||||
.context("Could not determine current directory"),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_anvil_project(path: &Path) -> Result<()> {
|
||||
let config_file = path.join(".anvil.toml");
|
||||
if !config_file.exists() {
|
||||
anyhow::bail!(
|
||||
"No .anvil.toml found in {}\n \
|
||||
Run this from inside an Anvil project, or use -d <DIR>",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read port and vid_pid from .anvil.local
|
||||
fn read_anvil_local(path: &Path) -> Result<(String, String)> {
|
||||
let content = fs::read_to_string(path)
|
||||
.context("Failed to read .anvil.local")?;
|
||||
|
||||
let mut port = String::new();
|
||||
let mut vid_pid = String::new();
|
||||
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with('#') || !trimmed.contains('=') {
|
||||
continue;
|
||||
}
|
||||
if let Some((key, val)) = trimmed.split_once('=') {
|
||||
let k = key.trim();
|
||||
let v = val.trim().trim_matches('"');
|
||||
if k == "port" {
|
||||
port = v.to_string();
|
||||
} else if k == "vid_pid" {
|
||||
vid_pid = v.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((port, vid_pid))
|
||||
}
|
||||
Reference in New Issue
Block a user