Command Based - Programming a Subsystem with Two Pistons

Consider a subsystem which utilizes two pistons. For example in this thread, let’s consider a psuedo 3-position piston which is constructed by mounting two pistons together at 180 degrees. Without loss of generality, let’s name these pistons topPiston and bottomPiston. Each of these pistons are connected to double solenoid valves.

Note: Technically, this system then has 4 possible states:

  1. topPiston=reverse, bottomPiston=reverse
  2. topPiston=reverse, bottomPiston=forward
  3. topPiston=forward, bottomPiston=reverse
  4. topPiston=forward, bottomPiston=forward

So we might think to program our subsystem as follows:


public class ThreePositionPiston extends Subsystem {

    private DoubleSolenoid topPiston, bottomPiston;
    
    public ThreePositionPiston() {
    	super();
    	
    	topPiston = new DoubleSolenoid(RobotMap.SOLENOID_3_POSITION_TOP_FORWARD,
    			                       RobotMap.SOLENOID_3_POSITION_TOP_REVERSE);
    	bottomPiston = new DoubleSolenoid(RobotMap.SOLENOID_3_POSITION_BOTTOM_FORWARD,
    			                          RobotMap.SOLENOID_3_POSITION_BOTTOM_REVERSE);

        // Start with both pistons retracted
        topPiston.set(Value.kReverse);
        bottomPiston.set(Value.kReverse);
    }

    public void initDefaultCommand() {
        // Setting this is part of the question, See below
        //setDefaultCommand(new MySpecialCommand());
    }
    
    public void extendTopPiston() {
    	topPiston.set(Value.kForward);   
    }
    
    public void retractTopPiston() {
    	topPiston.set(Value.kReverse);
    }
    
    public void extendBottomPiston() {
    	bottomPiston.set(Value.kForward);
    }
    
    public void retractBottomPiston() {
    	bottomPiston.set(Value.kReverse);
    }
}

Now is where things get a little messy. Typically, when I write a subsystem that contains a single piston, I can simply write two commands:

  1. SubsytemExtendPiston
  2. SubsystemRetractPiston

I can then bind these commands to buttons and I am happy with life. With two pistons it is less clear if I can just write a command for each of the four states.

  1. ThreePositionPostionExtendTop
  2. ThreePositionPostionRetractTop
  3. ThreePositionPostionExtendBottom
  4. ThreePositionPostionRetractBottom

For example, here is how we could write ThreePositionPostionExtendTop:


public class ThreePositionPistonExtendTop extends Command {

    public ThreePositionPistonExtendTop() {
        requires(Robot.threePositionPiston);
    }

    protected void initialize() {}

    // Not really sure if it needs to be repeatedly called OR can just be called in initialize
    protected void execute() {
    	Robot.threePositionPiston.extendTopPiston();
    }

    protected boolean isFinished() {
        return false;
    }

    protected void end() {}

    protected void interrupted() {}
}

The thing is, when I enter this command, I am no longer doing anything with the bottom piston. Will the bottom piston maintain it’s previous state or do I need to do something to update this? Also, do I need to repeatedly call extend or can I just call it once in initialize and get the same behavior? I guess this question is almost turning into, what is a good way to structure complex pneumatic systems (defined as those containing more than one solenoid) in Command Base?

The other solenoid should maintain its state, otherwise you can look into using an enum type and a switch statement. This would let you have one method that switches on the enum and would be explicitly more laid out if you feel the current code is confusing.

https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html

Yes, you certainly can. You recognize that with this list of commands you then need to setup a command group any time both positions require any change (including when both might require a change).

The alternative option which avoids* command groups is to add arguments into one Command for this same subsystem.

  • Well now command groups can be focused on multi-subsystem purposes.

public class PistonMover extends Command {

	Value moveTop;
	Value moveBottom;
    public PistonMover(Value moveTopParam, Value moveBottomParam) {
    	moveTop = moveTopParam;
    	moveBottom = moveBottomParam;
    	requires(Robot.threePositionPiston);
    }

    // Called just before this Command runs the first time
    protected void initialize() {
       	Robot.threePositionPiston.setTopPiston(moveTop);
       	Robot.threePositionPiston.setBottomiston(moveBottom);
    }

Then OI, and/or Auton, can reference like this…

// one button...
new PistonMover( Value.kForward, Value.kReverse );
// other button...
new PistonMover( Value.kReverse, Value.kForward );

Yes, you certainly can. You recognize that with this list of commands you then need to setup a command group any time both positions require any change (including when both might require a change).

Their is an alternative option which shortens how many command-classes are needed and also avoids CommandGroup requirements except for multi-subsystem purposes (when they are most useful!).


public class PistonMover extends Command {

	Value moveTop;
	Value moveBottom;
    public PistonMover(Value moveTopParam, Value moveBottomParam) {
    	moveTop = moveTopParam;
    	moveBottom = moveBottomParam;
    	requires(Robot.threePositionPiston);
    }

    // Called just before this Command runs the first time
    protected void initialize() {
       	Robot.threePositionPiston.setTopPiston(moveTop);
       	Robot.threePositionPiston.setBottomiston(moveBottom);
    }

Then OI, and/or Auton, can reference like this…

// one button...
new PistonMover( Value.kForward, Value.kReverse );
// other button...
new PistonMover( Value.kReverse, Value.kForward );

Their is an alternative option which shortens how many command-classes are needed and also avoids CommandGroup requirements except for multi-subsystem purposes (when they are most useful!).


public class PistonMover extends Command {

	Value moveTop;
	Value moveBottom;
    public PistonMover(Value moveTopParam, Value moveBottomParam) {
    	moveTop = moveTopParam;
    	moveBottom = moveBottomParam;
    	requires(Robot.threePositionPiston);
    }

    // Called just before this Command runs the first time
    protected void initialize() {
       	Robot.threePositionPiston.setTopPiston(moveTop);
       	Robot.threePositionPiston.setBottomiston(moveBottom);
    }

Then OI, and/or Auton, can reference like this…

// one button...
new PistonMover( Value.kForward, Value.kReverse );
// other button...
new PistonMover( Value.kReverse, Value.kForward );

Note that when you combine BigJ’s idea with mine, you no longer need to import doubleSolenoid.value into OI and Auton code files.

public class PistonMover extends Command {
	private Value moveTop;
	private Value moveBottom;
	public enum Positions {  InIn, InOut, OutIn, OutOut };

	public PistonMover(Positions position) {
		// switch code to set movetop & movebottom...
    	requires(Robot.threePositionPiston);
    }

I like it. Very clean and simple. Thanks!

I am not sure if I can describe this succinctly enough here, but here it goes…

I would take it one step further. The commands should be oblivious to the fact that the subsystem contains pistons. An enum value should be what is passed to the subsystem’s “go to position api” and it should interpret what that means in terms of pistons or whatever.

Plus I would change the enum value names to not reflect piston positions but rather the objective of using the subsystem. For example, enum value names like stow, carry, pickup and score for an arm that has different positions for being stowed, carrying a game piece, picking up a game piece from the floor or scoring the piece. That way, when the subsystem changes (think about this going on while iterating on design during build season), the command stays the same (unless the number or meaning of the enum values change) while the subsystem changes how it accomplishes the goals.

An alternative api design would be to have a different method on the subsystem for each of what are now enum values and the enum disappears.

In either api design, the command’s execute method keeps telling the subsystem where to get the mechanism to and the isFinished method uses other subsystem api to see if it has gotten to that position yet, and if so, returns true. Now that assumes some sort of sensory feedback on the state of the mechanism. In the case of these pistons, you may not have that. You could then simply use a timed command where the time would be empirically discovered.

Some food for thought,
Steve