Skip to content

1.5.10 - Advanced Mechanisms: Shooter, Feeder, and Winch

On The Panther Project, we know a competitive robot does more than just drive. To score, you need specialized mechanisms that all work together. In this section, we are adding three distinct mechanisms to our robot. Because they all do very different jobs, we have to program each one using a different strategy.

Here is a breakdown of the three systems we are building:

  1. The Shooter (Arduino Bridge): Needs massive power and speed to launch game pieces.
  2. The Feeder (Smart Motor): Needs high precision to load exactly one ball at a time.
  3. The Winch (Manual Motor): Needs direct driver control to aim the shooter.

The XRP has four built-in motor ports, but they are designed for small robotics. If you want to spin heavy flywheels to launch game pieces, you need larger motors and a separate, high-power motor controller.

Because we cannot plug a giant FRC-style motor directly into the XRP, we use an Arduino as a translator. We plug the Arduino into the XRP’s Servo 2 Port. The XRP sends a low-power “pulse” signal to the Arduino, and the Arduino commands the heavy-duty motor controller to spin the flywheels.

Open your Arduino IDE, paste this code, and upload it to your Arduino. It listens to Pin 2 for the XRP pulse signal and translates it into high-power motor movement:

// --- PIN DEFINITIONS ---
const int pwmInputPin = 2; // DIRECT WIRE from XRP Servo 2 Signal
// Motor 1 Pins (Shooter Left)
const int motor1_PinA = 5;
const int motor1_PinB = 6;
// Motor 2 Pins (Shooter Right)
const int motor2_PinA = 9;
const int motor2_PinB = 10;
// --- VOLATILE VARIABLES FOR THE STOPWATCH ---
volatile unsigned long pulseStartTime = 0;
volatile unsigned long pulseWidth = 1500;
volatile unsigned long lastPulseTime = 0;
void setup() {
pinMode(pwmInputPin, INPUT);
pinMode(motor1_PinA, OUTPUT);
pinMode(motor1_PinB, OUTPUT);
pinMode(motor2_PinA, OUTPUT);
pinMode(motor2_PinB, OUTPUT);
// Attach the interrupt to Pin 2 to listen for the XRP signal
attachInterrupt(digitalPinToInterrupt(pwmInputPin), measurePulse, CHANGE);
stopMotors();
}
void loop() {
noInterrupts();
unsigned long currentPulse = pulseWidth;
unsigned long signalAge = millis() - lastPulseTime;
interrupts();
// SAFETY: Stop if signal is lost or wire falls out (>100ms old)
if (signalAge > 100) {
stopMotors();
return;
}
int motorSpeed = 0;
// --- FORWARD (Shooting) ---
if (currentPulse > 1550 && currentPulse < 2500) {
motorSpeed = map(currentPulse, 1550, 2200, 0, 255);
motorSpeed = constrain(motorSpeed, 0, 255);
analogWrite(motor1_PinA, motorSpeed);
digitalWrite(motor1_PinB, LOW);
analogWrite(motor2_PinA, motorSpeed);
digitalWrite(motor2_PinB, LOW);
}
// --- STOP (Neutral ~1500us) ---
else {
stopMotors();
}
delay(20);
}
void stopMotors() {
digitalWrite(motor1_PinA, LOW);
digitalWrite(motor1_PinB, LOW);
digitalWrite(motor2_PinA, LOW);
digitalWrite(motor2_PinB, LOW);
}
void measurePulse() {
if (digitalRead(pwmInputPin) == HIGH) {
pulseStartTime = micros();
} else {
pulseWidth = micros() - pulseStartTime;
lastPulseTime = millis();
}
}

Now switch to VS Code. Even though we are spinning a motor, our Java code uses the XRPServo class to generate the signal. We hide this inside the shooter.java subsystem.

Create this file inside your subsystems folder.

package frc.robot.subsystems;
import edu.wpi.first.wpilibj.xrp.XRPServo;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
public class shooter extends SubsystemBase {
private final XRPServo shooterBridge = new XRPServo(5);
public shooter() {}
public void setTargetSpeed(double speed) {
double angle = (speed + 1.0) * 90.0;
shooterBridge.setAngle(angle);
}
public void stop() {
shooterBridge.setAngle(90.0);
}
public void runDefaultBehavior() {
stop();
}
@Override
public void periodic() {
// Left blank since idle mode tracking is removed
}
}

The feeder’s job is to push a ball into the spinning flywheels. We plug the feeder into Motor Port 3 (Channel 2 in code) and use its built-in encoder to turn it a precise amount every single time.

Create this file inside your subsystems folder.

package frc.robot.subsystems;
import edu.wpi.first.wpilibj.Encoder;
import edu.wpi.first.wpilibj.xrp.XRPMotor;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
public class feeder extends SubsystemBase {
private final XRPMotor feederMotor = new XRPMotor(2);
private final Encoder feederEncoder = new Encoder(8, 9);
public feeder() {
feederEncoder.setDistancePerPulse(1.0 / 585.6);
resetEncoder();
}
public void runFeeder(double speed) {
feederMotor.set(speed);
}
public void stop() {
feederMotor.set(0.0);
}
public double getRevolutions() {
return Math.abs(feederEncoder.getDistance());
}
public void resetEncoder() {
feederEncoder.reset();
}
@Override
public void periodic() {}
}

Our winch uses a string to change the shooter angle. We plug this into Motor Port 4 (Channel 3 in software) for direct manual control via the L1 and R1 buttons.

Create this file inside your subsystems folder.

package frc.robot.subsystems;
import edu.wpi.first.wpilibj.xrp.XRPMotor;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
public class winch extends SubsystemBase {
private final XRPMotor winchMotor = new XRPMotor(3);
public winch() {}
public void setPower(double speed) {
winchMotor.set(speed);
}
public void stop() {
winchMotor.set(0.0);
}
@Override
public void periodic() {}
}

4. Tying it Together: The AutoShoot Sequence

Section titled “4. Tying it Together: The AutoShoot Sequence”

We coordinate the Feeder and Shooter using a state machine command.

Create this file inside your commands folder.

package frc.robot.commands;
import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj2.command.Command;
import frc.robot.subsystems.shooter;
import frc.robot.subsystems.feeder;
public class AutoShoot extends Command {
private final shooter m_shooter;
private final feeder m_feeder;
private final Timer m_timer = new Timer();
private final double FEED_ROTATIONS = 0.125;
private final double INITIAL_SPIN_UP_TIME = 1.5;
private final double RECOVERY_TIME = 1.5;
private int m_state = 0;
public AutoShoot(shooter shooterSub, feeder feederSub) {
m_shooter = shooterSub;
m_feeder = feederSub;
addRequirements(m_shooter, m_feeder);
}
@Override
public void initialize() {
m_timer.restart();
m_state = 0;
m_feeder.stop();
m_feeder.resetEncoder();
}
@Override
public void execute() {
m_shooter.setTargetSpeed(0.8);
if (m_state == 0) {
if (m_timer.hasElapsed(INITIAL_SPIN_UP_TIME)) {
m_state = 1;
m_feeder.resetEncoder();
}
}
else if (m_state == 1) {
m_feeder.runFeeder(0.6);
if (m_feeder.getRevolutions() >= FEED_ROTATIONS) {
m_state = 2;
m_feeder.stop();
m_timer.restart();
}
}
else if (m_state == 2) {
if (m_timer.hasElapsed(RECOVERY_TIME)) {
m_state = 1;
m_feeder.resetEncoder();
}
}
}
@Override
public void end(boolean interrupted) {
m_shooter.stop();
m_feeder.stop();
}
@Override
public boolean isFinished() {
return false;
}
}

Now we can map our controller buttons and create bindings to the controller.

Modify the existing file inside your java/frc/robot folder.

package frc.robot;
import edu.wpi.first.wpilibj.PS4Controller;
import edu.wpi.first.wpilibj2.command.InstantCommand;
import edu.wpi.first.wpilibj2.command.RunCommand;
import edu.wpi.first.wpilibj2.command.button.Trigger;
import frc.robot.Constants.OperatorConstants;
import frc.robot.subsystems.XRPDrivetrain;
import frc.robot.subsystems.shooter;
import frc.robot.subsystems.feeder;
import frc.robot.subsystems.winch;
import frc.robot.commands.AutoShoot;
public class RobotContainer {
private final shooter m_shooter = new shooter();
private final feeder m_feeder = new feeder();
private final winch m_winch = new winch();
private final XRPDrivetrain xrp = new XRPDrivetrain();
private final PS4Controller driverController = new PS4Controller(OperatorConstants.DRIVER_CONTROLLER_PORT);
public RobotContainer() {
xrp.setDefaultCommand(new RunCommand(
() -> xrp.executeDrive(
driverController.getLeftY(),
driverController.getRightY(),
false,
driverController.getCircleButton()
),
xrp));
m_shooter.setDefaultCommand(new RunCommand(() -> m_shooter.runDefaultBehavior(), m_shooter));
configureButtonBindings();
}
private void configureButtonBindings() {
new Trigger(driverController::getL1Button)
.whileTrue(new RunCommand(() -> m_winch.setPower(0.7), m_winch))
.onFalse(new InstantCommand(() -> m_winch.stop(), m_winch));
new Trigger(driverController::getR1Button)
.whileTrue(new RunCommand(() -> m_winch.setPower(-0.7), m_winch))
.onFalse(new InstantCommand(() -> m_winch.stop(), m_winch));
new Trigger(driverController::getTriangleButton)
.whileTrue(new AutoShoot(m_shooter, m_feeder));
new Trigger(driverController::getSquareButton)
.onTrue(new InstantCommand(() -> xrp.setServoPositionOne()))
.onFalse(new InstantCommand(() -> xrp.setServoDefault()));
new Trigger(driverController::getCrossButton)
.onTrue(new InstantCommand(() -> xrp.setServoPositionTwo()))
.onFalse(new InstantCommand(() -> xrp.setServoDefault()));
}
}

6. Newbie Customization Guide: Tuning Your Robot

Section titled “6. Newbie Customization Guide: Tuning Your Robot”

When you test your robot code on the practice field, default parameters rarely fit every physical build perfectly. Here is how and why to customize them:

  • Where to look: AutoShoot.java
  • What to change: INITIAL_SPIN_UP_TIME (default: 1.5 seconds).
  • Why change it: If your motors reach maximum RPM faster than 1.5 seconds, lower this to 1.0 to cycle shots faster. If your flywheels are heavy, increase it to give them more time.
  • Where to look: AutoShoot.java
  • What to change: RECOVERY_TIME (default: 1.5 seconds).
  • Why change it: This controls the pause between fed balls. If the flywheels bog down too much when a ball enters, increase this delay so the motor recovers speed.
  • Where to look: AutoShoot.java
  • What to change: FEED_ROTATIONS (default: 0.125).
  • Why change it: Controls how far the feeder wheel rotates per shot. If a piece does not completely enter the flywheels, increase it slightly (e.g., 0.15).
  • Where to look: RobotContainer.java (Winch Bindings)
  • What to change: The power values (0.7 and -0.7).
  • Why change it: Lowering this to 0.4 or 0.5 gives the driver finer, safer control over the shooter tilt angle.