#!/usr/bin/env bash set -Eeuo pipefail # Set up the Artec Studuino sketchbook and its command-line workflow. SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ARDUINO_USER_OVERRIDE="${ARDUINO_USER:-}" ARDUINO_USER="${ARDUINO_USER:-$HOME/Arduino}" HARDWARE_SOURCE_DIR="$SCRIPT_DIR/hardware/studuino/avr" LIBRARY_SOURCE_DIR="$SCRIPT_DIR/libraries" FQBN="studuino:avr:studuino" MIN_AVR_CORE_VERSION="1.8.8" BUILD_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/arduino/studuino-compile-db" VERIFY_BUILD_DIR="$BUILD_DIR/verify" SMOKE_VERIFY_BUILD_DIR="$BUILD_DIR/verify-studuino-blink" SMOKE_SKETCH_DIR="$LIBRARY_SOURCE_DIR/Studuino/example/Blink" FORCE=0 CLEAN_LIBRARIES=0 SKIP_PACKAGES=0 SKIP_PERMISSIONS=0 SKIP_COMPILE_DB=0 WITH_EDITOR=0 WITH_MONITOR=0 CONFIG_FILE="${ARDUINO_CONFIG_FILE:-}" usage() { cat <<'EOF' Usage: ./setup.sh [options] Options: --force Replace hardware/libraries with checked-in versions. --clean Remove user libraries absent from this repository. --skip-packages Do not install Arch packages. --skip-permissions Do not modify serial-port group membership. --skip-compile-db Do not generate compile_commands.json. --with-editor Install Neovim, clang, and Arduino language-server tools. --with-monitor Also install picocom for a serial monitor. -h, --help Show this help. Environment: ARDUINO_USER Arduino sketchbook directory (default: ~/Arduino). ARDUINO_CONFIG_FILE Optional arduino-cli configuration file. EOF } while (($# > 0)); do case "$1" in --force) FORCE=1 ;; --clean) CLEAN_LIBRARIES=1 ;; --skip-packages) SKIP_PACKAGES=1 ;; --skip-permissions) SKIP_PERMISSIONS=1 ;; --skip-compile-db) SKIP_COMPILE_DB=1 ;; --with-editor) WITH_EDITOR=1 ;; --with-monitor) WITH_MONITOR=1 ;; -h|--help) usage exit 0 ;; *) printf 'Unknown option: %s\n' "$1" >&2 usage >&2 exit 2 ;; esac shift done if ((EUID == 0)); then ROOT_CMD=() TARGET_USER="${SUDO_USER:-$(id -un)}" else ROOT_CMD=(sudo) TARGET_USER="${USER:-$(id -un)}" fi CLI=(arduino-cli) if [[ -n "$CONFIG_FILE" ]]; then CLI+=(--config-file "$CONFIG_FILE") fi info() { printf '[studuino] %s\n' "$*" } warn() { printf '[studuino] WARNING: %s\n' "$*" >&2 } die() { printf '[studuino] ERROR: %s\n' "$*" >&2 exit 1 } as_root() { if ((${#ROOT_CMD[@]} > 0)); then "${ROOT_CMD[@]}" "$@" else "$@" fi } run_cli() { "${CLI[@]}" "$@" } require_command() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" } install_packages() { if ((SKIP_PACKAGES)); then info "Skipping Arch package installation." else require_command pacman local -a packages=(arduino-cli) if ((WITH_EDITOR)); then packages+=(clang neovim arduino-language-server) fi if ((WITH_MONITOR)); then packages+=(picocom) fi info "Installing required Arch packages." as_root pacman -S --needed "${packages[@]}" fi require_command arduino-cli if ((WITH_EDITOR)); then require_command clangd require_command nvim require_command arduino-language-server fi if ((WITH_MONITOR)); then require_command picocom fi } ensure_serial_permissions() { if ((SKIP_PERMISSIONS)); then info "Skipping serial-port group membership changes." return fi require_command id require_command getent local current_groups current_groups="$(id -nG "$TARGET_USER" 2>/dev/null || true)" local changed=0 local group for group in uucp dialout; do if ! getent group "$group" >/dev/null 2>&1; then warn "Serial-port group '$group' does not exist; skipping it." continue fi if ! grep -qw -- "$group" <<<"$current_groups"; then info "Adding $TARGET_USER to serial-port group '$group'." as_root usermod -a -G "$group" "$TARGET_USER" changed=1 fi done if ((changed)); then warn "Log out and back in before uploading to a serial device." fi } init_cli_config() { if [[ -n "$CONFIG_FILE" ]]; then mkdir -p -- "$(dirname -- "$CONFIG_FILE")" if [[ ! -f "$CONFIG_FILE" ]]; then info "Creating arduino-cli config: $CONFIG_FILE" arduino-cli config init --dest-file "$CONFIG_FILE" >/dev/null fi elif [[ ! -f "$HOME/.arduino15/arduino-cli.yaml" ]]; then info "Creating the default arduino-cli config." arduino-cli config init >/dev/null fi } resolve_arduino_user() { if [[ -n "$ARDUINO_USER_OVERRIDE" ]]; then run_cli config set directories.user "$ARDUINO_USER" >/dev/null info "Using configured Arduino sketchbook: $ARDUINO_USER" return fi local configured_user configured_user="$(run_cli config get directories.user 2>/dev/null | sed -n '1p' || true)" if [[ -n "$configured_user" && "$configured_user" != "null" ]]; then ARDUINO_USER="$configured_user" fi } validate_hardware_files() { local directory="$1" [[ -f "$directory/boards.txt" ]] || die "Missing Studuino board definition: $directory/boards.txt" [[ -f "$directory/platform.txt" ]] || die "Missing Studuino platform definition: $directory/platform.txt" [[ -f "$directory/platform.local.txt" ]] || die "Missing Studuino CLI overrides: $directory/platform.local.txt" grep -q '^studuino.name=' "$directory/boards.txt" \ || die "The Studuino board definition is missing its board entry." grep -q '^name=' "$directory/platform.txt" \ || die "The Studuino platform definition is missing its platform name." } hardware_matches_source() { local file for file in boards.txt platform.txt platform.local.txt; do cmp -s -- "$HARDWARE_SOURCE_DIR/$file" "$ARDUINO_USER/hardware/studuino/avr/$file" || return 1 done } ensure_hardware_layout() { [[ -d "$HARDWARE_SOURCE_DIR" ]] || die "Checked-in hardware directory not found: $HARDWARE_SOURCE_DIR" validate_hardware_files "$HARDWARE_SOURCE_DIR" local vendor_dir="$ARDUINO_USER/hardware/studuino" local hardware_dir="$vendor_dir/avr" local nested_dir="$vendor_dir/studuino/avr" # Older versions of the setup instructions could create this extra level. if [[ -d "$nested_dir" ]]; then if [[ ! -e "$hardware_dir" ]]; then info "Moving the legacy nested Studuino hardware path." mv -- "$nested_dir" "$hardware_dir" rmdir -- "$vendor_dir/studuino" 2>/dev/null || true elif ((FORCE)); then info "Removing the redundant nested Studuino hardware path." rm -rf -- "$nested_dir" rmdir -- "$vendor_dir/studuino" 2>/dev/null || true else die "Both $hardware_dir and $nested_dir exist; re-run with --force to remove the nested copy." fi fi if [[ "$HARDWARE_SOURCE_DIR" -ef "$hardware_dir" ]]; then info "Using the checked-in Studuino hardware definition." elif [[ -L "$hardware_dir" ]]; then die "Refusing to replace symlinked hardware directory: $hardware_dir" elif [[ -e "$hardware_dir" ]] && ((FORCE == 0)); then if hardware_matches_source; then info "Studuino hardware files are already installed; keeping them." else warn "Installed Studuino hardware differs from this repository; keeping it. Re-run with --force to replace it." fi else mkdir -p -- "$hardware_dir" if ((FORCE)) && [[ -e "$hardware_dir" ]]; then rm -rf -- "$hardware_dir" mkdir -p -- "$hardware_dir" fi cp -a -- "$HARDWARE_SOURCE_DIR/." "$hardware_dir/" info "Installed checked-in Studuino hardware files: $hardware_dir" fi } version_at_least() { local current="$1" local minimum="$2" [[ "$current" == "$minimum" ]] && return 0 [[ "$(printf '%s\n%s\n' "$minimum" "$current" | sort -V | head -n 1)" == "$minimum" ]] } ensure_avr_core() { local installed installed="$(run_cli core list | awk '$1 == "arduino:avr" { print $2; exit }')" if [[ -z "$installed" ]]; then info "Installing Arduino AVR core $MIN_AVR_CORE_VERSION." run_cli core update-index run_cli core install "arduino:avr@$MIN_AVR_CORE_VERSION" return fi if version_at_least "$installed" "$MIN_AVR_CORE_VERSION"; then info "Arduino AVR core $installed is installed." return fi warn "Arduino AVR core $installed is older than $MIN_AVR_CORE_VERSION; upgrading." run_cli core update-index run_cli core install "arduino:avr@$MIN_AVR_CORE_VERSION" } install_studuino_libraries() { [[ -d "$LIBRARY_SOURCE_DIR/Studuino" ]] \ || die "Checked-in Studuino library not found: $LIBRARY_SOURCE_DIR/Studuino" mkdir -p -- "$ARDUINO_USER/libraries" local source_dir destination name for source_dir in "$LIBRARY_SOURCE_DIR"/*; do [[ -d "$source_dir" && ! -L "$source_dir" ]] || continue name="${source_dir##*/}" destination="$ARDUINO_USER/libraries/$name" if [[ "$source_dir" -ef "$destination" ]]; then info "Using checked-in library: $name" elif [[ -L "$destination" ]]; then die "Refusing to replace symlinked library: $destination" elif [[ -e "$destination" ]] && ((FORCE == 0)); then info "Keeping existing Studuino library: $destination" else if [[ -e "$destination" ]]; then rm -rf -- "$destination" fi cp -a -- "$source_dir" "$destination" info "Installed checked-in Studuino library: $destination" fi done if ((CLEAN_LIBRARIES)); then local -A keep=() for source_dir in "$LIBRARY_SOURCE_DIR"/*; do [[ -d "$source_dir" && ! -L "$source_dir" ]] || continue keep["${source_dir##*/}"]=1 done local installed_dir installed_name for installed_dir in "$ARDUINO_USER/libraries"/*; do [[ -d "$installed_dir" && ! -L "$installed_dir" ]] || continue installed_name="${installed_dir##*/}" if [[ -z "${keep[$installed_name]+present}" ]]; then rm -rf -- "$installed_dir" info "Removed library not supplied by this repository: $installed_dir" fi done fi } ensure_servo() { if [[ -n "$(find "$ARDUINO_USER/libraries" -type f -name Servo.h -print -quit 2>/dev/null)" ]]; then info "Servo library is already installed." return fi if run_cli lib list | awk '$1 == "Servo" { found = 1 } END { exit !found }'; then info "Servo library is already registered." return fi info "Installing Servo, required by Studuino.h." run_cli lib install Servo } ensure_project_files() { local metadata="$SCRIPT_DIR/sketch.yaml" if [[ ! -f "$metadata" ]]; then cat >"$metadata" <>"$metadata" fi local sketch="$SCRIPT_DIR/Arduino.ino" if [[ ! -f "$sketch" ]]; then if [[ -f "$SCRIPT_DIR/sketch.ino" ]]; then info "Renaming sketch.ino to Arduino.ino." mv -- "$SCRIPT_DIR/sketch.ino" "$sketch" else cat >"$sketch" <<'EOF' #include #include // Keep the complete smoke-test dependency set in clangd's database. Studuino board; void setup() { board.InitSensorPort(PORT_A0, PIDLED); } void loop() { board.LED(PORT_A0, ON); board.Timer(1); board.LED(PORT_A0, OFF); board.Timer(1); } EOF info "Created a Studuino smoke-test sketch at $sketch" fi elif [[ -f "$SCRIPT_DIR/sketch.ino" ]]; then warn "Both Arduino.ino and sketch.ino exist; leaving sketch.ino untouched." fi } generate_compile_database() { if ((SKIP_COMPILE_DB)); then info "Skipping compile_commands.json generation." return fi mkdir -p -- "$BUILD_DIR" info "Generating the Neovim/clangd compilation database." if ! run_cli compile --clean --fqbn "$FQBN" --only-compilation-database \ --build-path "$BUILD_DIR" "$SCRIPT_DIR"; then die "Could not generate compile_commands.json." fi cp -- "$BUILD_DIR/compile_commands.json" "$SCRIPT_DIR/compile_commands.json" info "Wrote $SCRIPT_DIR/compile_commands.json" } verify_setup() { info "Verifying board discovery..." run_cli board listall | grep -F "$FQBN" >/dev/null \ || die "arduino-cli cannot discover $FQBN." info "Verifying the Studuino and Servo libraries..." run_cli lib list | grep -E '^Studuino[[:space:]]' >/dev/null \ || die "The Studuino library is not visible to arduino-cli." run_cli lib list | grep -E '^Servo[[:space:]]' >/dev/null \ || die "The Servo library is not visible to arduino-cli." [[ -f "$SMOKE_SKETCH_DIR/Blink.ino" ]] \ || die "Studuino smoke-test sketch not found: $SMOKE_SKETCH_DIR/Blink.ino" info "Compiling the Studuino Blink smoke test for $FQBN..." run_cli compile --clean --fqbn "$FQBN" \ --build-path "$SMOKE_VERIFY_BUILD_DIR" "$SMOKE_SKETCH_DIR" info "Compiling the project for $FQBN..." run_cli compile --clean --fqbn "$FQBN" \ --build-path "$VERIFY_BUILD_DIR" "$SCRIPT_DIR" } install_packages ensure_serial_permissions init_cli_config resolve_arduino_user ensure_hardware_layout ensure_avr_core install_studuino_libraries ensure_servo ensure_project_files generate_compile_database verify_setup info "Studuino setup complete." info "Compile with: arduino-cli compile" info "Check the port with: arduino-cli board list" info "Upload after connecting the board with: arduino-cli upload"