I am a new FRC programming mentor (I have experience with programming for FLL, FTC, and VEX EDR) with a programming team with no experience. We have implemented a command based robot using robot builder and Java.
On this year’s robot we have a rotating shoulder (a little over 180 degrees of motion). We use PID controls to move the shoulder to a set position. Sometimes, when we enable the robot (after a disable), the shoulder begins to move without a command being given – this has caused some catastrophic mechanical failures of our motor mount. We are currently working around this issue by powering off the robot every time we restart – which I wouldn’t think would be a good plan going into a tournament!
Our shoulder is powered by a bag motor attached to a Talon SRX. We have a command that we execute from shuffleboard (using a command widget) to move the shoulder to the proper height (hatch rocket low, hatch rocket mid, etc.). We also have a command that allows the operator to move the shoulder using a joystick – We rarely use this – the drive team quickly figured out that the program was far more efficient than they are. The shoulder sub-system does NOT have a default command. All of our encoder positions use values based on the ABSOLUTE position; however, our PID uses the relative value – we seed the relative value with the absolute value to take advantage of the much faster refresh rate of the relative position. Code for the shoulder sub-system, the command, and the encoder values we attempt to hit are shown below.
When this problem occurs on the actual robot, our shuffleboard shows that the relative and absolute encoders are not the same. My suspicion is that when we enable the robot, the Talon is always returning to the last “move to position” command that it had – we don’t ordinarily see it because the arm hasn’t moved – but when the absolute and relative encoders are no longer in sync the arm will move to an unachievable position.
Other Points
- Currently our command never ends (because I don’t know under what condition to actually end it) – it is simply interrupted by the next command requested on the shoulder sub-system.
- We have an identical setup on our wrist (this problem also occurs on the wrist but has yet to cause a catastrophic mechanical failure).
- Because of our gear ratios we are prone to moving past an absolute position of 0.
- I believe that this rogue movement is probably a newbie mistake – failing to initialize something in the right place, etc.
On our test system, we have a Talon SRX with no motor or encoder. This is what I see:
- Power On: Lights on Talon SRX are flashing orange
- Enable: Lights on Talon SRX are solid orange and then revert to flashing orange.
- Execute Command: Lights on Talon SRX are green (occasionally flash orange and then return to green.
- Disable: Lights on Talon SRX are flashing orange.
- Enable: Lights on Talon SRX are green (no command has been issued from shuffleboard)
- Power Off/On : Lights on Talon SRX are flashing orange
- Enable: Lights on Talon SRX are flashing orange.
THIS IS OUR SHOULDER SUBSYSTEM CODE:
// RobotBuilder Version: 2.0
//
// This file was generated by RobotBuilder. It contains sections of
// code that are automatically generated and assigned by robotbuilder.
// These sections will be updated in the future when you export to
// Java from RobotBuilder. Do not put any code or make any change in
// the blocks indicating autogenerated code or it will be lost on an
// update. Deleting the comments indicating the section will prevent
// it from being updated in the future.
package org.usfirst.frc5053.FRC2019DeepSpace.subsystems;
import org.usfirst.frc5053.FRC2019DeepSpace.Robot;
import org.usfirst.frc5053.FRC2019DeepSpace.commands.*;
import edu.wpi.first.wpilibj.livewindow.LiveWindow;
import edu.wpi.first.wpilibj.command.Subsystem;
import edu.wpi.first.wpilibj.PIDOutput;
import edu.wpi.first.wpilibj.PIDSource;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import com.ctre.phoenix.motorcontrol.ControlMode;
import com.ctre.phoenix.motorcontrol.FeedbackDevice;
import com.ctre.phoenix.motorcontrol.can.BaseMotorController;
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=IMPORTS
import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX;
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=IMPORTS
/**
*
*/
public class shoulderLift extends Subsystem {
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTANTS
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTANTS
final int kSlotIdx = 0;
final int kPIDLoopIdx = 0;
final int kTimeoutMs = 30;
boolean kSensorPhase = false;
boolean kMotorInvert = false;
final double kP = 8.0; // SNW 16.0 was good, except it started oscillating at the rocket high
final double kI = 0.0;
final double kD = 0.0;
final double kF = 0.0;
final int kIzone = 0;
final double kPeakOutput = 1.0;
final int encoderMariginOfError = 10;
final boolean kSoftLimitEnabled = true;
final int kSoftLimitForward = 3000;
final int kSoftLimitReverse = 100;
final boolean kEnableCurrentLimit = true;
final int kPeakCurrentLimit = 25; //amps`
final int kPeakCurrentDuration = 200; //milliseconds
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=DECLARATIONS
private WPI_TalonSRX shoulderTalonSRX;
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=DECLARATIONS
public shoulderLift() {
System.out.println("shoulderLift START");
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTRUCTORS
shoulderTalonSRX = new WPI_TalonSRX(2);
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTRUCTORS
SmartDashboard.putNumber("armHeightEnum", Robot.oi.INITIAL_AND_ERROR_BUTTON);
configureShoulder();
syncEncoderValues();
System.out.println("shoulderLift END");
}
@Override
public void initDefaultCommand() {
System.out.println("shoulderLift.initDefaultCommand START");
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=DEFAULT_COMMAND
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=DEFAULT_COMMAND
// Set the default command for a subsystem here.
// setDefaultCommand(new MySpecialCommand());
System.out.println("shoulderLift.initDefaultCommand END");
}
@Override
public void periodic() {
System.out.println("shoulderLift.periodic START");
// Put code here to be run every loop
int relPosition = shoulderTalonSRX.getSelectedSensorPosition(0);
int absPosition = shoulderTalonSRX.getSensorCollection().getPulseWidthPosition();
double shoulderSpeed = shoulderTalonSRX.get();
SmartDashboard.putNumber("ShoulderSensorPos: ", relPosition);
SmartDashboard.putNumber("ShoulderSensorAbs: ", absPosition);
SmartDashboard.putBoolean("shoulderSynced", isEncoderSynced());
SmartDashboard.putNumber("shoulderCurrent", shoulderTalonSRX.getOutputCurrent());
// SmartDashboard.putNumber("ShoulderSpeed: ", shoulderSpeed);
// SmartDashboard.putNumber("ShoulderJoystickAxis(1): ", Robot.oi.logitech.getRawAxis(1));
System.out.println("shoulderLift.periodic END");
}
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CMDPIDGETTERS
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CMDPIDGETTERS
// Put methods for controlling this subsystem
// here. Call these from Commands.
// 20190208 SNW begin added code
public void moveShoulder(double inputSpeed) {
System.out.println("shoulderLift.moveShoulder START");
shoulderTalonSRX.set(ControlMode.PercentOutput, inputSpeed);
System.out.println("shoulderLift.moveShoulder END");
}
public void stopShoulder(){
System.out.println("shoulderLift.stopShoulder START");
shoulderTalonSRX.set(ControlMode.PercentOutput, 0);
System.out.println("shoulderLift.stopShoulder END");
}
// 20190208 SNW end added code
public void startMoveToPosition(double targetPosition){
System.out.println("shoulderLift.startMoveToPosition START");
if (isEncoderSynced())
{
System.out.println("shoulderLift.startMoveToPosition ISSUE TALON COMMAND");
shoulderTalonSRX.set(ControlMode.Position, targetPosition);
SmartDashboard.putNumber("shoulderTargetPosition", shoulderTalonSRX.getClosedLoopTarget(0));
}
else
{
System.out.println("shoulderLift.startMoveToPosition OUT OF SYNC DO NOTHING");
SmartDashboard.putBoolean("shoulderSynced", false);
}
System.out.println("shoulderLift.startMoveToPosition END");
}
public void configureShoulder(){
System.out.println("shoulderLift.configureShoulder START");
// configure the shoulder encoder to have the soft limits of 100-3000
/* Config the sensor used for Primary PID and sensor direction */
shoulderTalonSRX.configSelectedFeedbackSensor(FeedbackDevice.CTRE_MagEncoder_Relative,
kPIDLoopIdx,
kTimeoutMs);
shoulderTalonSRX.setInverted(kMotorInvert);
/* Config the peak and nominal outputs, 12V means full */
shoulderTalonSRX.configNominalOutputForward(0, kTimeoutMs);
shoulderTalonSRX.configNominalOutputReverse(0, kTimeoutMs);
shoulderTalonSRX.configPeakOutputForward(1, kTimeoutMs);
shoulderTalonSRX.configPeakOutputReverse(-1, kTimeoutMs);
/**
* Config the allowable closed-loop error, Closed-Loop output will be
* neutral within this range. See Table in Section 17.2.1 for native
* units per rotation.
*/
shoulderTalonSRX.configAllowableClosedloopError(0, kPIDLoopIdx, kTimeoutMs);
/* Config Position Closed Loop gains in slot0, tsypically kF stays zero. */
shoulderTalonSRX.config_kF(kPIDLoopIdx, kF, kTimeoutMs);
shoulderTalonSRX.config_kP(kPIDLoopIdx, kP, kTimeoutMs);
shoulderTalonSRX.config_kI(kPIDLoopIdx, kI, kTimeoutMs);
shoulderTalonSRX.config_kD(kPIDLoopIdx, kD, kTimeoutMs);
// configures TalonSRX to prevent overrotation of the motor
shoulderTalonSRX.configForwardSoftLimitEnable(kSoftLimitEnabled);
shoulderTalonSRX.configForwardSoftLimitThreshold(kSoftLimitForward);
shoulderTalonSRX.configReverseSoftLimitEnable(kSoftLimitEnabled);
shoulderTalonSRX.configReverseSoftLimitThreshold(kSoftLimitReverse);
// enables current limits on the motor
shoulderTalonSRX.enableCurrentLimit(true);
shoulderTalonSRX.configPeakCurrentLimit(kPeakCurrentLimit);
shoulderTalonSRX.configPeakCurrentDuration(kPeakCurrentDuration);
System.out.println("shoulderLift.configureShoulder END");
}
public void syncEncoderValues(){
System.out.println("shoulderLift.syncEncoderValues START");
/**
* Grab the 360 degree position of the MagEncoder's absolute
* position, and intitally set the relative sensor to match.
*/
int absolutePosition = shoulderTalonSRX.getSensorCollection().getPulseWidthPosition();
/* Mask out overflows, keep bottom 12 bits */
absolutePosition &= 0xFFF;
if (kSensorPhase) { absolutePosition *= -1; }
if (kMotorInvert) { absolutePosition *= -1; }
/* Set the quadrature (relative) sensor to match absolute */
shoulderTalonSRX.setSelectedSensorPosition(absolutePosition, kPIDLoopIdx, kTimeoutMs);
System.out.println("shoulderLift.syncEncoderValues END");
}
public boolean isEncoderSynced()
{
System.out.println("shoulderLift.isEncoderSynced START");
/**
* Grab the 360 degree position of the MagEncoder's absolute
* position, and intitally set the relative sensor to match.
*/
int absolutePosition = shoulderTalonSRX.getSensorCollection().getPulseWidthPosition();
/* Mask out overflows, keep bottom 12 bits */
absolutePosition &= 0xFFF;
if (kSensorPhase) { absolutePosition *= -1; }
if (kMotorInvert) { absolutePosition *= -1; }
/* Set the quadrature (relative) sensor to match absolute */
int relativePosition= shoulderTalonSRX.getSelectedSensorPosition(kPIDLoopIdx);
if (relativePosition >= (absolutePosition-encoderMariginOfError) &&
relativePosition <= (absolutePosition+encoderMariginOfError))
{
System.out.println("shoulderLift.isEncoderSynced END: true");
return true;
}
else
{
System.out.println("shoulderLift.isEncoderSynced END: true");
return false;
}
}
}
THIS IS OUR COMMAND CODE
NOTE THIS COMMAND IS PART OF A GROUP COMMAND (BELOW)
// RobotBuilder Version: 2.0
//
// This file was generated by RobotBuilder. It contains sections of
// code that are automatically generated and assigned by robotbuilder.
// These sections will be updated in the future when you export to
// Java from RobotBuilder. Do not put any code or make any change in
// the blocks indicating autogenerated code or it will be lost on an
// update. Deleting the comments indicating the section will prevent
// it from being updated in the future.
package org.usfirst.frc5053.FRC2019DeepSpace.commands;
import edu.wpi.first.wpilibj.command.Command;
import org.usfirst.frc5053.FRC2019DeepSpace.Robot;
/**
*
*/
public class shoulderAutoPosition extends Command {
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=VARIABLE_DECLARATIONS
private double m_shoulderPosition;
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=VARIABLE_DECLARATIONS
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTRUCTOR
public shoulderAutoPosition(double shoulderPosition) {
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=CONSTRUCTOR
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=VARIABLE_SETTING
m_shoulderPosition = shoulderPosition;
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=VARIABLE_SETTING
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=REQUIRES
requires(Robot.shoulderLift);
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=REQUIRES
}
// Called just before this Command runs the first time
@Override
protected void initialize() {
}
// Called repeatedly when this Command is scheduled to run
@Override
protected void execute() {
Robot.shoulderLift.startMoveToPosition(m_shoulderPosition);
}
// Make this return true when this Command no longer needs to run execute()
@Override
protected boolean isFinished() {
return false;
// return true;
}
// Called once after isFinished returns true
@Override
protected void end() {
}
// Called when another command which requires one or more of the same
// subsystems is scheduled to run
@Override
protected void interrupted() {
}
}
THIS IS OUR GROUP COMMAND THAT GETS EXECUTED FROM SHUFFLEBOARD
// RobotBuilder Version: 2.0
//
// This file was generated by RobotBuilder. It contains sections of
// code that are automatically generated and assigned by robotbuilder.
// These sections will be updated in the future when you export to
// Java from RobotBuilder. Do not put any code or make any change in
// the blocks indicating autogenerated code or it will be lost on an
// update. Deleting the comments indicating the section will prevent
// it from being updated in the future.
package org.usfirst.frc5053.FRC2019DeepSpace.commands;
import edu.wpi.first.wpilibj.command.CommandGroup;
import org.usfirst.frc5053.FRC2019DeepSpace.subsystems.*;
/**
*
*/
public class operateArm extends CommandGroup {
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=PARAMETERS
public operateArm(double shoulderPosition, double wristPosition, double rightDistance, double leftDistance) {
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=PARAMETERS
// Add Commands here:
// e.g. addSequential(new Command1());
// addSequential(new Command2());
// these will run in order.
// To run multiple commands at the same time,
// use addParallel()
// e.g. addParallel(new Command1());
// addSequential(new Command2());
// Command1 and Command2 will run in parallel.
// A command group will require all of the subsystems that each member
// would require.
// e.g. if Command1 requires chassis, and Command2 requires arm,
// a CommandGroup containing them would require both the chassis and the
// arm.
// BEGIN AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=COMMAND_DECLARATIONS
//addParallel(new distanceAutoPosition(0, 0));
//addParallel(new shoulderAutoPosition(0));
//addParallel(new wristAutoPosition(0));
// END AUTOGENERATED CODE, SOURCE=ROBOTBUILDER ID=COMMAND_DECLARATIONS
/**********************
* Each time the JAVA is generated from robot builder, the automatically generated
* code does not include the parameters.
*
* After JAVA is generated from robot builder, comment out the addParallel
* commands in the AUTOGENERATED CODE ABOVE.
***********************/
addParallel(new distanceAutoPosition(rightDistance, leftDistance));
addParallel(new shoulderAutoPosition(shoulderPosition));
addParallel(new wristAutoPosition(wristPosition));
}
}
THESE ARE THE ABSOLUTE ENCODER POSITIONS (shoulder, wrist, right distance, left distance)
public double[][] operateArmParameters =
{
{0, 0, 0, 0}, // 0: Not Used
{671, 2421, 1, 2}, // 1: Cargo ==> Rocket High
{1321, 2239, 3, 4}, // 2: Cargo ==> Rocket Middle
{2096, 1749, 5, 6}, // 3: Cargo ==> Rocket Low
{537, 2864, 7, 8}, // 4: Hatch ==> Rocket High
{1607, 2166, 9, 10}, // 5: Hatch ==> Rocket Middle
{2605, 1341, 11, 12}, // 6: Hatch ==> Rocket Low
{2605, 1341, 13, 14}, // 7: Hatch ==> Cargo Ship
{1632, 2321, 15, 16}, // 8: Cargo ==> Cargo Ship
{2605, 1341, 17, 18}, // 9: Hatch ==> Feeder (Human Player)
{1738, 2048, 19, 20}, // 10: Cargo ==> Feeder (Human Player)
{2931, 580, 21, 22}, // 11: Hatch ==> Transport Position
{2837, 1447, 23, 24}, // 12: Cargo ==> Ground Pickup
{106, 1710, 25, 26} // 13: Cargo ==> Rocket High (from back of Robot)
};