Mini-Project: A Velocity-Controlled Shooter on REVLib
A shooter has to reach a consistent surface speed before it launches, or the 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
Section titled “The 2025 config model”In REVLib 2025 you no longer call setters one at a time on the controller. Instead you build a SparkFlexConfig and 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. That is worth doing for a one-time setup, but never call it every loop: flash writes block CAN comms.
Command a target RPM
Section titled “Command a target RPM”Command the velocity through the SparkClosedLoopController with setReference(). REVLib 2026 deprecates that method in favor of an identical setSetpoint(), so use that one on the 2026 library:
private static final double kTargetRpm = 4800;
public Command spinUp() { return run(() -> m_pid.setReference(kTargetRpm, ControlType.kVelocity));}
public boolean atSpeed() { return Math.abs(m_enc.getVelocity() - kTargetRpm) < 100; // RPM tolerance}Gate the feeder on “at speed”
Section titled “Gate the feeder on “at speed””Expose atSpeed() as a Trigger and run the feeder only when the flywheel is ready. This is the BoVLB best practice of asking yes/no questions in problem-domain language:
Trigger ready = new Trigger(m_shooter::atSpeed);// hold to spin upm_driver.rightTrigger().whileTrue(m_shooter.spinUp());// feed only once the wheel has recovered to speedm_driver.rightTrigger().and(ready).whileTrue(m_feeder.feed());Why velocityFF matters more than P
Section titled “Why velocityFF matters more than P”A flywheel’s steady-state voltage is almost entirely feedforward: V = kV * rpm. If velocityFF is set correctly, the controller jumps to near the right voltage immediately, and P only has to trim the last few percent. Teams that leave FF at zero and crank P instead 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. (REVLib is moving toward a feedForward config with explicit kS/kV terms, but 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
Section titled “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.
This lesson was adapted from learnfrc.com.
