PID Help

We can’t figure out what we’re doing wrong here. The goal here is to take the x value from the Limelight-generated network table, convert it to an angle, and create a PID turn-to-angle using the imu. We’re getting several syntax errors that we can’t seem to resolve.

  1. Because we don’t start with any previous error for our D term, we want the first time the loop runs to calculate the D_gain to be zero, so we want “last_error” to start at the same angle we’re at. So we put it in “initialize” and used it in “execute”…but this makes the double in initialize unused, and the same double used in execute and IsFinished a syntax error.

  2. We don’t want to get a new targetAngle from the Limelight every time the loop runs, we want to get it once at the beginning of the command, convert it from pixels to degrees, and then use it in math. Same attempt as above, same errors created. (Hey, at least it’s consistent…)

  3. Every time the loop runs, we want I_gain to update itself as I_gain = I_gain + error, but it doesn’t seem to like this at all, and if I comment out the other syntax errors, this one appears.

package frc.robot.commands;

import java.util.function.DoubleSupplier;

import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.wpilibj2.command.CommandBase;
import frc.robot.Constants;
import frc.robot.subsystems.DriveTrain;

public class Turn_Test extends CommandBase {
  private final DriveTrain m_drivetrain;
  private static final double turnkP = 0.005; // proportionality constant
  private static final double turnkI = 0;
  private static final double turnkD = 0;
  private static final double turnkF = 0;
  private static final double DeadBand = 1; //deadband range
  private static final double OffSet = 0; //Aim Offset for Limelight
  private static final double BaseSpeed = 0; // speed before PID modification

  public Turn_Test(DoubleSupplier leftspeed, DoubleSupplier rightspeed, DriveTrain driveTrain) {
    m_drivetrain = driveTrain;
    addRequirements(m_drivetrain);
  }

  // Called when the command is initially scheduled.
  @Override
  public void initialize() {
    last_error = DriveTrain.imu.getAngle();
    float tx = (float) NetworkTableInstance.getDefault().getTable("limelight").getEntry("tx").getDouble(0);
    double targetAngle = tx*Constants.Pixels_To_Degrees+OffSet;
  }

  // Called every time the scheduler runs while the command is scheduled.
  @Override
  public void execute() {
    
   
    double error = DriveTrain.imu.getAngle() - targetAngle;
    double P_gain = error;
    double I_gain = I_gain + error;
    double D_gain = error-last_error;

    double rightspeed = BaseSpeed + P_gain*turnkP + I_gain*turnkI + D_gain*turnkD;
    double leftspeed = -rightspeed;

    m_drivetrain.drive(leftspeed, rightspeed);
    error = last_error;
  }

  // Called once the command ends or is interrupted.
  @Override
  public void end(boolean interrupted) {
  }

  // Returns true when the command should end.
  @Override
  public boolean isFinished() {
    return Math.abs(error) <= targetAngle - DeadBand;
  }
}

For the record, we have no experience using the integral PID in the talons to set up a PID loop based on the IMU sensor–that would probably be ideal, but we have no idea what that code would look like, and the Phoenix Documentation example code looks like a bunch of code spaghetti to us for this purpose. We have never used motion profiling or motion magic, and don’t know where to even get started; at this point we’ll be happy with a home-brew PID that turns us to the angle we want to turn to.

We have other turn-to-target code that works reasonably well, but not quite well enough, that is modeled after the sample code on the Limelight website. It looks like this:

package frc.robot.commands;

import edu.wpi.first.wpilibj2.command.CommandBase;
import frc.robot.Constants;
import frc.robot.subsystems.DriveTrain;
import frc.robot.subsystems.Limelight;

public class RotateToTarget extends CommandBase {
  private final DriveTrain driveTrain;
  private final Limelight limelight;
  private static final double kP_limelight = 0.002; // Patrick Added
  
  public RotateToTarget(DriveTrain driveTrain, Limelight limelight) {
    this.driveTrain = driveTrain;
    this.limelight = limelight;
  }

  // Called when the command is initially scheduled.
  @Override
  public void initialize() {
  }

  public double rotateSpeed = Constants.SEEK_SPEED;
  public double deadband = Constants.DEADBAND_RANGE;

  // Called every time the scheduler runs while the command is scheduled.
  @Override
  public void execute() {
    double target = limelight.aimbot();
    //rotate left if aimbot is negative
    /* PATRICK COMMENTED THIS OUT TO TRY A kP + MinValue Adjust Instead
    if(target < deadband * -1)
      driveTrain.drive(rotateSpeed, -rotateSpeed);
    else if (target > deadband)
      driveTrain.drive(-rotateSpeed, rotateSpeed);
  } */

  //Patrick Added Stuff Starts Here
    double leftspeed = 0.0;
    double rightspeed = 0.0;
    double MinVal_limelight = Constants.MinVal_limelight;
    
    if (target < -deadband) {
      leftspeed = (rotateSpeed - target)*kP_limelight;
      rightspeed = (target - rotateSpeed)*kP_limelight;
      if (leftspeed < MinVal_limelight) {
        leftspeed = MinVal_limelight;
      }
        if (rightspeed > -MinVal_limelight) {
          rightspeed = -MinVal_limelight;
        }
      driveTrain.drive(leftspeed, rightspeed);
    }
  
    else if (target > deadband) {
      leftspeed = (target - rotateSpeed)*kP_limelight;    
      rightspeed = (rotateSpeed - target)*kP_limelight;   
      if (leftspeed > -MinVal_limelight) {
        leftspeed = -MinVal_limelight;
      }
        if (rightspeed < MinVal_limelight) {
          rightspeed = MinVal_limelight;
        }
      driveTrain.drive(leftspeed, rightspeed); 
    }
    //Patrick Added Stuff Ends Here
  }

  // Called once the command ends or is interrupted.
  @Override
  public void end(boolean interrupted) {
    driveTrain.drive(0, 0);
  }

  // Returns true when the command should end.
  @Override
  public boolean isFinished() {
    double target = limelight.aimbot();
    //if the target is negative, convert to a positive number.
    if(target < 0)
      target = target * -1;
    //return true if target is less than the deadband
    return target < deadband;
  }
}

It’s just not quite good enough.

Any help you could give would be most appreciated–we’re 98% there to a really killer robot, and this turn consistency issue on our aiming is one of the last things we need to hunt down.

Inconsistent? Too slow? I assume you’ve already tuned the deadband width and minval as low as you can, with no dice. There’s no clear compensation for robot inertia, so I can see how a minval high enough to get movement would be too strong for fine tuning.

Are there encoders on your drive, or is the IMU your only feedback? If there are encoders you may be able to write a position-based rather than velocity-based drive command that could help.

Asking obvious questions to bound the problem & generate traffic because I’m curious what the answers to your questions are when the programmers get to this thread :slight_smile:

Drive has encoders.

We have experimented with good success on hard to tune systems on our diff swerve and found that on your P component instead of linear function, you might try a square or cubed function. Make sure you keep the sign if you are using a square function. You have to reduce your gain significantly. If you have too much friction, then a square root or cube root function sometimes helps with that.

I’m sure someone can explain the math. And someone else can tell you not to do what I just said. But we have found some success and it might be worth experimenting with.

One other piece of advice that I’m not sure you are doing but set the talons on a velocity following PID and tune that. Then your external PID loop should get command angle from limelight, feedback from gyro, create a P loop which sends velocity command to your talons. Essentially setup what is called a cascading PID loop. Here also, the external P loop can benefit from a square or cube function of the error instead of just a linear function.

We’ve had some success doing this with some pretty similar PID code.

The biggest difference I can spot is that we update our target angle each iteration to be the angle offset of the limelight + the current gyro angle. Then use the gyro angle as your input.

I wasn’t involved much in the development, but I believe this was because the limelight reading can bounce around a bit. So rather than bank on a single reading you converge to a set of readings over time.

Here’s our PID class. It’s similar to the example on this page, but that example had a few problems for us.

package frc.robot;

public class PID {
	public PID(double P, double I, double D) {
		this.P = P;
		this.I = I;
		this.D = D;
	}

	double P, I, D;
	double previous_error, integral = 0;
	double setpoint = 0;
	double error;
	private double output;

	public void calculate(double actual) {
		error = setpoint - actual; // Error = Target - Actual
		this.integral += (error * .02); // Integral is increased by the error*time (which is .02 seconds using normal
		final double derivative = (error - this.previous_error) / .02;
		this.output = P * error + I * this.integral + D * derivative;
		this.previous_error = error;
	}

	public void setSetpoint(double setpoint) {
		this.setpoint = setpoint;
	}

	public double getOutput() {
		return output;
	}
}

Don’t have a limelight but what we found with our vision system is that you need to account for the frame rate of vision in relation to the “periodic” time of the robot. Otherwise, you may actually be feeding an inaccurate measurement into the PID loop.

Can’t speak to syntax errors, but from my understanding, tx already comes as an angle in degrees (as opposed to pixels) from the limelight, with all conversions applied. This squares with the documentation:

tx | Horizontal Offset From Crosshair To Target (LL1: -27 degrees to 27 degrees | LL2: -29.8 to 29.8 degrees)

Correct me if I’m reading this wrong, but it looks like tx is being treated as though it’s coming in pixels instead of degrees. This might cause issues down the line if your converted targetAngle value doesn’t match the units/scaling of your DriveTrain.imu.getAngle() value.

Our main issue at the moment is getting the code to work at all. It seems very strange to me that the variables set up in initialize aren’t passing through to execute.

I’m not seeing a declaration for last_error. I’d expect it at the instance level of Turn_Test if you intend to use it in initialize and execute. It’s certainly a syntax error as is. In the future, supplying the compile error will help others diagnose this kind of error if it’s not obvious on face.

Are there other compile errors you want to show us?

I didn’t even try to compile it because I knew it wouldn’t. I’ll dig into that tomorrow if I get a chance, thanks.

Sure thing. From your description (double is unused in initialize) I’m guessing instead of making a member variable (private double last_error; with those other static finals) someone just declared it inside initialize, and maybe also in execute. That’d make local variables which then aren’t available in other methods. If your team is pushing to a public git repo I’d be happy to take a look.

I thought about my solution more and realized I was about to tell you to write a motion profile for a set turn-to-angle using the distance traveled on one side or the other, so that your power applied would be shaped to overcome stiction then compensate for inertia on the other end, but you explicitly asked for not motion profiling.

Nonetheless If you have a way to tell your drive to move a set number of ticks rather than a speed, you might find it more controllable that way for this kind of maneuver.

I suspect the other posters are correct that you want to update the limelight reading regularly to hack around the high frequency noise (“jumping”) in that signal - or write a smoothed output (moving average) into your Limelight code object?

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.