#!/usr/bin/env bash # # arduino-build.sh -- Build, upload, and monitor Arduino sketches # for SparkFun RedBoard (and other Uno-compatible boards) # # Run with --help for full usage information. # Run with --setup for first-time toolchain installation. # Run with --devices to find connected boards. set -euo pipefail # -- Configuration (override via env vars if needed) ------------------------- FQBN="${ARDUINO_FQBN:-arduino:avr:uno}" BAUD="${ARDUINO_BAUD:-115200}" BUILD_DIR="${ARDUINO_BUILD_DIR:-/tmp/arduino-build}" ARDUINO_CLI="${ARDUINO_CLI_BIN:-arduino-cli}" VERSION="1.2.0" # -- Color output (disabled if stdout is not a terminal) --------------------- if [[ -t 1 ]]; then RED=$'\033[0;31m'; GRN=$'\033[0;32m'; YEL=$'\033[0;33m' CYN=$'\033[0;36m'; BLD=$'\033[1m'; DIM=$'\033[2m'; RST=$'\033[0m' else RED=''; GRN=''; YEL=''; CYN=''; BLD=''; DIM=''; RST='' fi info() { echo -e "${CYN}[INFO]${RST} $*"; } ok() { echo -e "${GRN}[ OK ]${RST} $*"; } warn() { echo -e "${YEL}[WARN]${RST} $*"; } die() { echo -e "${RED}[ERR]${RST} $*" >&2; exit 1; } # ============================================================================ # HELP # ============================================================================ show_help() { cat < ${BLD}DESCRIPTION${RST} A self-contained script that replaces the Arduino IDE for command-line workflows. Compiles sketches with arduino-cli, uploads to the board, and optionally opens a serial monitor -- all in one command. Designed for the SparkFun RedBoard but works with any Uno-compatible board. Just plug in, run --devices to find your port, and go. $(_help_section_commands) $(_help_section_build_options) $(_help_section_config_options) $(_help_section_env_vars) $(_help_section_project_config) $(_help_section_examples) $(_help_section_cheatsheet) $(_help_section_sketch_layout) $(_help_section_redboard) $(_help_section_port_permissions) $(_help_section_first_time) $(_help_section_troubleshooting) $(_help_section_usb_ids) EOF } _help_section_commands() { cat < blink/blink.ino << 'SKETCH' void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); Serial.println("ON"); delay(500); digitalWrite(LED_BUILTIN, LOW); Serial.println("OFF"); delay(500); } SKETCH EOF } _help_section_redboard() { cat < blink/blink.ino << 'SKETCH' void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(500); digitalWrite(LED_BUILTIN, LOW); delay(500); } SKETCH 5. Build and upload: ./arduino-build.sh ./blink 6. Verify the LED on the board is blinking. Done! EOF } _help_section_troubleshooting() { cat </dev/null)" [[ -f "$usb_dev_path/product" ]] && product="$(cat "$usb_dev_path/product" 2>/dev/null)" [[ -f "$usb_dev_path/idVendor" ]] && vid="$(cat "$usb_dev_path/idVendor" 2>/dev/null)" [[ -f "$usb_dev_path/idProduct" ]] && pid="$(cat "$usb_dev_path/idProduct" 2>/dev/null)" [[ -f "$usb_dev_path/serial" ]] && serial="$(cat "$usb_dev_path/serial" 2>/dev/null)" [[ -n "$vendor" ]] && echo " Manufacturer: $vendor" [[ -n "$product" ]] && echo " Product: $product" [[ -n "$vid" ]] && [[ -n "$pid" ]] && echo " USB ID: ${vid}:${pid}" [[ -n "$serial" ]] && echo " Serial: $serial" [[ -n "$driver" ]] && echo " Driver: $driver" echo -n " Likely board: " case "${vid}:${pid}" in 1a86:7523) echo "SparkFun RedBoard / CH340 clone" ;; 1a86:55d4) echo "SparkFun RedBoard Qwiic / CH340C" ;; 2341:0043) echo "Arduino Uno R3 (ATmega16U2)" ;; 2341:0001) echo "Arduino Uno (older revision)" ;; 2341:0010) echo "Arduino Mega 2560" ;; 2341:0042) echo "Arduino Mega 2560 R3" ;; 2341:003d) echo "Arduino Due (Programming Port)" ;; 2341:003e) echo "Arduino Due (Native USB)" ;; 2341:8036) echo "Arduino Leonardo" ;; 2341:8037) echo "Arduino Micro" ;; 0403:6001) echo "FTDI FT232R (older RedBoard / clone)" ;; 0403:6015) echo "FTDI FT231X (SparkFun breakout)" ;; 10c4:ea60) echo "CP2102 (NodeMCU / some ESP32 boards)" ;; 1a86:*) echo "CH340 variant (likely Arduino clone)" ;; 2341:*) echo "Arduino (unknown model -- USB ID ${vid}:${pid})" ;; *) echo "Unknown device -- USB ID ${vid}:${pid}" ;; esac else # Could not find USB device node, but we may still have the driver [[ -n "$driver" ]] && echo " Driver: $driver" echo " USB info: Could not read (sysfs walk failed)" fi if [[ -w "$port" ]]; then echo -e " Permissions: ${GRN}OK (writable)${RST}" else echo -e " Permissions: ${RED}NOT writable${RST} -- run: sudo usermod -aG dialout \$USER" fi echo "" done if [[ $found -eq 0 ]]; then echo -e " ${YEL}No serial devices found.${RST}" echo "" echo " Checklist:" echo " 1. Is the board plugged in via USB?" echo " 2. Is the USB cable a data cable (not charge-only)?" echo " 3. Check kernel log: dmesg | tail -20" echo " 4. Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341|0403'" echo " 5. Try a different USB port or cable" echo "" fi if command -v "$ARDUINO_CLI" &>/dev/null; then echo -e "${BLD}=== arduino-cli Board Detection ===${RST}" echo "" $ARDUINO_CLI board list 2>/dev/null || warn "arduino-cli board list failed (is the core installed?)" echo "" else warn "arduino-cli not found -- run --setup first" echo "" fi } # ============================================================================ # FIRST-TIME SETUP # ============================================================================ run_setup() { echo "" echo -e "${BLD}=== Arduino CLI First-Time Setup ===${RST}" echo "" if ! command -v "$ARDUINO_CLI" &>/dev/null; then die "arduino-cli not found in PATH. Install it: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh sudo mv bin/arduino-cli /usr/local/bin/ Or via package manager: sudo apt install arduino-cli (Debian/Ubuntu) brew install arduino-cli (macOS) yay -S arduino-cli (Arch) Then re-run: ./arduino-build.sh --setup" fi ok "arduino-cli found: $($ARDUINO_CLI version 2>/dev/null | head -1)" info "Updating board index..." $ARDUINO_CLI core update-index ok "Board index updated." if $ARDUINO_CLI core list 2>/dev/null | grep -q "arduino:avr"; then ok "arduino:avr core already installed." else info "Installing arduino:avr core (this may take a minute)..." $ARDUINO_CLI core install arduino:avr ok "arduino:avr core installed." fi if command -v avr-size &>/dev/null; then ok "avr-size found (binary size reporting enabled)." else warn "avr-size not found. Install for binary size details:" warn " sudo apt install gcc-avr" fi echo "" if groups 2>/dev/null | grep -q dialout; then ok "User '$(whoami)' is in the 'dialout' group." else warn "User '$(whoami)' is NOT in the 'dialout' group." warn "Fix with: sudo usermod -aG dialout \$USER" warn "Then log out and back in (or reboot)." fi echo "" scan_devices echo -e "${BLD}Setup complete!${RST}" echo "" echo " Next steps:" echo " 1. Plug in your RedBoard" echo " 2. ./arduino-build.sh --devices" echo " 3. ./arduino-build.sh ./your_sketch" echo "" } # ============================================================================ # AUTO-DETECT SERIAL PORT # ============================================================================ auto_detect_port() { local candidates=() for p in /dev/ttyUSB* /dev/ttyACM*; do [[ -e "$p" ]] && candidates+=("$p") done if [[ ${#candidates[@]} -eq 0 ]]; then die "No serial ports found. Is the board plugged in? Run: ./arduino-build.sh --devices" fi if [[ ${#candidates[@]} -eq 1 ]]; then echo "${candidates[0]}" return fi warn "Multiple serial ports detected:" for p in "${candidates[@]}"; do warn " $p" done # Prefer /dev/ttyUSB* (CH340 on RedBoard) over /dev/ttyACM* for p in "${candidates[@]}"; do if [[ "$p" == /dev/ttyUSB* ]]; then warn "Auto-selected $p (CH340 -- likely RedBoard)" warn "Use -p to override if this is wrong." echo "$p" return fi done warn "Auto-selected ${candidates[0]}. Use -p to override." echo "${candidates[0]}" } # ============================================================================ # PROJECT CONFIG DISCOVERY # ============================================================================ # Walk up from a directory to find .arduino-build.conf at the project root. # Echoes the project root path. Returns 1 if not found. find_project_root() { local dir="$1" local max_depth=10 local depth=0 while [[ $depth -lt $max_depth ]] && [[ "$dir" != "/" ]]; do if [[ -f "$dir/.arduino-build.conf" ]]; then echo "$dir" return 0 fi dir="$(dirname "$dir")" depth=$((depth + 1)) done return 1 } # Load project config and build extra compiler flags. # Sets PROJECT_ROOT and EXTRA_COMPILE_FLAGS as side effects. PROJECT_ROOT="" EXTRA_COMPILE_FLAGS="" load_project_config() { local sketch_dir="$1" local conf_file="" PROJECT_ROOT="$(find_project_root "$sketch_dir")" || { info "No .arduino-build.conf found (using default settings)." return 0 } conf_file="$PROJECT_ROOT/.arduino-build.conf" info "Project config: ${DIM}${conf_file}${RST}" # Parse the conf file (only accept known variables, ignore everything else) local include_dirs="" extra_flags="" while IFS='=' read -r key value; do # Strip leading/trailing whitespace key="$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" value="$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/^"//;s/"$//')" # Skip blank lines and comments [[ -z "$key" || "$key" == \#* ]] && continue case "$key" in INCLUDE_DIRS) include_dirs="$value" ;; EXTRA_BUILD_FLAGS) extra_flags="$value" ;; esac done < "$conf_file" # Build -I flags from INCLUDE_DIRS (paths relative to project root) local flags="" for dir in $include_dirs; do local abs_dir="$PROJECT_ROOT/$dir" if [[ -d "$abs_dir" ]]; then flags="$flags -I$abs_dir" info " Include: ${DIM}$dir${RST}" else warn " Include dir not found: $dir (skipped)" fi done # Append any extra flags if [[ -n "$extra_flags" ]]; then flags="$flags $extra_flags" info " Extra flags: ${DIM}$extra_flags${RST}" fi # Trim leading space EXTRA_COMPILE_FLAGS="$(echo "$flags" | sed 's/^[[:space:]]*//')" } # ============================================================================ # PERSISTENT SERIAL MONITOR (--watch) # ============================================================================ run_watch_monitor() { local port="$1" local baud="$2" # If no port specified, auto-detect once to get a starting point if [[ -z "$port" ]]; then # Don't die if no port yet -- we'll wait for it for p in /dev/ttyUSB* /dev/ttyACM*; do if [[ -e "$p" ]]; then port="$p" break fi done if [[ -z "$port" ]]; then # Default to the most common RedBoard port port="/dev/ttyUSB0" info "No port detected yet. Waiting for $port to appear..." fi fi info "Persistent monitor on ${BLD}$port${RST} at ${baud} baud" info "Reconnects automatically after upload / reset / replug." info "Press Ctrl+C to exit." echo "" trap 'echo ""; info "Monitor stopped."; exit 0' INT TERM while true; do # Wait for port to exist if [[ ! -e "$port" ]]; then echo -e "${DIM}--- Waiting for $port ...${RST}" while [[ ! -e "$port" ]]; do sleep 0.5 done # Brief settle time after port appears (CH340 needs a moment) sleep 1.0 echo -e "${GRN}--- $port connected ---${RST}" fi # Open the monitor (will exit when port disappears) $ARDUINO_CLI monitor -p "$port" -c baudrate="$baud" 2>/dev/null || true # If we get here, the monitor exited (port vanished during upload, # board reset, USB replug, etc.). Brief pause then retry. echo -e "${YEL}--- $port disconnected ---${RST}" sleep 0.5 done } # ============================================================================ # ARGUMENT PARSING # ============================================================================ SKETCH_DIR="" PORT="" VERIFY_ONLY=0 UPLOAD_ONLY=0 DO_MONITOR=0 DO_CLEAN=0 DO_VERBOSE=0 ACTION="" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_help exit 0 ;; --version) echo "arduino-build.sh v${VERSION}" exit 0 ;; --setup) ACTION="setup" shift ;; --devices) ACTION="devices" shift ;; --watch) ACTION="watch" shift ;; --verify) VERIFY_ONLY=1 shift ;; --upload) shift # default behavior, accepted for explicitness ;; --upload-only) UPLOAD_ONLY=1 shift ;; --monitor) DO_MONITOR=1 shift ;; --clean) DO_CLEAN=1 shift ;; --verbose) DO_VERBOSE=1 shift ;; -p|--port) [[ -n "${2:-}" ]] || die "--port requires a value (e.g., -p /dev/ttyUSB0)" PORT="$2" shift 2 ;; -b|--baud) [[ -n "${2:-}" ]] || die "--baud requires a value (e.g., -b 115200)" BAUD="$2" shift 2 ;; --fqbn) [[ -n "${2:-}" ]] || die "--fqbn requires a value (e.g., --fqbn arduino:avr:uno)" FQBN="$2" shift 2 ;; -*) die "Unknown option: $1 Run with --help for usage." ;; *) if [[ -z "$SKETCH_DIR" ]]; then SKETCH_DIR="$1" else die "Unexpected argument: $1 Sketch directory already set to: $SKETCH_DIR" fi shift ;; esac done # ============================================================================ # DISPATCH STANDALONE COMMANDS # ============================================================================ case "${ACTION:-}" in setup) run_setup; exit 0 ;; devices) scan_devices; exit 0 ;; watch) run_watch_monitor "$PORT" "$BAUD" ;; esac # ============================================================================ # VALIDATE SKETCH # ============================================================================ if [[ $UPLOAD_ONLY -eq 1 ]] && [[ $VERIFY_ONLY -eq 1 ]]; then die "--upload-only and --verify are contradictory. --verify means compile only, --upload-only means upload only." fi if [[ $UPLOAD_ONLY -eq 1 ]] && [[ $DO_CLEAN -eq 1 ]]; then die "--upload-only and --clean are contradictory. --clean would delete the cached build that --upload-only needs." fi if [[ -z "$SKETCH_DIR" ]]; then echo "" echo -e "${RED}Error: No sketch directory specified.${RST}" echo "" echo " Usage: ./arduino-build.sh [OPTIONS] " echo "" echo " Quick start:" echo " --help Full help with examples and troubleshooting" echo " --setup First-time toolchain installation" echo " --devices Find connected boards" echo " ./my_sketch Build and upload a sketch" echo "" exit 1 fi SKETCH_DIR="$(realpath "$SKETCH_DIR")" [[ -d "$SKETCH_DIR" ]] || die "Not a directory: $SKETCH_DIR" SKETCH_NAME="$(basename "$SKETCH_DIR")" INO_FILE="$SKETCH_DIR/$SKETCH_NAME.ino" if [[ ! -f "$INO_FILE" ]]; then ALT_INO="$(find "$SKETCH_DIR" -maxdepth 1 -name '*.ino' | head -1)" if [[ -n "$ALT_INO" ]]; then warn "Expected ${SKETCH_NAME}.ino but found $(basename "$ALT_INO")" warn "Arduino convention: .ino filename must match directory name." INO_FILE="$ALT_INO" SKETCH_NAME="$(basename "$INO_FILE" .ino)" else die "No .ino file found in $SKETCH_DIR Expected: $SKETCH_DIR/$SKETCH_NAME.ino Create one: touch $SKETCH_DIR/$SKETCH_NAME.ino" fi fi info "Sketch: ${BLD}$SKETCH_NAME${RST}" info "Board: $FQBN" # ============================================================================ # PRE-FLIGHT CHECKS # ============================================================================ command -v "$ARDUINO_CLI" &>/dev/null \ || die "arduino-cli not found. Run: ./arduino-build.sh --setup" if ! $ARDUINO_CLI core list 2>/dev/null | grep -q "arduino:avr"; then die "arduino:avr core not installed. Run: ./arduino-build.sh --setup" fi # ============================================================================ # CLEAN (if requested) # ============================================================================ SKETCH_BUILD_DIR="$BUILD_DIR/$SKETCH_NAME" if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$SKETCH_BUILD_DIR" ]]; then info "Cleaning build cache..." rm -rf "$SKETCH_BUILD_DIR" ok "Cache cleared." fi # ============================================================================ # COMPILE (skipped with --upload-only) # ============================================================================ if [[ $UPLOAD_ONLY -eq 1 ]]; then # Verify cached build artifacts exist if [[ ! -d "$SKETCH_BUILD_DIR" ]]; then die "No cached build found for '$SKETCH_NAME'. Run a compile first: ./arduino-build.sh --verify ./$SKETCH_NAME Build cache expected at: $SKETCH_BUILD_DIR" fi HEX_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.hex" if [[ ! -f "$HEX_FILE" ]]; then die "Build cache exists but no .hex file found. Try a clean rebuild: ./arduino-build.sh --clean ./$SKETCH_NAME" fi ok "Using cached build." info "Hex: $HEX_FILE" # Report binary size if possible ELF_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.elf" if command -v avr-size &>/dev/null && [[ -f "$ELF_FILE" ]]; then echo "" avr-size --mcu=atmega328p -C "$ELF_FILE" 2>/dev/null || avr-size "$ELF_FILE" echo "" fi else # Load per-project configuration (include paths, extra flags) load_project_config "$SKETCH_DIR" info "Compiling..." mkdir -p "$SKETCH_BUILD_DIR" COMPILE_ARGS=( --fqbn "$FQBN" --build-path "$SKETCH_BUILD_DIR" --warnings more ) [[ $DO_VERBOSE -eq 1 ]] && COMPILE_ARGS+=(--verbose) # Inject per-project build flags (include paths, defines, etc.) if [[ -n "$EXTRA_COMPILE_FLAGS" ]]; then COMPILE_ARGS+=(--build-property "build.extra_flags=$EXTRA_COMPILE_FLAGS") fi if ! $ARDUINO_CLI compile "${COMPILE_ARGS[@]}" "$SKETCH_DIR"; then die "Compilation failed." fi ok "Compile succeeded." # Report binary size ELF_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.elf" if command -v avr-size &>/dev/null && [[ -f "$ELF_FILE" ]]; then echo "" avr-size --mcu=atmega328p -C "$ELF_FILE" 2>/dev/null || avr-size "$ELF_FILE" echo "" fi fi # ============================================================================ # VERIFY-ONLY EXIT # ============================================================================ if [[ $VERIFY_ONLY -eq 1 ]]; then ok "Verify-only mode. Done." exit 0 fi # ============================================================================ # UPLOAD # ============================================================================ if [[ -z "$PORT" ]]; then PORT="$(auto_detect_port)" fi info "Uploading to ${BLD}$PORT${RST} ..." if [[ ! -e "$PORT" ]]; then die "Port $PORT does not exist. Run --devices to check." fi if [[ ! -w "$PORT" ]]; then warn "No write access to $PORT" warn "Fix: sudo usermod -aG dialout \$USER (then log out/in)" fi UPLOAD_ARGS=( --fqbn "$FQBN" --port "$PORT" --input-dir "$SKETCH_BUILD_DIR" ) [[ $DO_VERBOSE -eq 1 ]] && UPLOAD_ARGS+=(--verbose) if ! $ARDUINO_CLI upload "${UPLOAD_ARGS[@]}"; then die "Upload failed. Run with --verbose for details. Also try: ./arduino-build.sh --devices" fi ok "Upload complete!" # ============================================================================ # SERIAL MONITOR # ============================================================================ if [[ $DO_MONITOR -eq 1 ]]; then echo "" info "Opening serial monitor on $PORT at ${BAUD} baud..." info "Press Ctrl+C to exit." echo "" $ARDUINO_CLI monitor -p "$PORT" -c baudrate="$BAUD" else echo "" info "To open serial monitor:" echo " ./arduino-build.sh --monitor -p $PORT ./your_sketch" echo " arduino-cli monitor -p $PORT -c baudrate=$BAUD" echo " screen $PORT $BAUD" fi