Motor Actuator is implemented in the TrcMotor class in the Framework Library. It abstracts a superset of functionalities of what a motor controller can do. Some functionalities are supported by some motor controllers natively and others are simulated in software by TrcMotor. Since our Framework Library is shared between FTC and FRC, TrcMotor is platform independent and provides generic motor controller functionalities for both FTC and FRC. This allows our subsystem code to work in both FTC and FRC environment. In order for TrcMotor to support both FTC and FRC platforms, corresponding FTC and FRC subclasses must extend TrcMotor and will provide platform specific accesses to the corresponding motor controller hardware. For example, FtcDcMotor class in FTC and FrcCANPhoenix5/FrcCANPhoenix6/FrcCANSparkMax classes in FRC. To make it even easier to use, the Framework Library also provide wrapper classes FtcMotorActuator and FrcMotorActuator that contain code to instantiate and configure the motor controller/sensor hardware for the corresponding platform.
Motor Actuator supports different configurations of subsystems. Even though they sound different, they are actually the same. They all contain one or more motors for movement, an encoder for keeping track of their positions, and limit switches to limit their range of movement. Therefore, the same Motor Actuator code can support all these variations.
setPositionScaleAndOffset: Sets the position scale and offset to scale the position value to real world units. For example, for gravity compensation to work correctly, the arm must report its angle position in degrees relative to vertical which is 0-degree (i.e. arm angle is 90-degree at horizontal). You must determine the proper scaling factor by applying the following equation:
ARM_DEG_PER_COUNT = 360.0 / ENCODER_PPR / GEAR_RATIO;
The resting position of an arm is typically not vertical. Therefore, you must specify the resting position angle as the ARM_OFFSET. To determine the resting position, use a leveling app on your smart phone (download one from your app store if you don’t have it) resting up against the arm to measure its angle relative to vertical.
The following are the most commonly called methods provided by TrcMotor which is the object returned by the getActuator method:
setStallProtection: Sets stall protection. When stall protection is turned ON, it will monitor the motor movement for stalled condition and will cut power to protect the motor.
A motor is considered stalled if:
Note: By definition, holding target position doing software PID control is stalling. If you decide to enable stall protection while holding target, please make sure to set a stallMinPower greater than the power necessary to hold position against gravity with margin. However, if you want to zero calibrate on motor stall (e.g. don’t have lower limit switch), you want to make sure calPower is at least stallMinPower.
Since all these subsystems are derivatives of the Motor Actuator, we will just show the example of how an Arm subsystem for FTC is implemented. For FRC implementation, we will leave it for you as an exercise. It should be very similar. To create the arm subsystem, follow the steps below:
public class Arm
{
private final TrcMotor armMotor;
/**
* Constructor: Creates an instance of the object.
*/
public Arm()
{
FtcMotorActuator.Params armParams = new FtcMotorActuator.Params()
.setMotorInverted(RobotParams.ARM_MOTOR_INVERTED)
.setLowerLimitSwitch(RobotParams.ARM_HAS_LOWER_LIMIT_SWITCH, RobotParams.ARM_LOWER_LIMIT_INVERTED)
.setUpperLimitSwitch(RobotParams.ARM_HAS_UPPER_LIMIT_SWITCH, RobotParams.ARM_UPPER_LIMIT_INVERTED)
.setVoltageCompensationEnabled(RobotParams.ARM_VOLTAGE_COMP_ENABLED)
.setPositionScaleAndOffset(RobotParams.ARM_DEG_PER_COUNT, RobotParams.ARM_OFFSET)
.setPositionPresets(RobotParams.ARM_PRESET_TOLERANCE, RobotParams.ARM_PRESETS);
armMotor = new FtcMotorActuator(RobotParams.HWNAME_ARM, true, armParams).getActuator();
//
// If you are using motor native PID control, uncomment the following line. Otherwise, comment it out.
// For FTC motors, the built-in PID Coefficients are generally good enough, so we don't need to set it. But for
// FRC, you need to do a setPositionPidParameters here because the default Coefficients are probably zeros.
//
armMotor.setPositionPidTolerance(RobotParams.ARM_TOLERANCE);
//
// If you are using software PID control, uncomment the following section. Otherwise, comment it out.
//
// armMotor.setSoftwarePidEnabled(true);
// armMotor.setPositionPidParameters(
// RobotParams.ARM_KP, RobotParams.ARM_KI, RobotParams.ARM_KD, RobotParams.ARM_KF,
// RobotParams.ARM_IZONE, RobotParams.ARM_TOLERANCE);
// // The getPowerComp method will be called every time when PID is calculating the power to be applied to the
// // arm. The PowerComp value will be added to the arm power to compensate for gravity.
// arm.setPositionPidPowerComp(this::getPowerComp);
// armMotor.setPidStallDetectionEnabled(
// RobotParams.ARM_STALL_DETECTION_DELAY, RobotParams.ARM_STALL_DETECTION_TIMEOUT,
// RobotParams.ARM_STALL_ERR_RATE_THRESHOLD);
armMotor.setStallProtection(
RobotParams.ARM_STALL_MIN_POWER, RobotParams.ARM_STALL_TOLERANCE,
RobotParams.ARM_STALL_TIMEOUT, RobotParams.ARM_STALL_RESET_TIMEOUT);
}
public TrcMotor getArmMotor()
{
return armMotor;
}
/**
* This method is called to compute the power compensation to counteract gravity on the Arm.
*
* @param currPower specifies the current motor power (not used).
* @return gravity compensation for the arm.
*/
private double getPowerComp(double currPower)
{
// ARM_MAX_GRAVITY_COMP_POWER is the power required to hold the arm at horizontal position.
return RobotParams.ARM_MAX_GRAVITY_COMP_POWER * Math.sin(Math.toRadians(arm.getPosition()));
}
}
...
public TrcMotor arm;
...
public Robot(TrcRobot.RunMode runMode)
{
...
if (RobotParams.Preferences.useSubsystems)
{
...
if (RobotParams.Preferences.useArm)
{
arm = new Arm().getArmMotor();
// Zero calibrate the arm.
arm.zeroCalibrate(RobotParams.ARM_CAL_POWER);
}
...
}
...
}
The Arm class above is referencing a lot of constants from RobotParams.java. We need to define all those constants. In RobotParams.Preferences, add a switch so that we can enable/disable the subsystem. This is very useful during development because the subsystem may not exist yet. At the end of the RobotParam.java class, add the Arm subsystem section like below.
The Arm consists of a DC motor with no limit switches. Therefore, we are using zero calibration by motor stall. Since the code above is using motor native close-loop control, the Software PID Control Coefficients section is not really required. It is there in case you want to change the code to use Software PID control instead.
public static class Preferences
{
...
// Subsystems
public static boolean useSubsystems = true;
public static boolean useArm = true;
...
}
...
//
// Arm subsystem: All values below are just an example implementation, you need to change them to fit your subsystem
// and tune some of the values (e.g. PID Coefficients).
//
public static final String HWNAME_ARM = "arm";
// Actuator parameters.
public static final boolean ARM_MOTOR_INVERTED = false;
public static final boolean ARM_HAS_LOWER_LIMIT_SWITCH = false;
public static final boolean ARM_LOWER_LIMIT_INVERTED = false;
public static final boolean ARM_HAS_UPPER_LIMIT_SWITCH = false;
public static final boolean ARM_UPPER_LIMIT_INVERTED = false;
public static final boolean ARM_VOLTAGE_COMP_ENABLED = true;
public static final double ARM_ENCODER_PPR = 537.6898; // Motor encoder PPR
public static final double ARM_GEAR_RATIO = 28.0;
public static final double ARM_DEG_PER_COUNT = 360.0 / ARM_ENCODER_PPR / ARM_GEAR_RATIO;
public static final double ARM_OFFSET = 27.0; // Arm resting position angle in deg
public static final double ARM_MIN_POS = 27.3; // Arm min angle in deg
public static final double ARM_MAX_POS = 300.0; // Arm max angle in deg
public static final double ARM_POWER_SCALE = 0.5; // Slowing down the arm
// Preset positions.
public static final double ARM_PRESET_TOLERANCE = 5.0; // in deg
// Presets array must be sorted in ascending order in the unit of deg
public static final double[] ARM_PRESETS = new double[] {
30.0, 60.0, 90.0, 120, 150.0, 180.0, 210.0, 240.0, 270.0
};
// Software PID Control Coefficients.
public static final double ARM_KP = 0.0162;
public static final double ARM_KI = 0.0;
public static final double ARM_KD = 0.0;
public static final double ARM_KF = 0.0;
public static final double ARM_IZONE = 0.0;
public static final double ARM_TOLERANCE = 2.0; // in deg
public static final double ARM_MAX_GRAVITY_COMP_POWER = 0.1675; // in percentage power
public static final double ARM_STALL_DETECTION_DELAY = 0.5; // in seconds
public static final double ARM_STALL_DETECTION_TIMEOUT = 0.1; // in seconds
public static final double ARM_STALL_ERR_RATE_THRESHOLD = 10.0; // in deg/sec
public static final double ARM_CAL_POWER = -0.25; // in percentage power
public static final double ARM_STALL_MIN_POWER = Math.abs(ARM_CAL_POWER);
public static final double ARM_STALL_TOLERANCE = 0.1; // in deg
public static final double ARM_STALL_TIMEOUT = 0.2; // in second
public static final double ARM_STALL_RESET_TIMEOUT = 0.0; // in second
public void updateStatus()
{
...
if (arm != null)
{
dashboard.displayPrintf(
++lineNum,
"Arm: power=" + arm.getPower() +
",pos=" + arm.getPosition() +
",target=" + arm.getPidTarget());
}
...
}
private double armPrevPower = 0.0;
private boolean operatorAltFunc = false;
...
public void periodic(double elapsedTime, boolean slowPeriodicLoop)
{
...
//
// Other subsystems.
//
if (RobotParams.Preferences.useSubsystems)
{
// Arm subsystem: only do this if the arm is enabled.
if (robot.arm != null)
{
// Send power value to the arm if it is different from before.
// This prevents repeatedly sending zero power to the arm if the joystick is not moved.
double armPower = operatorGamepad.getRightStickY(true) * RobotParams.ARM_POWER_SCALE;
if (armPower != armPrevPower)
{
if (operatorAltFunc)
{
// By definition, Manual Override should not observe any safety.
// Therefore, set arm power directly bypassing all safety checks.
robot.arm.setPower(armPower);
}
else
{
robot.arm.armSetPidPower(
null, armPower, RobotParams.ARM_MIN_POS, RobotParams.ARM_MAX_POS);
}
armPrevPower = armPower;
}
}
...
}
...
}
...
protected void operatorButtonEvent(TrcGameController gamepad, int button, boolean pressed)
{
...
switch (button)
{
...
case FtcGamepad.GAMEPAD_RBUMPER:
robot.globalTracer.traceInfo(moduleName, ">>>>> operatorAltFunc=" + pressed);
operatorAltFunc = pressed;
break;
case FtcGamepad.DPAD_UP:
// Check if arm is enabled.
if (robot.arm != null && pressed)
{
robot.globalTracer.traceInfo(moduleName, ">>>>> Arm Preset Up");
robot.arm.presetPositionUp(moduleName, RobotParams.ARM_POWER_SCALE);
}
break;
case FtcGamepad.DPAD_DOWN:
// Check if arm is enabled.
if (robot.arm != null && pressed)
{
robot.globalTracer.traceInfo(moduleName, ">>>>> Arm Preset Down");
robot.arm.presetPositionDown(moduleName, RobotParams.ARM_POWER_SCALE);
}
break;
}
...
}