Reusing commands in multiple command groups

When a Command is composed into a CommandGroup, it cannot be part of another CommandGroup. If you have a non-trivial command that you would be useful in multiple other commands, what techniques have you found most useful?

For example, let’s say you were building autonomous commands for Crescendo, and defined one command that would pick up a few particular notes, and you wanted to use that in sequence with other commands to build a variety of autonomous commands, all of which included that same sequence.

One option might be to use a generator, which produces equivalent commands, but a unique one for each time it might be composed. This could be by creating a dedicated class and creating a new instance each time, or by defining a lambda that generates a unique object, which you call each time you want to compose a sequence with that functionality.

Another option might be to use .asProxy() to provide a level of indirection from the actual command. This should (? we haven’t tested it) allow for a common Command to executed, but just the ProxyCommands to be composed. However, this isn’t a stated use case for ProxyCommand, and the documentation suggests limited use of ProxyCommand.

Have you found either of these options, or another option we haven’t covered here, to be useful in command-based programming? I’d like to ensure we are using best practices as we prepare for Reefscape.

1 Like

When a command is composed as part of a command group only that instance cannot be reused. For this reason when defining command groups that we feel can be reused in multiple places we define them in a separate file as a class. This allows for more separation of concerns and easier maintainability too.

Here is an example

Write a factory function!

11 Likes

this is our usual approach. We avoid creating many command classes, and get away with almost exclusively using the Commands.java factories for everything. It feels very readable, and also seems to keep the project less cluttered. Less code is less bugs.

1 Like

Factory methods for sure and we dare to introduce just a little bit more code in order to have smaller files as Robot or RobotContainer can grow to be unwieldy. It requires a single RobotContainer class injection into the split-off abstract class with static methods to make them easy to access. [Sorry, our abstract class is not named perfectly right.]
Example Romi training code:

package frc.robot.controls;

import java.util.function.DoubleSupplier;

import edu.wpi.first.wpilibj2.command.Commands;
import edu.wpi.first.wpilibj2.command.button.CommandXboxController;
import edu.wpi.first.wpilibj2.command.button.Trigger;
import frc.robot.RobotContainer;
import frc.robot.sensors.Bumper;
import frc.robot.subsystems.RomiDrivetrain;
import frc.robot.subsystems.RomiLED;

public abstract class TriggerBindings 
{
    private static CommandXboxController xbox;
    private static RomiDrivetrain romiDrivetrain;
    private static RomiLED greenLED;
    private static RomiLED redLED;
    private static Bumper frontBumper;
    private static DoubleSupplier leftYAxisSupplier;
    private static DoubleSupplier leftXAxisSupplier;

    private TriggerBindings()
    {}

    public static void createBindings(RobotContainer robotContainer)
    {
        xbox = robotContainer.getXbox();
        romiDrivetrain = robotContainer.getRomiDrivetrain();
        greenLED = robotContainer.getGreenLED();
        redLED = robotContainer.getRedLED();
        frontBumper = robotContainer.getFrontBumper();

        configSuppliers();

        configAButton();
        configBButton();
        configXYButtons();
        configFrontBumper();
        configDefaultCommands();
    }

    private static void configSuppliers()
    {
        leftYAxisSupplier = () -> -xbox.getRawAxis(1);
        leftXAxisSupplier = () -> -xbox.getRawAxis(0);
    }

    private static void configAButton()
    {
        Trigger aButtonTrigger = xbox.a();
        aButtonTrigger
            .whileTrue( Commands.runEnd( greenLED::on, greenLED::off ) );
    }

    private static void configBButton()
    {
        Trigger bButtonTrigger = xbox.b();
        bButtonTrigger
            .onTrue(redLED.onCommand())
            .onFalse(redLED.offCommand());
    }

    private static void configXYButtons()
    {
        Trigger xButtonTrigger = xbox.x();
        Trigger yButtonTrigger = xbox.y();

        xButtonTrigger.or(yButtonTrigger)
            .onTrue( romiDrivetrain.arcadeDriveCommand( () -> 0.5, () -> 0.0 ) )
            .onFalse( romiDrivetrain.stopDriveCommand() );
    
        // xButtonTrigger.and(yButtonTrigger)
        //     .onTrue( romiDrivetrain.arcadeDriveCommand( () -> 0.5, () -> 0.0 ) );
    }

    private static void configFrontBumper()
    {
        Trigger frontBumperTrigger = new Trigger(frontBumper.isPressedSupplier());
        frontBumperTrigger
            .whileTrue(romiDrivetrain.onlyDriveBackwardCommand(leftYAxisSupplier));
    }

    private static void configDefaultCommands()
    {
        if(romiDrivetrain != null)
        {
            romiDrivetrain.setDefaultCommand(
                Commands.runEnd(
                    () -> romiDrivetrain.arcadeDrive(leftYAxisSupplier, leftXAxisSupplier),
                    () -> { romiDrivetrain.stopDrive(); System.out.println("Stopped"); },
                    romiDrivetrain
                )
            );
        }
    }
}
1 Like

That’s actually how we got here. We have code like (simplifying for the sake of the example):

addAutoCommand(
    Commands.sequence(
        new PathPlannerAuto("A"),
        Commands.either(
            new PathPlannerAuto("B"),
            new PathPlannerAuto("C"),
            indexer::haveNote
        )
    )
);

And it turns out that we used the Commands.either() part with the same pair of choices multiple times. It seems undesirable to have that repetition in the code. Instead, it seems DRYer to have something like:

Command BorC =
    Commands.either(
        new PathPlannerAuto("B"),
        new PathPlannerAuto("C"),
        indexer::haveNote
    );

And then to use BorC in those other command definitions, but BorC itself could only be used once. That’s why I asking for opinions (hopefully informed by experience) on the nicest way to handle that.

This looks like the popular choice. This is what I was referring to with the lambda, but I supposed that a specific instance of a generating function, and it could done multiple ways.

In this case, maybe we go even more generic, and don’t need something specific for the B and C combination. For example, we could have something like

Command noteOption(String withNote, String withoutNote) {
    return Commands.either(
        new PathPlannerAuto(withNote),
        new PathPlannerAuto(withoutNote),
        indexer::haveNote
    );
}

And then use noteOption("B", "C") multiple places. Since a factory function (lambda or otherwise) to be a popular choice, I think we’ll pursue that path, and see how it turns out for us.

Thanks for the responses!

2 Likes