Trobotix 8696

Programming Site

< Back | < General Info

Power Play

Or however else it’s titled.

Final robot. Robot on field. Spot.

This season’s robot name is Octo.
Development was continuously running during the later half of the season, so some decisions may seem odd.
League Tournament: Winning Alliance Captain
Super Qualifiers: 2nd Place Think Award
State: Rank #5 (did not advance)

Note: We bumped up the Android API level to 24 in the Gradle common build file.


Non-OpMode Classes

For programmers from other teams: Please attribute accordingly if the script section of OctoConfig is used in your project.

Action
package org.firstinspires.ftc.teamcode.octo;

public interface Action {
    int execute(int [] arguments);
}
Instruction
package org.firstinspires.ftc.teamcode.octo;

public class Instruction {
    public String action;
    public int [] arguments;
    public int    line;
    public Instruction(String a, int [] args, int l) {
        action = a;
        arguments = args;
        line = l;
    }
}
OctoConfig
/*
OctoConfig

This not an Autonomous nor TeleOp.
Utilities class for obtaining configuration values, usually prior to game start.
Current Status:
    Supports configuration for double and boolean values.
    TUI interface in driver station telemetry.
    Obtains battery power percentage (not level).

    !!! Interpreter for pseudo-code like script used to control autonomous movements. !!!

Potential Additions:
    Format text as HTML for fancy formatting.
    Complicated UI to allow for modifying past inputs.
    Input from second controller? Customized controls?

*/

package org.firstinspires.ftc.teamcode.octo;

import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Arrays;

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import org.firstinspires.ftc.robotcore.external.Telemetry;
import com.qualcomm.robotcore.hardware.VoltageSensor;

// vII-XXI

public abstract class OctoConfig extends LinearOpMode {

    // Configuration

    // public static final long BOOL_DELAY = 100;
    // public static final long DOUBLE_DELAY = 250;
    // public static final long FULL_CHARGE_VOLTAGE = 14;
    public long BOOL_DELAY = 100;
    public long DOUBLE_DELAY = 250;
    public long FULL_CHARGE_VOLTAGE = 14;

    public boolean bool(boolean defaultValue, String caption, String label0, String label1) {
        boolean autoClear = telemetry.isAutoClear();
        if (autoClear) telemetry.setAutoClear(false); // Can't clear items now.
        boolean b = defaultValue; // Not strictly necessary, but good for being explicit.
        Telemetry.Item telItem = telemetry.addData(caption, b ? label1 : label0);
        while (!gamepad1.a && !isStopRequested()) {
            telItem.setValue(b ? label1 : label0);
            telemetry.update();
            if (gamepad1.dpad_up) b = false;
            else if (gamepad1.dpad_down) b = true;
            sleep(BOOL_DELAY);
        }
        while (gamepad1.a); // In case they hold A for too long.
        telItem.setCaption(caption.toUpperCase());
        telemetry.update();
        if (autoClear) telemetry.setAutoClear(true); // Reset to original state.
        return b;
    }
    public double float64(double defaultValue, double var, int precision, String caption) {
        boolean autoClear = telemetry.isAutoClear();
        if (autoClear) telemetry.setAutoClear(false);
        double num = defaultValue;
        Telemetry.Item telItem = telemetry.addData(caption, (long) (num * Math.pow(10, precision)) / Math.pow(10, precision));
        while (!gamepad1.a && !isStopRequested()) {
            telItem.setValue((long) (num * Math.pow(10, precision)) / Math.pow(10, precision));
            telemetry.update();
            if (gamepad1.dpad_up) num += var;
            else if (gamepad1.dpad_down) num -= var;
            sleep(DOUBLE_DELAY);
        }
        while (gamepad1.a);
        telItem.setCaption(caption.toUpperCase());
        telemetry.update();
        if (autoClear) telemetry.setAutoClear(true);
        return num;
    }
    public double batteryPower() { // Returns infinity upon error. This is not a level, but the power from 0 to 1.
        double p = Double.POSITIVE_INFINITY;
        for (VoltageSensor sensor : hardwareMap.voltageSensor) {
            double voltage = sensor.getVoltage();
            if (voltage > 0) p = Math.min(p, voltage);
        }
        return (p == Double.POSITIVE_INFINITY ? p : p / FULL_CHARGE_VOLTAGE);
    }
    public void success() {
        telemetry.log().add("CONFIGURATION COMPLETE");
        telemetry.update();
        telemetry.speak("Configuration Complete");
    }

    // Script Parser and Executor / Interpreter
    /*
    Script:
        Each line contains exactly one command and its parameters.
        The command consists of a name without spaces.
        It is then followed by a space and its arguments (space separated).
            "Command 0 9 8 7 6"
        The arguments must be integers, for now.
        Lines starting with "//" are comments, and will be ignored.
        An instruction is a term describing a command followed by its arguments.
    Parser:
        Parsing of the entire script is done prior to execution.
        Leading and trailing whitespace for each line is removed.
        The parser will notify of an error if:
            Extra spaces are found within a command.
            The number of arguments do not match the parameter count determined by command registration.
            The command is not registered (see below).
            An argument is invalid.
    Run:
        Call to parse the script.
        Returns false upon parse failure or if runState is set to false by other Java, and true upon success.
    
    Registering Commands (advanced):
        Any commands used by the script must first be registered in the HashMap `actions`.
            This includes if-else logic and loops.
        A command is registered by adding its name and parameter count, separated by a '-', to the HashMap.
            Along with a lambda expression that expects an integer array dictating what it does.
            The lambda must return an integer exit code, which may be used by other registered commands. This value can be anything.
            "
            actions.put("Command-2", (int [] args) -> {
                telemetry.log().add("Executing Command " + args[0] + ' ' + args[1]);
                return 0;
            });
            "
    Misc:
        A function called `jumpTo` which accepts a single integer will move script execution to that instruction.
            All subsequent executions will be from that instruction.
            Use this function with care, as it can easily create infinite loops and go out of indexing bounds.
            Note: Instructions are indexed from zero.
        Note: Disables telemetry auto clear. Does not disable upon exit.
        Note: If you change the Instruction ArrayList you must update the instructionCount variable!
    */

    public HashMap <String, Action> actions = new HashMap <> ();
    public ArrayList <Instruction> instructions = null;
    public int instructionCount = -1, instructionNum = 0, returnCode = 0;
    public boolean runState = true;
    public boolean run(String script) {
        telemetry.setAutoClear(false);
        instructions = parse(script);
        if (instructions == null) return false;
        instructionCount = instructions.size();
        for (; runState && instructionNum < instructionCount && instructionNum > -1; ++instructionNum) {
            Instruction i = instructions.get(instructionNum);
            telemetry.log().add(i.line + ": " + 
                                i.action.substring(0, i.action.length() - 2) + 
                                ' ' + Arrays.toString(i.arguments));
            returnCode = actions.get(i.action).execute(i.arguments);
            telemetry.update();
        }
        return runState;
    }
    public ArrayList <Instruction> parse(String script) {
        ArrayList <Instruction> instructions = new ArrayList <> ();
        String [] lines = script.split("\n");
        int lineNum = 0;
        for (String line : lines) {
            ++lineNum;
            line = line.trim();
            if (line.isEmpty() || line.startsWith("//")) continue;
            String [] v = line.split(" ");
            for (String i : v) if (i.isEmpty()) {
                error(lineNum, "Extra spaces detected.");
                return null;
            }
            v[0] += "-" + (v.length - 1);
            if (!actions.containsKey(v[0])) {
                error(lineNum, "Unidentified instruction name / not declared:" + v[0]);
                return null;
            }
            int [] arguments = new int [v.length - 1];
            for (int i = 1; i < v.length; ++i) {
                int arg;
                try {
                    arg = Integer.parseInt(v[i]);
                }
                catch (Exception e) {
                    error(lineNum, "Invalid argument passed as parameter. Must be an integer.");
                    return null;
                }
                arguments[i - 1] = arg;
            }
            instructions.add(new Instruction (v[0], arguments, lineNum));
        }
        return instructions;
    }
    public void jumpTo(int lineNum) {
        instructionNum = lineNum - 2;
    }
    public void error(int lineNum, String msg) {
        telemetry.log().add("Script error on line: " + lineNum + " --> " + msg);
        telemetry.update();
    }

    // For loading script stored in src/main/assets.
    public String loadScript(String path) {
        
        // Reference: https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java
        // Note: The app's context is required to get assets. Conveniently, the SDK provides a medium for this through the hardwareMap.

        try {
            return new BufferedReader(
                new InputStreamReader(hardwareMap.appContext.getAssets().open(path))).lines().collect(Collectors.joining("\n")
            );
        }
        catch (IOException e) {
            runState = false;
            error(0, "Could not open script file.");
            return "";
        }
    }
}
OctoWare
/*
OctoWare

This not an Autonomous nor TeleOp.

Temporary hardware class to avoid boilerplate and repetition.

*/

package org.firstinspires.ftc.teamcode.octo;

import com.qualcomm.robotcore.hardware.ColorSensor;
import com.qualcomm.robotcore.hardware.TouchSensor;
import com.qualcomm.robotcore.hardware.DcMotorEx;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.Servo;
import com.qualcomm.robotcore.hardware.IMU;

import com.qualcomm.hardware.rev.RevHubOrientationOnRobot;

// vII-XVIII

public abstract class OctoWare extends OctoConfig {
    public DcMotor     frontLeft, frontRight, rearLeft, rearRight;
    public TouchSensor clawRest, magnet;
    public ColorSensor colorSensor;
    public DcMotorEx   slideUp;
    public Servo       claw;
    public IMU         imu;

    public void setHardware(boolean encoders) {
        // Connect Motors
        frontLeft  = hardwareMap.get(DcMotor.class, "frontLeft");
        frontRight = hardwareMap.get(DcMotor.class, "frontRight");
        rearLeft   = hardwareMap.get(DcMotor.class, "rearLeft");
        rearRight  = hardwareMap.get(DcMotor.class, "rearRight");
        slideUp    = hardwareMap.get(DcMotorEx.class, "slideUp");

        // Connect Servos
        claw = hardwareMap.get(Servo.class, "claw");

        // Connect Color Sensor
        colorSensor = hardwareMap.get(ColorSensor.class, "colorSensor");

        // Connect IMU
        imu = hardwareMap.get(IMU.class, "imu");

        // Connect Sensors
        clawRest = hardwareMap.get(TouchSensor.class, "clawRest");
        magnet   = hardwareMap.get(TouchSensor.class, "magnet");

        // Set Motor Directions
        frontLeft.setDirection(DcMotor.Direction.REVERSE);
        frontRight.setDirection(DcMotor.Direction.FORWARD);
        rearLeft.setDirection(DcMotor.Direction.REVERSE);
        rearRight.setDirection(DcMotor.Direction.FORWARD);
        slideUp.setDirection(DcMotorEx.Direction.REVERSE);

        // Set Motors to Brake
        frontLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
        frontRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
        rearLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
        rearRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
        slideUp.setZeroPowerBehavior(DcMotorEx.ZeroPowerBehavior.BRAKE);

        // Reset Encoder Counts
        if (encoders) {
            frontLeft.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            frontRight.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            rearLeft.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            rearRight.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            
            frontLeft.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
            frontRight.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
            rearLeft.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
            rearRight.setMode(DcMotor.RunMode.RUN_USING_ENCODER);

            slideUp.setMode(DcMotorEx.RunMode.STOP_AND_RESET_ENCODER);
            slideUp.setMode(DcMotorEx.RunMode.RUN_WITHOUT_ENCODER);
        }

        // Configure IMU
        // Copied from SDK example.
        RevHubOrientationOnRobot.LogoFacingDirection logoDirection = RevHubOrientationOnRobot.LogoFacingDirection.LEFT;
        RevHubOrientationOnRobot.UsbFacingDirection  usbDirection  = RevHubOrientationOnRobot.UsbFacingDirection.BACKWARD;
        RevHubOrientationOnRobot orientationOnRobot = new RevHubOrientationOnRobot(logoDirection, usbDirection);
        imu.initialize(new IMU.Parameters(orientationOnRobot));
    }
}

Autonomous

AutoOctoGamma
/*
AutoOctoGamma

Current Status: Uses a pseudo code like script for easy adjustments.
                Utilizes wheel encoders and an IMU for odometry.
                The slide uses BOTH encoders and magnetic limit switches.

*/

package org.firstinspires.ftc.teamcode.octo;

import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
import com.qualcomm.robotcore.hardware.DcMotorEx;

import com.qualcomm.robotcore.util.ElapsedTime;

import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;

@Autonomous(name="AutoOcto III-III", group="Octo")

public class AutoOctoGamma extends OctoWare {

    // Runtimes
    ElapsedTime runtime = new ElapsedTime ();
    ElapsedTime runtime2 = new ElapsedTime ();

    // Constants
    final int ACTION_PAUSE = 50; // Pause after driving or moving the claw (milliseconds).
    final int SLIDE_TIMEOUT = 3;
    final double DEFAULT_POWER = 0.5; // Driving
    final double CLAW_OPEN = 0.3, CLAW_CLOSE = 0.65;
    final double AUTO_SLIDE_POWER = 0.7;

    // Mutable Variables
    boolean direction = true; // false --> left, true --> right
    boolean searching = false;
    boolean rest, last, now = true;
    boolean debug = false;
    int curLevel = 0, toLevel = 0;
    int globalOffset = 0;
    double al = 0, as = 0, bl = 0, bs = 0;
    double akp = 0.9, aki = 0, akd = 0;
    double bkp = 0.005, bki = 0, bkd = 0;

    // For data interchange between commands.
    boolean ifState = false;
    int parkZone = 3;

    @Override
    public void runOpMode() {
        setHardware(true);
        setCommands();

        // Critical Warning
        if (!clawRest.isPressed()) {
            String warning = "Warning. The linear slide is not in ground position. Press A to continue.";
            telemetry.speak(warning);
            telemetry.log().add(warning);
            telemetry.update();
            while (!gamepad1.a) idle();
        }

        telemetry.addData("CONFIGURATION", "Use dpad up / down to adjust values. Hold A to confirm.");

        direction = bool(direction, "Field Side", "Left", "Right");
        globalOffset = (int)float64(globalOffset, 25, 0, "Global Encoder Offset");
        debug = bool(debug, "Debug Mode (turn off for competition)", "Off", "On");

        success();

        // Wait
        waitForStart();

        // Ensure yaw is zeroed.
        imu.resetYaw();

        boolean success = run(loadScript("AutoOctoScriptGamma.txt"));
        telemetry.log().add("Script completed with exit status: " + success);
        telemetry.update();
        while (debug && opModeIsActive()) idle();
    }
    public int averageCount() {
        return (frontLeft.getCurrentPosition() +
                frontRight.getCurrentPosition() +
                rearLeft.getCurrentPosition() +
                rearRight.getCurrentPosition()) / 4;
    }
    public double PIDMP(double error, double Kp, double Ki, double Kd, Double lastError, Double intSum/*, double threshold*/) {
        intSum += error * runtime.seconds();
        double deriv = (error - lastError) / runtime.seconds();
        lastError = error;
        runtime.reset();
        return error * Kp + intSum * Ki + deriv * Kd;
    }
    public double angleWrap(double radians) {
        while (radians > Math.PI) radians -= 2 * Math.PI;
        while (radians < -Math.PI) radians += 2 * Math.PI;
        return radians;
    }
    public void stopAll() {
        frontLeft.setPower(0);
        frontRight.setPower(0);
        rearLeft.setPower(0);
        rearRight.setPower(0);
    }
    public void setCommands() {
        // Command Registration for Script
        actions.put("Drive-2", (int [] ticks) -> {
            bl = bs = 0.0;
            al = as = 0.0;
            runtime.reset();
            runtime2.reset();
            double target = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS);
            // int start = averageCount();
            while (opModeIsActive() && runtime2.milliseconds() < ticks[1]) {
                double drivePower = Math.min(DEFAULT_POWER, Math.max(-DEFAULT_POWER, PIDMP(ticks[0]/* + start*/ + globalOffset - averageCount(), bkp, bki, bkd, bl, bs)));
                double turnPower = PIDMP(angleWrap(imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS) - target), akp, aki, akd, al, as);
                double max = Math.max(Math.abs(drivePower) + Math.abs(turnPower), 1);
                double fl = (drivePower + turnPower) / max;
                double fr = (drivePower - turnPower) / max;
                double rl = (drivePower + turnPower) / max;
                double rr = (drivePower - turnPower) / max;
                frontLeft.setPower(fl);
                frontRight.setPower(fr);
                rearLeft.setPower(rl);
                rearRight.setPower(rr);
            }
            stopAll();

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("Strafe-2", (int [] ticks) -> {
            bl = bs = 0.0;
            al = as = 0.0;
            runtime.reset();
            runtime2.reset();
            double target = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS);
            // int start = (frontLeft.getCurrentPosition() + rearRight.getCurrentPosition()) / 2;
            while (opModeIsActive() && runtime2.milliseconds() < ticks[1]) {
                double drivePower = PIDMP(ticks[0]/* + start*/ + globalOffset - (frontLeft.getCurrentPosition() + rearRight.getCurrentPosition()) / 2,
                                          bkp, bki, bkd, bl, bs);
                double turnPower = PIDMP(angleWrap(imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS) - target), akp, aki, akd, al, as);
                double max = Math.max(Math.abs(drivePower) + Math.abs(turnPower), 1);
                double fl = (drivePower + turnPower) / max;
                double fr = (-drivePower - turnPower) / max;
                double rl = (-drivePower + turnPower) / max;
                double rr = (drivePower - turnPower) / max;
                frontLeft.setPower(fl);
                frontRight.setPower(fr);
                rearLeft.setPower(rl);
                rearRight.setPower(rr);
            }
            stopAll();

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("TurnTo-2", (int [] deg) -> {
            al = as = 0.0;
            runtime.reset();
            runtime2.reset();
            while (opModeIsActive() && runtime2.milliseconds() < deg[1]) {
                double power = PIDMP(angleWrap(imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS) - Math.toRadians(deg[0])),
                                     akp, aki, akd, al, as);
                frontLeft.setPower(power);
                frontRight.setPower(-power);
                rearLeft.setPower(power);
                rearRight.setPower(-power);
            }
            stopAll();

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("ReadSleeve-0", (int [] none) -> {
            // Calculate color reading percentages.
            double red = colorSensor.red(), green = colorSensor.green(), blue = colorSensor.blue();
            double total = red + green + blue;
            double pred = red / total, pgreen = green / total, pblue = blue / total; // Calculate percentage of each.

            // Determine zone to move to.
            if (pblue > pred && pblue > pgreen) parkZone = 1;
            else if (pred > pblue && pred > pgreen) parkZone = 2;
            else if (pgreen > pred && pgreen > pblue) parkZone = 3;

            telemetry.addData("Zone Read", parkZone);

            return 0;
        });
        actions.put("SlidePosition-1", (int [] pos) -> {
            ElapsedTime t = new ElapsedTime ();
            toLevel = pos[0];
            while (true) {
                // Sensors
                rest = clawRest.isPressed();
                last = now;
                now = magnet.isPressed() || rest;

                if (!searching && curLevel == toLevel) {
                    slideUp.setPower(0);
                    break;
                } else if (!searching && curLevel < toLevel) {
                    searching = true;
                    slideUp.setPower(AUTO_SLIDE_POWER);
                } else if (!searching && curLevel > toLevel) {
                    searching = true;
                    slideUp.setPower(-AUTO_SLIDE_POWER / Math.sqrt(curLevel)); // Slower if higher up.
                } else if (searching && curLevel < toLevel && !last && now) {
                    searching = false;
                    ++curLevel;
                } else if (searching && curLevel > toLevel && !last && now) {
                    searching = false;
                    --curLevel;
                }

                // Failsafes
                if (searching && curLevel > toLevel && rest) {
                    searching = false;
                    curLevel = toLevel = 0;
                }
                if (t.seconds() > SLIDE_TIMEOUT) {
                    slideUp.setPower(0);
                    actions.get("STOP-0").execute(new int [] {});
                    break;
                }
            }

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("SlidePositionE-1", (int [] pos) -> {
            slideUp.setMode(DcMotorEx.RunMode.RUN_USING_ENCODER);
            slideUp.setTargetPosition(slideUp.getCurrentPosition() + pos[0]);
            slideUp.setMode(DcMotorEx.RunMode.RUN_TO_POSITION);
            slideUp.setPower(AUTO_SLIDE_POWER);

            while (slideUp.isBusy() && !slideUp.isOverCurrent()) idle();

            slideUp.setPower(0);
            slideUp.setMode(DcMotorEx.RunMode.RUN_WITHOUT_ENCODER);

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("LowerSlideNB-0", (int [] none) -> {
            Runnable asyncStuff = () -> {
                slideUp.setPower(-AUTO_SLIDE_POWER);
                while (!clawRest.isPressed()) idle();
                slideUp.setPower(0);
            };
            Thread runStuff = new Thread (asyncStuff);
            runStuff.start();

            return 0;
        });
        actions.put("ClawState-1", (int [] pos) -> {
            if (pos[0] == 0) claw.setPosition(CLAW_OPEN);
            else if (pos[0] == 1) claw.setPosition(CLAW_CLOSE);

            actions.get("WaitSeconds-1").execute(new int [] {ACTION_PAUSE});

            return 0;
        });
        actions.put("WaitSeconds-1", (int [] msec) -> {
            sleep((long)msec[0]);

            return 0;
        });
        // Expand if-else statement logic later.
        actions.put("If-1", (int [] v) -> {
            ifState = false;

            if (v[0] == returnCode) return returnCode;

            int nest = 1;
            for (instructionNum += 1; instructionNum < instructionCount; ++instructionNum) {
                String act = instructions.get(instructionNum).action;
                if (act.equals("If-1") || act.equals("Elseif-1") || act.equals("Else-0")) ++nest;
                else if (act.equals("End-0")) {
                    --nest;
                    if (nest == 0) break;
                }
            }

            return returnCode;
        });
        actions.put("End-0", (int [] none) -> {
            if (instructionNum == 0) {
                error(instructions.get(instructionNum).line, "Cannot be at start.");
                runState = false;
                return 0;
            }

            ifState = true;

            return returnCode;
        });
        actions.put("Else-0", (int [] none) -> {
            if (instructionNum == 0) {
                error(instructions.get(instructionNum).line, "Cannot be at start.");
                runState = false;
                return 0;
            } else if (!instructions.get(instructionNum - 1).action.equals("End-0")) {
                error(instructions.get(instructionNum - 1).line, "Requires previous action to be End.");
                runState = false;
                return 0;
            }

            if (!ifState) return returnCode;

            int nest = 1;
            for (instructionNum += 1; instructionNum < instructionCount; ++instructionNum) {
                String act = instructions.get(instructionNum).action;
                if (act.equals("If-1") || act.equals("Elseif-1") || act.equals("Else-0")) ++nest;
                else if (act.equals("End-0")) {
                    --nest;
                    if (nest == 0) break;
                }
            }

            return returnCode;
        });
        actions.put("Elseif-1", (int [] v) -> {
            int preInsNum = instructionNum;
            actions.get("Else-0").execute(new int [] {});
            if (preInsNum == instructionNum) actions.get("If-1").execute(v);

            return returnCode;
        });
        actions.put("GetZone-0", (int [] none) -> {
            return parkZone;
        });
        actions.put("GetSide-0", (int [] none) -> {
            return direction ? 1 : 0;
        });
        actions.put("SpeakVersion-2", (int [] version) -> {
            telemetry.speak("Version " + version[0] + '-' + version[1] + ".");

            return 0;
        });
        actions.put("STOP-0", (int [] none) -> {
            instructionNum = 1000; // Crude implementation.

            return 0;
        });
    }
}
AutoOctoScriptGamma [TXT]
SpeakVersion 3 3

ClawState 1
WaitSeconds 1000
GetSide
If 1
    // Start Right
    SlidePosition 3
    Drive 1023 1000
    ReadSleeve
    Drive 2647 1800
    Drive 2290 1000

    TurnTo 45 1000

    Drive 2684 1000

    WaitSeconds 500
    ClawState 0

    // Score
    Drive 2317 1000
    SlidePosition 1
    TurnTo -92 1500
    // Old turn to -94.
    Drive 3464 2000

    SlidePositionE -942
    ClawState 1
    WaitSeconds 1000
    SlidePositionE 2694

    Drive 2311 2000
    TurnTo 43 1000
    // Old turn to 38.
    Drive 2646 1000

    WaitSeconds 500
    ClawState 0
    WaitSeconds 500

    Drive 2301 1000
    TurnTo 0 1000

    // Drive 646 1753 2579 3895
    // Drive 87 2238 2083 4403

    // Park
    GetZone
    If 1
        // Blue
        Strafe 1006 1000
    End
    Elseif 2
        // Red
        Drive 2301 1000
    End
    Elseif 3
        // Green
       Strafe 3407 1000
    End
End

Elseif 0
    // Start Left
    SlidePosition 3
    Drive 1023 1000
    ReadSleeve
    Drive 2647 1800
    Drive 2290 1000

    TurnTo -45 1000

    Drive 2684 1000

    WaitSeconds 500
    ClawState 0

    // Score
    Drive 2317 1000
    SlidePosition 1
    TurnTo 92 1500
    // Old turn to -94.
    Drive 3464 2000

    SlidePositionE -942
    ClawState 1
    WaitSeconds 1000
    SlidePositionE 2694

    Drive 2311 2000
    TurnTo -43 1000
    // Old turn to 38.
    Drive 2646 1000

    WaitSeconds 500
    ClawState 0
    WaitSeconds 500

    Drive 2301 1000
    TurnTo 0 1000

    // Drive 646 1753 2579 3895
    // Drive 87 2238 2083 4403

    // Park
    GetZone
    If 1
        // Blue
        Strafe 1006 1000
    End
    Elseif 2
        // Red
        Drive 2301 1000
    End
    Elseif 3
        // Green
       Strafe 3407 1000
    End
End

TeleOp

OctoBeta
/*
OctoBeta

Current Status: Field-Centric Mecanum Drive with linear slide extender and claw servo.
                The linear slide has a driver enhancement that automatically moves it to levels using encoders.
                This also makes use of a magnetic limit switch beneath the claw to account for error
                    caused by variations in the spooling of the wire.
                Also has a failsafe for when motor strain is detected.

*/

package org.firstinspires.ftc.teamcode.octo;

import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotorEx;
import com.qualcomm.robotcore.hardware.DcMotor;

import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;

@TeleOp(name="Octo II-VIII", group="Octo")

public class OctoBeta extends OctoWare {

    // Mutable Variables
    double drive, strafe, turn;
    double fl, fr, rl, rr, max;
    double extender;
    double heading, rotX, rotY; // For field-centric drive.
    int level = 0, lastLevel;
    boolean manual = false, rest = true, lastRest;

    // Constants
    final double TURN_SPEED_MAX = 0.9;
    final double CLAW_OPEN = 0.3, CLAW_CLOSE = 0.65;
    final double AUTO_SLIDE_POWER = 1;
    final int [] SLIDE_ENCODER_LEVELS = new int [] {0, 1223, 2120, 3033};

    @Override
    public void runOpMode() {
        setHardware(false);
        waitForStart();

        slideUp.setMode(DcMotorEx.RunMode.STOP_AND_RESET_ENCODER);

        while (opModeIsActive()) {
            // Controller with Improved Sensitivity
            drive     = -gamepad1.left_stick_y;
            strafe    = gamepad1.left_stick_x;
            turn      = gamepad1.right_stick_x * TURN_SPEED_MAX;
            drive    *= Math.abs(drive);
            strafe   *= Math.abs(strafe);
            turn     *= Math.abs(turn);
            extender  = -gamepad2.left_stick_y;
            lastRest  = rest;
            rest      = clawRest.isPressed();
            heading   = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS);
            lastLevel = level;
            if      (gamepad2.a) level = 0;
            else if (gamepad2.b) level = 1;
            else if (gamepad2.y) level = 2;
            else if (gamepad2.x) level = 3;
            if (gamepad2.a || gamepad2.b || gamepad2.y || gamepad2.x) manual = false;

            // Calculations based on GM0.
            rotX = strafe * Math.cos(-heading) - drive * Math.sin(-heading);
            rotY = strafe * Math.sin(-heading) + drive * Math.cos(-heading);
            // Normalize
            max = Math.max(Math.abs(rotY) + Math.abs(rotX) + Math.abs(turn), 1);
            fl = (rotY + rotX + turn) / max;
            fr = (rotY - rotX - turn) / max;
            rl = (rotY - rotX + turn) / max;
            rr = (rotY + rotX - turn) / max;

            // Act
            frontLeft.setPower(fl);
            frontRight.setPower(fr);
            rearLeft.setPower(rl);
            rearRight.setPower(rr);

            if (manual || extender != 0) {
                slideUp.setMode(DcMotorEx.RunMode.RUN_WITHOUT_ENCODER);
                manual = true;
                if (extender > 0) slideUp.setPower(extender);
                else if (extender < 0 && !rest) slideUp.setPower(extender);
                else slideUp.setPower(0);
            } else {
                if (level != lastLevel) {
                    // Stop if currently running.
                    slideUp.setPower(0);
                    slideUp.setMode(DcMotor.RunMode.RUN_USING_ENCODER);

                    slideUp.setTargetPosition(SLIDE_ENCODER_LEVELS[level]);
                    slideUp.setMode(DcMotor.RunMode.RUN_TO_POSITION);
                    slideUp.setPower(AUTO_SLIDE_POWER);
                }
                if (!slideUp.isBusy()) {
                    // Stop if currently running.
                    slideUp.setPower(0);
                    slideUp.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
                }
            }
            if (rest && !lastRest) slideUp.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            // Failsafe
            if (slideUp.isOverCurrent()) manual = true;

            if (gamepad2.left_bumper) claw.setPosition(CLAW_CLOSE);
            else if (gamepad2.right_bumper) claw.setPosition(CLAW_OPEN);

            // Telemetry
            telemetry.addData("Manual", manual);
            telemetry.addData("Current Level", level);
            telemetry.addData("Slide", slideUp.getCurrentPosition());
            telemetry.update();
        }
    }
}