464 lines
14 KiB
Bash
Executable File
464 lines
14 KiB
Bash
Executable File
#!/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" <<EOF
|
|
default_fqbn: $FQBN
|
|
EOF
|
|
info "Created $metadata"
|
|
elif grep -q '^default_fqbn:' "$metadata"; then
|
|
sed -i -E "s/^default_fqbn:.*/default_fqbn: $FQBN/" "$metadata"
|
|
elif grep -q '^fqbn:' "$metadata"; then
|
|
sed -i -E "s/^fqbn:.*/default_fqbn: $FQBN/" "$metadata"
|
|
else
|
|
printf '\ndefault_fqbn: %s\n' "$FQBN" >>"$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 <Studuino.h>
|
|
#include <Clock.h> // 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"
|