From 60ba7c7bedf5233e2dd20a1a494b4922e1191396 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Wed, 18 Feb 2026 20:15:46 -0600 Subject: [PATCH] Better --- Cargo.toml | 4 +- src/commands/devices.rs | 173 +++++++++++++++++++++++- src/commands/mod.rs | 3 +- src/commands/refresh.rs | 191 +++++++++++++++++++++++++++ src/main.rs | 48 ++++++- templates/basic/_detect_port.ps1 | 29 ++++ templates/basic/_dot_anvil.toml.tmpl | 1 - templates/basic/_dot_gitignore | 3 + templates/basic/build.bat | 46 +++---- templates/basic/monitor.bat | 62 +++++++-- templates/basic/upload.bat | 80 ++++++----- 11 files changed, 559 insertions(+), 81 deletions(-) create mode 100644 src/commands/refresh.rs create mode 100644 templates/basic/_detect_port.ps1 diff --git a/Cargo.toml b/Cargo.toml index 9e0081e..97ef13b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,10 @@ colored = "2.1" which = "5.0" home = "=0.5.9" -[dev-dependencies] +# Temp dirs (for refresh command) tempfile = "3.13" + +[dev-dependencies] assert_cmd = "2.0" predicates = "3.1" diff --git a/src/commands/devices.rs b/src/commands/devices.rs index 4095a46..d25ddbb 100644 --- a/src/commands/devices.rs +++ b/src/commands/devices.rs @@ -1,5 +1,7 @@ -use anyhow::Result; +use anyhow::{Result, Context}; use colored::*; +use std::path::PathBuf; +use std::fs; use crate::board; @@ -69,5 +71,174 @@ pub fn scan_devices() -> Result<()> { println!(); } + Ok(()) +} + +/// Read and display the saved port from .anvil.local. +pub fn get_port(project_dir: Option<&str>) -> Result<()> { + let project_path = match project_dir { + Some(dir) => PathBuf::from(dir), + None => std::env::current_dir() + .context("Could not determine current directory")?, + }; + + let config_file = project_path.join(".anvil.toml"); + if !config_file.exists() { + anyhow::bail!( + "No .anvil.toml found in {}\n \ + Run this from inside an Anvil project, or specify the path:\n \ + anvil devices --get ", + project_path.display() + ); + } + + 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 content = fs::read_to_string(&local_file) + .context("Failed to read .anvil.local")?; + + let mut port = 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('=') { + if key.trim() == "port" { + port = val.trim().trim_matches('"').to_string(); + } + } + } + + if port.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()); + } else { + println!( + "{} Saved port: {}", + "ok".green(), + port.bright_white().bold() + ); + println!( + " {}", + format!("Source: {}", local_file.display()).bright_black() + ); + println!(); + println!(" To change: {}", "anvil devices --set ".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 = match project_dir { + Some(dir) => PathBuf::from(dir), + None => std::env::current_dir() + .context("Could not determine current directory")?, + }; + + // Verify this is an Anvil project + let config_file = project_path.join(".anvil.toml"); + if !config_file.exists() { + anyhow::bail!( + "No .anvil.toml found in {}\n \ + Run this from inside an Anvil project, or specify the path:\n \ + anvil devices --set [PORT] ", + project_path.display() + ); + } + + // Resolve the port + let resolved_port = match port { + Some(p) => { + // Normalize to uppercase on Windows (COM3 not com3) + if cfg!(target_os = "windows") { + p.to_uppercase() + } else { + p.to_string() + } + } + None => { + // Auto-detect the best port + println!("Detecting best port..."); + println!(); + + let ports = board::list_ports(); + 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"); + } + println!(); + + selected.port_name.clone() + } + }; + + // Write .anvil.local + let local_file = project_path.join(".anvil.local"); + let content = format!( + "# Machine-specific Anvil config (not tracked by git)\n\ + # Created by: anvil devices --set\n\ + # To change: anvil devices --set \n\ + # To remove: delete this file\n\ + port = \"{}\"\n", + resolved_port + ); + + 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() + ); + println!( + " {}", + "This file is gitignored -- each machine keeps its own." + .bright_black() + ); + Ok(()) } \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 884cbcb..e808bac 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod new; pub mod doctor; pub mod setup; -pub mod devices; \ No newline at end of file +pub mod devices; +pub mod refresh; \ No newline at end of file diff --git a/src/commands/refresh.rs b/src/commands/refresh.rs new file mode 100644 index 0000000..e3e25ff --- /dev/null +++ b/src/commands/refresh.rs @@ -0,0 +1,191 @@ +use anyhow::{Result, Context}; +use colored::*; +use std::path::PathBuf; +use std::fs; + +use crate::project::config::ProjectConfig; +use crate::templates::{TemplateManager, TemplateContext}; +use crate::version::ANVIL_VERSION; + +/// Files that anvil owns and can safely refresh. +/// These are build/deploy infrastructure -- not user source code. +const REFRESHABLE_FILES: &[&str] = &[ + "build.sh", + "build.bat", + "upload.sh", + "upload.bat", + "monitor.sh", + "monitor.bat", + "_detect_port.ps1", + "test/run_tests.sh", + "test/run_tests.bat", +]; + +pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> { + // Resolve project directory + let project_path = match project_dir { + Some(dir) => PathBuf::from(dir), + None => std::env::current_dir() + .context("Could not determine current directory")?, + }; + + let project_root = ProjectConfig::find_project_root(&project_path)?; + let config = ProjectConfig::load(&project_root)?; + + println!( + "Refreshing project: {}", + config.project.name.bright_white().bold() + ); + println!( + "Project directory: {}", + project_root.display().to_string().bright_black() + ); + println!(); + + // Generate fresh copies of all refreshable files from the template + let template_name = "basic"; + let context = TemplateContext { + project_name: config.project.name.clone(), + anvil_version: ANVIL_VERSION.to_string(), + }; + + // Extract template into a temp directory so we can compare + let temp_dir = tempfile::tempdir() + .context("Failed to create temp directory")?; + TemplateManager::extract(template_name, temp_dir.path(), &context)?; + + // Compare each refreshable file + let mut up_to_date = Vec::new(); + let mut will_create = Vec::new(); + let mut has_changes = Vec::new(); + + for &filename in REFRESHABLE_FILES { + let existing = project_root.join(filename); + let fresh = temp_dir.path().join(filename); + + if !fresh.exists() { + // Template doesn't produce this file (shouldn't happen) + continue; + } + + let fresh_content = fs::read(&fresh) + .context(format!("Failed to read template file: {}", filename))?; + + if !existing.exists() { + will_create.push(filename); + continue; + } + + let existing_content = fs::read(&existing) + .context(format!("Failed to read project file: {}", filename))?; + + if existing_content == fresh_content { + up_to_date.push(filename); + } else { + has_changes.push(filename); + } + } + + // Report status + if !up_to_date.is_empty() { + println!( + "{} {} file(s) already up to date", + "ok".green(), + up_to_date.len() + ); + } + + if !will_create.is_empty() { + for f in &will_create { + println!(" {} {} (new)", "+".bright_green(), f.bright_white()); + } + } + + if !has_changes.is_empty() { + for f in &has_changes { + println!( + " {} {} (differs from latest)", + "~".bright_yellow(), + f.bright_white() + ); + } + } + + // Decide what to do + if has_changes.is_empty() && will_create.is_empty() { + println!(); + println!( + "{}", + "All scripts are up to date. Nothing to do." + .bright_green() + .bold() + ); + return Ok(()); + } + + if !has_changes.is_empty() && !force { + println!(); + println!( + "{} {} script(s) differ from the latest Anvil templates.", + "!".bright_yellow(), + has_changes.len() + ); + println!( + "This is normal after upgrading Anvil. To update them, run:" + ); + println!(); + println!(" {}", "anvil refresh --force".bright_cyan()); + println!(); + println!( + " {}", + "Only build scripts are replaced. Your .anvil.toml and source code are never touched." + .bright_black() + ); + return Ok(()); + } + + // Apply updates + let files_to_write: Vec<&str> = if force { + will_create.iter().chain(has_changes.iter()).copied().collect() + } else { + will_create.to_vec() + }; + + for filename in &files_to_write { + let fresh = temp_dir.path().join(filename); + let dest = project_root.join(filename); + + // Ensure parent directory exists + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent)?; + } + + fs::copy(&fresh, &dest) + .context(format!("Failed to write: {}", filename))?; + } + + // Make shell scripts executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + for filename in &files_to_write { + if filename.ends_with(".sh") { + let path = project_root.join(filename); + if let Ok(meta) = fs::metadata(&path) { + let mut perms = meta.permissions(); + perms.set_mode(0o755); + let _ = fs::set_permissions(&path, perms); + } + } + } + } + + println!(); + println!( + "{} Updated {} file(s).", + "ok".green(), + files_to_write.len() + ); + + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 30a2710..9875859 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,32 @@ enum Commands { Setup, /// List connected boards and serial ports - Devices, + Devices { + /// Save a port to .anvil.local for this project + #[arg(long, conflicts_with = "get")] + set: bool, + + /// Show the saved port for this project + #[arg(long, conflicts_with = "set")] + get: bool, + + /// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set. + port_or_dir: Option, + + /// Path to project directory (defaults to current directory) + #[arg(long, short = 'd', value_name = "DIR")] + dir: Option, + }, + + /// Update project scripts to the latest version + Refresh { + /// Path to project directory (defaults to current directory) + dir: Option, + + /// Overwrite scripts even if they have been modified + #[arg(long)] + force: bool, + }, } fn main() -> Result<()> { @@ -77,8 +102,25 @@ fn main() -> Result<()> { Commands::Setup => { commands::setup::run_setup() } - Commands::Devices => { - commands::devices::scan_devices() + Commands::Devices { set, get, port_or_dir, dir } => { + if set { + commands::devices::set_port( + port_or_dir.as_deref(), + dir.as_deref(), + ) + } else if get { + commands::devices::get_port( + dir.as_deref().or(port_or_dir.as_deref()), + ) + } else { + commands::devices::scan_devices() + } + } + Commands::Refresh { dir, force } => { + commands::refresh::run_refresh( + dir.as_deref(), + force, + ) } } } diff --git a/templates/basic/_detect_port.ps1 b/templates/basic/_detect_port.ps1 new file mode 100644 index 0000000..e9bc21b --- /dev/null +++ b/templates/basic/_detect_port.ps1 @@ -0,0 +1,29 @@ +# _detect_port.ps1 -- Detect the best serial port via arduino-cli +# +# Called by upload.bat and monitor.bat. Outputs a single port name +# (e.g. COM3) or nothing if no port is found. +# Prefers USB serial ports over legacy motherboard COM ports. + +$ErrorActionPreference = 'SilentlyContinue' + +$raw = arduino-cli board list --format json 2>$null +if (-not $raw) { exit } + +$data = $raw | ConvertFrom-Json +if (-not $data.detected_ports) { exit } + +$serial = $data.detected_ports | Where-Object { $_.port.protocol -eq 'serial' } +if (-not $serial) { exit } + +# Prefer USB serial ports (skip legacy COM1-style ports) +$usb = $serial | Where-Object { $_.port.protocol_label -like '*USB*' } | Select-Object -First 1 +if ($usb) { + Write-Output $usb.port.address + exit +} + +# Fall back to any serial port +$any = $serial | Select-Object -First 1 +if ($any) { + Write-Output $any.port.address +} \ No newline at end of file diff --git a/templates/basic/_dot_anvil.toml.tmpl b/templates/basic/_dot_anvil.toml.tmpl index b098e40..c8afc54 100644 --- a/templates/basic/_dot_anvil.toml.tmpl +++ b/templates/basic/_dot_anvil.toml.tmpl @@ -10,4 +10,3 @@ extra_flags = ["-Werror"] [monitor] baud = 115200 -# port = "/dev/ttyUSB0" # Uncomment to skip auto-detect diff --git a/templates/basic/_dot_gitignore b/templates/basic/_dot_gitignore index 6032bd0..e90b836 100644 --- a/templates/basic/_dot_gitignore +++ b/templates/basic/_dot_gitignore @@ -2,6 +2,9 @@ .build/ test/build/ +# Machine-specific config (created by: anvil devices --set) +.anvil.local + # IDE .vscode/.browse* .vscode/*.log diff --git a/templates/basic/build.bat b/templates/basic/build.bat index 4983f00..26db5f3 100644 --- a/templates/basic/build.bat +++ b/templates/basic/build.bat @@ -19,23 +19,21 @@ if not exist "%CONFIG%" ( ) :: -- Parse .anvil.toml ---------------------------------------------------- -for /f "tokens=1,* delims==" %%a in ('findstr /b "name " "%CONFIG%"') do ( - set "SKETCH_NAME=%%b" +:: Read file directly, skip comments and section headers +for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do ( + set "_K=%%a" + if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" ( + set "_K=!_K: =!" + set "_V=%%b" + if defined _V ( + set "_V=!_V: =!" + set "_V=!_V:"=!" + ) + if "!_K!"=="name" set "SKETCH_NAME=!_V!" + if "!_K!"=="fqbn" set "FQBN=!_V!" + if "!_K!"=="warnings" set "WARNINGS=!_V!" + ) ) -for /f "tokens=1,* delims==" %%a in ('findstr /b "fqbn " "%CONFIG%"') do ( - set "FQBN=%%b" -) -for /f "tokens=1,* delims==" %%a in ('findstr /b "warnings " "%CONFIG%"') do ( - set "WARNINGS=%%b" -) - -:: Strip quotes and whitespace -set "SKETCH_NAME=%SKETCH_NAME: =%" -set "SKETCH_NAME=%SKETCH_NAME:"=%" -set "FQBN=%FQBN: =%" -set "FQBN=%FQBN:"=%" -set "WARNINGS=%WARNINGS: =%" -set "WARNINGS=%WARNINGS:"=%" if "%SKETCH_NAME%"=="" ( echo FAIL: Could not read project name from .anvil.toml @@ -102,19 +100,7 @@ echo. if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" -set "COMPILE_CMD=arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS%" - -if not "%BUILD_FLAGS%"=="" ( - set "COMPILE_CMD=%COMPILE_CMD% --build-property "build.extra_flags=%BUILD_FLAGS%"" -) - -if not "%VERBOSE%"=="" ( - set "COMPILE_CMD=%COMPILE_CMD% %VERBOSE%" -) - -set "COMPILE_CMD=%COMPILE_CMD% "%SKETCH_DIR%"" - -%COMPILE_CMD% +arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "build.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%" if errorlevel 1 ( echo. echo FAIL: Compilation failed. @@ -123,4 +109,4 @@ if errorlevel 1 ( echo. echo ok Compile succeeded. -echo. +echo. \ No newline at end of file diff --git a/templates/basic/monitor.bat b/templates/basic/monitor.bat index 2291038..ef329db 100644 --- a/templates/basic/monitor.bat +++ b/templates/basic/monitor.bat @@ -12,6 +12,7 @@ setlocal enabledelayedexpansion set "SCRIPT_DIR=%~dp0" set "CONFIG=%SCRIPT_DIR%.anvil.toml" +set "LOCAL_CONFIG=%SCRIPT_DIR%.anvil.local" if not exist "%CONFIG%" ( echo FAIL: No .anvil.toml found in %SCRIPT_DIR% @@ -19,11 +20,37 @@ if not exist "%CONFIG%" ( ) :: -- Parse .anvil.toml ---------------------------------------------------- -for /f "tokens=1,* delims==" %%a in ('findstr /b "baud " "%CONFIG%"') do ( - set "BAUD=%%b" +:: Read file directly, skip comments and section headers +for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do ( + set "_K=%%a" + if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" ( + set "_K=!_K: =!" + set "_V=%%b" + if defined _V ( + set "_V=!_V: =!" + set "_V=!_V:"=!" + ) + if "!_K!"=="baud" set "BAUD=!_V!" + ) ) -set "BAUD=%BAUD: =%" -set "BAUD=%BAUD:"=%" + +:: -- Parse .anvil.local (machine-specific, not in git) -------------------- +set "LOCAL_PORT=" +if exist "%LOCAL_CONFIG%" ( + for /f "usebackq tokens=1,* delims==" %%a in ("%LOCAL_CONFIG%") do ( + set "_K=%%a" + if not "!_K:~0,1!"=="#" ( + set "_K=!_K: =!" + set "_V=%%b" + if defined _V ( + set "_V=!_V: =!" + set "_V=!_V:"=!" + ) + if "!_K!"=="port" set "LOCAL_PORT=!_V!" + ) + ) +) + if "%BAUD%"=="" set "BAUD=115200" :: -- Parse arguments ------------------------------------------------------ @@ -54,21 +81,28 @@ if errorlevel 1 ( exit /b 1 ) -:: -- Auto-detect port ----------------------------------------------------- +:: -- Resolve port --------------------------------------------------------- +:: Priority: -p flag > .anvil.local > auto-detect if "%PORT%"=="" ( - for /f "tokens=1" %%p in ('arduino-cli board list 2^>nul ^| findstr /i "serial" ^| findstr /n "." ^| findstr "^1:"') do ( - set "PORT=%%p" + if not "%LOCAL_PORT%"=="" ( + set "PORT=!LOCAL_PORT!" + echo info Using port !PORT! ^(from .anvil.local^) + ) else ( + :: Use PowerShell helper for reliable JSON-based detection + for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%_detect_port.ps1"') do ( + if "!PORT!"=="" set "PORT=%%p" + ) + if "!PORT!"=="" ( + echo FAIL: No serial port detected. Specify with: monitor.bat -p COM3 + echo Or save a default: anvil devices --set COM3 + exit /b 1 + ) + echo warn Auto-detected port: !PORT! ^(use -p to override, or: anvil devices --set^) ) - set "PORT=!PORT:1:=!" - if "!PORT!"=="" ( - echo FAIL: No serial port detected. Specify with: monitor.bat -p COM3 - exit /b 1 - ) - echo warn Auto-detected port: !PORT! (use -p to override) ) :: -- Monitor -------------------------------------------------------------- echo Opening serial monitor on %PORT% at %BAUD% baud... echo Press Ctrl+C to exit. echo. -arduino-cli monitor -p %PORT% -c "baudrate=%BAUD%" +arduino-cli monitor -p %PORT% -c "baudrate=%BAUD%" \ No newline at end of file diff --git a/templates/basic/upload.bat b/templates/basic/upload.bat index 25bb8ef..15f6c8b 100644 --- a/templates/basic/upload.bat +++ b/templates/basic/upload.bat @@ -14,6 +14,7 @@ setlocal enabledelayedexpansion set "SCRIPT_DIR=%~dp0" set "CONFIG=%SCRIPT_DIR%.anvil.toml" +set "LOCAL_CONFIG=%SCRIPT_DIR%.anvil.local" if not exist "%CONFIG%" ( echo FAIL: No .anvil.toml found in %SCRIPT_DIR% @@ -21,27 +22,39 @@ if not exist "%CONFIG%" ( ) :: -- Parse .anvil.toml ---------------------------------------------------- -for /f "tokens=1,* delims==" %%a in ('findstr /b "name " "%CONFIG%"') do ( - set "SKETCH_NAME=%%b" -) -for /f "tokens=1,* delims==" %%a in ('findstr /b "fqbn " "%CONFIG%"') do ( - set "FQBN=%%b" -) -for /f "tokens=1,* delims==" %%a in ('findstr /b "warnings " "%CONFIG%"') do ( - set "WARNINGS=%%b" -) -for /f "tokens=1,* delims==" %%a in ('findstr /b "baud " "%CONFIG%"') do ( - set "BAUD=%%b" +:: Read file directly, skip comments and section headers +for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do ( + set "_K=%%a" + if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" ( + set "_K=!_K: =!" + set "_V=%%b" + if defined _V ( + set "_V=!_V: =!" + set "_V=!_V:"=!" + ) + if "!_K!"=="name" set "SKETCH_NAME=!_V!" + if "!_K!"=="fqbn" set "FQBN=!_V!" + if "!_K!"=="warnings" set "WARNINGS=!_V!" + if "!_K!"=="baud" set "BAUD=!_V!" + ) ) -set "SKETCH_NAME=%SKETCH_NAME: =%" -set "SKETCH_NAME=%SKETCH_NAME:"=%" -set "FQBN=%FQBN: =%" -set "FQBN=%FQBN:"=%" -set "WARNINGS=%WARNINGS: =%" -set "WARNINGS=%WARNINGS:"=%" -set "BAUD=%BAUD: =%" -set "BAUD=%BAUD:"=%" +:: -- Parse .anvil.local (machine-specific, not in git) -------------------- +set "LOCAL_PORT=" +if exist "%LOCAL_CONFIG%" ( + for /f "usebackq tokens=1,* delims==" %%a in ("%LOCAL_CONFIG%") do ( + set "_K=%%a" + if not "!_K:~0,1!"=="#" ( + set "_K=!_K: =!" + set "_V=%%b" + if defined _V ( + set "_V=!_V: =!" + set "_V=!_V:"=!" + ) + if "!_K!"=="port" set "LOCAL_PORT=!_V!" + ) + ) +) if "%SKETCH_NAME%"=="" ( echo FAIL: Could not read project name from .anvil.toml @@ -84,18 +97,25 @@ if errorlevel 1 ( exit /b 1 ) -:: -- Auto-detect port ----------------------------------------------------- +:: -- Resolve port --------------------------------------------------------- +:: Priority: -p flag > .anvil.local > auto-detect if "%PORT%"=="" ( - for /f "tokens=1" %%p in ('arduino-cli board list 2^>nul ^| findstr /i "serial" ^| findstr /n "." ^| findstr "^1:"') do ( - set "PORT=%%p" + if not "%LOCAL_PORT%"=="" ( + set "PORT=!LOCAL_PORT!" + echo info Using port !PORT! ^(from .anvil.local^) + ) else ( + :: Use PowerShell helper for reliable JSON-based detection + for /f "delims=" %%p in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%_detect_port.ps1"') do ( + if "!PORT!"=="" set "PORT=%%p" + ) + if "!PORT!"=="" ( + echo FAIL: No serial port detected. Is the board plugged in? + echo Specify manually: upload.bat -p COM3 + echo Or save a default: anvil devices --set COM3 + exit /b 1 + ) + echo warn Auto-detected port: !PORT! ^(use -p to override, or: anvil devices --set^) ) - :: Strip the line number prefix - set "PORT=!PORT:1:=!" - if "!PORT!"=="" ( - echo FAIL: No serial port detected. Specify with: upload.bat -p COM3 - exit /b 1 - ) - echo warn Auto-detected port: !PORT! (use -p to override) ) :: -- Clean ---------------------------------------------------------------- @@ -141,4 +161,4 @@ if "%DO_MONITOR%"=="1" ( echo Press Ctrl+C to exit. echo. arduino-cli monitor -p %PORT% -c "baudrate=%BAUD%" -) +) \ No newline at end of file