#!/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