Trying to decipher how code works

Hello, I am trying to go through and understand how every piece of code works in our program so I can better explain it to next years programmer since I am leaving. However I am stuck on the drivetrain and default drive commands. This year we used swerve for the first time so I used the SDS Template to get started, and never really had to mess with the default command at all so I never understood it. This is our current build where I’ve tried commenting through everything, but still not sure exactly how the default drive command works. To try to understand it, I used the same method on a tank drive program here. It started making a bit more sense but ultimately I was lost. So if anyone can decipher what is happening, any explanation is greatly appreciated. Thank you

1 Like

After rereading my post, I realized it is a bit vague in what I know and don’t know. I believe the defaultdrive command uses the constructor method so get values for the left and right motors alongside which drivetrainsubsystem it will be using. Not sure why you can’t hard code the subsystem since I have never seen a robot with 2 drivetrain subsystems before. It then passes those values down to the execute which gets the supplier values as doubles and then sets those into the drive command. But that is the conceptual stuff, and I do not understand how the actual Suppliers work and how they are different to normal doubles. Why does it need to run the .getasDouble command, and within the robotcontainer, what does the () → - do.

1 Like

Your comment is a good starting point. There are three main things going on here:

  • We keep all the joystick stuff in RobotContainer and deal with “double suppliers” instead as a form of abstraction. We could use different joysticks, or even an AI without changing this code at all. This is a good thing and makes your code break less, because it makes fewer assumptions.
  • You’re using “field-centric control”. This means that X is away from your alliance wall and Y is to the driver’s left, regardless of which way the robot is pointing. That’s very convenient for the driver, but less so for the robot. ChassisSpeeds.fromFieldRelativeSpeeds does the conversion from field-centric to robot-centric for you, using the robot’s current heading from the gyroscope.
  • Finally you can pass the robot-centric co-ordinates to the drive subsystem.

Within the drive subsystem, these will be converted into instructions for the four swerve drive modules, depending on their orientation relative to the robot.

1 Like

Oh okay, that makes a bit more sense. I guess now what I am confused about is just Java itself. I’ll use the tankdrive version I mocked up since it is the same concept but with less things. So lets start in the DefaultDriveCommand . I understand how the variables get set to whatever is plugged into the function and “this.” refers to that classes variables. It then takes those values and plugs them into the drive command. My question is what purpose does the DoubleSuppliers serve? From what I have read they are good for changing values and return a double using the .getasDouble function, but I just don’t understand them enough to be able to explain this code to others. Next and probably the most confusing to me is the the RobotContainer. I got multiple questions so i’ll try to do some cool formatting to make it easier to read.

  1. I learned everything from looking at previous years robot code and they all created subsystems in the Robot.java file. But why does everything put the drivetrain Subsystem in the RobotContainer and does it really matter?

Also I tested putting the constructor for the new subsystem in Robot.java then just creating an instance of it by doing DrivetrainSubsystem drivetrainSubsystem; at the top and it didn’t have any red lines. Would that work?

Actually, I just thought about it and a similar thing is done when creating motors. I set the ID and create the motor itself in Constants, but in each subsystem, I have to make a new variable for each motor and set it to the value of the one from constants. Why is that?

  1. The main issue im having is im confused about () → -. I found a video on Suppliers in java that said it was a lambda thing but it was poorly explained. What does it do exactly? and why is it needed alongside the DoubleSuppliers?

Thank you

OK. I’ll do my best to explain. :slight_smile: Never stop asking questions!

RobotContainer was invented around 2019 (?), and it split off a lot of robot-specific stuff (subsystems, commands, joysticks/buttons) from the robot-independent code. I think that it’s technically optional, but it’s considered best practice to have it. That explains why older code might not have it, or might be using it wrongly. See Structuring a Command-Based Robot Project for more.

The constants file is a good place to gather “magic numbers” such as CAN bus ids, PID controller configuration, and any other values you use in calculations. Again, it’s technically optional, but here are a number of reasons to do this including:

  • It forces you to name your magic numbers.
  • You can use the same value in two different code files.
  • You can track changes to constants separately from code changes.
  • It’s good to be able see all your port numbers (e.g. CAN bus id, DIO) in one place.
  • If your team builds two robots, you can manage which constants change between robots.

One thing you should not do is create any objects in the constants file. For example,

public static CANSparkMax ShootMotor1 = new CANSparkMax(Constants.SHOOTER_MOTOR_1, MotorType.kBrushless);

belongs in the subsystem class.

DoubleSupplier is a “functional interface”. That means it is an interface (and hence can be used as a type and has methods you can call) that also behaves like a function. One important aspect of behaving like a function is that you can instantiate the interface using a function, specifically a lambda function.

A lambda function like:

() -> -modifyAxis(rightJoy.getX()) * DrivetrainSubsystem.MAX_VELOCITY_METERS_PER_SECOND`

creates a function that returns the X value on a specific joystick (appropriately adjusted). This function secretly carries with it a binding to the private rightJoy variable, and hence can be called without knowing anything about the joysticks or where the information comes from.

When you pass such a lambda function as a DoubleSupplier, you’re passing an object that can be used to get the value of that function whenever you want and in any context. When the default drive command wants to know what to do, it calls this function (using getAsDouble), which reads the joystick. So the robot is being driven by the joystick, but the command has no idea where the numbers are coming from. (For example, I’ve seen a set up that had two drive joysticks with different buttons, and the default drive command used the sum of the two joysticks, so either one could be used to drive the robot.)

3 Likes

Thanks for taking the time to go through this.

So starting from the top.

Guess I should go through my code and bring all of those motor constructors into their respective subsystem. Seems to work just fine but if anything cleans up Constants and keeps its purpose better. I’ll try to adopt that organizational method.

As for where to put the drivetrain subsystem constructor, guess it works in a similar fashion and should just keep it in Robotcontainer since that is where it will be referenced the most.

Then the fun lambda function and DoubleSuppliers. Your explanation helped a lot. You mentions that the lamda “binds” to the rightJoy variable. So that means it will constantly read the value of the joystick and return it. Then that value goes through the .getasDouble function which then feeds into the drive command. It makes sense, but I do wonder why do this when you could just pass the getX and getY value directly into the drive command. I assume it has its benefits, but to me it just seems needlessly complex for something you could just do

public void initDefaultCommand() {
setDefaultCommand(new TankDrive());
rightSlave.follow(rightMotor);
leftSlave.follow(leftMotor);
}

public void drive() {
  leftMotor.set(-Robot.m_oi.getLeftJoyY()); 
  rightMotor.set(Robot.m_oi.getRightJoyY());

}

Which I got from one of our previous years repositories.

Practice has changed a lot over the years, which is why you have to be a little careful about old code. FIRST used to recommend setting the default command in the subsystem, but now recommends doing it in RobotContainer. One reason for this is that the old way essentially required the subsystem to know about joysticks, which made the code more fragile and less reusable. DoubleSuppliers and lambda functions only arrived in Java 8 (which was released in 2014, but FIRST waited a few years before forcing everyone to upgrade). Once you get your head around it, I think you’ll find it’s a better way.

So the code fragment you show has initDefaultCommand, which is now considered obsolete/bad practice. You should never have been configuring your followers in initDefaultCommand (and be aware that many people now choose to avoid using master/slave terminology in computer software). The drive method you show uses joysticks directly and hence could not be used from an autonomous command.

As a thought experiment, how would the different approaches support autonomous commands, two driver joysticks, switching between joysticks and game controllers, etc.?

One more thought: I recommend that programmers read the WPILIB documentation carefully, especially the changes from year to year. Remember that the new software is released significantly in advance of kick-off day, so you don’t need to do this learning during build season. You can upgrade last year’s code and test out new features on last year’s robot.

1 Like

Makes sense a lot of code has been made obsolete, so I guess this paste wasn’t the best example so I’ll just type it out myself.

public void drive(Double leftOutput, Double rightOutput) {
leftMotors.set(leftOutput);
rightMotors.set(rightOutput);
}

Then in the robot container you could do something like

m_drivetrainSubsystem.setDefaultCommand(new drive(rightJoy.gety, leftJoy.getY) {
}

Thinking something like this would be a lot simplier cuase you are plugging the joystick values directly into the drive command.

As for your thought experiments

Auto would just call the drive command with something like drive(.5, .5); and make the command end on a timer or something like I have in all my auto commands from the swerve build.

Since I am doing these examples for the tank drive version i made, it already uses 2 joysticks, but you would just change the rightJoy to leftJoy in the default drive command to change what joysticks give the values. Originally I did have the leftJoy do y and x values while the rightJoy only did z axix, but later we decided all on one was easier to control.

Switching between joystick and game controllers is also pretty easy, just gotta find the ID of the axis you want on the gamepad since there are 2 joysticks on it, and then do something like gamepad.getRawAxis(3); or something.

You’re writing this like a command object is just a function getting run repeatedly. Unfortunately you can’t directly use new with a function, but the good news is that there is a RunCommand(Runnable runnable, Subsystem… requirements), and a Runnable is just a function with no parameters and no outputs that is masquerading as an object. You can use the following syntax:

m_drivetrainSubsystem.setDefaultCommand(new RunCommand(()->{
    m_drivetrainSubsystem.drive(rightJoy.getY(), leftJoy.getY());
},
m_drivetrainSubsystem
);

What happens here is that you declare a function directly in your RunCommand constructor. This function has no parameters and no return, but when run, it calls drive() with the joystick values. Because this function matches the parameter list and return type of Runnable, you can use it in the RunCommand constructor directly, which will give you a command that just runs that Runnable every loop while the command is active.

The other thing in that RunCommand constructor is the drivetrain subsystem. This means that the drivetrain subsystem should be “required” by the command, so that only one command that requires a specific subsystem can run at a time.

2 Likes

True I did forget to turn the function into a command cause I was rushing through the example. Trying to think of a way to make it into a command. So I would probably put the drive(rightJoy.getAxis… and put that into the execute part in a command. Then in the setdefaultcommand, just run the new command, lets call it DefaultDrive, like this.

m_drivetrainSubsystem.setDefaultCommand(new DefaultDrive() {}

Conceptually this would work since the defaultDrive gets the joystick value and sets drive command with the values, all the robot needs to do it run the defaultdrive command constantly, which is achieved by setting it as the default command. Works very similar to how my limelight commands are setup cause they also use getY and getX within the command itself and requires no parameters since everything is set in the execute.

1 Like

You could absolutely write the command that does nothing but runs that function in execute(). But, you could also pass that function into the RunCommand, which does exactly the same thing, but with less typing and extra files on your part.

I will keep that method in mind, thank you for telling me about it.

1 Like

Just replying this here so I do not lose it. But I found a thread that has some explanations I may want to look into.

You may want to look at WPILIB’s DifferentialDrive.arcadeDrive, which is intended for just this sort of purpose, and handles the dead-band and sign-preserving square for you.

Oh didn’t know about that parameter. Also I have seen squaring the joystick values. I assumed it was for some Swerve drive math stuff, but apparently not so what does it do?

Most drivers find the robot more controllable if pushing the stick over halfway doesn’t bring the robot to half speed. When you do a (sign-preserving) square, then pushing the stick over halfway gives you quarter speed. Some drivers prefer a cube, but a square is most common. See the explanation here and the WPILIB code that does this.

Looking at your code again, I see you’re currently using tankDrive, which is fine if that’s what your drivers like, but I believe most teams use arcadeDrive or curvatureDrive. See the explanation here.

2 Likes

Just to clarify: You’re applying deadband, then calling tankDrive which applies deadband, so you’re applying deadband twice, which is bad mathematically, although probably hard to detect in practice.

As the documentation says:

Inputs smaller then 0.02 will be set to 0, and larger values will be scaled so that the full range is still used. This deadband value can be changed with RobotDriveBase.setDeadband(double).

2 Likes

Oh good to know that I do not need to make my own deadband. Also tank drive was just a quick and easy thing I knew how to make since our only working robot is a swerve drive rn, figured tank might work best for running on that confg. The squaring idea makes a lot more sense now. Slightly similar to how a mouse has mouse acceleration, so it moves slower at slow speeds and vise versa instead of a constant rate of change.

As for the duplicate deadbands, I removed the deadband function and in the modifyAxis function, I just pasted the copysin() from our swerve build. And I placed the .setDeadband function in the DrivetrainSubsystem method right below the constructor for the differenctial drive so I would assume that works.

Hmm, also it looks like there are two constructors for the tankdrive, or arcade drive, just depends which one I decide would work better. But all I need to do is put a true at the end of the list of parameters and I can completely remove the modifyAxis function? If so that makes my life a lot easier. So many built in functions I just never knew about but are very useful.

I’m a little confused here. If you have a swerve drive, then I don’t think you should be trying to use DifferentialDrive at all. Have you looked into Swerve Drive Kinematics and Swerve Drive Odometry? If you’re using COTS swerve modules, they typically come with associated code.

We have fully functional swerve code, however I am just trying to learn as much of WPI as I can while on spring break and have no access to our shop, so I made a quick and easy Tank Drive. Guess it would make more sense to go though and learn how the swerve works, guess that will be my next goal, building a swerve without the need of SDS libraries. Guess Zero To Autonomous has a pretty good video regarding that, so I might watch through that one. Tried it once but was having issues since they use AnalogInputs and we use CAN network CANCoders. I’ll go through everything a few times and try to get as far as I can and report back.