Restructured linux to match Windows

This commit is contained in:
Eric Ratliff
2026-01-24 12:39:32 -06:00
parent b1593a4f87
commit fd9c573131
30 changed files with 3120 additions and 1746 deletions

290
linux/lib.sh Executable file
View File

@@ -0,0 +1,290 @@
#!/bin/bash
# FTC Project Generator - Shared Library Functions
# Copyright (c) 2026 Nexus Workshops LLC
# Licensed under MIT License
# Get the directory where this script lives
get_script_dir() {
echo "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
}
# Get generator version
get_generator_version() {
local script_dir="$(get_script_dir)"
local version_file="$script_dir/../VERSION"
if [ -f "$version_file" ]; then
cat "$version_file" | tr -d '\n\r'
else
echo "unknown"
fi
}
# Process template file, replacing placeholders
process_template() {
local input="$1"
local output="$2"
sed -e "s|{{PROJECT_NAME}}|${PROJECT_NAME}|g" \
-e "s|{{SDK_DIR}}|${FTC_SDK_DIR}|g" \
-e "s|{{FTC_VERSION}}|${FTC_VERSION}|g" \
-e "s|{{GENERATOR_VERSION}}|${GENERATOR_VERSION}|g" \
"$input" > "$output"
}
# Copy template file
copy_template() {
local src="$1"
local dest="$2"
cp "$src" "$dest"
}
# Create project structure
create_project_structure() {
local project_dir="$1"
mkdir -p "$project_dir"
cd "$project_dir"
mkdir -p src/main/java/robot/subsystems
mkdir -p src/main/java/robot/hardware
mkdir -p src/main/java/robot/opmodes
mkdir -p src/test/java/robot/subsystems
mkdir -p src/test/java/robot/hardware
mkdir -p gradle/wrapper
}
# Install all project templates
install_templates() {
local project_dir="$1"
local template_dir="$2"
cd "$project_dir"
copy_template "$template_dir/Pose2d.java" "src/main/java/robot/Pose2d.java"
copy_template "$template_dir/Drive.java" "src/main/java/robot/subsystems/Drive.java"
copy_template "$template_dir/MecanumDrive.java" "src/main/java/robot/hardware/MecanumDrive.java"
copy_template "$template_dir/TeleOp.java" "src/main/java/robot/opmodes/TeleOp.java"
copy_template "$template_dir/DriveTest.java" "src/test/java/robot/subsystems/DriveTest.java"
copy_template "$template_dir/build.gradle.kts" "build.gradle.kts"
process_template "$template_dir/settings.gradle.kts.template" "settings.gradle.kts"
process_template "$template_dir/gitignore.template" ".gitignore"
process_template "$template_dir/README.md.template" "README.md"
copy_template "$template_dir/build.sh" "build.sh"
chmod +x "build.sh"
create_deploy_script "$project_dir"
echo "${GENERATOR_VERSION}" > ".ftc-generator-version"
}
# Create deploy script
create_deploy_script() {
local project_dir="$1"
cat > "$project_dir/deploy-to-robot.sh" <<'ENDSCRIPT'
#!/bin/bash
set -e
CONTROL_HUB_IP="${CONTROL_HUB_IP:-192.168.43.1}"
CONTROL_HUB_PORT="5555"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) echo "Deploy to Control Hub"; echo "Usage: $0 [--usb|--wifi] [-i IP]"; exit 0 ;;
-i|--ip) CONTROL_HUB_IP="$2"; shift 2 ;;
*) shift ;;
esac
done
echo "Deploying to SDK..."
./gradlew deployToSDK || exit 1
echo "Building APK..."
cd "${HOME}/ftc-sdk" && ./gradlew build || exit 1
APK_PATH="${HOME}/ftc-sdk/FtcRobotController/build/outputs/apk/debug/FtcRobotController-debug.apk"
[ -f "$APK_PATH" ] || { echo "APK not found"; exit 1; }
echo "Installing..."
if adb devices | grep -q device; then
adb install -r "$APK_PATH" && echo "✓ Deployed!" || exit 1
else
adb connect "$CONTROL_HUB_IP:$CONTROL_HUB_PORT" && sleep 2
adb install -r "$APK_PATH" && echo "✓ Deployed!" || exit 1
fi
ENDSCRIPT
chmod +x "$project_dir/deploy-to-robot.sh"
}
# Setup Gradle wrapper
setup_gradle_wrapper() {
local project_dir="$1"
cd "$project_dir"
# Create gradle wrapper properties
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.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
EOF
# Check if gradle is available
if ! command -v gradle &> /dev/null; then
echo "Error: gradle command not found"
echo "Gradle must be installed to generate wrapper scripts"
echo ""
echo "Install:"
echo " Ubuntu/Debian: sudo apt install gradle"
echo " macOS: brew install gradle"
return 1
fi
echo "Generating Gradle wrapper..."
# Try with system gradle first
if gradle wrapper --gradle-version 8.9 --no-daemon 2>/dev/null; then
echo "✓ Gradle wrapper created"
else
echo ""
echo "System Gradle failed. Using fallback method..."
# Download wrapper jar directly
local wrapper_jar_url="https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar"
if command -v curl &> /dev/null; then
curl -sL "$wrapper_jar_url" -o gradle/wrapper/gradle-wrapper.jar 2>/dev/null
elif command -v wget &> /dev/null; then
wget -q "$wrapper_jar_url" -O gradle/wrapper/gradle-wrapper.jar 2>/dev/null
else
echo "Error: Need curl or wget to download wrapper jar"
return 1
fi
if [ ! -f "gradle/wrapper/gradle-wrapper.jar" ]; then
echo "Error: Failed to download gradle-wrapper.jar"
return 1
fi
# Create proper gradlew script
cat > gradlew <<'GRADLEW_END'
#!/bin/sh
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_BASE_NAME=${0##*/}
# Determine the Java command
if [ -n "$JAVA_HOME" ] ; then
JAVACMD=$JAVA_HOME/bin/java
else
JAVACMD=java
fi
# Check if Java is available
if ! "$JAVACMD" -version > /dev/null 2>&1; then
echo "ERROR: JAVA_HOME is not set and no 'java' command could be found"
exit 1
fi
# Execute Gradle
exec "$JAVACMD" -Xmx64m -Xms64m -Dorg.gradle.appname="$APP_BASE_NAME" -classpath "$(dirname "$0")/gradle/wrapper/gradle-wrapper.jar" org.gradle.wrapper.GradleWrapperMain "$@"
GRADLEW_END
chmod +x gradlew
# Create Windows batch file
cat > gradlew.bat <<'GRADLEWBAT_END'
@if "%DEBUG%" == "" @echo off
setlocal enabledelayedexpansion
set DIRNAME=%~dp0
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
set CLASSPATH=%APP_HOME%gradle\wrapper\gradle-wrapper.jar
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
goto execute
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%\bin\java.exe
:execute
"%JAVA_EXE%" -Xmx64m -Xms64m -Dorg.gradle.appname="%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
GRADLEWBAT_END
echo "✓ Gradle wrapper created (via fallback)"
fi
# Verify wrapper was created
if [ ! -f "gradlew" ]; then
echo "Error: gradlew script not created"
return 1
fi
}
# Check if project is a generator project
is_generator_project() {
local project_dir="$1"
[ -f "$project_dir/.ftc-generator-version" ]
}
# Get project generator version
get_project_generator_version() {
local project_dir="$1"
if [ -f "$project_dir/.ftc-generator-version" ]; then
cat "$project_dir/.ftc-generator-version" | tr -d '\n\r'
else
echo "unknown"
fi
}
# Upgrade project
upgrade_project() {
local project_dir="$1"
local template_dir="$2"
local old_version=$(get_project_generator_version "$project_dir")
echo "Upgrading project from $old_version to ${GENERATOR_VERSION}..."
cd "$project_dir"
copy_template "$template_dir/build.gradle.kts" "build.gradle.kts"
process_template "$template_dir/settings.gradle.kts.template" "settings.gradle.kts"
process_template "$template_dir/gitignore.template" ".gitignore"
copy_template "$template_dir/build.sh" "build.sh"
chmod +x "build.sh"
create_deploy_script "$project_dir"
echo "${GENERATOR_VERSION}" > ".ftc-generator-version"
echo "✓ Upgrade complete"
}
# Initialize git repo
init_git_repo() {
local project_dir="$1"
cd "$project_dir"
if [ ! -d ".git" ]; then
git init > /dev/null 2>&1
git add . > /dev/null 2>&1
git commit -m "Initial commit from FTC Project Generator v${GENERATOR_VERSION}" > /dev/null 2>&1
echo "✓ Git repository initialized"
fi
}
# Check prerequisites
check_prerequisites() {
local missing=()
command -v git &> /dev/null || missing+=("git")
command -v java &> /dev/null || missing+=("java")
command -v gradle &> /dev/null || missing+=("gradle")
if [ "${#missing[@]}" -gt 0 ]; then
echo "Error: Missing tools: ${missing[*]}"
echo "Install: sudo apt install ${missing[*]}"
return 1
fi
echo "✓ All prerequisites satisfied"
return 0
}

View File

@@ -0,0 +1,70 @@
package robot.subsystems;
import robot.Pose2d;
/**
* Drive subsystem - business logic only.
* Hardware interface defined as inner interface.
* Tests use inline mocks - no FTC SDK needed.
*/
public class Drive {
private final Hardware hardware;
private Pose2d pose;
public Drive(Hardware hardware) {
this.hardware = hardware;
this.pose = new Pose2d();
}
/**
* Drive using field-centric controls.
* @param forward Forward/backward speed (-1.0 to 1.0)
* @param strafe Left/right speed (-1.0 to 1.0)
* @param rotate Rotation speed (-1.0 to 1.0)
*/
public void drive(double forward, double strafe, double rotate) {
// Your drive logic here
double heading = hardware.getHeading();
// Example: field-centric conversion
double cos = Math.cos(heading);
double sin = Math.sin(heading);
double rotatedForward = forward * cos - strafe * sin;
double rotatedStrafe = forward * sin + strafe * cos;
hardware.setPowers(rotatedForward, rotatedStrafe, rotate);
}
/**
* Stop all drive motors.
*/
public void stop() {
hardware.setPowers(0, 0, 0);
}
/**
* Get current estimated pose.
*/
public Pose2d getPose() {
return pose;
}
/**
* Hardware interface - implement this for real robot.
*/
public interface Hardware {
/**
* Get robot heading in radians.
*/
double getHeading();
/**
* Set drive motor powers.
* @param forward Forward power
* @param strafe Strafe power
* @param rotate Rotation power
*/
void setPowers(double forward, double strafe, double rotate);
}
}

View File

@@ -0,0 +1,66 @@
package robot.subsystems;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Drive subsystem.
* Uses inline mock - no FTC SDK required.
*/
class DriveTest {
/**
* Simple mock implementation of Drive.Hardware.
* Captures method calls for verification in tests.
*/
static class MockHardware implements Drive.Hardware {
double lastForward = 0;
double lastStrafe = 0;
double lastRotate = 0;
double heading = 0;
int setPowersCallCount = 0;
@Override
public double getHeading() {
return heading;
}
@Override
public void setPowers(double forward, double strafe, double rotate) {
this.lastForward = forward;
this.lastStrafe = strafe;
this.lastRotate = rotate;
this.setPowersCallCount++;
}
}
@Test
void testDriveCallsSetPowers() {
MockHardware mock = new MockHardware();
Drive drive = new Drive(mock);
drive.drive(0.5, 0.3, 0.1);
assertEquals(1, mock.setPowersCallCount, "setPowers should be called once");
}
@Test
void testStopSetsZeroPower() {
MockHardware mock = new MockHardware();
Drive drive = new Drive(mock);
drive.stop();
assertEquals(0.0, mock.lastForward, 0.001);
assertEquals(0.0, mock.lastStrafe, 0.001);
assertEquals(0.0, mock.lastRotate, 0.001);
}
@Test
void testGetPoseReturnsNonNull() {
MockHardware mock = new MockHardware();
Drive drive = new Drive(mock);
assertNotNull(drive.getPose(), "Pose should never be null");
}
}

View File

@@ -0,0 +1,74 @@
package robot.hardware;
import robot.subsystems.Drive;
// Uncomment when deploying to robot:
// import com.qualcomm.robotcore.hardware.DcMotor;
// import com.qualcomm.robotcore.hardware.HardwareMap;
// import com.qualcomm.robotcore.hardware.IMU;
// import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
/**
* Mecanum drive hardware implementation.
* Implements Drive.Hardware interface.
*
* DEPLOYMENT NOTE:
* Uncomment FTC imports and implementation when ready to deploy.
* Keep commented during development/testing on PC.
*/
public class MecanumDrive implements Drive.Hardware {
// Uncomment when deploying:
// private final DcMotor frontLeft;
// private final DcMotor frontRight;
// private final DcMotor backLeft;
// private final DcMotor backRight;
// private final IMU imu;
public MecanumDrive(/* HardwareMap hardwareMap */) {
// Uncomment when deploying:
// frontLeft = hardwareMap.get(DcMotor.class, "frontLeft");
// frontRight = hardwareMap.get(DcMotor.class, "frontRight");
// backLeft = hardwareMap.get(DcMotor.class, "backLeft");
// backRight = hardwareMap.get(DcMotor.class, "backRight");
// imu = hardwareMap.get(IMU.class, "imu");
//
// // Configure motors
// frontLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
// frontRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
// backLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
// backRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
}
@Override
public double getHeading() {
// Stub for testing - returns 0
return 0.0;
// Uncomment when deploying:
// return imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS);
}
@Override
public void setPowers(double forward, double strafe, double rotate) {
// Stub for testing - does nothing
// Uncomment when deploying:
// // Mecanum drive kinematics
// double frontLeftPower = forward + strafe + rotate;
// double frontRightPower = forward - strafe - rotate;
// double backLeftPower = forward - strafe + rotate;
// double backRightPower = forward + strafe - rotate;
//
// // Normalize powers
// double maxPower = 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 / maxPower);
// frontRight.setPower(frontRightPower / maxPower);
// backLeft.setPower(backLeftPower / maxPower);
// backRight.setPower(backRightPower / maxPower);
}
}

View File

@@ -0,0 +1,26 @@
package robot;
/**
* Simple 2D pose representation (x, y, heading).
* Pure data class - no dependencies.
*/
public class Pose2d {
public final double x;
public final double y;
public final double heading;
public Pose2d(double x, double y, double heading) {
this.x = x;
this.y = y;
this.heading = heading;
}
public Pose2d() {
this(0, 0, 0);
}
@Override
public String toString() {
return String.format("Pose2d(x=%.2f, y=%.2f, heading=%.2f)", x, y, heading);
}
}

View File

@@ -0,0 +1,132 @@
# {{PROJECT_NAME}}
FTC robot project generated by FTC Project Generator v{{GENERATOR_VERSION}}.
## Quick Start
```bash
# Run tests
./gradlew test
# Watch tests (auto-rerun)
./gradlew test --continuous
# Build and check
./build.sh
# Deploy to robot
./deploy-to-robot.sh
```
## Project Structure
```
{{PROJECT_NAME}}/
├── 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)
```
## Development Workflow
1. **Write code** in `src/main/java/robot/`
2. **Write tests** in `src/test/java/robot/`
3. **Run tests** with `./gradlew test --continuous`
4. **Tests pass** → You're good!
## Deployment to Robot
When ready to test on actual hardware:
1. **Uncomment FTC imports** in:
- `src/main/java/robot/hardware/MecanumDrive.java`
- `src/main/java/robot/opmodes/TeleOp.java`
2. **Run deployment script:**
```bash
./deploy-to-robot.sh
```
The script will:
- Deploy your code to SDK TeamCode
- Build APK
- Install to Control Hub (via USB or WiFi)
### Connection Methods
- **USB**: Just plug in and run (recommended)
- **WiFi**: Connect to 'FIRST-xxxx-RC' network (IP: 192.168.43.1)
- **Custom**: `./deploy-to-robot.sh -i 192.168.1.x`
## Adding New Subsystems
Follow the pattern:
1. **Create subsystem** with inner Hardware interface:
```java
public class MySubsystem {
public interface Hardware {
void doThing();
}
}
```
2. **Create test** with inline mock:
```java
class MySubsystemTest {
static class MockHardware implements MySubsystem.Hardware {
boolean didThing = false;
public void doThing() { didThing = true; }
}
}
```
3. **Create hardware impl** for robot (keep FTC imports commented during dev)
## Tips
- Keep FTC imports commented during development
- Write tests for everything - they run instantly on PC
- Use `./gradlew test --continuous` for fast iteration
- Multiple projects can share the same FTC SDK
## Commands
```bash
# Development (on PC)
./gradlew test Run all tests
./gradlew test --continuous Watch mode
# Before deployment
./build.sh Check for compile errors
./build.sh --clean Clean build
# Deploy to robot
./deploy-to-robot.sh Full deployment
./deploy-to-robot.sh --help Show options
# Other
./gradlew clean Clean build artifacts
./gradlew tasks List available tasks
```
## Upgrading
To upgrade this project to a newer version of the generator:
```bash
# From parent directory
ftc-new-project {{PROJECT_NAME}} --upgrade
```
This will update build files and scripts while preserving your code.
## Generated by FTC Project Generator
This project structure separates your robot code from the FTC SDK,
making it easy to test on PC and deploy when ready.
Generator version: {{GENERATOR_VERSION}}
FTC SDK version: {{FTC_VERSION}}

View File

@@ -0,0 +1,60 @@
package robot.opmodes;
import robot.hardware.MecanumDrive;
import robot.subsystems.Drive;
// Uncomment when deploying to robot:
// import com.qualcomm.robotcore.eventloop.opmode.OpMode;
// import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
/**
* Main TeleOp OpMode.
*
* DEPLOYMENT NOTE:
* Uncomment FTC imports and @TeleOp annotation when ready to deploy.
* Keep commented during development/testing on PC.
*/
// @TeleOp(name="Main TeleOp")
public class TeleOp /* extends OpMode */ {
private Drive drive;
// Uncomment when deploying:
// @Override
public void init() {
// Uncomment when deploying:
// MecanumDrive hardware = new MecanumDrive(hardwareMap);
// drive = new Drive(hardware);
//
// telemetry.addData("Status", "Initialized");
// telemetry.update();
}
// Uncomment when deploying:
// @Override
public void loop() {
// Uncomment when deploying:
// // Get gamepad inputs
// double forward = -gamepad1.left_stick_y; // Inverted
// double strafe = gamepad1.left_stick_x;
// double rotate = gamepad1.right_stick_x;
//
// // Drive the robot
// drive.drive(forward, strafe, rotate);
//
// // Show telemetry
// telemetry.addData("Forward", "%.2f", forward);
// telemetry.addData("Strafe", "%.2f", strafe);
// telemetry.addData("Rotate", "%.2f", rotate);
// telemetry.update();
}
// Uncomment when deploying:
// @Override
public void stop() {
// Uncomment when deploying:
// if (drive != null) {
// drive.stop();
// }
}
}

View File

@@ -0,0 +1,49 @@
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("$homeDir/ftc-sdk")
from("src/main/java") {
include("robot/**/*.java")
}
into(layout.projectDirectory.dir("${sdkDir.get()}/TeamCode/src/main/java"))
doLast {
println("✓ Code deployed to TeamCode - ready to build APK")
}
}

40
linux/templates/build.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Quick build/check script for FTC project
set -e
CLEAN=false
while [[ $# -gt 0 ]]; do
case $1 in
--clean|-c)
CLEAN=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [ "$CLEAN" = true ]; then
echo "Cleaning build..."
./gradlew clean
fi
echo "Building and testing..."
./gradlew build test
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " ✓ Build Successful!"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo "Your code compiles and all tests pass."
echo "Ready to deploy to robot when you are."
echo ""
echo "Next steps:"
echo " 1. Uncomment FTC imports in hardware and opmodes"
echo " 2. Run: ./deploy-to-robot.sh"
echo ""

View File

@@ -0,0 +1,19 @@
# Gradle
.gradle/
build/
# IDE
.idea/
*.iml
.vscode/
# OS
.DS_Store
Thumbs.db
# Java
*.class
*.log
# Gradle wrapper jar
gradle/wrapper/gradle-wrapper.jar

View File

@@ -0,0 +1,2 @@
// Include FTC SDK as composite build
includeBuild("{{SDK_DIR}}")