Add chute controller with homing and position unwrapping
Implements automated homing, potentiometer wraparound tracking, and position control for FTC chute mechanism. Includes mock hardware for testing and real hardware adapters for deployment.
This commit is contained in:
@@ -1,19 +1,24 @@
|
||||
plugins {
|
||||
java
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Exclude FTC hardware files from PC compilation (they need Android SDK)
|
||||
tasks.withType<JavaCompile> {
|
||||
exclude("**/FtcMotor.java")
|
||||
exclude("**/FtcPotentiometer.java")
|
||||
exclude("**/opmodes/ChuteOpMode.java")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
@@ -22,7 +27,6 @@ tasks.test {
|
||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||
}
|
||||
}
|
||||
|
||||
// Task to deploy code to FTC SDK
|
||||
tasks.register<Copy>("deployToSDK") {
|
||||
group = "ftc"
|
||||
@@ -40,7 +44,6 @@ tasks.register<Copy>("deployToSDK") {
|
||||
println("✓ Code deployed to TeamCode")
|
||||
}
|
||||
}
|
||||
|
||||
// Task to build APK
|
||||
tasks.register<Exec>("buildApk") {
|
||||
group = "ftc"
|
||||
@@ -60,4 +63,4 @@ tasks.register<Exec>("buildApk") {
|
||||
doLast {
|
||||
println("✓ APK built successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.firstinspires.ftc.teamcode.opmodes;
|
||||
|
||||
import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
|
||||
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
|
||||
import com.qualcomm.robotcore.hardware.DcMotor;
|
||||
import com.qualcomm.robotcore.hardware.AnalogInput;
|
||||
|
||||
import org.firstinspires.ftc.teamcode.subsystems.chute.Chute;
|
||||
import org.firstinspires.ftc.teamcode.subsystems.chute.FtcMotor;
|
||||
import org.firstinspires.ftc.teamcode.subsystems.chute.FtcPotentiometer;
|
||||
import org.firstinspires.ftc.teamcode.subsystems.chute.ChuteController;
|
||||
|
||||
/**
|
||||
* Example OpMode demonstrating chute control with real hardware.
|
||||
*
|
||||
* Hardware Config:
|
||||
* - Motor: "chute_motor" (DcMotor)
|
||||
* - Potentiometer: "chute_pot" (AnalogInput)
|
||||
*/
|
||||
@TeleOp(name = "Chute Control")
|
||||
public class ChuteOpMode extends LinearOpMode {
|
||||
|
||||
private Chute chute;
|
||||
|
||||
// Position presets (in radians)
|
||||
private static final double HOME = 0.0;
|
||||
private static final double LOW = Math.PI;
|
||||
private static final double MID = 2 * Math.PI;
|
||||
private static final double HIGH = 3 * Math.PI;
|
||||
private static final double MAX = 4 * Math.PI;
|
||||
|
||||
@Override
|
||||
public void runOpMode() {
|
||||
// Get hardware from config
|
||||
DcMotor chuteMotor = hardwareMap.get(DcMotor.class, "chute_motor");
|
||||
AnalogInput chutePot = hardwareMap.get(AnalogInput.class, "chute_pot");
|
||||
|
||||
// Create hardware adapters
|
||||
FtcMotor motor = new FtcMotor(chuteMotor);
|
||||
FtcPotentiometer pot = new FtcPotentiometer(chutePot);
|
||||
|
||||
// Create chute controller with real hardware
|
||||
ChuteController controller = new ChuteController(motor, pot, MAX);
|
||||
chute = new Chute(controller, motor, pot);
|
||||
|
||||
telemetry.addLine("Chute initialized");
|
||||
telemetry.addLine("Press A to home");
|
||||
telemetry.update();
|
||||
|
||||
waitForStart();
|
||||
|
||||
while (opModeIsActive()) {
|
||||
// Update chute (50Hz)
|
||||
chute.update(0.02);
|
||||
|
||||
// Control with gamepad
|
||||
handleControls();
|
||||
|
||||
// Display status
|
||||
updateTelemetry();
|
||||
|
||||
sleep(20); // 50Hz loop
|
||||
}
|
||||
}
|
||||
|
||||
private void handleControls() {
|
||||
// Manual homing
|
||||
if (gamepad1.a) {
|
||||
chute.home();
|
||||
}
|
||||
|
||||
// Emergency stop
|
||||
if (gamepad1.b) {
|
||||
chute.stop();
|
||||
}
|
||||
|
||||
// Position presets
|
||||
if (gamepad1.dpad_down) {
|
||||
chute.setTargetPosition(HOME);
|
||||
}
|
||||
if (gamepad1.dpad_left) {
|
||||
chute.setTargetPosition(LOW);
|
||||
}
|
||||
if (gamepad1.dpad_right) {
|
||||
chute.setTargetPosition(MID);
|
||||
}
|
||||
if (gamepad1.dpad_up) {
|
||||
chute.setTargetPosition(HIGH);
|
||||
}
|
||||
|
||||
// Fine control with triggers
|
||||
if (gamepad1.left_trigger > 0.1) {
|
||||
double current = chute.getPosition();
|
||||
chute.setTargetPosition(current - 0.1);
|
||||
}
|
||||
if (gamepad1.right_trigger > 0.1) {
|
||||
double current = chute.getPosition();
|
||||
chute.setTargetPosition(current + 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTelemetry() {
|
||||
telemetry.addLine("=== Chute Status ===");
|
||||
telemetry.addData("Homed", chute.isHomed() ? "YES" : "NO");
|
||||
telemetry.addData("Moving", chute.isMoving() ? "YES" : "NO");
|
||||
telemetry.addData("Position", "%.2f rad (%.0f°)",
|
||||
chute.getPosition(), Math.toDegrees(chute.getPosition()));
|
||||
|
||||
if (chute.isHomed()) {
|
||||
double percent = (chute.getPosition() / MAX) * 100;
|
||||
telemetry.addData("Extension", "%.0f%%", percent);
|
||||
}
|
||||
|
||||
telemetry.addLine();
|
||||
telemetry.addLine("=== Controls ===");
|
||||
telemetry.addLine("A: Home | B: Stop");
|
||||
telemetry.addLine("D-Pad: Presets");
|
||||
telemetry.addLine("Triggers: Fine adjust");
|
||||
|
||||
telemetry.update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
/**
|
||||
* Simple chute subsystem.
|
||||
*/
|
||||
public class Chute {
|
||||
private final ChuteController controller;
|
||||
private final MockMotor motor;
|
||||
private final MockPotentiometer pot;
|
||||
|
||||
/**
|
||||
* Create chute with mock hardware (for testing).
|
||||
*/
|
||||
public Chute(double maxExtension) {
|
||||
this.motor = new MockMotor();
|
||||
this.pot = new MockPotentiometer();
|
||||
this.controller = new ChuteController(motor, pot, maxExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chute with real hardware (for robot).
|
||||
* Use FtcMotor and FtcPotentiometer adapters.
|
||||
*/
|
||||
public Chute(ChuteController controller, MockMotor motor, MockPotentiometer pot) {
|
||||
this.controller = controller;
|
||||
this.motor = motor;
|
||||
this.pot = pot;
|
||||
}
|
||||
|
||||
public void update(double dt) {
|
||||
controller.update(dt);
|
||||
}
|
||||
|
||||
public void home() {
|
||||
controller.home();
|
||||
}
|
||||
|
||||
public void setTargetPosition(double position) {
|
||||
controller.moveToPosition(position);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
controller.stop();
|
||||
}
|
||||
|
||||
public double getPosition() {
|
||||
return controller.getPosition();
|
||||
}
|
||||
|
||||
public boolean isHomed() {
|
||||
return controller.isHomed();
|
||||
}
|
||||
|
||||
public boolean isMoving() {
|
||||
return controller.isMoving();
|
||||
}
|
||||
|
||||
public boolean isAtTarget() {
|
||||
return isHomed() && !isMoving();
|
||||
}
|
||||
|
||||
// For testing
|
||||
public MockMotor getMotor() { return motor; }
|
||||
public MockPotentiometer getPot() { return pot; }
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
/**
|
||||
* Simple chute controller with homing and position control.
|
||||
*/
|
||||
public class ChuteController {
|
||||
private final MockMotor motor;
|
||||
private final MockPotentiometer pot;
|
||||
private final double maxExtension;
|
||||
|
||||
private boolean homed = false;
|
||||
private boolean homing = false;
|
||||
private double homeVoltage = 0.0;
|
||||
private double currentPosition = 0.0;
|
||||
private int wrapCount = 0;
|
||||
private double lastVoltage = 0.0;
|
||||
private double targetPosition = 0.0;
|
||||
private boolean moving = false;
|
||||
private boolean needsToMove = false;
|
||||
|
||||
private int stableCount = 0;
|
||||
private static final int STABLE_REQUIRED = 5;
|
||||
private static final double TOLERANCE = 0.15; // Generous tolerance
|
||||
|
||||
/**
|
||||
* Create a new chute controller.
|
||||
* @param motor Motor that drives the chute
|
||||
* @param pot Potentiometer that measures position
|
||||
* @param maxExtension Maximum extension from home in radians
|
||||
*/
|
||||
public ChuteController(MockMotor motor, MockPotentiometer pot, double maxExtension) {
|
||||
this.motor = motor;
|
||||
this.pot = pot;
|
||||
this.maxExtension = maxExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start homing sequence - drives down until pot stops changing, then captures reference voltage.
|
||||
*/
|
||||
public void home() {
|
||||
motor.setPower(-0.5);
|
||||
homed = false;
|
||||
homing = true;
|
||||
stableCount = 0;
|
||||
lastVoltage = pot.getVoltage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to target position. Automatically homes first if not already homed.
|
||||
* @param target Target position in radians from home
|
||||
*/
|
||||
public void moveToPosition(double target) {
|
||||
target = Math.max(0, Math.min(maxExtension, target));
|
||||
targetPosition = target;
|
||||
|
||||
if (!homed) {
|
||||
needsToMove = true;
|
||||
if (!homing) {
|
||||
home();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
moving = true;
|
||||
needsToMove = false;
|
||||
|
||||
if (target > currentPosition) {
|
||||
motor.setPower(0.7);
|
||||
} else {
|
||||
motor.setPower(-0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update controller state - call this every loop iteration.
|
||||
* @param dt Time since last update in seconds
|
||||
*/
|
||||
public void update(double dt) {
|
||||
motor.update(dt);
|
||||
pot.updatePosition(motor.getPower() * dt);
|
||||
|
||||
double voltage = pot.getVoltage();
|
||||
|
||||
if (homing) {
|
||||
updateHoming(voltage);
|
||||
} else if (moving) {
|
||||
updateMoving(voltage);
|
||||
}
|
||||
|
||||
lastVoltage = voltage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update homing state - waits for stable pot readings then captures home voltage.
|
||||
*/
|
||||
private void updateHoming(double voltage) {
|
||||
if (Math.abs(voltage - lastVoltage) < 0.01) {
|
||||
stableCount++;
|
||||
if (stableCount >= STABLE_REQUIRED) {
|
||||
motor.setPower(0);
|
||||
homeVoltage = voltage;
|
||||
currentPosition = 0.0;
|
||||
wrapCount = 0;
|
||||
homed = true;
|
||||
homing = false;
|
||||
stableCount = 0;
|
||||
|
||||
if (needsToMove) {
|
||||
needsToMove = false;
|
||||
moving = true;
|
||||
if (targetPosition > 0) {
|
||||
motor.setPower(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stableCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update moving state - tracks wraps, calculates position, stops at target.
|
||||
*/
|
||||
private void updateMoving(double voltage) {
|
||||
double delta = voltage - lastVoltage;
|
||||
|
||||
// Detect wraps - look for large jumps
|
||||
if (Math.abs(delta) > 2.0) {
|
||||
if (motor.getPower() > 0) {
|
||||
// Moving up: voltage wraps from ~0 to pi (positive jump)
|
||||
wrapCount++;
|
||||
} else {
|
||||
// Moving down: voltage wraps from pi to ~0 (negative jump)
|
||||
wrapCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate position: offset from home + wraps
|
||||
double offset = voltage - homeVoltage;
|
||||
if (offset < 0) offset += Math.PI; // Handle wraparound in offset
|
||||
|
||||
currentPosition = offset + (wrapCount * Math.PI);
|
||||
if (currentPosition < 0) currentPosition = 0;
|
||||
|
||||
// Check if at target
|
||||
if (Math.abs(currentPosition - targetPosition) < TOLERANCE) {
|
||||
motor.setPower(0);
|
||||
moving = false;
|
||||
}
|
||||
|
||||
// Safety limit
|
||||
if (currentPosition >= maxExtension) {
|
||||
motor.setPower(0);
|
||||
moving = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chute has been homed.
|
||||
* @return True if homing is complete
|
||||
*/
|
||||
public boolean isHomed() {
|
||||
return homed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if chute is currently moving to target.
|
||||
* @return True if moving
|
||||
*/
|
||||
public boolean isMoving() {
|
||||
return moving;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current position of chute.
|
||||
* @return Position in radians from home
|
||||
*/
|
||||
public double getPosition() {
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the home voltage reference captured during homing.
|
||||
* @return Home voltage in range [0, pi]
|
||||
*/
|
||||
public double getHomeVoltage() {
|
||||
return homeVoltage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emergency stop - immediately stops motor and cancels movement.
|
||||
*/
|
||||
public void stop() {
|
||||
motor.setPower(0);
|
||||
moving = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
import com.qualcomm.robotcore.hardware.DcMotor;
|
||||
|
||||
/**
|
||||
* Adapter that wraps a real FTC DcMotor and makes it look like a MockMotor.
|
||||
* Use this to connect the chute controller to actual hardware.
|
||||
*/
|
||||
public class FtcMotor extends MockMotor {
|
||||
private final DcMotor motor;
|
||||
|
||||
/**
|
||||
* Create motor adapter.
|
||||
* @param motor The real FTC motor from hardwareMap
|
||||
*/
|
||||
public FtcMotor(DcMotor motor) {
|
||||
super();
|
||||
this.motor = motor;
|
||||
|
||||
// Configure motor
|
||||
motor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
|
||||
motor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(double power) {
|
||||
super.setPower(power); // Track power in parent
|
||||
motor.setPower(power); // Send to real motor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(double dt) {
|
||||
// Real motor updates itself - we don't simulate
|
||||
// But we still call parent to track internal position for mock purposes
|
||||
super.update(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
import com.qualcomm.robotcore.hardware.AnalogInput;
|
||||
|
||||
/**
|
||||
* Adapter that wraps a real FTC AnalogInput (potentiometer) and makes it look like a MockPotentiometer.
|
||||
* Use this to connect the chute controller to actual hardware.
|
||||
*/
|
||||
public class FtcPotentiometer extends MockPotentiometer {
|
||||
private final AnalogInput pot;
|
||||
private final double maxVoltage;
|
||||
|
||||
/**
|
||||
* Create potentiometer adapter.
|
||||
* @param pot The real FTC analog input from hardwareMap
|
||||
*/
|
||||
public FtcPotentiometer(AnalogInput pot) {
|
||||
super();
|
||||
this.pot = pot;
|
||||
this.maxVoltage = pot.getMaxVoltage(); // Usually 3.3V
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getVoltage() {
|
||||
// Read actual voltage from hardware
|
||||
double rawVoltage = pot.getVoltage();
|
||||
|
||||
// Scale to [0, pi] range
|
||||
// Assumes pot uses full voltage range (0 to maxVoltage)
|
||||
return (rawVoltage / maxVoltage) * Math.PI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePosition(double delta) {
|
||||
// Real pot updates itself - ignore this
|
||||
// (This method is only used in simulation)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(double pos) {
|
||||
// Can't set real hardware position - ignore
|
||||
// (This method is only used in simulation)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
/**
|
||||
* Simple mock motor.
|
||||
*/
|
||||
public class MockMotor {
|
||||
private double power;
|
||||
private double position;
|
||||
private double speed = 1.0; // rad/s at full power
|
||||
|
||||
public MockMotor() {
|
||||
this.power = 0.0;
|
||||
this.position = 0.0;
|
||||
}
|
||||
|
||||
public void setPower(double power) {
|
||||
this.power = Math.max(-1.0, Math.min(1.0, power));
|
||||
}
|
||||
|
||||
public double getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public void update(double dt) {
|
||||
position += power * speed * dt;
|
||||
if (position < 0) position = 0; // Hard stop at home
|
||||
}
|
||||
|
||||
public double getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(double pos) {
|
||||
this.position = pos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
/**
|
||||
* Simple mock potentiometer with wraparound at pi.
|
||||
*/
|
||||
public class MockPotentiometer {
|
||||
private double position;
|
||||
|
||||
public MockPotentiometer() {
|
||||
this.position = 0.0;
|
||||
}
|
||||
|
||||
public double getVoltage() {
|
||||
// Wrap to [0, pi]
|
||||
double wrapped = position % Math.PI;
|
||||
if (wrapped < 0) wrapped += Math.PI;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
public void setPosition(double pos) {
|
||||
this.position = pos;
|
||||
}
|
||||
|
||||
public void updatePosition(double delta) {
|
||||
this.position += delta;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# This file ensures the directory is tracked by git even when empty
|
||||
@@ -1,27 +0,0 @@
|
||||
package robot.opmodes;
|
||||
|
||||
/**
|
||||
* Basic OpMode for chute-drive
|
||||
*
|
||||
* This is a placeholder to demonstrate project structure.
|
||||
* To use this with FTC SDK:
|
||||
* 1. Run: weevil deploy chute-drive
|
||||
* 2. Add FTC SDK imports (OpMode, TeleOp, etc.)
|
||||
* 3. Extend OpMode and implement methods
|
||||
*
|
||||
* For local testing (without robot), write unit tests in src/test/java/robot/
|
||||
* Run tests with: ./gradlew test
|
||||
*
|
||||
* Created by Weevil 1.1.0-rc1
|
||||
* Template: basic
|
||||
*/
|
||||
public class BasicOpMode {
|
||||
|
||||
// This placeholder compiles without FTC SDK dependencies
|
||||
// Replace with actual OpMode code when deploying to robot
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("chute-drive - Ready for deployment");
|
||||
System.out.println("Run: weevil deploy chute-drive");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# This file ensures the directory is tracked by git even when empty
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.firstinspires.ftc.teamcode.subsystems.chute;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Basic tests for chute system.
|
||||
*/
|
||||
public class ChuteTest {
|
||||
|
||||
// Time step for simulation: 20ms = 50Hz update rate (mimics real robot loop)
|
||||
private static final double DT = 0.02;
|
||||
|
||||
// Tolerance for floating point comparisons (small acceptable error)
|
||||
private static final double EPSILON = 0.01;
|
||||
|
||||
/**
|
||||
* Verify potentiometer wraps from pi back to 0 (sawtooth wave behavior).
|
||||
* Tests that pot correctly simulates 0→pi→0→pi pattern.
|
||||
*/
|
||||
@Test
|
||||
public void testPotWraps() {
|
||||
MockPotentiometer pot = new MockPotentiometer();
|
||||
|
||||
pot.setPosition(0.0);
|
||||
assertEquals(0.0, pot.getVoltage(), EPSILON);
|
||||
|
||||
pot.setPosition(Math.PI / 2);
|
||||
assertEquals(Math.PI / 2, pot.getVoltage(), EPSILON);
|
||||
|
||||
// Wrap at pi
|
||||
pot.setPosition(Math.PI);
|
||||
assertEquals(0.0, pot.getVoltage(), EPSILON);
|
||||
|
||||
pot.setPosition(2 * Math.PI);
|
||||
assertEquals(0.0, pot.getVoltage(), EPSILON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test homing sequence: drive down until pot stops changing, then capture home voltage.
|
||||
* Verifies motor stops at home position with zero power.
|
||||
*/
|
||||
@Test
|
||||
public void testHoming() {
|
||||
Chute chute = new Chute(10.0);
|
||||
|
||||
// Start somewhere
|
||||
chute.getMotor().setPosition(0.5);
|
||||
chute.getPot().setPosition(0.5);
|
||||
|
||||
chute.home();
|
||||
|
||||
// Run until homed
|
||||
for (int i = 0; i < 200; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isHomed());
|
||||
assertEquals(0.0, chute.getMotor().getPower(), EPSILON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic position control: home first, then move to target position.
|
||||
* Verifies chute can accurately reach a commanded position.
|
||||
*/
|
||||
@Test
|
||||
public void testMoveToPosition() {
|
||||
Chute chute = new Chute(10.0);
|
||||
|
||||
// Home first
|
||||
chute.getMotor().setPosition(0.2);
|
||||
chute.getPot().setPosition(0.2);
|
||||
chute.home();
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isHomed(), "Should be homed");
|
||||
|
||||
// Move to target
|
||||
chute.setTargetPosition(2.0);
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isAtTarget(), "Should reach target");
|
||||
assertTrue(Math.abs(chute.getPosition() - 2.0) < 0.3,
|
||||
"Position should be close to 2.0, got: " + chute.getPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test unwrap logic through multiple pot wraps (3.5 rotations).
|
||||
* Verifies controller correctly tracks absolute position across multiple pi boundaries.
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleRotations() {
|
||||
Chute chute = new Chute(15.0);
|
||||
|
||||
// Home
|
||||
chute.getMotor().setPosition(0.2);
|
||||
chute.getPot().setPosition(0.2);
|
||||
chute.home();
|
||||
for (int i = 0; i < 200; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isHomed(), "Should be homed");
|
||||
|
||||
// Move to 3.5 rotations (need more time)
|
||||
double target = 3.5 * Math.PI;
|
||||
chute.setTargetPosition(target);
|
||||
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
|
||||
// Should track through wraps
|
||||
assertTrue(chute.getPosition() > 3.0 * Math.PI,
|
||||
"Position should be > 3pi, got: " + chute.getPosition());
|
||||
assertTrue(Math.abs(chute.getPosition() - target) < 0.5,
|
||||
"Position should be near target, expected: " + target + ", got: " + chute.getPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reassembly scenario: pot has different voltage at home after disassembly.
|
||||
* Verifies homing captures new reference voltage and positioning still works correctly.
|
||||
*/
|
||||
@Test
|
||||
public void testReassembly() {
|
||||
Chute chute = new Chute(10.0);
|
||||
|
||||
// Pot at different voltage when at home
|
||||
chute.getMotor().setPosition(0.0);
|
||||
chute.getPot().setPosition(1.5);
|
||||
|
||||
chute.home();
|
||||
for (int i = 0; i < 150; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isHomed(), "Should be homed");
|
||||
|
||||
// Should still work
|
||||
chute.setTargetPosition(2.0);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
|
||||
assertTrue(chute.isAtTarget(), "Should reach target");
|
||||
assertTrue(Math.abs(chute.getPosition() - 2.0) < 0.3,
|
||||
"Position should be close to 2.0, got: " + chute.getPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test maximum extension limit enforcement.
|
||||
* Verifies chute stops at max extension even when commanded to go beyond.
|
||||
*/
|
||||
@Test
|
||||
public void testMaxExtensionLimit() {
|
||||
Chute chute = new Chute(2.0); // Only 2 radians max
|
||||
|
||||
chute.getMotor().setPosition(0.1);
|
||||
chute.getPot().setPosition(0.1);
|
||||
chute.home();
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
// Try to go beyond max
|
||||
chute.setTargetPosition(5.0);
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
|
||||
// Should stop at max
|
||||
assertTrue(chute.getPosition() <= 2.0,
|
||||
"Should not exceed max: " + chute.getPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bidirectional movement: move up, then back down.
|
||||
* Verifies unwrap logic works correctly in both directions.
|
||||
*/
|
||||
@Test
|
||||
public void testBidirectionalMovement() {
|
||||
Chute chute = new Chute(10.0);
|
||||
|
||||
chute.getMotor().setPosition(0.1);
|
||||
chute.getPot().setPosition(0.1);
|
||||
chute.home();
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isHomed()) break;
|
||||
}
|
||||
|
||||
// Move up
|
||||
chute.setTargetPosition(3.0);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
double upPos = chute.getPosition();
|
||||
assertTrue(upPos > 2.5, "Should move up");
|
||||
|
||||
// Move back down
|
||||
chute.setTargetPosition(1.0);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
chute.update(DT);
|
||||
if (chute.isAtTarget()) break;
|
||||
}
|
||||
double downPos = chute.getPosition();
|
||||
assertTrue(downPos < 1.5, "Should move back down");
|
||||
assertTrue(downPos < upPos, "Down position should be less than up");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# This file ensures the directory is tracked by git even when empty
|
||||
@@ -1,11 +0,0 @@
|
||||
package robot;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class BasicTest {
|
||||
@Test
|
||||
void testBasic() {
|
||||
assertTrue(true, "Basic test should pass");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user