Files
anvil-bash/arduino-build.sh
Eric Ratliff 61f4659462 Arduino CLI build system with HAL-based test architecture
Build and upload tool (arduino-build.sh):
- Compile, upload, and monitor via arduino-cli
- Device discovery with USB ID identification (--devices)
- Persistent reconnecting serial monitor (--watch)
- Split compile/upload workflow (--verify, --upload-only)
- First-time setup wizard (--setup)
- Comprehensive --help with troubleshooting and RedBoard specs

Testable application architecture:
- Hardware abstraction layer (lib/hal/) decouples logic from Arduino API
- Google Mock HAL for unit tests (exact call verification)
- Simulated HAL for system tests (GPIO state, virtual clock, I2C devices)
- Example I2C temperature sensor simulator (TMP102)
- Host-side test suite via CMake + Google Test (21 tests)

Example sketch:
- blink/ -- LED blink with button-controlled speed, wired through HAL
2026-02-14 09:50:13 -06:00

999 lines
33 KiB
Bash
Executable File

#!/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.1.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 <<EOF
${BLD}NAME${RST}
arduino-build.sh - Build, upload, and monitor Arduino sketches
${BLD}VERSION${RST}
${VERSION}
${BLD}SYNOPSIS${RST}
./arduino-build.sh ${BLD}--setup${RST}
./arduino-build.sh ${BLD}--devices${RST}
./arduino-build.sh [OPTIONS] <sketch_dir>
${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_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 <<EOF
${BLD}STANDALONE COMMANDS${RST}
These run independently. No sketch directory needed.
${BLD}-h, --help${RST}
Show this help and exit. You are reading it now.
${BLD}--version${RST}
Print version string and exit.
${BLD}--setup${RST}
First-time setup wizard. Installs the arduino:avr core, checks
that your user is in the dialout group (serial port access), and
verifies the toolchain. Run once after installing arduino-cli.
${BLD}--devices${RST}
Scan for connected boards. For each /dev/ttyUSB* and /dev/ttyACM*
port found, shows:
- USB vendor and product strings
- USB vendor:product ID (hex)
- Kernel driver in use (ch341-uart, cdc_acm, etc.)
- Best guess at what board it is
- Whether you have write permission to the port
Also runs 'arduino-cli board list' for cross-reference.
${BLD}--watch${RST}
Open a persistent serial monitor that automatically reconnects
when the port disappears and reappears (e.g., during upload from
another terminal). Run in one terminal, build/upload in another.
Survives: upload resets, board power cycles, USB re-plugs.
Exit with Ctrl+C.
Combine with -p and -b:
./arduino-build.sh --watch -p /dev/ttyUSB0 -b 9600
EOF
}
_help_section_build_options() {
cat <<EOF
${BLD}BUILD & UPLOAD OPTIONS${RST}
${BLD}--verify${RST}
Compile only -- do not upload. Good for syntax checking, CI
pipelines, or when the board is not plugged in. Still reports
binary size so you can track flash usage.
${BLD}--upload${RST}
Compile and upload. This is the default when you pass a sketch
directory, so you almost never need to type it explicitly.
${BLD}--upload-only${RST}
Upload previously compiled artifacts without recompiling. Requires
a prior --verify or build to have cached the .hex file. Useful for
separating build and flash steps:
Terminal 1: ./arduino-build.sh --verify ./blink (compile)
Terminal 2: ./arduino-build.sh --upload-only ./blink (flash)
Fails if no cached build exists for the sketch.
${BLD}--monitor${RST}
After a successful upload, automatically open the serial monitor
at the configured baud rate. Ctrl+C exits the monitor. Combines
the build-upload-monitor cycle into a single command.
${BLD}--clean${RST}
Delete cached build artifacts for this sketch before compiling.
Forces a full recompile from scratch. Use when you suspect stale
object files or after changing compiler flags.
${BLD}--verbose${RST}
Show full compiler and avrdude output. Helpful for debugging
linker errors, upload failures, or library conflicts.
EOF
}
_help_section_config_options() {
cat <<EOF
${BLD}CONFIGURATION OPTIONS${RST}
${BLD}-p, --port PORT${RST}
Use this serial port instead of auto-detecting.
Example: -p /dev/ttyUSB0
${BLD}-b, --baud RATE${RST}
Serial monitor baud rate (default: 115200). Only affects the
--monitor feature; upload speed is set by the bootloader.
Common values: 9600, 19200, 38400, 57600, 115200
${BLD}--fqbn FQBN${RST}
Override the Fully Qualified Board Name. Default: arduino:avr:uno
Common FQBNs:
arduino:avr:uno Uno / RedBoard
arduino:avr:mega:cpu=atmega2560 Mega 2560
arduino:avr:nano:cpu=atmega328 Nano (old bootloader)
arduino:avr:nano:cpu=atmega328old Nano (new bootloader)
esp32:esp32:esp32 ESP32 DevKit
SparkFun:avr:RedBoard SparkFun board package
To list all installed FQBNs:
arduino-cli board listall
EOF
}
_help_section_env_vars() {
cat <<EOF
${BLD}ENVIRONMENT VARIABLES${RST}
Override defaults without editing the script or passing flags.
Variable Default What it does
-------- ------- ------------
ARDUINO_FQBN arduino:avr:uno Board identifier
ARDUINO_BAUD 115200 Serial monitor baud rate
ARDUINO_BUILD_DIR /tmp/arduino-build Where .o/.elf/.hex go
ARDUINO_CLI_BIN arduino-cli Path to arduino-cli binary
EOF
}
_help_section_examples() {
cat <<EOF
${BLD}EXAMPLES${RST}
${DIM}# --- Getting started (do these once) ---${RST}
./arduino-build.sh --setup
Install the AVR toolchain and verify permissions.
./arduino-build.sh --devices
See what boards are plugged in and which port to use.
${DIM}# --- Day-to-day usage ---${RST}
./arduino-build.sh ./blink
Compile and upload blink sketch. Port auto-detected.
./arduino-build.sh --verify ./blink
Compile only. Good for checking code on a laptop with no board.
./arduino-build.sh -p /dev/ttyUSB0 ./blink
Upload to a specific port (skip auto-detection).
./arduino-build.sh --monitor ./blink
Build, upload, and open serial monitor in one shot.
./arduino-build.sh --clean --verbose ./blink
Nuke the cache and rebuild with full compiler output.
${DIM}# --- Two-terminal workflow (monitor + build separately) ---${RST}
${DIM}# Terminal 1: persistent monitor that reconnects after uploads${RST}
./arduino-build.sh --watch
${DIM}# Terminal 2: build and upload (monitor in T1 reconnects)${RST}
./arduino-build.sh ./blink
${DIM}# --- Split compile and upload (CI / unit test workflow) ---${RST}
./arduino-build.sh --verify ./blink
Compile and cache artifacts. No board needed.
./arduino-build.sh --upload-only ./blink
Flash cached artifacts. No recompile.
${DIM}# --- Different boards ---${RST}
./arduino-build.sh --fqbn arduino:avr:mega:cpu=atmega2560 ./my_sketch
Upload to an Arduino Mega instead of a RedBoard.
ARDUINO_FQBN=esp32:esp32:esp32 ./arduino-build.sh ./my_esp_sketch
Upload to an ESP32 board via environment variable.
EOF
}
_help_section_cheatsheet() {
cat <<EOF
${BLD}QUICK REFERENCE (tape this to your monitor)${RST}
Command What it does
------- ------------
--help You are here
--setup One-time toolchain install
--devices What's plugged in?
--verify ./sketch Does it compile?
./sketch Build + upload (auto port)
--upload-only ./sketch Flash cached build (no compile)
-p /dev/ttyUSB0 ./sketch Build + upload (manual port)
--monitor ./sketch Build + upload + serial console
--watch Persistent monitor (reconnects)
--clean ./sketch Full rebuild (no cache)
--verbose ./sketch Show all compiler output
${BLD}Two-terminal workflow:${RST}
Terminal 1: ./arduino-build.sh --watch
Terminal 2: ./arduino-build.sh ./blink (watch reconnects)
EOF
}
_help_section_sketch_layout() {
cat <<EOF
${BLD}SKETCH DIRECTORY LAYOUT${RST}
Arduino requires the .ino filename to match the directory name:
my_sketch/ <-- pass this directory to the script
my_sketch.ino <-- main sketch file (name must match dir)
config.h <-- optional extra files
utils.cpp <-- optional extra files
Quick way to create a new sketch:
mkdir blink
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 <<EOF
${BLD}SPARKFUN REDBOARD SPECS${RST}
MCU: ATmega328P at 16 MHz
Flash: 32 KB total (31.5 KB usable; 512 B bootloader)
SRAM: 2 KB (runtime variables and stack)
EEPROM: 1 KB (persistent storage)
Digital I/O: 14 pins (6 with PWM on pins 3, 5, 6, 9, 10, 11)
Analog inputs: 6 pins (A0-A5, 10-bit ADC)
USB chip: CH340G (shows up as /dev/ttyUSB*)
Voltage: 5V logic, 7-15V barrel jack input
FQBN: arduino:avr:uno (same as Arduino Uno)
The RedBoard is electrically identical to an Arduino Uno. The only
real difference is the CH340 USB-to-serial chip (Uno uses ATmega16U2).
The CH340 shows up as /dev/ttyUSB* while the 16U2 shows as /dev/ttyACM*.
EOF
}
_help_section_port_permissions() {
cat <<EOF
${BLD}SERIAL PORT PERMISSIONS${RST}
If you get "Permission denied" on /dev/ttyUSB0 or /dev/ttyACM0:
sudo usermod -aG dialout \$USER
Then ${BLD}log out and back in${RST} (or reboot). Verify with:
groups | grep dialout
Alternatively, for a quick one-off test:
sudo chmod 666 /dev/ttyUSB0 (resets on reboot/replug)
EOF
}
_help_section_first_time() {
cat <<EOF
${BLD}FIRST-TIME INSTALL (step by step)${RST}
1. Install arduino-cli:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
sudo mv bin/arduino-cli /usr/local/bin/
2. Run first-time setup:
./arduino-build.sh --setup
3. Plug in your RedBoard and verify detection:
./arduino-build.sh --devices
4. Create a test sketch:
mkdir blink && 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 <<EOF
${BLD}TROUBLESHOOTING${RST}
${BLD}Board not detected (--devices shows nothing)${RST}
- Try a different USB cable. Many cables are charge-only with no
data lines. If your phone does not offer file transfer with the
cable, it is probably charge-only.
- Check the USB bus directly:
lsusb | grep -i -E 'ch34|arduino|1a86|2341'
- Check kernel log for attach/detach events:
dmesg | tail -20
- Try a different USB port (front panel ports can be flaky).
${BLD}Upload fails: "avrdude: stk500_getsync(): not in sync"${RST}
- Most common cause: wrong port. Run --devices to double-check.
- Close any other program using the port (serial monitors, other
IDE instances, screen sessions, etc.)
- Try pressing the board reset button just before upload starts.
- Bad or loose USB cable.
${BLD}"Sketch uses N bytes ... maximum is 32256 bytes"${RST}
- The ATmega328P has 32,256 bytes for program storage.
- Use --verify to check size without uploading.
- Tips to reduce size: use F() macro for string literals,
avoid the String class (use char arrays), remove unused libraries.
- If you genuinely need more space, use a Mega 2560 (256 KB).
${BLD}Compilation errors about missing libraries${RST}
- Search: arduino-cli lib search "Servo"
- Install: arduino-cli lib install "Servo"
- List: arduino-cli lib list
${BLD}Serial monitor shows garbage characters${RST}
- Baud rate mismatch. Make sure -b/--baud matches the
Serial.begin() value in your sketch. Default here is 115200.
${BLD}CH340 driver not loading (Linux)${RST}
- Most kernels since 4.x include ch341 out of the box.
- Verify: lsmod | grep ch341
- If missing: sudo modprobe ch341
- Still gone: sudo apt install linux-modules-extra-\$(uname -r)
EOF
}
_help_section_usb_ids() {
cat <<EOF
${BLD}KNOWN USB VENDOR:PRODUCT IDS${RST}
Used by --devices to identify boards.
ID Board / Chip
---- ------------
1a86:7523 CH340 (SparkFun RedBoard, most Arduino clones)
1a86:55d4 CH340C (RedBoard Qwiic, newer clones)
2341:0043 Arduino Uno R3 (ATmega16U2)
2341:0001 Arduino Uno (older revision)
2341:0010 Arduino Mega 2560
2341:0042 Arduino Mega 2560 R3
2341:003d Arduino Due (Programming Port)
2341:003e Arduino Due (Native USB)
2341:8036 Arduino Leonardo
2341:8037 Arduino Micro
0403:6001 FTDI FT232R (older RedBoard, FTDI clones)
0403:6015 FTDI FT231X (SparkFun FTDI breakout)
10c4:ea60 CP2102 (NodeMCU, some ESP32 boards)
EOF
}
# ============================================================================
# DEVICE SCANNER
# ============================================================================
scan_devices() {
echo ""
echo -e "${BLD}=== Connected Serial Devices ===${RST}"
echo ""
local found=0
for port in /dev/ttyUSB* /dev/ttyACM*; do
[[ -e "$port" ]] || continue
found=1
echo -e " ${BLD}${GRN}$port${RST}"
local sysfs_path="" usb_dev_path=""
# Find the kernel driver for this tty
local driver=""
local tty_device="/sys/class/tty/$(basename "$port")/device/driver"
if [[ -L "$tty_device" ]]; then
driver="$(basename "$(readlink -f "$tty_device")")"
fi
# Walk up from the tty device node to find the USB device node.
# The tty sits under a USB interface node; idVendor/idProduct
# live on the USB device node which is one or more levels above.
if [[ -e "/sys/class/tty/$(basename "$port")/device" ]]; then
sysfs_path="$(readlink -f "/sys/class/tty/$(basename "$port")/device")"
usb_dev_path="$sysfs_path"
# Walk up until we find idVendor (max 5 levels to avoid runaway)
local walk=0
while [[ $walk -lt 5 ]] && [[ "$usb_dev_path" != "/" ]]; do
if [[ -f "$usb_dev_path/idVendor" ]]; then
break
fi
usb_dev_path="$(dirname "$usb_dev_path")"
walk=$((walk + 1))
done
# If we didn't find idVendor, clear the path
if [[ ! -f "$usb_dev_path/idVendor" ]]; then
usb_dev_path=""
fi
fi
if [[ -n "$usb_dev_path" ]]; then
local vendor="" product="" vid="" pid="" serial=""
[[ -f "$usb_dev_path/manufacturer" ]] && vendor="$(cat "$usb_dev_path/manufacturer" 2>/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]}"
}
# ============================================================================
# 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] <sketch_dir>"
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
# ============================================================================
# ============================================================================
# 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
info "Compiling..."
mkdir -p "$SKETCH_BUILD_DIR"
COMPILE_ARGS=(
--fqbn "$FQBN"
--build-path "$SKETCH_BUILD_DIR"
--warnings all
)
[[ $DO_VERBOSE -eq 1 ]] && COMPILE_ARGS+=(--verbose)
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