.claude/skills/cuyamaca-arduino-flash/SKILL.md
Integrate arduino-cli into Cuyamaca for compiling and flashing Arduino sketches to boards. Use this skill whenever the user wants to add board flashing, integrate arduino-cli, implement the compile and upload workflow, detect connected boards, manage arduino-cli as a child process, or references "phase 5", "arduino-cli", "flash", "compile", "upload sketch", "board detection", or "flashing workflow". Also trigger when the user asks about arduino-cli commands, board FQBN strings, core installation, or the compile-flash pipeline. This skill assumes Phase 4 is complete (code generation, sketch approval, tools.json).
npx skillsauth add yuyanghu06/cuyamaca cuyamaca-arduino-flashInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill adds the ability to compile and flash approved sketches to Arduino boards using arduino-cli. The Rust backend manages arduino-cli as a child process, handles board/core detection, and provides a compile → flash pipeline triggered by the "Approve & Flash" button.
On app startup, check if arduino-cli is installed and accessible:
// src-tauri/src/services/arduino.rs
pub struct ArduinoService {
cli_path: PathBuf,
}
impl ArduinoService {
pub async fn detect() -> Result<ArduinoService, String> {
// Try to find arduino-cli in PATH
let output = tokio::process::Command::new("arduino-cli")
.arg("version")
.output()
.await;
match output {
Ok(out) if out.status.success() => {
Ok(ArduinoService {
cli_path: PathBuf::from("arduino-cli"),
})
}
_ => Err("arduino-cli not found".to_string()),
}
}
pub async fn install() -> Result<ArduinoService, String> {
// Platform-specific installation
// macOS: brew install arduino-cli OR direct binary download
// Windows: winget install Arduino.ArduinoCLI OR direct download
// Download from: https://github.com/arduino/arduino-cli/releases
}
}
If arduino-cli is not found:
macOS: Download the macOS arm64 or amd64 binary from GitHub releases. Make it executable with chmod +x.
Windows: Download the Windows zip from GitHub releases. Extract the .exe.
Store the path to the installed binary in the app's config store so it persists across restarts.
impl ArduinoService {
pub async fn list_boards(&self) -> Result<Vec<DetectedBoard>, String> {
let output = self.run_cli(&["board", "list", "--format", "json"]).await?;
let boards: Vec<DetectedBoard> = serde_json::from_str(&output)?;
Ok(boards)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectedBoard {
pub port: String, // /dev/cu.usbmodem14201 or COM3
pub fqbn: Option<String>, // arduino:avr:uno (None if unrecognized)
pub board_name: Option<String>, // "Arduino Uno"
pub protocol: String, // "serial"
}
arduino-cli board list --format json returns a JSON array of connected boards with their port, FQBN (Fully Qualified Board Name), and protocol.
Use this to:
serialport crate enumeration from Phase 3)Arduino cores must be installed before a board can be compiled for. For example, an Arduino Uno needs the arduino:avr core.
impl ArduinoService {
pub async fn ensure_core_installed(&self, fqbn: &str) -> Result<(), String> {
let core = extract_core_from_fqbn(fqbn)?; // "arduino:avr:uno" → "arduino:avr"
// Check if already installed
let installed = self.run_cli(&["core", "list", "--format", "json"]).await?;
if core_is_installed(&installed, &core) {
return Ok(());
}
// Update index and install
self.run_cli(&["core", "update-index"]).await?;
self.run_cli(&["core", "install", &core]).await?;
Ok(())
}
}
fn extract_core_from_fqbn(fqbn: &str) -> Result<String, String> {
// "arduino:avr:uno" → "arduino:avr"
let parts: Vec<&str> = fqbn.splitn(3, ':').collect();
if parts.len() < 2 {
return Err(format!("Invalid FQBN: {}", fqbn));
}
Ok(format!("{}:{}", parts[0], parts[1]))
}
For ESP32 boards, the user needs to add the ESP32 board manager URL first:
arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Handle this automatically when the FQBN starts with esp32:.
impl ArduinoService {
pub async fn compile(
&self,
sketch_path: &Path,
fqbn: &str,
on_progress: impl Fn(CompileProgress) + Send + 'static,
) -> Result<CompileResult, String> {
// Ensure the core is installed
self.ensure_core_installed(fqbn).await?;
on_progress(CompileProgress::Started);
let output = self.run_cli(&[
"compile",
"--fqbn", fqbn,
sketch_path.to_str().unwrap(),
"--format", "json",
]).await;
match output {
Ok(json) => {
on_progress(CompileProgress::Succeeded);
Ok(CompileResult::from_json(&json)?)
}
Err(e) => {
on_progress(CompileProgress::Failed(e.clone()));
Err(e)
}
}
}
}
#[derive(Debug, Clone, Serialize)]
pub enum CompileProgress {
Started,
Succeeded,
Failed(String),
}
#[derive(Debug, Clone, Serialize)]
pub struct CompileResult {
pub binary_size: u64,
pub max_size: u64,
pub warnings: Vec<String>,
}
The sketch must be in a directory named after itself for arduino-cli:
{temp_dir}/sketch/sketch.ino
Create a temporary directory, copy the sketch there, compile, and clean up.
impl ArduinoService {
pub async fn flash(
&self,
sketch_path: &Path,
fqbn: &str,
port: &str,
on_progress: impl Fn(FlashProgress) + Send + 'static,
) -> Result<(), String> {
on_progress(FlashProgress::Compiling);
// Compile first (arduino-cli upload can compile + upload in one step)
let output = self.run_cli(&[
"upload",
"--fqbn", fqbn,
"--port", port,
sketch_path.to_str().unwrap(),
"--format", "json",
]).await;
match output {
Ok(_) => {
on_progress(FlashProgress::Succeeded);
Ok(())
}
Err(e) => {
on_progress(FlashProgress::Failed(e.clone()));
Err(e)
}
}
}
}
#[derive(Debug, Clone, Serialize)]
pub enum FlashProgress {
Compiling,
Uploading,
Succeeded,
Failed(String),
}
arduino-cli upload compiles and uploads in one step. Use this instead of separate compile + upload unless you need to cache the compiled binary.
All arduino-cli invocations go through a shared runner:
impl ArduinoService {
async fn run_cli(&self, args: &[&str]) -> Result<String, String> {
let output = tokio::process::Command::new(&self.cli_path)
.args(args)
.output()
.await
.map_err(|e| format!("Failed to run arduino-cli: {}", e))?;
if output.status.success() {
String::from_utf8(output.stdout)
.map_err(|e| e.to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("arduino-cli error: {}", stderr))
}
}
}
Key considerations:
--format json is used#[tauri::command]
pub async fn detect_arduino_cli(
state: tauri::State<'_, AppState>,
) -> Result<bool, String> {
// Check if arduino-cli is available
// Update health status
}
#[tauri::command]
pub async fn install_arduino_cli(
state: tauri::State<'_, AppState>,
) -> Result<(), String> {
// Download and install arduino-cli
}
#[tauri::command]
pub async fn detect_boards(
state: tauri::State<'_, AppState>,
) -> Result<Vec<DetectedBoard>, String> {
// Run arduino-cli board list
}
#[tauri::command]
pub async fn flash_sketch(
state: tauri::State<'_, AppState>,
on_progress: Channel<FlashEvent>,
) -> Result<(), String> {
// Get active project's sketch, board FQBN, and port from manifest
// Create temp directory with sketch
// Compile and flash
// Stream progress events to frontend via Channel
// On success, transition to runtime mode (Phase 7)
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
pub enum FlashEvent {
Compiling,
Uploading,
Succeeded { binary_size: u64 },
Failed { error: String },
}
Update the "Approve & Flash" button flow:
┌─────────────────────────────────────────┐
│ │
│ ◉ Compiling sketch... │
│ [━━━━━━━━━░░░░░░░░░░] │
│ │
│ Board: Arduino Uno │
│ Port: /dev/cu.usbmodem14201 │
│ │
└─────────────────────────────────────────┘
Before starting the flash:
detect_boards and confirm the port is present)Update the arduino-cli sidebar indicator:
detect_arduino_cli()Also add a board connection indicator — green if a board is detected on the configured port, red otherwise. Poll every 10 seconds or on window focus.
detect_boards returns connected boards with FQBN and portarduino-cli not in PATH: Don't rely on PATH. Store the full binary path and invoke it directly. The auto-install puts it in the app data directory.
Port busy (macOS): If another process has the serial port open (like Serial Monitor in Arduino IDE), flashing will fail. The error message should mention closing other serial connections.
Core installation takes time: The first compile for a new board type requires downloading the core (can be 100MB+ for ESP32). Show a progress indicator and don't timeout.
Board requires manual reset: Some boards (Leonardo, Micro) need a manual reset to enter bootloader mode. Detect this from the board type and show instructions.
Windows COM port permissions: Windows may require the user to install USB drivers separately. Include troubleshooting guidance for common drivers (CH340, CP2102, FTDI).
development
Build the Settings view and apply final polish to Cuyamaca — model configuration UI, API key management, process health monitoring, accessibility improvements, responsive refinements, and overall UX tightening. Use this skill whenever the user wants to build the settings view, configure model providers in the UI, add API key entry, polish the app's responsiveness, improve accessibility, refine animations, add keyboard navigation, or references "phase 8", "settings", "settings view", "API key management", "model configuration", "accessibility", "polish", "keyboard navigation", or "responsive refinement". Also trigger when the user asks about WCAG compliance for glass effects, reduce transparency mode, or the settings UI for Cuyamaca. This skill assumes Phase 7 is complete (runtime agent loop, all core functionality working).
tools
Build serial communication, structured output parsing, sensor state management, and sensor visualization rendering for Cuyamaca. Use this skill whenever the user wants to implement serial port reading/writing, parse structured sensor output, build the sensor state panel, render sensor visualization images, manage the serial connection lifecycle, or references "phase 6", "serial communication", "serial port", "sensor parsing", "sensor state", "sensor visualization", "structured output", "serial monitor", or "serial reader". Also trigger when the user asks about the SENSOR_ID:VALUE protocol, concurrent serial read/write, sensor image rendering, or real-time state updates. This skill assumes Phase 5 is complete (arduino-cli integration, compile and flash working).
testing
Scaffold a Tauri v2 desktop app for the Cuyamaca project — an Arduino robotics controller with natural language control. Use this skill whenever the user wants to initialize the Cuyamaca project, set up the Tauri v2 scaffold, create the base layout and warm-white liquid glass UI theme, verify the IPC bridge, or references "phase 1", "scaffold", "project setup", "initialize Cuyamaca", "create the app skeleton", or "base layout". Also trigger when the user asks about Cuyamaca's three-panel layout, the warm-white glass design language, or setting up the Tauri project structure from scratch.
tools
Build the runtime agent loop for Cuyamaca — the agentic control loop where the runtime model reads sensor context, decides tool calls, writes serial commands, and iterates until the user stops it. Use this skill whenever the user wants to implement the runtime window, build the agent loop, assemble multimodal context for the runtime model, implement tool call dispatch via serial, add the kill button, or references "phase 7", "runtime agent", "agent loop", "runtime window", "runtime model", "tool calling", "kill button", "agentic loop", "multimodal context", or "control loop". Also trigger when the user asks about feeding sensor data to a vision model, executing tool calls as serial commands, or the observe-decide-act cycle. This skill assumes Phase 6 is complete (serial communication, sensor parsing, sensor visualization).