Sloth Added, LimeLight support, and Routines
This commit is contained in:
@@ -12,8 +12,20 @@
|
|||||||
// Custom definitions may go here
|
// Custom definitions may go here
|
||||||
|
|
||||||
// Include common definitions from above.
|
// Include common definitions from above.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "https://repo.dairy.foundation/releases" }
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "dev.frozenmilk:Load:0.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apply from: '../build.common.gradle'
|
apply from: '../build.common.gradle'
|
||||||
apply from: '../build.dependencies.gradle'
|
apply from: '../build.dependencies.gradle'
|
||||||
|
apply plugin: 'dev.frozenmilk.sinister.sloth.load'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = 'org.firstinspires.ftc.teamcode'
|
namespace = 'org.firstinspires.ftc.teamcode'
|
||||||
@@ -23,6 +35,16 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url = "https://repo.dairy.foundation/releases" }
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':FtcRobotController')
|
implementation project(':FtcRobotController')
|
||||||
|
// 1. Install Sloth
|
||||||
|
implementation("dev.frozenmilk.sinister:Sloth:0.2.4")
|
||||||
|
|
||||||
|
// 2. Install the Sloth version of Panels
|
||||||
|
implementation("com.bylazar.sloth:fullpanels:0.2.4+1.0.12")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,30 @@ import com.qualcomm.hardware.lynx.LynxModule;
|
|||||||
import com.qualcomm.robotcore.eventloop.opmode.OpMode;
|
import com.qualcomm.robotcore.eventloop.opmode.OpMode;
|
||||||
import com.qualcomm.robotcore.util.ElapsedTime;
|
import com.qualcomm.robotcore.util.ElapsedTime;
|
||||||
import org.firstinspires.ftc.robotcore.external.Telemetry;
|
import org.firstinspires.ftc.robotcore.external.Telemetry;
|
||||||
|
import com.qualcomm.robotcore.hardware.VoltageSensor;
|
||||||
|
|
||||||
// PANELS IMPORTS
|
|
||||||
import com.bylazar.telemetry.JoinedTelemetry;
|
import com.bylazar.telemetry.JoinedTelemetry;
|
||||||
|
|
||||||
|
import org.firstinspires.ftc.teamcode.lib.actions.RoutineManager;
|
||||||
import org.firstinspires.ftc.teamcode.teleOp.Constants;
|
import org.firstinspires.ftc.teamcode.teleOp.Constants;
|
||||||
import org.firstinspires.ftc.teamcode.util.FPSCounter;
|
import org.firstinspires.ftc.teamcode.util.FPSCounter;
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class BaseOpMode extends OpMode {
|
public abstract class BaseOpMode extends OpMode {
|
||||||
protected final SubsysManager manager = new SubsysManager();
|
protected final SubsysManager manager = new SubsysManager();
|
||||||
|
|
||||||
|
protected final RoutineManager routines = new RoutineManager();
|
||||||
protected final FPSCounter fps = new FPSCounter();
|
protected final FPSCounter fps = new FPSCounter();
|
||||||
private final ElapsedTime telemetryTimer = new ElapsedTime();
|
private final ElapsedTime telemetryTimer = new ElapsedTime();
|
||||||
private List<LynxModule> allHubs;
|
private List<LynxModule> allHubs;
|
||||||
|
private final ElapsedTime voltageTimer = new ElapsedTime();
|
||||||
|
private VoltageSensor batterySensor;
|
||||||
|
|
||||||
|
|
||||||
|
protected EnhancedGamepad g1;
|
||||||
|
protected EnhancedGamepad g2;
|
||||||
|
|
||||||
protected Telemetry unifiedTelemetry;
|
protected Telemetry unifiedTelemetry;
|
||||||
|
|
||||||
@@ -31,18 +41,24 @@ public abstract class BaseOpMode extends OpMode {
|
|||||||
public final void init() {
|
public final void init() {
|
||||||
unifiedTelemetry = new JoinedTelemetry(telemetry);
|
unifiedTelemetry = new JoinedTelemetry(telemetry);
|
||||||
|
|
||||||
|
batterySensor = hardwareMap.voltageSensor.iterator().next();
|
||||||
|
PIDController.currentSystemVoltage = batterySensor.getVoltage();
|
||||||
|
|
||||||
allHubs = hardwareMap.getAll(LynxModule.class);
|
allHubs = hardwareMap.getAll(LynxModule.class);
|
||||||
for (LynxModule hub : allHubs) {
|
for (LynxModule hub : allHubs) {
|
||||||
hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
|
hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
g1 = new EnhancedGamepad(gamepad1);
|
||||||
|
g2 = new EnhancedGamepad(gamepad2);
|
||||||
|
|
||||||
setupSubsystems();
|
setupSubsystems();
|
||||||
|
|
||||||
manager.initAll(hardwareMap);
|
manager.initAll(hardwareMap);
|
||||||
|
|
||||||
onInit();
|
onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void start() {
|
public final void start() {
|
||||||
fps.reset();
|
fps.reset();
|
||||||
@@ -52,23 +68,30 @@ public abstract class BaseOpMode extends OpMode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void loop() {
|
public final void loop() {
|
||||||
|
fps.startLoop();
|
||||||
for (LynxModule hub : allHubs) {
|
for (LynxModule hub : allHubs) {
|
||||||
hub.clearBulkCache();
|
hub.clearBulkCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
fps.startLoop();
|
|
||||||
stateMachineUpdate();
|
|
||||||
|
|
||||||
|
g1.update();
|
||||||
|
g2.update();
|
||||||
|
|
||||||
|
if (voltageTimer.milliseconds() >= Constants.PIDController.VOLTAGE_CHECK_INTERVAL_MS) {
|
||||||
|
PIDController.currentSystemVoltage = batterySensor.getVoltage();
|
||||||
|
voltageTimer.reset();
|
||||||
|
}
|
||||||
|
stateMachineUpdate();
|
||||||
manager.updateAll();
|
manager.updateAll();
|
||||||
|
routines.updateAll();
|
||||||
|
|
||||||
if (telemetryTimer.milliseconds() >= Constants.GLOBAL.TELEMETRY_DELAY_MS) {
|
if (telemetryTimer.milliseconds() >= Constants.GLOBAL.TELEMETRY_DELAY_MS) {
|
||||||
|
|
||||||
manager.publishTelemetryAll(unifiedTelemetry);
|
manager.publishTelemetryAll(unifiedTelemetry);
|
||||||
|
|
||||||
if (Constants.GLOBAL.DEBUG_MODE) {
|
if (Constants.GLOBAL.DEBUG_MODE) {
|
||||||
|
unifiedTelemetry.addData("System Voltage", PIDController.currentSystemVoltage);
|
||||||
unifiedTelemetry.addData("Loop Time (ms)", fps.getCurrentLoopTime());
|
unifiedTelemetry.addData("Loop Time (ms)", fps.getCurrentLoopTime());
|
||||||
unifiedTelemetry.addData("Target FPS", Constants.GLOBAL.TARGET_FPS);
|
unifiedTelemetry.addData("FPS", fps.getAverageFps());
|
||||||
unifiedTelemetry.addData("Current FPS", fps.getAverageFps());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unifiedTelemetry.update();
|
unifiedTelemetry.update();
|
||||||
@@ -78,6 +101,6 @@ public abstract class BaseOpMode extends OpMode {
|
|||||||
long sleepTime = fps.getSyncTime(Constants.GLOBAL.TARGET_FPS);
|
long sleepTime = fps.getSyncTime(Constants.GLOBAL.TARGET_FPS);
|
||||||
if (sleepTime > 0) {
|
if (sleepTime > 0) {
|
||||||
SystemClock.sleep(sleepTime);
|
SystemClock.sleep(sleepTime);
|
||||||
}
|
} // CAP FPS Logic (check Constants.GLOBAL.TARGET_FPS, if 0 then uncapped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package org.firstinspires.ftc.teamcode.lib;
|
||||||
|
|
||||||
|
import com.qualcomm.robotcore.hardware.Gamepad;
|
||||||
|
import org.firstinspires.ftc.teamcode.teleOp.Constants;
|
||||||
|
|
||||||
|
public class EnhancedGamepad {
|
||||||
|
private final Gamepad hardwareGamepad;
|
||||||
|
private final Gamepad currentState;
|
||||||
|
private final Gamepad previousState;
|
||||||
|
|
||||||
|
public EnhancedGamepad(Gamepad gamepad) {
|
||||||
|
this.hardwareGamepad = gamepad;
|
||||||
|
this.currentState = new Gamepad();
|
||||||
|
this.previousState = new Gamepad();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
previousState.copy(currentState);
|
||||||
|
currentState.copy(hardwareGamepad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double scale(double input) {
|
||||||
|
if (Math.abs(input) < Constants.Gamepad.STICK_DEADBAND) return 0;
|
||||||
|
return Math.pow(input, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean triggerAsBtn(float val) {
|
||||||
|
return val > Constants.Gamepad.TRIGGER_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double left_stick_x() { return scale(currentState.left_stick_x); }
|
||||||
|
public double left_stick_y() { return scale(currentState.left_stick_y); }
|
||||||
|
public double right_stick_x() { return scale(currentState.right_stick_x); }
|
||||||
|
public double right_stick_y() { return scale(currentState.right_stick_y); }
|
||||||
|
|
||||||
|
public double left_trigger() { return currentState.left_trigger; }
|
||||||
|
public double right_trigger() { return currentState.right_trigger; }
|
||||||
|
|
||||||
|
// --- BUTTONS: CURRENT STATE ---
|
||||||
|
public boolean a() { return currentState.a; }
|
||||||
|
public boolean b() { return currentState.b; }
|
||||||
|
public boolean x() { return currentState.x; }
|
||||||
|
public boolean y() { return currentState.y; }
|
||||||
|
|
||||||
|
public boolean dpad_up() { return currentState.dpad_up; }
|
||||||
|
public boolean dpad_down() { return currentState.dpad_down; }
|
||||||
|
public boolean dpad_left() { return currentState.dpad_left; }
|
||||||
|
public boolean dpad_right() { return currentState.dpad_right; }
|
||||||
|
|
||||||
|
public boolean left_bumper() { return currentState.left_bumper; }
|
||||||
|
public boolean right_bumper() { return currentState.right_bumper; }
|
||||||
|
public boolean left_stick_button() { return currentState.left_stick_button; }
|
||||||
|
public boolean right_stick_button() { return currentState.right_stick_button; }
|
||||||
|
|
||||||
|
public boolean back() { return currentState.back; }
|
||||||
|
public boolean start() { return currentState.start; }
|
||||||
|
public boolean guide() { return currentState.guide; }
|
||||||
|
public boolean options() { return currentState.options; }
|
||||||
|
|
||||||
|
// PS Specific Aliases
|
||||||
|
public boolean ps() { return currentState.ps; }
|
||||||
|
public boolean share() { return currentState.share; }
|
||||||
|
public boolean touchpad() { return currentState.touchpad; }
|
||||||
|
public boolean circle() { return currentState.circle; }
|
||||||
|
public boolean cross() { return currentState.cross; }
|
||||||
|
public boolean triangle() { return currentState.triangle; }
|
||||||
|
public boolean square() { return currentState.square; }
|
||||||
|
|
||||||
|
// Triggers as Buttons
|
||||||
|
public boolean left_trigger_btn() { return triggerAsBtn(currentState.left_trigger); }
|
||||||
|
public boolean right_trigger_btn() { return triggerAsBtn(currentState.right_trigger); }
|
||||||
|
|
||||||
|
// --- BUTTONS: WAS PRESSED (Rising Edge) ---
|
||||||
|
public boolean aWasPressed() { return currentState.a && !previousState.a; }
|
||||||
|
public boolean bWasPressed() { return currentState.b && !previousState.b; }
|
||||||
|
public boolean xWasPressed() { return currentState.x && !previousState.x; }
|
||||||
|
public boolean yWasPressed() { return currentState.y && !previousState.y; }
|
||||||
|
|
||||||
|
public boolean dpad_upWasPressed() { return currentState.dpad_up && !previousState.dpad_up; }
|
||||||
|
public boolean dpad_downWasPressed() { return currentState.dpad_down && !previousState.dpad_down; }
|
||||||
|
public boolean dpad_leftWasPressed() { return currentState.dpad_left && !previousState.dpad_left; }
|
||||||
|
public boolean dpad_rightWasPressed() { return currentState.dpad_right && !previousState.dpad_right; }
|
||||||
|
|
||||||
|
public boolean left_bumperWasPressed() { return currentState.left_bumper && !previousState.left_bumper; }
|
||||||
|
public boolean right_bumperWasPressed() { return currentState.right_bumper && !previousState.right_bumper; }
|
||||||
|
|
||||||
|
public boolean left_stick_buttonWasPressed() { return currentState.left_stick_button && !previousState.left_stick_button; }
|
||||||
|
public boolean right_stick_buttonWasPressed() { return currentState.right_stick_button && !previousState.right_stick_button; }
|
||||||
|
|
||||||
|
public boolean backWasPressed() { return currentState.back && !previousState.back; }
|
||||||
|
public boolean startWasPressed() { return currentState.start && !previousState.start; }
|
||||||
|
public boolean optionsWasPressed() { return currentState.options && !previousState.options; }
|
||||||
|
public boolean touchpadWasPressed() { return currentState.touchpad && !previousState.touchpad; }
|
||||||
|
|
||||||
|
public boolean left_trigger_btnWasPressed() { return triggerAsBtn(currentState.left_trigger) && !triggerAsBtn(previousState.left_trigger); }
|
||||||
|
public boolean right_trigger_btnWasPressed() { return triggerAsBtn(currentState.right_trigger) && !triggerAsBtn(previousState.right_trigger); }
|
||||||
|
|
||||||
|
// --- BUTTONS: WAS RELEASED (Falling Edge) ---
|
||||||
|
public boolean aWasReleased() { return !currentState.a && previousState.a; }
|
||||||
|
public boolean bWasReleased() { return !currentState.b && previousState.b; }
|
||||||
|
public boolean xWasReleased() { return !currentState.x && previousState.x; }
|
||||||
|
public boolean yWasReleased() { return !currentState.y && previousState.y; }
|
||||||
|
|
||||||
|
public boolean left_bumperWasReleased() { return !currentState.left_bumper && previousState.left_bumper; }
|
||||||
|
public boolean right_bumperWasReleased() { return !currentState.right_bumper && previousState.right_bumper; }
|
||||||
|
|
||||||
|
// --- RUMBLE CONTROLS ---
|
||||||
|
public void rumble(int durationMs) { hardwareGamepad.rumble(durationMs); }
|
||||||
|
public void rumbleBlips(int count) { hardwareGamepad.rumbleBlips(count); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.firstinspires.ftc.teamcode.lib;
|
||||||
|
|
||||||
|
import com.qualcomm.hardware.limelightvision.LLResult;
|
||||||
|
import com.qualcomm.hardware.limelightvision.Limelight3A;
|
||||||
|
import com.qualcomm.robotcore.hardware.HardwareMap;
|
||||||
|
import com.qualcomm.robotcore.util.ElapsedTime;
|
||||||
|
|
||||||
|
import org.firstinspires.ftc.teamcode.teleOp.Constants;
|
||||||
|
|
||||||
|
public class LLUtil {
|
||||||
|
private Limelight3A limelight;
|
||||||
|
private LLResult lastResult;
|
||||||
|
private final ElapsedTime timer = new ElapsedTime();
|
||||||
|
private double lastKnownDistance = 0;
|
||||||
|
|
||||||
|
public LLUtil(HardwareMap hwMap) {
|
||||||
|
limelight = hwMap.get(Limelight3A.class, Constants.LIMELIGHT.NAME);
|
||||||
|
limelight.setPollRateHz(Constants.LIMELIGHT.POLL_RATE);
|
||||||
|
limelight.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
lastResult = limelight.getLatestResult();
|
||||||
|
if (hasTarget()) {
|
||||||
|
timer.reset();
|
||||||
|
lastKnownDistance = getTrigDistance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTarget() {
|
||||||
|
return lastResult != null && lastResult.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDataFresh() {
|
||||||
|
return hasTarget() || (timer.seconds() < Constants.LIMELIGHT.STALE_DATA_TIMEOUT_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTx() {
|
||||||
|
return hasTarget() ? lastResult.getTx() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTy() {
|
||||||
|
return hasTarget() ? lastResult.getTy() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTa() {
|
||||||
|
return hasTarget() ? lastResult.getTa() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTrigDistance() { // Trig Distance
|
||||||
|
if (!hasTarget()) return lastKnownDistance;
|
||||||
|
|
||||||
|
double angleToTargetRad = Math.toRadians(Constants.LIMELIGHT.MOUNT_ANGLE_DEGREES + getTy());
|
||||||
|
double heightDiff = Constants.LIMELIGHT.TARGET_HEIGHT_IN - Constants.LIMELIGHT.MOUNT_HEIGHT_IN;
|
||||||
|
|
||||||
|
return heightDiff / Math.tan(angleToTargetRad);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAreaDistance() { // TA Regr
|
||||||
|
if (!hasTarget()) return lastKnownDistance;
|
||||||
|
return (Constants.LIMELIGHT.AREA_COEFF * Math.pow(getTa(), Constants.LIMELIGHT.AREA_EXPONENT))
|
||||||
|
+ Constants.LIMELIGHT.AREA_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPipeline(int index) {
|
||||||
|
limelight.pipelineSwitch(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
limelight.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.firstinspires.ftc.teamcode.lib;
|
package org.firstinspires.ftc.teamcode.lib;
|
||||||
|
|
||||||
import com.qualcomm.robotcore.util.ElapsedTime;
|
import com.qualcomm.robotcore.util.ElapsedTime;
|
||||||
|
import org.firstinspires.ftc.teamcode.teleOp.Constants;
|
||||||
|
|
||||||
public class PIDController {
|
public class PIDController {
|
||||||
private double kP, kI, kD;
|
public static double currentSystemVoltage = 12.0;
|
||||||
|
|
||||||
|
private double kP, kI, kD;
|
||||||
private double lastError = 0;
|
private double lastError = 0;
|
||||||
private double integralSum = 0;
|
private double integralSum = 0;
|
||||||
private final ElapsedTime timer = new ElapsedTime();
|
private final ElapsedTime timer = new ElapsedTime();
|
||||||
@@ -39,7 +41,11 @@ public class PIDController {
|
|||||||
integralSum += (error * dt);
|
integralSum += (error * dt);
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
|
||||||
return (kP * error) + (kI * integralSum) + (kD * derivative);
|
double rawOutput = (kP * error) + (kI * integralSum) + (kD * derivative);
|
||||||
|
|
||||||
|
double compensationMultiplier = Constants.PIDController.VOLTAGE_REFERENCE / currentSystemVoltage;
|
||||||
|
|
||||||
|
return rawOutput * compensationMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.firstinspires.ftc.teamcode.lib.actions;
|
||||||
|
|
||||||
|
public interface Action {
|
||||||
|
/** Called exactly once when the action begins. */
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called every loop while the action is active.
|
||||||
|
* @return true if the action is finished, false if still running.
|
||||||
|
*/
|
||||||
|
boolean update();
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package org.firstinspires.ftc.teamcode.lib.actions;
|
||||||
|
|
||||||
|
import com.qualcomm.robotcore.util.ElapsedTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
public class Routine {
|
||||||
|
|
||||||
|
/** Runs a single block of code instantly, then finishes. */
|
||||||
|
public static Action instant(Runnable runnable) {
|
||||||
|
return new InstantAction(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Waits for a specific amount of milliseconds without freezing the robot. */
|
||||||
|
public static Action wait(double milliseconds) {
|
||||||
|
return new WaitAction(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Waits until a specific condition becomes true (e.g. Color Sensor detects pixel) */
|
||||||
|
public static Action waitUntil(BooleanSupplier condition) {
|
||||||
|
return new ConditionAction(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs multiple actions one after the other. */
|
||||||
|
public static Action sequence(Action... actions) {
|
||||||
|
return new SequentialAction(Arrays.asList(actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs multiple actions at the exact same time. Finishes when ALL are done. */
|
||||||
|
public static Action parallel(Action... actions) {
|
||||||
|
return new ParallelAction(Arrays.asList(actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InstantAction implements Action {
|
||||||
|
private final Runnable task;
|
||||||
|
public InstantAction(Runnable task) { this.task = task; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() { task.run(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update() { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class WaitAction implements Action {
|
||||||
|
private final double duration;
|
||||||
|
private final ElapsedTime timer = new ElapsedTime();
|
||||||
|
|
||||||
|
public WaitAction(double duration) { this.duration = duration; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() { timer.reset(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update() { return timer.milliseconds() >= duration; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ConditionAction implements Action {
|
||||||
|
private final BooleanSupplier condition;
|
||||||
|
public ConditionAction(BooleanSupplier condition) { this.condition = condition; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update() { return condition.getAsBoolean(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SequentialAction implements Action {
|
||||||
|
private final List<Action> actions;
|
||||||
|
private int currentIndex = 0;
|
||||||
|
private boolean isCurrentStarted = false;
|
||||||
|
|
||||||
|
public SequentialAction(List<Action> actions) { this.actions = actions; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
currentIndex = 0;
|
||||||
|
isCurrentStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update() {
|
||||||
|
if (currentIndex >= actions.size()) return true;
|
||||||
|
|
||||||
|
if (!isCurrentStarted) {
|
||||||
|
actions.get(currentIndex).start();
|
||||||
|
isCurrentStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.get(currentIndex).update()) {
|
||||||
|
currentIndex++;
|
||||||
|
isCurrentStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentIndex >= actions.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ParallelAction implements Action {
|
||||||
|
private final List<Action> actions;
|
||||||
|
private final boolean[] finished;
|
||||||
|
|
||||||
|
public ParallelAction(List<Action> actions) {
|
||||||
|
this.actions = actions;
|
||||||
|
this.finished = new boolean[actions.size()];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
for (int i = 0; i < actions.size(); i++) {
|
||||||
|
finished[i] = false;
|
||||||
|
actions.get(i).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean update() {
|
||||||
|
boolean allDone = true;
|
||||||
|
for (int i = 0; i < actions.size(); i++) {
|
||||||
|
if (!finished[i]) {
|
||||||
|
finished[i] = actions.get(i).update();
|
||||||
|
if (!finished[i]) allDone = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allDone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.firstinspires.ftc.teamcode.lib.actions;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RoutineManager {
|
||||||
|
private final List<Action> activeRoutines = new ArrayList<>();
|
||||||
|
|
||||||
|
public void run(Action action) {
|
||||||
|
action.start();
|
||||||
|
activeRoutines.add(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAll() {
|
||||||
|
Iterator<Action> iterator = activeRoutines.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Action action = iterator.next();
|
||||||
|
if (action.update()) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelAll() {
|
||||||
|
activeRoutines.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBusy() {
|
||||||
|
return !activeRoutines.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Routines & Action Sequencer 🎬
|
||||||
|
|
||||||
|
The **Routines** system is one of the most powerful features in `ftc-lib`. It allows you to write complex, multi-step robot scripts (like an Auto-Score macro) **without freezing the robot**.
|
||||||
|
|
||||||
|
In standard linear OpModes, calling `sleep(1000)` completely stops the robot from driving. The `ftc-lib` Routine Manager runs your scripts asynchronously in the background. Your driver retains full control of the drivetrain while the robot automatically orchestrates the lift, intake, and servos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 The Building Blocks (Actions)
|
||||||
|
|
||||||
|
Everything in a routine is an `Action`. You do not need to create these manually; use the `Routine` factory class to build them instantly.
|
||||||
|
|
||||||
|
| Method | Description | Example |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `Routine.instant()` | Runs a single block of code immediately. | `Routine.instant(() -> lift.setTarget(1000))` |
|
||||||
|
| `Routine.wait()` | Pauses **only this sequence** for `x` milliseconds. | `Routine.wait(500)` |
|
||||||
|
| `Routine.waitUntil()` | Pauses the sequence until a condition is `true`. | `Routine.waitUntil(() -> lift.isAtTarget())` |
|
||||||
|
| `Routine.sequence()` | Runs multiple Actions **one after the other**. | `Routine.sequence(action1, action2)` |
|
||||||
|
| `Routine.parallel()` | Runs multiple Actions **at the exact same time**. | `Routine.parallel(action1, action2)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Running a Routine in TeleOp
|
||||||
|
|
||||||
|
Because `BaseOpMode` automatically manages the `RoutineManager` under the hood, all you have to do is call `routines.run()` inside your `stateMachineUpdate()` loop.
|
||||||
|
|
||||||
|
Here is a real-world example of an **Auto-Score Macro** mapped to the `Y` button:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
protected void stateMachineUpdate() {
|
||||||
|
|
||||||
|
// Drive code continues running perfectly while routines run!
|
||||||
|
drive.setDriveVectors(-g1.left_stick_y(), g1.left_stick_x(), g1.right_stick_x());
|
||||||
|
|
||||||
|
// 1. Trigger the Routine
|
||||||
|
if (g1.yWasPressed() && !routines.isBusy()) {
|
||||||
|
|
||||||
|
routines.run(
|
||||||
|
Routine.sequence(
|
||||||
|
// Step 1: Tell the lift to go up
|
||||||
|
Routine.instant(() -> lift.setTargetState(LiftState.SCORING_HIGH)),
|
||||||
|
|
||||||
|
// Step 2: Wait until the lift physically reaches the top
|
||||||
|
Routine.waitUntil(() -> lift.isAtTarget()),
|
||||||
|
|
||||||
|
// Step 3: Open the claw
|
||||||
|
Routine.instant(() -> claw.open()),
|
||||||
|
|
||||||
|
// Step 4: Wait 250ms for the pixel/sample to fall out
|
||||||
|
Routine.wait(250),
|
||||||
|
|
||||||
|
// Step 5: Retract the lift AND close the claw simultaneously
|
||||||
|
Routine.parallel(
|
||||||
|
Routine.instant(() -> lift.setTargetState(LiftState.RETRACTED)),
|
||||||
|
Routine.instant(() -> claw.close())
|
||||||
|
),
|
||||||
|
|
||||||
|
// Step 6: Vibrate the controller to tell the driver it's done
|
||||||
|
Routine.instant(() -> g1.rumble(500))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛑 Driver Overrides & Canceling (Crucial for Safety)
|
||||||
|
|
||||||
|
Because routines run automatically, **you must give your driver a way to cancel them** in case something goes wrong (e.g., the lift gets stuck on another robot).
|
||||||
|
|
||||||
|
Use `routines.cancelAll()` mapped to a panic button (like `B`) to instantly kill all running scripts and return manual control to the driver.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
protected void stateMachineUpdate() {
|
||||||
|
|
||||||
|
// PANIC BUTTON: Instantly kill the macro
|
||||||
|
if (g1.bWasPressed()) {
|
||||||
|
routines.cancelAll();
|
||||||
|
lift.setTargetState(LiftState.RETRACTED); // Force hardware to a safe state
|
||||||
|
g1.rumbleBlips(2); // Audio/Haptic feedback that the macro died
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... routine triggering logic ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Best Practices
|
||||||
|
|
||||||
|
1. **Check `!routines.isBusy()`**: Always ensure a routine isn't currently running before starting a new one, otherwise you might trigger the "Auto-Score" sequence twice at the same time and cause conflicting hardware commands.
|
||||||
|
2. **Use Subsystem States, Not Hardware!**: Do **not** put raw hardware commands (like `motor.setPower(1)`) inside a `Routine.instant()`. Always use your Subsystem's `setTargetState()` methods. The Subsystem should still be in charge of calculating its own PID and limits.
|
||||||
|
3. **Use `waitUntil` instead of `wait`**: Whenever possible, wait for a sensor or an encoder position rather than a fixed amount of time. If your battery is low, a `wait(500)` might not be enough time for the lift to go up. A `waitUntil(() -> lift.isAtTarget())` works perfectly on *every* battery voltage.
|
||||||
@@ -12,6 +12,33 @@ public class Constants {
|
|||||||
public static int TARGET_FPS = 0; // No target FPS (UNCAPPED)
|
public static int TARGET_FPS = 0; // No target FPS (UNCAPPED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Gamepad {
|
||||||
|
public static double STICK_DEADBAND = 0.05;
|
||||||
|
public static double TRIGGER_THRESHOLD = 0.3;
|
||||||
|
}
|
||||||
|
public static class PIDController {
|
||||||
|
public static double VOLTAGE_REFERENCE = 12.0; // Voltage PID Controller was tuned at
|
||||||
|
public static double VOLTAGE_CHECK_INTERVAL_MS = 500.0; // Every half second
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configurable
|
||||||
|
public static class LIMELIGHT {
|
||||||
|
public static String NAME = "limelight";
|
||||||
|
public static int POLL_RATE = 100;
|
||||||
|
|
||||||
|
|
||||||
|
// Trig Distance
|
||||||
|
public static double MOUNT_HEIGHT_IN = 0.0;
|
||||||
|
public static double TARGET_HEIGHT_IN = 29.5; // DECODE SEAON GOAL HEIGHT
|
||||||
|
public static double MOUNT_ANGLE_DEGREES = 0.0;
|
||||||
|
|
||||||
|
public static double AREA_COEFF = 74.34095;
|
||||||
|
public static double AREA_EXPONENT = -0.518935;
|
||||||
|
public static double AREA_OFFSET = 4.0;
|
||||||
|
|
||||||
|
public static double STALE_DATA_TIMEOUT_SEC = 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
@Configurable
|
@Configurable
|
||||||
public static class DRIVE {
|
public static class DRIVE {
|
||||||
// Hardware Names
|
// Hardware Names
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import org.firstinspires.ftc.teamcode.lib.BaseOpMode;
|
|||||||
import org.firstinspires.ftc.teamcode.subsys.Drivetrain;
|
import org.firstinspires.ftc.teamcode.subsys.Drivetrain;
|
||||||
import org.firstinspires.ftc.teamcode.util.AutoTransfer;
|
import org.firstinspires.ftc.teamcode.util.AutoTransfer;
|
||||||
|
|
||||||
@TeleOp(name = "Competition TeleOp", group = "Main")
|
|
||||||
|
@TeleOp(name = "Basic TeleOp", group = "Main")
|
||||||
public class teleOp extends BaseOpMode {
|
public class teleOp extends BaseOpMode {
|
||||||
|
|
||||||
public enum GlobalState {
|
public enum GlobalState {
|
||||||
@@ -32,14 +33,24 @@ public class teleOp extends BaseOpMode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void stateMachineUpdate() {
|
protected void stateMachineUpdate() {
|
||||||
drive.setDriveVectors(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x);
|
if (g1.backWasPressed()) {
|
||||||
|
|
||||||
|
routines.cancelAll();
|
||||||
|
|
||||||
|
robotState = GlobalState.IDLE;
|
||||||
|
|
||||||
|
drive.setTargetState(Drivetrain.DriveState.NORMAL);
|
||||||
|
|
||||||
|
g1.rumbleBlips(2);
|
||||||
|
}
|
||||||
|
drive.setDriveVectors(-g1.left_stick_y(), g1.left_stick_x(), g1.right_stick_y());
|
||||||
|
|
||||||
switch (robotState) {
|
switch (robotState) {
|
||||||
|
|
||||||
case IDLE:
|
case IDLE:
|
||||||
drive.setTargetState(Drivetrain.DriveState.NORMAL);
|
drive.setTargetState(Drivetrain.DriveState.NORMAL);
|
||||||
|
|
||||||
if (gamepad1.aWasPressed()) {
|
if (g1.aWasPressed()) {
|
||||||
robotState = GlobalState.SCORING;
|
robotState = GlobalState.SCORING;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -47,7 +58,7 @@ public class teleOp extends BaseOpMode {
|
|||||||
case SCORING:
|
case SCORING:
|
||||||
drive.setTargetState(Drivetrain.DriveState.PRECISION);
|
drive.setTargetState(Drivetrain.DriveState.PRECISION);
|
||||||
|
|
||||||
if (gamepad1.bWasPressed()) {
|
if (g2.bWasPressed()) {
|
||||||
robotState = GlobalState.IDLE;
|
robotState = GlobalState.IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user