cmdr-joystick/install.sh

545 lines
16 KiB
Bash
Executable File

#!/bin/bash
# CMtec CMDR Joystick 25 Install Script
# Supports building, testing, and deploying RP2040 firmware
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Show usage information
show_usage() {
echo -e "${GREEN}CMtec CMDR Joystick 25 Install Script${NC}"
echo "========================================"
echo
echo "Usage: $0 [flash|test|check|clean] [options]"
echo
echo "Commands:"
echo " flash Build and flash RP2040 firmware"
echo " test Run comprehensive test suite"
echo " check Quick compilation and lint checks"
echo " clean Clean build artifacts and temp files"
echo
echo "flash options:"
echo " --local Build + flash locally (RP2040 mass storage)"
echo " --ssh Build + transfer UF2 via SSH"
echo " --mount PATH Remote RP2040 mount (default /Volumes/RPI-RP2)"
echo
echo "SSH options (when using --ssh):"
echo " --target user@host Combined user and host"
echo " --user USER SSH username"
echo " --host HOST SSH hostname or IP"
echo " --port PORT SSH port (default 22)"
echo " --key PATH SSH private key path"
echo
echo "Examples:"
echo " $0 # Show this help"
echo " $0 test # Run all tests"
echo " $0 check # Quick compilation check"
echo " $0 flash --local # Build and flash firmware locally"
echo " $0 flash --ssh --target user@host --mount /Volumes/RPI-RP2"
echo " $0 clean # Clean build artifacts"
exit "${1:-0}"
}
# Parse command line arguments
COMMAND="${1:-}"
shift 1 || true
# If no command provided, show usage
if [ -z "$COMMAND" ]; then
show_usage 0
fi
case "$COMMAND" in
flash | test | check | clean) ;;
-h | --help | help)
show_usage 0
;;
*)
echo -e "${RED}Error: Unknown command '$COMMAND'${NC}"
echo
show_usage 1
;;
esac
echo -e "${GREEN}CMtec CMDR Joystick 25 Install Script${NC}"
echo "========================================"
echo -e "${BLUE}Mode: $COMMAND${NC}"
echo
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Ensure cargo-binutils is available for `cargo objcopy`
ensure_cargo_binutils() {
if ! command -v cargo-objcopy >/dev/null 2>&1; then
echo "Installing cargo-binutils and llvm-tools-preview (required for UF2 conversion)..."
rustup component add llvm-tools-preview || true
if ! cargo install cargo-binutils; then
echo -e "${RED}Error: Failed to install cargo-binutils${NC}"
echo "Install manually: rustup component add llvm-tools-preview && cargo install cargo-binutils"
exit 1
fi
fi
}
# Function to check prerequisites
check_prerequisites() {
echo "Checking prerequisites..."
if ! command -v cargo &>/dev/null; then
echo -e "${RED}Error: cargo (Rust toolchain) not found${NC}"
echo "Install Rust: https://rustup.rs/"
exit 1
fi
if ! command -v python3 &>/dev/null; then
echo -e "${RED}Error: python3 not found${NC}"
echo "Install Python 3"
exit 1
fi
# Check for required Rust target
if ! rustup target list --installed | grep -q "thumbv6m-none-eabi"; then
echo "Installing Rust target thumbv6m-none-eabi..."
rustup target add thumbv6m-none-eabi
fi
# Check for x86_64 target for testing
if ! rustup target list --installed | grep -q "x86_64-unknown-linux-gnu"; then
echo "Installing Rust target x86_64-unknown-linux-gnu (for testing)..."
rustup target add x86_64-unknown-linux-gnu
fi
echo -e "${GREEN}${NC} Prerequisites check complete"
echo
}
# Function to run comprehensive test suite
run_tests() {
echo "Running comprehensive test suite..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Test 1: Cargo check for embedded target
echo "Testing embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Embedded target compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Embedded target compilation check passed"
# Test 2: Host target tests (17 comprehensive tests)
echo "Running host target test suite..."
if ! cargo test --lib --target x86_64-unknown-linux-gnu --features std; then
echo -e "${RED}${NC} Host target tests failed"
exit 1
fi
echo -e "${GREEN}${NC} Host target tests passed (17 tests)"
# Test 3: Clippy for code quality (warnings allowed for now)
echo "Running clippy code quality checks..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy code quality check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy code quality check passed"
# Test 4: Release build test
echo "Testing release build..."
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Release build failed"
exit 1
fi
echo -e "${GREEN}${NC} Release build passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}All tests completed successfully!${NC}"
echo
echo "Test summary:"
echo -e "${GREEN}${NC} Embedded target compilation check"
echo -e "${GREEN}${NC} Host target test suite (17 tests: expo + storage modules)"
echo -e "${GREEN}${NC} Clippy code quality checks"
echo -e "${GREEN}${NC} Release build verification"
echo
echo -e "${GREEN}Codebase is ready for deployment.${NC}"
}
# Function to run quick checks
run_check() {
echo "Running quick compilation and lint checks..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Quick cargo check
echo "Checking embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Compilation check passed"
# Quick clippy check (warnings allowed for now)
echo "Running clippy..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy check passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Quick checks completed successfully!${NC}"
}
# Function to clean build artifacts
clean_build() {
echo "Cleaning build artifacts and temporary files..."
echo
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Clean cargo artifacts
echo "Cleaning cargo build artifacts..."
cargo clean
echo -e "${GREEN}${NC} Cargo artifacts cleaned"
# Remove UF2 and binary files
if [ -f "firmware.uf2" ]; then
rm -f firmware.uf2
echo -e "${GREEN}${NC} Removed firmware.uf2"
fi
if [ -f "target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin" ]; then
rm -f target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin
echo -e "${GREEN}${NC} Removed binary files"
fi
# Clean any temp files
rm -f /tmp/cmdr_*.tmp 2>/dev/null || true
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Cleanup completed!${NC}"
}
# Function to build firmware locally
build_firmware() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
check_prerequisites
# Build firmware
echo "Building RP2040 firmware..."
cd "$RP2040_DIR"
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}Error: Failed to build firmware${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Firmware build complete"
echo
# Create UF2 file
echo "Converting to UF2 format..."
ensure_cargo_binutils
if ! cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to create binary file${NC}"
exit 1
fi
# Check for uf2conv.py script
if [ ! -f "uf2conv.py" ]; then
echo "Downloading uf2conv.py..."
if ! curl -L -o uf2conv.py "https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2conv.py"; then
echo -e "${RED}Error: Failed to download uf2conv.py${NC}"
exit 1
fi
chmod +x uf2conv.py
fi
if ! python3 uf2conv.py -b 0x10000000 -f 0xe48bff56 -c -o firmware.uf2 target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to convert to UF2 format${NC}"
exit 1
fi
echo -e "${GREEN}${NC} UF2 conversion complete"
echo
}
# Function to build and transfer firmware via SSH
flash_firmware_ssh() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
# Parse CLI args for SSH
SSH_HOST=""; SSH_USER=""; SSH_PORT="22"; SSH_KEY=""; REMOTE_MOUNT="/Volumes/RPI-RP2"
while [ $# -gt 0 ]; do
case "$1" in
--target) shift; TARGET="$1"; SSH_USER="${TARGET%@*}"; SSH_HOST="${TARGET#*@}" ;;
--host) shift; SSH_HOST="$1" ;;
--user) shift; SSH_USER="$1" ;;
--port) shift; SSH_PORT="$1" ;;
--key) shift; SSH_KEY="$1" ;;
--mount) shift; REMOTE_MOUNT="$1" ;;
*) echo -e "${YELLOW}Warning: Unknown SSH option: $1${NC}" ;;
esac
shift || true
done
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ]; then
echo -e "${RED}Error: provide SSH credentials via --target user@host or --user/--host${NC}"
exit 1
fi
# Build firmware locally first
build_firmware
echo "Testing SSH connection to $SSH_USER@$SSH_HOST:$SSH_PORT..."
mkdir -p "${HOME}/.ssh"
SSH_CONTROL_PATH="${HOME}/.ssh/cmdr_%C"
local SSH_OPTS=(
-p "$SSH_PORT"
-o "StrictHostKeyChecking=accept-new"
-o "ControlMaster=auto"
-o "ControlPersist=60s"
-o "ControlPath=$SSH_CONTROL_PATH"
-o "PreferredAuthentications=publickey,password,keyboard-interactive"
)
if [ -n "$SSH_KEY" ]; then
SSH_OPTS+=( -i "$SSH_KEY" )
fi
# Test SSH connection
if ! ssh "${SSH_OPTS[@]}" -o ConnectTimeout=10 -fN "$SSH_USER@$SSH_HOST" 2>/dev/null; then
echo -e "${RED}Error: Cannot connect to $SSH_USER@$SSH_HOST:$SSH_PORT${NC}"
echo "Tips:"
echo "- Verify network reachability and SSH port"
echo "- Ensure SSH keys are properly configured"
exit 1
fi
echo -e "${GREEN}${NC} SSH connection successful"
# Check if remote mount exists
echo "Checking remote RP2040 mount path: $REMOTE_MOUNT"
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$SSH_HOST" "test -d \"$REMOTE_MOUNT\""; then
echo -e "${RED}Remote mount not found: $REMOTE_MOUNT${NC}"
echo -e "${YELLOW}Ensure the RP2040 is in BOOTSEL mode and mounted on the remote host.${NC}"
echo "Common paths: /Volumes/RPI-RP2 (macOS), /media/\$USER/RPI-RP2 (Linux)"
exit 1
fi
echo "Transferring firmware.uf2 to remote host..."
local SCP_OPTS=( -P "$SSH_PORT" -o "StrictHostKeyChecking=accept-new" -o "ControlPath=$SSH_CONTROL_PATH" )
if [ -n "$SSH_KEY" ]; then
SCP_OPTS+=( -i "$SSH_KEY" )
fi
cd "$RP2040_DIR"
if ! scp "${SCP_OPTS[@]}" firmware.uf2 "$SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"; then
echo -e "${RED}${NC} Failed to transfer firmware to remote RP2040"
exit 1
fi
echo -e "${GREEN}${NC} Transferred firmware.uf2 to $SSH_USER@$SSH_HOST:$REMOTE_MOUNT"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}SSH firmware deployment completed!${NC}"
echo "Remote: $SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"
echo -e "${YELLOW}The remote RP2040 should now restart with new firmware.${NC}"
}
# Function to flash firmware locally
flash_firmware_local() {
build_firmware
RP2040_DIR="$SCRIPT_DIR/rp2040"
cd "$RP2040_DIR"
# Detect OS and set up mount detection
OS="$(uname -s)"
# Wait for and flash to RP2040
echo "Waiting for RP2040 in bootloader mode..."
echo -e "${YELLOW}Put your RP2040 into bootloader mode (hold BOOTSEL button while plugging in USB)${NC}"
case "$OS" in
Linux*)
# Wait for RPI-RP2 mount point with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/media/RPI-RP2" ] && [ ! -d "/media/$USER/RPI-RP2" ] && [ ! -d "/run/media/$USER/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount (RPI-RP2)${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
echo "Expected mount at one of: /media/RPI-RP2, /media/$USER/RPI-RP2, /run/media/$USER/RPI-RP2"
exit 1
fi
done
# Find the actual mount point
if [ -d "/media/RPI-RP2" ]; then
MOUNT_POINT="/media/RPI-RP2"
elif [ -d "/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/media/$USER/RPI-RP2"
elif [ -d "/run/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/run/media/$USER/RPI-RP2"
else
echo -e "${RED}Error: Could not find RPI-RP2 mount point${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Found RP2040 at $MOUNT_POINT"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "$MOUNT_POINT/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
Darwin*)
# Wait for RPI-RP2 volume with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/Volumes/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount at /Volumes/RPI-RP2${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
exit 1
fi
done
echo -e "${GREEN}${NC} Found RP2040 at /Volumes/RPI-RP2"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "/Volumes/RPI-RP2/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
*)
echo -e "${RED}Error: Unsupported operating system for local flashing${NC}"
echo "This script supports Linux and macOS for local flashing."
exit 1
;;
esac
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Firmware installation completed!${NC}"
echo
echo "Firmware file: $RP2040_DIR/firmware.uf2"
echo -e "${GREEN}RP2040 firmware deployed successfully!${NC}"
echo -e "${YELLOW}The device should now restart with new firmware.${NC}"
}
# Detect OS
case "$(uname -s)" in
Linux*)
OS="Linux"
;;
Darwin*)
OS="macOS"
;;
*)
echo -e "${RED}Error: Unsupported operating system${NC}"
echo "This script supports Linux and macOS only."
exit 1
;;
esac
echo "Detected OS: $OS"
if [ "$COMMAND" = "flash" ]; then
echo "RP2040 firmware directory: $SCRIPT_DIR/rp2040"
elif [ "$COMMAND" = "test" ]; then
echo "Testing Rust embedded firmware with comprehensive test suite"
fi
echo
# Execute command
case "$COMMAND" in
flash)
MODE=""
REM_ARGS=()
for arg in "$@"; do
case "$arg" in
--local) MODE="local" ;;
--ssh) MODE="ssh" ;;
*) REM_ARGS+=("$arg") ;;
esac
done
if [ "$MODE" = "local" ]; then
flash_firmware_local
elif [ "$MODE" = "ssh" ]; then
flash_firmware_ssh "${REM_ARGS[@]}"
else
echo -e "${RED}Error: specify --local or --ssh for flash${NC}"
show_usage 1
fi
;;
test)
run_tests
;;
check)
run_check
;;
clean)
clean_build
;;
esac