Motors & Sensors
TalonFX (Phoenix 6), current detection, CANcoder, Pigeon 2 — hardware used in the Rebuilt codebase.
TalonFX & Phoenix 6
The TalonFX (Falcon 500 / Kraken X60) is our primary drive and mechanism motor. It uses CTRE's Phoenix 6 library — different API from Phoenix 5, and what the Rebuilt codebase is built on. Learn this one well.
Basic Setup
import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.configs.TalonFXConfiguration; import com.ctre.phoenix6.signals.NeutralModeValue; import com.ctre.phoenix6.signals.InvertedValue; public class ShooterSubsystem extends SubsystemBase { private final TalonFX m_shooterMotor; private static final int kMotorID = 11; public ShooterSubsystem() { m_shooterMotor = new TalonFX(kMotorID); TalonFXConfiguration config = new TalonFXConfiguration(); config.MotorOutput.NeutralMode = NeutralModeValue.Coast; config.MotorOutput.Inverted = InvertedValue.CounterClockwise_Positive; config.CurrentLimits.SupplyCurrentLimit = 40; config.CurrentLimits.SupplyCurrentLimitEnable = true; m_shooterMotor.getConfigurator().apply(config); } }
Phoenix 6 config approach: Instead of calling dozens of separate setter methods, Phoenix 6 uses a single TalonFXConfiguration object. Build your full config, then apply it once with getConfigurator().apply(config). Cleaner and less error-prone.
Control Requests — How to Drive a TalonFX
Phoenix 6 uses control request objects instead of a single set() method. You create the request once, update its value, and call setControl(). The Rebuilt codebase uses these heavily.
import com.ctre.phoenix6.controls.DutyCycleOut; import com.ctre.phoenix6.controls.VelocityTorqueCurrentFOC; import com.ctre.phoenix6.controls.MotionMagicVelocityTorqueCurrentFOC; public class ShooterSubsystem extends SubsystemBase { private final TalonFX m_shooterMotor; // Create request objects ONCE — reuse them every 20ms private final DutyCycleOut m_dutyCycleReq = new DutyCycleOut(0); private final VelocityTorqueCurrentFOC m_velocityReq = new VelocityTorqueCurrentFOC(0); // Simple percent output (-1.0 to 1.0) public void setPercent(double percent) { m_shooterMotor.setControl(m_dutyCycleReq.withOutput(percent)); } // Velocity control in rotations per second (for spinning up to shot speed) public void setVelocity_rps(double velocity_rps) { m_shooterMotor.setControl(m_velocityReq.withVelocity(velocity_rps)); } public void stop() { m_shooterMotor.stopMotor(); } }
Reading TalonFX Signals
In Phoenix 6, sensor reads are signals — you get a StatusSignal<Double> object, then call .getValueAsDouble(). Get the signal object once in the constructor (sig_ prefix), then read it in periodic.
import com.ctre.phoenix6.StatusSignal; public class ShooterSubsystem extends SubsystemBase { private final TalonFX m_shooterMotor; // Get signal objects once — sig_ prefix (WRT convention) private final StatusSignal<Double> sig_velocity; private final StatusSignal<Double> sig_current; private static final double kAtSpeedThreshold_rps = 2.0; private static final double kTargetSpeed_rps = 80.0; public ShooterSubsystem() { m_shooterMotor = new TalonFX(11); // ... config ... sig_velocity = m_shooterMotor.getVelocity(); sig_current = m_shooterMotor.getSupplyCurrent(); } public double getVelocity_rps() { return sig_velocity.getValueAsDouble(); } public double getCurrent_A() { return sig_current.getValueAsDouble(); } public boolean atSpeed() { return Math.abs(getVelocity_rps() - kTargetSpeed_rps) < kAtSpeedThreshold_rps; } @Override public void periodic() { StatusSignal.refreshAll(sig_velocity, sig_current); // batch refresh SmartDashboard.putNumber("Shooter/Velocity_rps", getVelocity_rps()); SmartDashboard.putBoolean("Shooter/AtSpeed", atSpeed()); } }
StatusSignal.refreshAll(): Call this in periodic() to refresh multiple signals at once. This is more efficient than calling .refresh() on each signal separately — Phoenix 6 can batch the CAN reads.
Topic 1 — Quick Check
Current-Based Game Piece Detection
One of the most common patterns in WRT code: detect a game piece by watching for a stator current spike. When intake rollers stall against a note or ball, current jumps. No extra sensor needed.
How it Works
Stator current is proportional to torque. When a motor stalls, torque and current both spike. Set a threshold — if current exceeds it, something is blocking the mechanism.
private static final double kPieceDetectedCurrent_A = 30.0; // tune on robot public boolean hasPiece() { return m_rollerMotor.getStatorCurrent().getValueAsDouble() > kPieceDetectedCurrent_A; } // In Robot.java, wire as a Trigger: var trg_hasPiece = new Trigger(m_intake::hasPiece);
StatusSignals are cached. getStatorCurrent() returns a StatusSignal. Call .getValueAsDouble() to read the latest value. For tighter timing, use BaseStatusSignal.refreshAll() to batch-refresh multiple signals per CAN frame.
Debounce to Avoid False Positives
A single spike can be a transient. Use WPILib's Debouncer to require the current to stay high for multiple loop cycles before confirming a detection.
import edu.wpi.first.math.filter.Debouncer; private final Debouncer m_pieceDebouncer = new Debouncer(0.1); // 100ms public boolean hasPiece() { boolean over = m_rollerMotor.getStatorCurrent().getValueAsDouble() > kPieceDetectedCurrent_A; return m_pieceDebouncer.calculate(over); }
tune kPieceDetectedCurrent_A on the actual robot. start high (~50A), watch telemetry during normal operation, then lower until detection is reliable without false positives.
CANcoder & Pigeon 2
CTRE's CANcoder is an absolute magnetic encoder used on swerve module steering. The Pigeon 2 is our IMU (gyroscope + accelerometer). Both use the Phoenix 6 API.
CANcoder Setup
import com.ctre.phoenix6.hardware.CANcoder; import com.ctre.phoenix6.configs.CANcoderConfiguration; import com.ctre.phoenix6.signals.AbsoluteSensorRangeValue; CANcoder m_encoder = new CANcoder(20); // CAN ID 20 CANcoderConfiguration config = new CANcoderConfiguration(); // kUnsigned_0To1 = 0 to 1 rotation (easier math than -0.5 to 0.5) config.MagnetSensor.AbsoluteSensorRange = AbsoluteSensorRangeValue.Unsigned_0To1; // Offset to align 0° to the physical "forward" position of the module config.MagnetSensor.MagnetOffset = 0.247; // set per-module during calibration m_encoder.getConfigurator().apply(config); // Read absolute position in rotations StatusSignal<Double> sig_absPos = m_encoder.getAbsolutePosition(); double absPosition_rot = sig_absPos.getValueAsDouble(); // 0.0 to 1.0
Pigeon 2 Gyroscope
import com.ctre.phoenix6.hardware.Pigeon2; public class DrivetrainSubsystem extends SubsystemBase { private final Pigeon2 m_gyro; private final StatusSignal<Double> sig_yaw; private final StatusSignal<Double> sig_pitch; public DrivetrainSubsystem() { m_gyro = new Pigeon2(0); // CAN ID 0 sig_yaw = m_gyro.getYaw(); sig_pitch = m_gyro.getPitch(); } // Returns heading in degrees (-180 to 180 by default) public double getHeading_deg() { return sig_yaw.getValueAsDouble(); } // Reset heading (used at start of auto) public void resetHeading() { m_gyro.reset(); } @Override public void periodic() { StatusSignal.refreshAll(sig_yaw, sig_pitch); SmartDashboard.putNumber("Drive/Heading_deg", getHeading_deg()); } }
Pigeon 2 vs NavX: Rebuilt uses Pigeon 2 (CTRE) rather than the NavX. The Pigeon 2 has better Phoenix 6 integration — signals, latency compensation, and swerve odometry all work more cleanly with it than with third-party IMUs.
Topic 3 — Quick Check
Logging & SmartDashboard
Good logging is how you debug at competition without a laptop plugged into the robot. Put useful data on the dashboard and it'll save you when something weird happens on the field.
SmartDashboard Basics
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; // In subsystem periodic() — runs every 20ms in all modes @Override public void periodic() { // Use "Subsystem/Value" format — creates organized groups in Shuffleboard SmartDashboard.putNumber("Shooter/Velocity_rps", getVelocity_rps()); SmartDashboard.putNumber("Shooter/Current_A", getCurrent_A()); SmartDashboard.putBoolean("Shooter/AtSpeed", atSpeed()); SmartDashboard.putString("Shooter/State", m_state.toString()); }
What to Log
| Always log | Why it matters |
|---|---|
| Motor velocity (rps or rpm) | Check if mechanism is actually spinning |
| Motor current (A) | Detect stalls, game piece detection, mechanical issues |
| At-speed / at-position booleans | Confirm state machine transitions happen correctly |
| Sensor readings (encoder pos, gyro heading) | Debug odometry and positioning in auto |
| Active command name | Know what command is controlling what subsystem |
DataLog (for Post-Match Analysis)
SmartDashboard is live. DataLog writes to a USB drive on the roboRIO for reviewing after a match. WPILib's DataLogManager makes this easy — Rebuilt uses it for deeper diagnostics.
import edu.wpi.first.wpilibj.DataLogManager; import edu.wpi.first.util.datalog.DoubleLogEntry; import edu.wpi.first.util.datalog.DataLog; // In robotInit(): DataLogManager.start(); // writes to USB if present, otherwise /home/lvuser/ // In subsystem constructor: DataLog log = DataLogManager.getLog(); DoubleLogEntry m_velocityLog = new DoubleLogEntry(log, "/shooter/velocity_rps"); // In periodic(): m_velocityLog.append(getVelocity_rps());
Advantage Scope: Download log files from the roboRIO and visualize them in Advantage Scope. It shows motor velocity, current, and position over time — invaluable for tuning PID and debugging auto failures after the match.
Topic 4 — Coding Prompt
Write a complete ShooterSubsystem that: creates a TalonFX on CAN ID 11, configures it with Coast neutral mode and a 40A current limit, creates sig_velocity and sig_current signals, exposes setVelocity_rps(double), stop(), getVelocity_rps(), and atSpeed() (within 2 rps of 80 rps target), and logs velocity and at-speed to SmartDashboard in periodic().
Topic 4 — Quick Check
Weekly Test
covers motors, sensors, and Phoenix 6. your score goes to the leads :)