Mini-Project: A Velocity-Controlled Shooter on REVLib
Spin a flywheel to a target RPM with a NEO Vortex on a SPARK Flex using REVLib 2025's declarative closed-loop config, and only fire when it is up to speed.
Sign in to track progress, earn XP, and save lessons.
A shooter must reach a consistent surface speed before launching, or shots scatter. We'll run a NEO Vortex (free speed 6784 RPM, Kv 565) on a SPARK Flex in closed-loop velocity mode using REVLib 2025's declarative configuration API.
The 2025 config model
In REVLib 2025 you no longer call setters one at a time on the controller. You build a SparkFlexConfig, then configure() it once with explicit reset and persist modes:
private final SparkFlex m_motor = new SparkFlex(31, MotorType.kBrushless);
private final SparkClosedLoopController m_pid = m_motor.getClosedLoopController();
private final RelativeEncoder m_enc = m_motor.getEncoder();
public Shooter() {
SparkFlexConfig config = new SparkFlexConfig();
config.closedLoop
.feedbackSensor(FeedbackSensor.kPrimaryEncoder)
.pid(0.0001, 0.0, 0.0) // velocity P,I,D
.velocityFF(1.0 / 6784.0); // ~1/free-speed-RPM for a NEO Vortex (6784 RPM)
config.smartCurrentLimit(60);
m_motor.configure(config,
ResetMode.kResetSafeParameters,
PersistMode.kPersistParameters);
}
PersistMode.kPersistParameters writes to flash so the config survives a brown-out reboot -- worth it for a one-time setup, but never call it every loop (flash writes block CAN comms).
Command a target RPM
In REVLib 2025, setReference() is deprecated in favor of setSetpoint() on the SparkClosedLoopController:
private static final double kTargetRpm = 4800;
public Command spinUp() {
return run(() ->
m_pid.setSetpoint(kTargetRpm, ControlType.kVelocity));
}
public boolean atSpeed() {
return Math.abs(m_enc.getVelocity() - kTargetRpm) < 100; // RPM tolerance
}
Gate the feeder on "at speed"
Expose atSpeed() as a Trigger and only run the feeder when the flywheel is ready -- this is exactly the BoVLB best practice of asking yes/no questions in problem-domain language:
Trigger ready = new Trigger(m_shooter::atSpeed);
// hold to spin up
m_driver.rightTrigger().whileTrue(m_shooter.spinUp());
// feed only once the wheel has recovered to speed
m_driver.rightTrigger().and(ready).whileTrue(m_feeder.feed());
Why velocityFF matters more than P
A flywheel's steady-state voltage is almost entirely feedforward: V = kV * rpm. If you set velocityFF correctly, the controller jumps near the right voltage instantly and P only trims the last few percent. Teams that leave FF at zero and crank P get a sluggish, oscillating flywheel that dips badly when a game piece loads it. The SPARK's velocityFF multiplies the RPM setpoint to produce a duty-cycle output, so its value is roughly 1 / free-speed-RPM. Find it from a SysId run, or empirically: command a fixed duty cycle, read the steady RPM, and velocityFF = appliedOutput / rpm. (Note: REVLib is moving toward a feedForward config with explicit kS/kV terms; velocityFF() still works in 2025.)
Plot m_enc.getVelocity() against the setpoint in AdvantageScope. A good shooter recovers to within tolerance in well under half a second after each shot.
Key takeaways
- REVLib 2025 uses declarative SparkFlexConfig/SparkMaxConfig objects applied with configure(), not per-parameter setters.
- In REVLib 2025 use SparkClosedLoopController.setSetpoint() -- setReference() is deprecated.
- Use PersistMode.kPersistParameters once at setup; never persist every loop -- flash writes block CAN.
- Flywheels are feedforward-dominated: set velocityFF (~1/Kv) correctly and keep P small; gate the feeder on an atSpeed() Trigger.
Lesson quiz
RequiredAnswer all 3 questions correctly to complete this lesson.
1.When running velocity closed-loop control on a SPARK MAX/Flex with REVLib, which control type do you request?
2.What units does the SPARK's onboard velocity closed loop use for the setpoint by default?
3.For a flywheel shooter, what is the recommended way to combine feedforward and PID gains in REVLib?
Answer every question to submit.