Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Competition Mode #875

Open
ATK422 opened this issue Dec 14, 2023 · 8 comments
Open

Competition Mode #875

ATK422 opened this issue Dec 14, 2023 · 8 comments

Comments

@ATK422
Copy link

ATK422 commented Dec 14, 2023

Currently there is no way to enable/disable features during runtime to make a program competition legal. This makes development a pain when working with vision based algorithms (Cannot view camera stream if op mode is running) or developing external software (such as ftc dashboard). Adding a toggle that could be viewed in the sdk would fix this. A simple toggle in the driver station app would suffice or you could make a more advanced one (I. E. Time Locked/Password Locked).

@YourAverageFTCPerson
Copy link

Taking advantage of the fact that the robot controller app doesn't actually compile, run and stop programs each time they are run, instead just compiling them into one big robot controller app (meaning that static variables persist ) (https://www.reddit.com/r/FTC/comments/5yfnhj/help_passing_data_between_opmodes/?rdt=39096), I wrote this:

package org.firstinspires.ftc.team24388;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.Gamepad;

import java.lang.reflect.Field;
import java.util.BitSet;
import java.util.HashMap;

@TeleOp(name = "Tweakable Numbers", group = "Utilities")
public class TweakableNumbers extends LinearOpMode {
    public static final HashMap<String, Long> NUMBERS = new HashMap<>();

    private static final String NUMBER_FIELD_NAME;

    static {
        String temp;
        try {
            // This really does not need to be here.
            Field field = TweakableNumbers.class.getDeclaredField("NUMBERS");
            // No need for Class.getTypeName() because TweakableNumbers.class is never an array.
            temp = field.getDeclaringClass().getName() + '.' + field.getName();
        } catch (NoSuchFieldException e) {
            // If we can't find the field, whatever. It really doesn't matter.
            // Note that this should not happen unless I renamed NUMBERS.
            temp = TweakableNumbers.class.getName() + ".NUMBERS";
        }
        NUMBER_FIELD_NAME = temp;

        // Maybe set some numbers here in advance? (Default values)
    }

    private static long convert(BitSet bits) {
        long value = 0L;
        for (int i = 0; i < bits.length(); i++) {
            value += bits.get(i) ? (1L << i) : 0L;
        }
        return value;
    }

    @Override
    public void runOpMode() {
        // Over-engineered displaying of numbers.
        telemetry.addData(NUMBER_FIELD_NAME, NUMBERS);
        telemetry.addLine("Please start the program to set values.");
        telemetry.update();

        waitForStart();

        String key = null;
        BitSet bits = new BitSet();

        Gamepad previousGamepad1 = new Gamepad();
        Gamepad currentGamepad1 = new Gamepad();

        boolean nowSettingValue = false;
        int length = 0;

        for (;;) {
            telemetry.addData("bits: ", bits.toString());

            previousGamepad1.copy(currentGamepad1);
            currentGamepad1.copy(gamepad1);

            if (currentGamepad1.triangle) {
                break;
            }

            if (currentGamepad1.left_bumper && !previousGamepad1.left_bumper) { // Press left bumper to record the current gamepad (this may be changed to suit your needs.)
                if (nowSettingValue) {
                    if (currentGamepad1.right_bumper) {
                        bits.set(length);
                    }
                    length++;
                } else {
                    // We use left_bumper to control when the input is recorded so it's always true
                    // and therefore redundant.
                    key = currentGamepad1.toString().replace("left_bumper ", "");
                    nowSettingValue = true;
                }
            }
        }
        NUMBERS.put(key, convert(bits));

        telemetry.addData(NUMBER_FIELD_NAME, NUMBERS);
        telemetry.update();
        while (opModeIsActive()) {
        }
    }
}

(Please note that I over-engineered the printing of NUMBERS.)

This OpMode has a static HashMap that may be accessed from other classes (for enabling/disabling features). The keys of the HashMap are toString()s of saved gamepad states. The values of the HashMap are Longs constructed with little-endian binary, one bit at a time. (You can expand on this further, maybe corresponding numbers 0-9 with buttons on the gamepad.)

If you just want to enable/disable features on the fly, just use a static boolean.

Also, I've never tested this so this may or may not work (it does compile though).

@ATK422
Copy link
Author

ATK422 commented Feb 19, 2024

Our team uses Kotlin so we can just use a "object" (singleton) to do the same thing, however I feel like this should be base functionality.

Also, you are unable to view the camera if you are running your op mode, which is built into the driver station and cannot be changed at runtime.

@YourAverageFTCPerson
Copy link

Sorry, I don't understand. (I'm new to FTC but not Java) What do you mean by view the camera?

@ATK422
Copy link
Author

ATK422 commented Feb 19, 2024

On the driver station, when a camera is connected/you use opencv you can open a camera feed from the robot.

@YourAverageFTCPerson
Copy link

Apparently phone driver stations let you view the camera even during runtime. I can't find any way to do this on a regular DS.

@YourAverageFTCPerson
Copy link

If you want view the camera while the program is running, you can just make it run during initialization:
Change

@Override
public void runOpMode() {
    ...
    waitForStart();
    while (opModeIsActive()) {
        ...
    }
}

to

@Override
public void runOpMode() {
    ...
    while (!isStopRequested()) {
        ...
    }
}

@YourAverageFTCPerson
Copy link

Ok, I actually tested and fixed TweakableNumbers (if you still want to use it):

package org.firstinspires.ftc.team24388;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.Gamepad;

import java.lang.reflect.Field;
import java.util.BitSet;
import java.util.HashMap;

@TeleOp(name = "Tweakable Numbers", group = "Utilities")
public class TweakableNumbers extends LinearOpMode {
    public static final HashMap<String, Long> NUMBERS = new HashMap<>();

    private static final String NUMBER_FIELD_NAME;

    static {
        String temp;
        try {
            Field field = TweakableNumbers.class.getDeclaredField("NUMBERS");
            // No need for Class.getTypeName() because TweakableNumbers.class is never an array.
            temp = field.getDeclaringClass().getName() + '.' + field.getName();
        } catch (NoSuchFieldException e) {
            // If we can't find the field, whatever. It really doesn't matter.
            // Note that this should not happen unless I renamed NUMBERS.
            temp = TweakableNumbers.class.getName() + ".NUMBERS";
        }
        NUMBER_FIELD_NAME = temp;

        // Maybe set some numbers here in advance? (Default values)
    }

    private static long convert(BitSet bits) {
        long value = 0L;
        for (int i = 0; i < bits.length(); i++) {
            value += bits.get(i) ? (1L << i) : 0L; // Big-endian doesn't actually work for some reason.
        }
        return value;
    }

    @Override
    public void runOpMode() {
        // Over-engineered displaying of numbers.
        telemetry.addData(NUMBER_FIELD_NAME, NUMBERS);
        telemetry.addLine("Please start the program to set values.");
        telemetry.update();

        waitForStart();

        String key = null;
        // Bits is where the bits are stored. (These are controlled by whether gamepad1.right_bumper is true or not) 
        BitSet bits = new BitSet();

        Gamepad previousGamepad1 = new Gamepad();
        Gamepad currentGamepad1 = new Gamepad();

        boolean nowSettingValue = false;
        int length = 0;

        for (;;) {
            telemetry.addData("bits", bits);
            telemetry.addData("nowSettingValue", nowSettingValue);
            telemetry.addLine("dpad_up to exit");
            telemetry.update();

            previousGamepad1.copy(currentGamepad1);
            currentGamepad1.copy(gamepad1);

            if (currentGamepad1.dpad_up && !previousGamepad1.dpad_up) {
                break;
            }

            if (currentGamepad1.left_bumper && !previousGamepad1.left_bumper) {
                if (nowSettingValue) {
                    if (currentGamepad1.right_bumper) {
                        bits.set(length);
                    }
                    length++;
                } else {
                    // We use left_bumper to control when the input is recorded so it's always true
                    // and therefore redundant.
                    // User is always 1 because we check for input from gamepad1.
                    // The replaceAll removes "ID: ..." because that is subject to change every time the gamepad is plugged in. (The regex was generated by ChatGPT)
                    key = currentGamepad1.toString().replace("left_bumper ", "").replace("user: 1 ", "").replaceAll("^(ID: \\d+)\\s", "");
                    nowSettingValue = true;
                }
            }
        }
        if (key != null) { // If null just don't set it (quit without crashing the program or saving changes)
            NUMBERS.put(key, convert(bits));
        }

        telemetry.addData(NUMBER_FIELD_NAME, NUMBERS);
        telemetry.update();
        while (opModeIsActive()) {
        }
    }
}

Example use:

...
Long temp;
// This is the toString() of gamepad1 with nothing down with the necessary things removed
// (I don't recommend using the values of lx, ly, rx, ry, lt or rt for keys [i.e. don't press them
// because user input is random and the measurements are too precise to be reproduced] but I'm too lazy to remove them)
// Also the question mark and colon are the ternary operator
// (if (booleanValue) { value is 5 } else { value is something else } => value = booleanValue ? 5 : something else)
final long numberOfIterations = (temp = TweakableNumbers.NUMBERS.get("lx:  0.00 ly:  0.00 rx:  0.00 ry:  0.00 lt: 0.00 rt: 0.00 ")) == null ? 5L : temp;
for (long i = 0; i <= numberOfIterations; i++) {
    doSomething();
}
...

@j5155
Copy link

j5155 commented Jul 18, 2024

A Competition Mode of some kind could also be a clear signal to disable certain testing features that are illegal in competition. For example, FTC Dashboard's remote access features must currently be disabled manually (which teams often forget to do). This could automate that disabling.
Being in Testing Mode could allow you to perform some of these features, such as real time camera previews, but also (for example) display a red outline on the driver station that would be clear to FTAs. This would allow for more flexibility in testing without compromising in legality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants