CommandGroup Irregularity

Currently, the CommandGroup class only allows one to add commands in parallel or in series. For parallel commands, all commands that are running parallel to each other start at the same time. However, I am seeking a solution to start a command, in parallel, when another command (in the same parallel group) is almost finished.

For example, a useful scenario would be to raise a robot’s elevator to full height as its decelerating near the end of a drive forward trajectory. This way, you won’t have the robot racing across the field at full speed with its elevator up, but still save time by having the elevator at the desired position when the robot’s drivetrain reaches its target.

To solve this issue, I created the QueueCommand class, which essentially starts a command once a conditional has been met. It’s designed to keep on running until the given inner command has terminated.


public class QueueCommand extends Command {

    private Command command;
    private QueueCommandConditional conditional;

    private boolean executed;

    public QueueCommand(Command command, QueueCommandConditional conditional) {
        this.command = command;
        this.conditional = conditional;
    }

    @Override
    protected void initialize() {
        this.executed = false;
    }

    @Override
    protected void execute() {
        if (this.conditional.isTrue() && !this.executed) {
            this.command.start();
            this.executed = true;
        }
    }

    @Override
    protected boolean isFinished() {
        return this.executed && !this.command.isRunning();
    }

    @Override
    protected void end() {
        this.command.cancel();
    }

    @Override
    protected void interrupted() {
        this.end();
    }

    public interface QueueCommandConditional {

        public boolean isTrue();

    }

}

I’ve experienced some irregularities when using the QueueCommand inside a CommandGroup. For example:


        CommandGroup auton = new CommandGroup();

        DriveTrajectoryCommand drive = new DriveTrajectoryCommand(250);

        auton.addSequential(CGUtils.parallel(
                drive,
                new QueueCommand(
                        new ElevatorPositionCommand("up"),
                        () -> drive.isAlmostFinished(100)
                )
        ));
        auton.addSequential(new IntakeCommand("Out"));

will function as expected. The robot will start driving forward, the elevator will rise when there are 100 ticks left in DriveTrajectoryCommand, and the IntakeCommand will start and stop when both DriveTrajectoryCommand and ElevatorPositionCommand has finished.

However, the following CommandGroup will produce an irregularity:


        CommandGroup auton = new CommandGroup();

        DriveTrajectoryCommand drive = new DriveTrajectoryCommand(250);

        auton.addSequential(CGUtils.parallel(
                drive,
                new QueueCommand(
                        new ElevatorPositionCommand("up"),
                        () -> drive.isAlmostFinished(100)
                )
        ));
        auton.addSequential(new IntakeCommand("Out"));
        auton.addSequential(CGUtils.parallel(
                new ElevatorPositionCommand("down"),
                new DriveTrajectoryCommand(50)
        ));

DriveTrajectoryCommand will start as normal, but will be interrupted when there are 100 ticks left. The next cycle (20 ms after DriveTrajectoryCommand has terminated), ElevatorPositionCommand(“up”) will initialize and complete. No other command after that will run. It seems like the second ElevatorPositionCommand(“down”) causes the initial DriveTrajectoryCommand to terminate when ElevatorPositionCommand(“up”) is initialized.

I’ve created a basic tester to help debug this issue without the roboRIO present:


import edu.wpi.first.wpilibj.*;
import edu.wpi.first.wpilibj.command.*;

import java.util.Timer;
import java.util.TimerTask;

public class SubsystemTest {

    static {
        HLUsageReporting.SetImplementation(new HLUsageReporting.Interface() {
            @Override
            public void reportScheduler() {

            }

            @Override
            public void reportPIDController(int num) {

            }

            @Override
            public void reportSmartDashboard() {

            }
        });

        RobotState.SetImplementation(new RobotStateInterface());

        edu.wpi.first.wpilibj.Timer.SetImplementation(new edu.wpi.first.wpilibj.Timer.StaticInterface() {
            @Override
            public double getFPGATimestamp() {
                return 0;
            }

            @Override
            public double getMatchTime() {
                return 0;
            }

            @Override
            public void delay(double seconds) {

            }

            @Override
            public edu.wpi.first.wpilibj.Timer.Interface newTimer() {
                return null;
            }
        });
    }

    public static Drivetrain drivetrain = new Drivetrain();
    public static Elevator elevator = new Elevator();
    public static Intake intake = new Intake();

    public static void main(String] args) throws Exception {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Scheduler.getInstance().run();
            }
        }, 0, 20);

        CommandGroup auton = new CommandGroup();

        DriveTrajectoryCommand drive = new DriveTrajectoryCommand(250);

        auton.addSequential(CGUtils.parallel(
                drive,
                new QueueCommand(
                        new ElevatorPositionCommand("up"),
                        () -> drive.isAlmostFinished(100)
                )
        ));
        auton.addSequential(new IntakeCommand("Out"));
        auton.addSequential(CGUtils.parallel(
                new ElevatorPositionCommand("down")
               // new DriveTrajectoryCommand(50)
        ));

        auton.start();
    }

    public static class Drivetrain extends Subsystem {

        @Override
        protected void initDefaultCommand() {

        }

    }

    public static class Elevator extends Subsystem {

        @Override
        protected void initDefaultCommand() {

        }

    }

    public static class Intake extends Subsystem {

        @Override
        protected void initDefaultCommand() {

        }

    }

    public static class TickCommand extends Command {

        private int tick;
        private int targetTick;

        private String name;

        public TickCommand(int targetTick) {
            this(targetTick, "Base");
        }

        public TickCommand(int targetTick, String name) {
            this.tick = 0;
            this.targetTick = targetTick;

            this.name = name;
        }

        @Override
        protected void initialize() {
            System.out.println(this.getClass().getSimpleName() + ":" + this.name + " initialized @ " + System.currentTimeMillis());
        }

        @Override
        protected void end() {
            System.out.println(this.getClass().getSimpleName() + ":" + this.name + " ended @ " + System.currentTimeMillis() + ", ticksLeft = " + (this.targetTick - this.tick));
        }

        @Override
        protected void interrupted() {
            System.out.println(this.getClass().getSimpleName() + ":" + this.name + " interrupted @ " + System.currentTimeMillis() + ", ticksLeft = " + (this.targetTick - this.tick));
        }

        @Override
        protected void execute() {
            this.tick++;
        }

        @Override
        protected boolean isFinished() {
            return this.tick >= this.targetTick;
        }

        public boolean isAlmostFinished(int ticksLeft) {
            return this.tick + ticksLeft >= this.targetTick;
        }

    }

    public static class DriveTrajectoryCommand extends TickCommand {

        public DriveTrajectoryCommand(int ticks) {
            super(ticks);

            this.requires(SubsystemTest.drivetrain);
        }

        @Override
        protected void interrupted() {
            super.interrupted();
        }

    }

    public static class ElevatorPositionCommand extends TickCommand {

        public ElevatorPositionCommand(String name) {
            super(75, name);

            this.requires(SubsystemTest.elevator);
        }

    }

    public static class IntakeCommand extends TickCommand {

        public IntakeCommand(String name) {
            super(1, name);

            this.requires(SubsystemTest.intake);
        }

    }

    public static class WaitCommand extends TickCommand {

        public WaitCommand(int ticks) {
            super(ticks);
        }

    }

}

CGUtils:


public class CGUtils {

    public static CommandGroup sequential(Command... commands) {
        CommandGroup commandGroup = new CommandGroup();

        for (Command command : commands) {
            commandGroup.addSequential(command);
        }

        return commandGroup;
    }

    public static CommandGroup parallel(Command... commands) {
        CommandGroup commandGroup = new CommandGroup();

        for (Command command : commands) {
            commandGroup.addParallel(command);
        }

        return commandGroup;
    }

}

It is difficult to be sure without seeing all the code but I think this is due to a difference in the two command group’s subsystem requirements. I am going to assume that you have separate drive, elevator and intake subsystems and that the various commands (not the groups) require the obvious subsystem.

The first command group will require only the drive and intake subsystems. It collects these from the commands that are added to it, the DriveTrajectoryCommand and the IntakeCommand. It does not know about the ElevatorPositionCommand since it is never added to the command group directly; therefore, the group does not require the elevator subsystem. Everything works because when the QueueCommand starts the elevator command, it does not conflict with the requirements of the command group and both the group and this completely independent elevator command, from the point of view of the scheduler, can run at the same time.

The second command group will require all three subsystems due to the second ElevatorPositionCommand be added to it directly. Therefore, when the first elevator command is started by the QueueCommand, it conflicts with the requirements of the command group and the command group is cancelled in favor of this new independent, from the point of view of the scheduler, elevator command.

As to how to fix this, it is even more difficult to say as I am not in front of my computer that has wpilib installed. Could you put the second elevator movement in another QueueCommand that waits on the second drive command to just start. This would keep the second command group from requiring the elevator subsystem.

Steve

It indeed seems to have been an issue with subsystem requirements. What confuses me is why DriveTrajectoryCommand would be interrupted, even though it depends on a completely different subsystem that is never used after the initial command group.

After having QueueCommand require all child subsystems in its constructor and clearing the inner command’s requirements, everything seems to work as expected.

In case anyone would like to use the command, I’ll post the code below.


import edu.wpi.first.wpilibj.command.Command;
import edu.wpi.first.wpilibj.command.Subsystem;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Vector;

public class QueueCommand extends Command {

    private Command command;
    private QueueCommandConditional conditional;

    private boolean executed;

    public QueueCommand(Command command, QueueCommandConditional conditional) {
        if (command == null) {
            throw new NullPointerException("Command cannot be null.");
        }

        this.command = command;
        this.conditional = conditional;

        try {
            Field requirementsField = Command.class.getDeclaredField("m_requirements");
            requirementsField.setAccessible(true);

            Object requirements = requirementsField.get(command);

            Field setField = requirements.getClass().getDeclaredField("m_set");
            setField.setAccessible(true);

            Vector subsystems = (Vector) setField.get(requirements);

            for (Object subsystem : subsystems) {
                this.requires((Subsystem) subsystem);
            }
        } catch (Exception e) {
            e.printStackTrace();

            throw new RuntimeException("Cannot retrieve " + command.getClass().getName() + "'s dependencies.");
        }
    }

    @Override
    protected void initialize() {
        this.executed = false;
    }

    @Override
    protected void execute() {
        if (this.conditional.isTrue() && !this.executed) {
            try {
                Method clearRequirements = Command.class.getDeclaredMethod("clearRequirements");

                clearRequirements.setAccessible(true);
                clearRequirements.invoke(this.command);
            } catch (Exception e) {
                e.printStackTrace();

                throw new RuntimeException("Cannot clear " + this.command.getClass().getName() + "'s requirements.");
            }

            this.command.start();
            this.executed = true;
        }
    }

    @Override
    protected boolean isFinished() {
        return this.executed && !this.command.isRunning();
    }

    @Override
    protected void end() {
        if (this.command.isRunning()) {
            this.command.cancel();
        }
    }

    @Override
    protected void interrupted() {
        this.end();
    }

    public interface QueueCommandConditional {

        public boolean isTrue();

    }

}

Initially I had a very similar Command, but encountered issues with required subsystems (some important methods for subclassing/creating your own CommandGroup are package private). After some thought, we handled this by nesting CommandGroups:


CommandGroup cmd = new CommandGroup();

DynamicPathCommand path = buildPath();

CommandGroup riseUp = new CommandGroup();
if (path.duration() > 4) {
    riseUp.addSequential(new TimedCommand(path.duration() - 4));
}
riseUp.addSequential(new MoveCollectorToScale());
cmd.addParallel(riseUp);
cmd.addSequential(new EjectCube());

cmd.start();

My team was thinking about how to raise the elevator when it’s near the end of the path and we found out that you can actually addParallel a CommandGroup. In the second CommandGroup, I put a WaitCommand at the start for a set duration and then add other commands that I want it to do.

It seems to work perfectly for us. Hope that helps. Cheers.

Just a minor point - your QueueCommandConditional interface is unnecessary, you could just use a BooleanSupplier.