Files
FTC-Project-Gen/ftc-new-project
Eric Ratliff 81452a8670 Initial release: FTC Project Generator
Generate clean, testable FTC robot projects with proper separation from SDK bloat.

Features:
- Composite build setup - one shared SDK, multiple clean projects
- Subsystem pattern with hardware interfaces for easy testing
- JUnit scaffolding - tests run on PC without robot
- Minimal project structure (~50KB vs 200MB SDK)
- Support for multiple FTC SDK versions

Philosophy: Your code should be YOUR code. SDK is just a dependency.

Built by Nexus Workshops for FTC teams tired of fighting the standard structure.

License: MIT
2026-01-13 23:58:43 -06:00

1548 lines
47 KiB
Bash
Executable File

#!/bin/bash
#
# FTC Project Generator
# Copyright (c) 2026 Nexus Workshops LLC
# Licensed under MIT License
#
set -e
# FTC Project Generator
# Creates clean, minimal FTC robot projects with shared SDK
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FTC_SDK_DIR="${FTC_SDK_DIR:-$HOME/ftc-sdk}"
DEFAULT_FTC_VERSION="v10.1.1"
usage() {
cat << EOF
╔════════════════════════════════════════════════════════════════╗
║ FTC Project Generator - Clean Robot Projects ║
╚════════════════════════════════════════════════════════════════╝
Creates clean, testable FTC robot projects with shared SDK.
Your code stays separate - SDK is just a build dependency.
USAGE:
$(basename $0) <project-name> [options]
OPTIONS:
-v, --version <tag> FTC SDK git tag (default: $DEFAULT_FTC_VERSION)
-d, --sdk-dir <path> SDK location (default: $FTC_SDK_DIR)
-h, --help Show this help
EXAMPLES:
# Create new project with latest SDK
$(basename $0) my-robot
# Create project with specific FTC version
$(basename $0) competition-bot -v v10.0.0
# Use custom SDK location
$(basename $0) test-bot -d ~/my-ftc-sdk
# Create second project (reuses existing SDK!)
$(basename $0) another-robot
WORKFLOW:
1. Create project: $(basename $0) my-robot
2. Develop & test: cd my-robot && ./gradlew test --continuous
3. Deploy to SDK: ./gradlew deployToSDK
4. Build APK: cd ~/ftc-sdk && ./gradlew build
5. Install to robot: Use Android Studio or adb
WHAT IT DOES:
✓ Checks/clones FTC SDK (shared across all projects)
✓ Creates YOUR clean project structure
✓ Generates test scaffolding with examples
✓ Sets up composite build to reference SDK
✓ Ready to code immediately
WHY THIS IS BETTER:
Traditional FTC: This Way:
• Clone 200MB repo • Your project: 50KB
• Your code mixed in • SDK separate, shared
• Hard to test • Tests run on PC instantly
• One repo per project • One SDK, many projects
• Forced BSD license • Your license, your code
ENVIRONMENT:
Set defaults to avoid typing them every time:
export FTC_SDK_DIR=~/ftc-sdk # SDK location
export FTC_VERSION=v10.1.1 # Default version
PROJECT STRUCTURE:
my-robot/
├── src/main/java/robot/
│ ├── subsystems/ → Your robot logic (tested on PC)
│ ├── hardware/ → FTC hardware implementations
│ └── opmodes/ → FTC OpModes for Control Hub
└── src/test/java/robot/ → Unit tests (run without robot)
MULTIPLE PROJECTS:
One SDK, unlimited projects:
$(basename $0) swerve-bot # Creates project 1
$(basename $0) mecanum-bot # Reuses SDK - just creates project 2
$(basename $0) test-chassis # Reuses SDK - creates project 3
All projects share same SDK but are completely independent!
INSTALLATION:
Add to PATH for global access:
# Option 1: Symlink
sudo ln -s $(pwd)/$(basename $0) /usr/local/bin/
# Option 2: Add to PATH
echo 'export PATH=\$PATH:$(pwd)' >> ~/.bashrc
Then use from anywhere:
cd ~/robotics && $(basename $0) awesome-bot
DOCUMENTATION:
See README.md for detailed information and examples.
EOF
exit 0
}
# Parse arguments
PROJECT_NAME=""
FTC_VERSION="$DEFAULT_FTC_VERSION"
while [[ $# -gt 0 ]]; do
case $1 in
-v|--version)
FTC_VERSION="$2"
shift 2
;;
-d|--sdk-dir)
FTC_SDK_DIR="$2"
shift 2
;;
-h|--help)
usage
;;
*)
if [ -z "$PROJECT_NAME" ]; then
PROJECT_NAME="$1"
else
echo "Error: Unknown argument: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$PROJECT_NAME" ]; then
echo "Error: Project name required"
echo "Usage: $0 <project-name> [options]"
echo "Run '$0 --help' for more info"
exit 1
fi
PROJECT_DIR="$(pwd)/$PROJECT_NAME"
echo "============================================"
echo "FTC Project Generator"
echo "============================================"
echo "Project: $PROJECT_NAME"
echo "SDK Dir: $FTC_SDK_DIR"
echo "SDK Version: $FTC_VERSION"
echo "Target: $PROJECT_DIR"
echo ""
# Step 1: Check/setup FTC SDK
echo ">>> Checking FTC SDK..."
if [ -d "$FTC_SDK_DIR" ]; then
echo "SDK directory exists, checking version..."
cd "$FTC_SDK_DIR"
# Check if it's a git repo
if [ -d ".git" ]; then
CURRENT_TAG=$(git describe --tags --exact-match 2>/dev/null || echo "none")
if [ "$CURRENT_TAG" = "$FTC_VERSION" ]; then
echo "✓ SDK already at version $FTC_VERSION"
else
echo "Current version: $CURRENT_TAG"
echo "Fetching and checking out $FTC_VERSION..."
git fetch --tags
git checkout "$FTC_VERSION"
echo "✓ Updated to $FTC_VERSION"
fi
else
echo "Warning: SDK directory exists but is not a git repo"
echo "Using existing directory anyway..."
fi
else
echo "Cloning FTC SDK $FTC_VERSION..."
echo "This will take a minute (SDK is ~200MB)..."
git clone --depth 1 --branch "$FTC_VERSION" \
https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \
"$FTC_SDK_DIR" || {
echo "Error: Failed to clone FTC SDK"
echo "Check your internet connection or try a different version tag"
exit 1
}
echo "✓ Cloned FTC SDK $FTC_VERSION to $FTC_SDK_DIR"
fi
# Step 2: Create project structure
echo ""
echo ">>> Creating project: $PROJECT_NAME"
if [ -d "$PROJECT_DIR" ]; then
echo "Error: Project directory already exists: $PROJECT_DIR"
exit 1
fi
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
# Create directory structure
mkdir -p src/main/java/robot/{subsystems,hardware,opmodes}
mkdir -p src/test/java/robot/{subsystems,hardware}
# Step 3: Generate build files
echo "Generating build configuration..."
cat > build.gradle.kts << 'EOF'
plugins {
java
}
repositories {
mavenCentral()
google()
}
dependencies {
// Testing (runs on PC without SDK)
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.mockito:mockito-core:5.5.0")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = false
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
// Task to deploy to FTC SDK
tasks.register<Copy>("deployToSDK") {
group = "ftc"
description = "Copy code to FTC SDK TeamCode for deployment"
val homeDir = System.getProperty("user.home")
val sdkDir = providers.gradleProperty("ftcSdkDir")
.orElse(providers.environmentVariable("FTC_SDK_DIR"))
.getOrElse("${'$'}homeDir/ftc-sdk")
from("src/main/java")
into("${'$'}sdkDir/TeamCode/src/main/java")
doLast {
println("✓ Code deployed to TeamCode")
println("")
println("Next steps:")
println(" cd ${'$'}sdkDir")
println(" ./gradlew build")
}
}
EOF
cat > settings.gradle.kts << EOF
rootProject.name = "$PROJECT_NAME"
EOF
cat > gradle.properties << EOF
# FTC SDK location
ftcSdkDir=$FTC_SDK_DIR
# Gradle daemon
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
EOF
# Step 4: Generate source files
echo "Generating source scaffolding..."
cat > src/main/java/robot/Pose2d.java << 'EOF'
package robot;
/** Simple 2D pose representation */
public class Pose2d {
public final double x, y, heading;
public Pose2d(double x, double y, double heading) {
this.x = x;
this.y = y;
this.heading = heading;
}
public Pose2d() { this(0, 0, 0); }
public double distanceTo(Pose2d other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
EOF
cat > src/main/java/robot/subsystems/Drive.java << 'EOF'
package robot.subsystems;
import robot.Pose2d;
/**
* Drive subsystem - your robot logic lives here.
* Tests run on PC, deploys to robot.
*/
public class Drive {
private final Hardware hardware;
private boolean fieldRelative = true;
public Drive(Hardware hardware) {
this.hardware = hardware;
}
public void drive(double x, double y, double rotation) {
if (fieldRelative) {
double heading = hardware.getHeading();
double temp = x * Math.cos(-heading) - y * Math.sin(-heading);
y = x * Math.sin(-heading) + y * Math.cos(-heading);
x = temp;
}
hardware.setPower(x, y, rotation);
}
public Pose2d getPose() {
return hardware.getPose();
}
public void setFieldRelative(boolean enabled) {
this.fieldRelative = enabled;
}
/** Hardware interface - implement for real robot */
public interface Hardware {
void setPower(double x, double y, double rotation);
Pose2d getPose();
double getHeading();
}
}
EOF
cat > src/main/java/robot/hardware/MecanumDrive.java << 'EOF'
package robot.hardware;
import robot.Pose2d;
import robot.subsystems.Drive;
// Uncomment when deploying to robot:
// import com.qualcomm.robotcore.hardware.DcMotor;
// import com.qualcomm.robotcore.hardware.HardwareMap;
/**
* Real hardware implementation.
* Stub for testing, uncomment FTC imports for robot.
*/
public class MecanumDrive implements Drive.Hardware {
// Uncomment for robot:
// private DcMotor frontLeft, frontRight, backLeft, backRight;
private Pose2d pose = new Pose2d();
public MecanumDrive(/* HardwareMap hardwareMap */) {
// frontLeft = hardwareMap.get(DcMotor.class, "frontLeft");
// frontRight = hardwareMap.get(DcMotor.class, "frontRight");
// backLeft = hardwareMap.get(DcMotor.class, "backLeft");
// backRight = hardwareMap.get(DcMotor.class, "backRight");
}
@Override
public void setPower(double x, double y, double rotation) {
// Mecanum kinematics
double frontLeftPower = y + x + rotation;
double frontRightPower = y - x - rotation;
double backLeftPower = y - x + rotation;
double backRightPower = y + x - rotation;
// Normalize
double max = Math.max(1.0, Math.max(
Math.max(Math.abs(frontLeftPower), Math.abs(frontRightPower)),
Math.max(Math.abs(backLeftPower), Math.abs(backRightPower))
));
// frontLeft.setPower(frontLeftPower / max);
// frontRight.setPower(frontRightPower / max);
// backLeft.setPower(backLeftPower / max);
// backRight.setPower(backRightPower / max);
System.out.printf("Drive: FL=%.2f FR=%.2f BL=%.2f BR=%.2f%n",
frontLeftPower/max, frontRightPower/max, backLeftPower/max, backRightPower/max);
}
@Override
public Pose2d getPose() {
return pose;
}
@Override
public double getHeading() {
return pose.heading;
}
}
EOF
cat > src/main/java/robot/opmodes/TeleOp.java << 'EOF'
package robot.opmodes;
// Uncomment when deploying to robot:
// import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
// import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import robot.subsystems.Drive;
import robot.hardware.MecanumDrive;
/**
* Main TeleOp mode.
* Uncomment annotations and imports when deploying.
*/
// @TeleOp(name="Main TeleOp", group="Competition")
public class TeleOp /* extends LinearOpMode */ {
/*
@Override
public void runOpMode() {
// Initialize hardware
Drive.Hardware hardware = new MecanumDrive(hardwareMap);
Drive drive = new Drive(hardware);
telemetry.addLine("Ready!");
telemetry.update();
waitForStart();
while (opModeIsActive()) {
// Driver controls
double x = -gamepad1.left_stick_x;
double y = -gamepad1.left_stick_y;
double rotation = -gamepad1.right_stick_x;
drive.drive(x, y, rotation);
// Telemetry
telemetry.addData("Pose", drive.getPose());
telemetry.update();
sleep(20);
}
}
*/
// Stub for testing compilation
public static void main(String[] args) {
System.out.println("TeleOp OpMode - deploy to robot to run");
}
}
EOF
cat > src/test/java/robot/subsystems/DriveTest.java << 'EOF'
package robot.subsystems;
import org.junit.jupiter.api.Test;
import robot.Pose2d;
import static org.junit.jupiter.api.Assertions.*;
class DriveTest {
// Simple inline mock
static class MockHardware implements Drive.Hardware {
double lastX, lastY, lastRot;
Pose2d pose = new Pose2d();
@Override
public void setPower(double x, double y, double rotation) {
lastX = x; lastY = y; lastRot = rotation;
}
@Override
public Pose2d getPose() { return pose; }
@Override
public double getHeading() { return pose.heading; }
}
@Test
void testRobotRelativeDrive() {
MockHardware mock = new MockHardware();
Drive drive = new Drive(mock);
drive.setFieldRelative(false);
drive.drive(1.0, 0.5, 0.0);
assertEquals(1.0, mock.lastX, 0.01);
assertEquals(0.5, mock.lastY, 0.01);
assertEquals(0.0, mock.lastRot, 0.01);
}
@Test
void testGetPose() {
MockHardware mock = new MockHardware();
mock.pose = new Pose2d(1, 2, 0.5);
Drive drive = new Drive(mock);
Pose2d pose = drive.getPose();
assertEquals(1, pose.x, 0.01);
assertEquals(2, pose.y, 0.01);
}
}
EOF
# Step 5: Create README
cat > LICENSE << 'EOF'
MIT License
Copyright (c) $(date +%Y) $PROJECT_NAME
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
EOF
cat > README.md << EOF
# $PROJECT_NAME
FTC Robot Code - Clean architecture, fully testable on PC.
## Quick Start
\`\`\`bash
# Run tests (on PC, no robot needed)
./gradlew test
# Watch tests (auto-rerun on save)
./gradlew test --continuous
# Check for compile errors
./build.sh
# Deploy to robot
./deploy-to-robot.sh
\`\`\`
## Project Structure
\`\`\`
src/
├── main/java/robot/
│ ├── subsystems/ # Your robot logic (tested on PC)
│ ├── hardware/ # Hardware implementations
│ └── opmodes/ # FTC OpModes
└── test/java/robot/ # Unit tests (run without robot)
\`\`\`
## Development Workflow
### 1. Develop on PC (Fast Iteration)
\`\`\`bash
./gradlew test --continuous
\`\`\`
Write your robot logic in \`subsystems/\`, test with mocks. No robot required!
### 2. Check Build
\`\`\`bash
./build.sh
\`\`\`
Catches compile errors before deployment.
### 3. Deploy to Robot
\`\`\`bash
./deploy-to-robot.sh
\`\`\`
Builds and installs to Control Hub (USB or WiFi).
## Adding Subsystems
Every subsystem follows this pattern:
\`\`\`java
public class MySubsystem {
private final Hardware hardware;
public MySubsystem(Hardware hardware) {
this.hardware = hardware;
}
// Your robot logic here - tests on PC!
public void doSomething() {
hardware.doThing();
}
// Inner interface - implement for robot
public interface Hardware {
void doThing();
}
}
\`\`\`
Test with inline mock:
\`\`\`java
class MySubsystemTest {
static class MockHardware implements MySubsystem.Hardware {
boolean didThing = false;
public void doThing() { didThing = true; }
}
@Test
void test() {
MockHardware mock = new MockHardware();
MySubsystem sys = new MySubsystem(mock);
sys.doSomething();
assertTrue(mock.didThing);
}
}
\`\`\`
## Commands
| Command | Description |
|---------|-------------|
| \`./gradlew test\` | Run all unit tests |
| \`./gradlew test --continuous\` | Watch mode (auto-rerun on save) |
| \`./build.sh\` | Build and check for errors |
| \`./build.sh --clean\` | Clean build |
| \`./deploy-to-robot.sh\` | Deploy to Control Hub |
| \`./deploy-to-robot.sh --help\` | Show all deployment options |
## Configuration
- **FTC SDK Location**: \`$FTC_SDK_DIR\`
- **FTC SDK Version**: \`$FTC_VERSION\`
- **Java Version**: 11
## Generated By
[ftc-new-project](https://github.com/your-org/ftc-project-gen) - Clean FTC robot projects
## License
MIT - See LICENSE file
EOF
cat > .gitignore << 'EOF'
# Gradle
.gradle/
build/
gradle-app.setting
!gradle-wrapper.jar
# IntelliJ IDEA
.idea/
*.iml
*.ipr
*.iws
out/
# VS Code
.vscode/
# Eclipse
.classpath
.project
.settings/
bin/
# macOS
.DS_Store
# Linux
*~
# Windows
Thumbs.db
desktop.ini
# Logs
*.log
# Test output
*.class
hs_err_pid*
EOF
cat > .gitattributes << 'EOF'
# Auto detect text files and perform LF normalization
* text=auto
# Java sources
*.java text diff=java
*.gradle text diff=java
*.gradle.kts text diff=java
# Shell scripts
*.sh text eol=lf
# These files are text and should be normalized (convert crlf => lf)
*.css text
*.html text
*.js text
*.json text
*.md text
*.txt text
*.xml text
*.yaml text
*.yml text
# These files are binary and should be left untouched
*.class binary
*.jar binary
*.so binary
*.dll binary
*.dylib binary
EOF
cat > .editorconfig << 'EOF'
# EditorConfig for consistent coding styles
# Works with: VS Code, Sublime, IntelliJ, etc.
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.java]
indent_style = space
indent_size = 4
[*.{gradle,gradle.kts}]
indent_style = space
indent_size = 4
[*.{md,markdown}]
trim_trailing_whitespace = false
[*.{sh,bash}]
indent_style = space
indent_size = 2
EOF
# Initialize git repo
git init -q
git add .
git commit -q -m "Initial commit: $PROJECT_NAME
Generated by ftc-new-project
FTC SDK: $FTC_VERSION
Project structure:
- Pure Java core for PC testing
- FTC hardware implementations
- Deployment scripts included
"
echo ""
echo "✓ Git repository initialized"
# Step 6: Create Gradle wrapper
mkdir -p gradle/wrapper
cat > gradle/wrapper/gradle-wrapper.properties << 'EOF'
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
EOF
cat > gradlew << 'GRADLEW'
#!/bin/sh
GRADLE_VERSION=8.5
GRADLE_HOME="$HOME/.gradle/wrapper/dists/gradle-${GRADLE_VERSION}-bin"
if [ ! -d "$GRADLE_HOME" ]; then
echo "Downloading Gradle ${GRADLE_VERSION}..."
mkdir -p "$GRADLE_HOME"
wget -q "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -O /tmp/gradle.zip || {
echo "Download failed. Install gradle manually from https://gradle.org"
exit 1
}
unzip -q /tmp/gradle.zip -d "$GRADLE_HOME/.."
rm /tmp/gradle.zip
fi
exec "$GRADLE_HOME/../gradle-${GRADLE_VERSION}/bin/gradle" "$@"
GRADLEW
chmod +x gradlew
# Step 7: Create build script
cat > build.sh << 'BUILDSCRIPT'
#!/bin/bash
# Build robot code for FTC deployment
set -e
FTC_SDK_DIR="${FTC_SDK_DIR:-$HOME/ftc-sdk}"
show_help() {
cat << EOF
╔════════════════════════════════════════════════════════════════╗
║ Build FTC Robot Code - Help ║
╚════════════════════════════════════════════════════════════════╝
Builds your robot code into an APK without deploying to Control Hub.
Useful for checking compile errors before deployment.
USAGE:
$(basename $0) [options]
OPTIONS:
-h, --help Show this help
-c, --clean Clean build (remove old artifacts)
-v, --verbose Verbose output
--check-only Only check if code compiles (no APK)
WHAT THIS SCRIPT DOES:
1. Runs local tests (./gradlew test)
2. Deploys code to FTC SDK
3. Builds APK (checks for compile errors)
4. Shows APK location
WHY USE THIS:
✓ Check for compile errors before deploying
✓ Faster than full deployment
✓ Safe - doesn't touch the robot
✓ Validates FTC imports are correct
EXAMPLES:
# Normal build
./build.sh
# Clean build (fresh start)
./build.sh --clean
# Just check compilation, don't build APK
./build.sh --check-only
# Verbose output
./build.sh --verbose
WORKFLOW:
During development:
./gradlew test --continuous # Fast PC testing
Before deployment:
./build.sh # Check compile errors
Ready to deploy:
./deploy-to-robot.sh # Build and deploy to robot
SEE ALSO:
./gradlew test Run unit tests
./deploy-to-robot.sh Build and deploy to robot
./deploy-to-robot.sh --help Full deployment options
EOF
exit 0
}
# Parse arguments
CLEAN_BUILD=false
VERBOSE=false
CHECK_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
;;
-c|--clean)
CLEAN_BUILD=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
--check-only)
CHECK_ONLY=true
shift
;;
*)
echo "Error: Unknown option: $1"
echo "Run with --help for usage information"
exit 1
;;
esac
done
GRADLE_ARGS=""
if [ "$VERBOSE" = true ]; then
GRADLE_ARGS="--info"
fi
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Build FTC Robot Code ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Step 1: Run tests
echo ">>> Step 1: Running unit tests..."
./gradlew test $GRADLE_ARGS || {
echo ""
echo "✗ Tests failed!"
echo "Fix test errors before building for robot."
exit 1
}
echo "✓ All tests passed"
echo ""
if [ "$CHECK_ONLY" = true ]; then
echo ">>> Check complete (--check-only)"
echo "✓ Code compiles and tests pass"
exit 0
fi
# Step 2: Deploy to SDK
echo ">>> Step 2: Deploying code to FTC SDK..."
./gradlew deployToSDK $GRADLE_ARGS || {
echo ""
echo "✗ Failed to deploy code"
exit 1
}
echo ""
# Step 3: Build APK
if [ "$CLEAN_BUILD" = true ]; then
echo ">>> Step 3: Clean build..."
cd "$FTC_SDK_DIR"
./gradlew clean $GRADLE_ARGS
echo ""
fi
echo ">>> Step 3: Building APK..."
cd "$FTC_SDK_DIR"
# Check for Android SDK before building
if [ ! -f "local.properties" ] && [ -z "$ANDROID_HOME" ]; then
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Android SDK Not Configured ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "FTC SDK needs Android SDK to build the APK."
echo ""
echo "Quick setup:"
echo ""
echo "1. Install Android Studio:"
echo " https://developer.android.com/studio"
echo ""
echo "2. After install, Android SDK is usually at:"
echo " Linux: ~/Android/Sdk"
echo " macOS: ~/Library/Android/sdk"
echo " Windows: C:\\Users\\<you>\\AppData\\Local\\Android\\Sdk"
echo ""
echo "3. Create local.properties in FTC SDK:"
echo " echo 'sdk.dir=$HOME/Android/Sdk' > $FTC_SDK_DIR/local.properties"
echo ""
echo "Or set environment variable:"
echo " export ANDROID_HOME=~/Android/Sdk"
echo " echo 'export ANDROID_HOME=~/Android/Sdk' >> ~/.bashrc"
echo ""
echo "After setup, run ./build.sh again"
echo ""
exit 1
fi
./gradlew assembleDebug $GRADLE_ARGS || {
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Build Failed - Compile Errors ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Common issues:"
echo ""
echo "1. FTC imports not uncommented"
echo " → Edit hardware and OpMode files"
echo " → Uncomment: import com.qualcomm.robotcore..."
echo ""
echo "2. Missing FTC SDK"
echo " → Check: ls $FTC_SDK_DIR"
echo " → Should see: RobotCore, Hardware, FtcRobotController"
echo ""
echo "3. Syntax errors"
echo " → Check error messages above"
echo " → Fix in your src/ files"
echo ""
echo "Run with --verbose for more details"
echo ""
exit 1
}
APK_PATH="$FTC_SDK_DIR/FtcRobotController/build/outputs/apk/debug/FtcRobotController-debug.apk"
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ✓ Build Successful! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "APK Location:"
echo " $APK_PATH"
echo ""
echo "APK Size: $(du -h "$APK_PATH" | cut -f1)"
echo ""
echo "Next steps:"
echo ""
echo " Deploy to robot:"
echo " ./deploy-to-robot.sh --skip-build"
echo ""
echo " Or just run full deployment:"
echo " ./deploy-to-robot.sh"
echo ""
BUILDSCRIPT
chmod +x build.sh
# Step 8: Create deployment script
cat > deploy-to-robot.sh << 'DEPLOYSCRIPT'
#!/bin/bash
# Deploy to FTC Control Hub
set -e
CONTROL_HUB_IP="${CONTROL_HUB_IP:-192.168.43.1}"
CONTROL_HUB_PORT="${CONTROL_HUB_PORT:-5555}"
FTC_SDK_DIR="${FTC_SDK_DIR:-$HOME/ftc-sdk}"
show_help() {
cat << EOF
╔════════════════════════════════════════════════════════════════╗
║ Deploy to FTC Control Hub - Help ║
╚════════════════════════════════════════════════════════════════╝
Builds your robot code and deploys it to the Control Hub.
USAGE:
$(basename $0) [options]
OPTIONS:
-h, --help Show this help
-u, --usb Force USB connection (skip WiFi)
-w, --wifi Force WiFi connection (skip USB)
-i, --ip <address> Custom Control Hub IP address
-p, --port <port> Custom adb port (default: 5555)
--skip-build Skip building, just install existing APK
--build-only Build APK but don't install
CONNECTION METHODS:
1. USB (Recommended - Most Reliable)
────────────────────────────────────────
• Plug Control Hub into computer via USB
• Run: ./deploy-to-robot.sh
• That's it!
Pros: Fast, reliable, no network needed
Cons: Requires cable
2. WiFi Direct (Default FTC Network)
────────────────────────────────────────
• Connect computer to 'FIRST-xxxx-RC' network
• Run: ./deploy-to-robot.sh
• Uses IP: 192.168.43.1:5555 (FTC standard)
Pros: Wireless, convenient
Cons: Can be slower, network issues
3. Custom Network
────────────────────────────────────────
• Control Hub connected to your network
• Find IP: Check router or Control Hub settings
• Run: ./deploy-to-robot.sh -i 192.168.1.100
Pros: Can use your network
Cons: Need to find IP address
EXAMPLES:
# Auto-detect (tries USB first, then WiFi)
./deploy-to-robot.sh
# Force USB only
./deploy-to-robot.sh --usb
# Force WiFi only
./deploy-to-robot.sh --wifi
# Custom IP address
./deploy-to-robot.sh -i 192.168.1.100
# Custom IP with environment variable
CONTROL_HUB_IP=10.0.0.50 ./deploy-to-robot.sh
# Build only, no install (for testing build)
./deploy-to-robot.sh --build-only
# Install existing APK (skip build)
./deploy-to-robot.sh --skip-build
TROUBLESHOOTING:
"adb: command not found"
├─> Install Android Platform Tools:
│ Ubuntu/Debian: sudo apt install android-tools-adb
│ Arch: sudo pacman -S android-tools
│ Download: https://developer.android.com/studio/releases/platform-tools
"cannot connect to 192.168.43.1:5555"
├─> Make sure you're connected to FIRST-xxxx-RC WiFi network
├─> Try USB instead: ./deploy-to-robot.sh --usb
└─> Check Control Hub is powered on
"device unauthorized"
├─> On Control Hub, allow USB debugging
└─> Check Control Hub screen for authorization prompt
"no devices/emulators found"
├─> USB: Check cable, try different port
├─> WiFi: Verify network connection
└─> Run: adb devices (to see what adb sees)
WHAT THIS SCRIPT DOES:
1. Deploys your code to ~/ftc-sdk/TeamCode
2. Builds APK using FTC SDK
3. Connects to Control Hub (USB or WiFi)
4. Installs APK to Control Hub
5. Your OpModes appear on Driver Station
AFTER DEPLOYMENT:
On Driver Station:
1. Go to OpModes menu
2. Select TeleOp → "Main TeleOp"
3. Press INIT, then START
4. Your code is now running!
ENVIRONMENT VARIABLES:
CONTROL_HUB_IP Control Hub IP (default: 192.168.43.1)
CONTROL_HUB_PORT adb port (default: 5555)
FTC_SDK_DIR FTC SDK location (default: ~/ftc-sdk)
Example:
CONTROL_HUB_IP=192.168.1.100 FTC_SDK_DIR=~/my-ftc ./deploy-to-robot.sh
SEE ALSO:
adb devices List connected devices
adb connect <ip> Connect via WiFi manually
adb disconnect Disconnect WiFi connection
EOF
exit 0
}
# Parse arguments
FORCE_USB=false
FORCE_WIFI=false
SKIP_BUILD=false
BUILD_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
;;
-u|--usb)
FORCE_USB=true
shift
;;
-w|--wifi)
FORCE_WIFI=true
shift
;;
-i|--ip)
CONTROL_HUB_IP="$2"
shift 2
;;
-p|--port)
CONTROL_HUB_PORT="$2"
shift 2
;;
--skip-build)
SKIP_BUILD=true
shift
;;
--build-only)
BUILD_ONLY=true
shift
;;
*)
echo "Error: Unknown option: $1"
echo "Run with --help for usage information"
exit 1
;;
esac
done
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Deploy to FTC Control Hub ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
if [ "$SKIP_BUILD" = false ]; then
# Step 1: Deploy code to SDK
echo ">>> Step 1: Deploying code to FTC SDK..."
./gradlew deployToSDK || {
echo "Error: Failed to deploy code"
exit 1
}
echo ""
# Step 2: Build APK
echo ">>> Step 2: Building APK..."
cd "$FTC_SDK_DIR"
./gradlew assembleDebug || {
echo "Error: Failed to build APK"
exit 1
}
echo ""
else
echo ">>> Skipping build (--skip-build)"
cd "$FTC_SDK_DIR"
echo ""
fi
APK_PATH="$FTC_SDK_DIR/FtcRobotController/build/outputs/apk/debug/FtcRobotController-debug.apk"
if [ ! -f "$APK_PATH" ]; then
echo "Error: APK not found at $APK_PATH"
echo "Try building first: ./deploy-to-robot.sh (without --skip-build)"
exit 1
fi
echo "✓ APK ready: $(basename $APK_PATH)"
echo ""
if [ "$BUILD_ONLY" = true ]; then
echo ">>> Build complete (--build-only)"
echo ""
echo "APK location: $APK_PATH"
echo ""
echo "To install manually:"
echo " adb install -r $APK_PATH"
exit 0
fi
# Step 3: Install to robot
echo ">>> Step 3: Installing to Control Hub..."
echo ""
# Check if adb is available
if ! command -v adb &> /dev/null; then
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ERROR: adb not found ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Android Debug Bridge (adb) is required for deployment."
echo ""
echo "Install it:"
echo " Ubuntu/Debian: sudo apt install android-tools-adb"
echo " Arch Linux: sudo pacman -S android-tools"
echo " macOS: brew install android-platform-tools"
echo ""
echo "Or download Android Platform Tools:"
echo " https://developer.android.com/studio/releases/platform-tools"
echo ""
exit 1
fi
INSTALLED=false
# Try USB first (unless forcing WiFi)
if [ "$FORCE_WIFI" = false ]; then
echo "Checking for USB connection..."
USB_DEVICES=$(adb devices | grep -v "List" | grep "device$" | wc -l)
if [ "$USB_DEVICES" -gt 0 ]; then
echo "✓ Control Hub connected via USB"
echo ""
adb install -r "$APK_PATH" && INSTALLED=true
else
if [ "$FORCE_USB" = true ]; then
echo "✗ No USB device found (--usb specified)"
echo ""
echo "Make sure:"
echo " • Control Hub is powered on"
echo " • USB cable is connected"
echo " • USB debugging is enabled on Control Hub"
echo ""
echo "Check with: adb devices"
exit 1
else
echo " No USB device detected"
fi
fi
fi
# Try WiFi if USB didn't work (unless forcing USB)
if [ "$INSTALLED" = false ] && [ "$FORCE_USB" = false ]; then
echo "Trying WiFi connection to $CONTROL_HUB_IP:$CONTROL_HUB_PORT..."
echo ""
# Try to connect
adb connect "$CONTROL_HUB_IP:$CONTROL_HUB_PORT" 2>&1 | grep -v "cannot connect" || true
# Wait for connection
sleep 2
# Check if connected
if adb devices | grep -q "$CONTROL_HUB_IP"; then
echo "✓ Connected via WiFi"
echo ""
adb install -r "$APK_PATH" && INSTALLED=true
fi
fi
if [ "$INSTALLED" = false ]; then
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Could not connect to Control Hub ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Connection options:"
echo ""
echo "1. USB Connection (Recommended)"
echo " ────────────────────────────────"
echo " • Plug Control Hub into computer with USB cable"
echo " • Run: ./deploy-to-robot.sh --usb"
echo ""
echo "2. WiFi Direct Connection"
echo " ────────────────────────────────"
echo " • Connect to 'FIRST-xxxx-RC' network"
echo " • Run: ./deploy-to-robot.sh --wifi"
echo " • Default IP: 192.168.43.1:5555"
echo ""
echo "3. Custom Network"
echo " ────────────────────────────────"
echo " • Find Control Hub IP on your network"
echo " • Run: ./deploy-to-robot.sh -i YOUR_IP"
echo ""
echo "4. Manual Install"
echo " ────────────────────────────────"
echo " • APK built at: $APK_PATH"
echo " • Use Android Studio to deploy"
echo " • Or: adb connect <ip>:5555 && adb install -r <apk>"
echo ""
echo "Debug with:"
echo " adb devices # See connected devices"
echo " adb connect 192.168.43.1 # Connect manually"
echo ""
echo "Run with --help for more options"
echo ""
exit 1
fi
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ✓ Deployment Complete! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "On Driver Station:"
echo " 1. Go to: OpModes menu"
echo " 2. Select: TeleOp → 'Main TeleOp'"
echo " 3. Press: INIT, then START"
echo ""
echo "Your code is now running on the robot! 🤖"
echo ""
DEPLOYSCRIPT
chmod +x deploy-to-robot.sh
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ✓ Project Created! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Project: $PROJECT_NAME"
echo "Location: $PROJECT_DIR"
echo "SDK: $FTC_SDK_DIR ($FTC_VERSION)"
echo "Git: Initialized with initial commit"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " QUICK START"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "1. Enter project and run tests:"
echo " cd $PROJECT_NAME"
echo " ./gradlew test"
echo ""
echo "2. Watch tests (auto-rerun on save):"
echo " ./gradlew test --continuous"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " PROJECT STRUCTURE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "$PROJECT_NAME/"
echo "├── src/main/java/robot/"
echo "│ ├── subsystems/"
echo "│ │ └── Drive.java ← Your robot logic (EDIT THIS)"
echo "│ ├── hardware/"
echo "│ │ └── MecanumDrive.java ← Hardware impl (EDIT THIS)"
echo "│ └── opmodes/"
echo "│ └── TeleOp.java ← FTC OpMode (EDIT THIS)"
echo "│"
echo "└── src/test/java/robot/"
echo " └── subsystems/"
echo " └── DriveTest.java ← Unit tests (ADD MORE)"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " DEVELOPMENT WORKFLOW"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Day-to-day development (on your PC):"
echo ""
echo " 1. Edit code in src/main/java/robot/"
echo " 2. Edit tests in src/test/java/robot/"
echo " 3. Run: ./gradlew test --continuous"
echo " 4. Tests pass → you're good!"
echo ""
echo "Example: Add a new subsystem:"
echo ""
echo " # Create subsystem with inner Hardware interface"
echo " src/main/java/robot/subsystems/Intake.java"
echo ""
echo " # Create test with inline mock"
echo " src/test/java/robot/subsystems/IntakeTest.java"
echo ""
echo " # Run tests"
echo " ./gradlew test"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " DEPLOYMENT TO ROBOT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "When ready to test on actual robot:"
echo ""
echo " 1. Uncomment FTC imports in:"
echo " - src/main/java/robot/hardware/MecanumDrive.java"
echo " - src/main/java/robot/opmodes/TeleOp.java"
echo ""
echo " 2. Run deployment script:"
echo " ./deploy-to-robot.sh"
echo ""
echo " The script will:"
echo " ✓ Deploy your code to SDK"
echo " ✓ Build APK"
echo " ✓ Install to Control Hub (via USB or WiFi)"
echo ""
echo " Connection methods:"
echo " • USB: Just plug in and run"
echo " • WiFi: Connect to 'FIRST-xxxx-RC' network (IP: 192.168.43.1)"
echo " • Custom: CONTROL_HUB_IP=192.168.1.x ./deploy-to-robot.sh"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " KEY FILES TO EDIT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Start here:"
echo ""
echo " src/main/java/robot/subsystems/Drive.java"
echo " → Your drive logic, tested on PC"
echo ""
echo " src/main/java/robot/hardware/MecanumDrive.java"
echo " → Real motor control (uncomment FTC code when deploying)"
echo ""
echo " src/main/java/robot/opmodes/TeleOp.java"
echo " → Your main OpMode (uncomment FTC code when deploying)"
echo ""
echo " src/test/java/robot/subsystems/DriveTest.java"
echo " → Unit tests - add more as you build!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " USEFUL COMMANDS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Development (on PC):"
echo " ./gradlew test Run all tests"
echo " ./gradlew test --continuous Watch mode (auto-rerun)"
echo ""
echo " Before deployment:"
echo " ./build.sh Check for compile errors"
echo " ./build.sh --clean Clean build"
echo ""
echo " Deploy to robot:"
echo " ./deploy-to-robot.sh Full deployment (build + install)"
echo " ./deploy-to-robot.sh --help Show all deployment options"
echo ""
echo " Other:"
echo " ./gradlew clean Clean build artifacts"
echo " ./gradlew tasks List all available tasks"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " TIPS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " • Keep FTC imports commented during development"
echo " • Write tests for everything - they run instantly on PC"
echo " • Use './gradlew test --continuous' for fast iteration"
echo " • See README.md for detailed documentation"
echo " • Multiple projects can share the same FTC SDK!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Ready to start coding? Run:"
echo ""
echo " cd $PROJECT_NAME && ./gradlew test --continuous"
echo ""
echo "Happy coding! 🤖"
echo ""