sign in
Start typing to search across all 16 weeks…
home / offseason / week o4
Offseason · Week 4 of 8

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

java — TalonFX setup (Phoenix 6)
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.

java — control requests (from Rebuilt)
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.

java — StatusSignal reads (sig_ prefix from Rebuilt)
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.

java — current spike detection (TalonFX)
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.

java — debounced 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

java — CANcoder (swerve steering absolute encoder)
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

java — Pigeon 2 (Phoenix 6)
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

java — dashboard logging in periodic()
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 logWhy 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 booleansConfirm state machine transitions happen correctly
Sensor readings (encoder pos, gyro heading)Debug odometry and positioning in auto
Active command nameKnow 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.

java — DataLog setup (in Robot.java)
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

Build a Shooter Subsystem
TalonFX + Phoenix 6 signals + SmartDashboard

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 :)

📋
O4 weekly test
TalonFX, SparkMax, CANcoder, Pigeon 2, SmartDashboard · 8 questions