SysId

Table of contents

  1. Overview
  2. Setup
  3. Binding the routines
  4. Pulling the data into WPILib SysId
  5. Custom routines
  6. Mechanisms that work out of the box
  7. Workflow tips

Overview

Every Catalyst mechanism gets WPILib SysId for free. v0.4.0 wires the WPILib SysIdRoutine into CatalystMotor and exposes zero-argument helpers on CatalystMechanism so each mechanism’s primary motor is characterizable with a single command bind.

The data capture goes through Phoenix-6’s SignalLogger — there’s no per-signal logging boilerplate to write. Teams just turn the logger on once at robot init and bind the routine to a button.


Setup

One line in Robot.robotInit() is everything the library needs:

import com.ctre.phoenix6.SignalLogger;

@Override
public void robotInit() {
    SignalLogger.start();          // start capturing signals to .hoot
    container = new RobotContainer();
}

By default SignalLogger writes a .hoot file to the USB stick mounted on the roboRIO (or to /home/lvuser/logs/ if no USB is present).


Binding the routines

Each mechanism exposes both routines as Commands. Bind them to buttons that the driver only uses in characterization sessions:

import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine.Direction;

// Elevator
driver.povUp()    .whileTrue(elevator.sysIdQuasistatic(Direction.kForward));
driver.povDown()  .whileTrue(elevator.sysIdQuasistatic(Direction.kReverse));
driver.povRight() .whileTrue(elevator.sysIdDynamic(Direction.kForward));
driver.povLeft()  .whileTrue(elevator.sysIdDynamic(Direction.kReverse));

Run each routine until the mechanism reaches its soft limit (or you back off the button). Repeat for each mechanism you want to characterize.

⚠️ The SysId routines drive the motor with voltage and bypass every closed-loop controller. Make sure you have room above and below the current position, and use the soft limits in your config — those still apply.


Pulling the data into WPILib SysId

The Phoenix tools ship a converter that turns the .hoot file into the WPILib SysId JSON format:

java -jar phoenix-tools.jar hoot2sysid <input>.hoot <output>.json

Then open WPILib SysId, load the JSON, and read off the gains. Drop those into your Catalyst config:

LinearMechanism.Config.builder()
    // ...
    .feedforward(kS, kV, kA)
    .gravityGain(kG)
    .pid(kP, 0, kD)
    // ...
    .build();

Re-deploy and you have a properly characterized mechanism.


Custom routines

The defaults are 1 V/s ramp, 4 V dynamic step, 10 s timeout. Override when you need to:

import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine;
import edu.wpi.first.units.Units;

SysIdRoutine.Config slowRoutine = new SysIdRoutine.Config(
    Units.Volts.of(0.5).per(Units.Seconds.of(1)),  // 0.5 V/s ramp
    Units.Volts.of(3.0),                            // 3 V dynamic step
    Units.Seconds.of(8.0),                          // 8 s timeout
    state -> SignalLogger.writeString("State", state.toString()));

SysIdRoutine routine = elevator.getMotor().sysIdRoutine(elevator, slowRoutine);

driver.povUp().whileTrue(routine.quasistatic(Direction.kForward));

Useful when the default 4 V step would slam the mechanism into a hard stop too fast.


Mechanisms that work out of the box

Mechanism Primary motor used for SysId
LinearMechanism Master + every follower
RotationalMechanism Master + every follower
FlywheelMechanism Primary wheel + its followers
RollerMechanism Master + every follower
WinchMechanism First motor only (use the underlying second motor’s routine separately for dual-arm climbers)
ClawMechanism Master + every follower
DifferentialWristMechanism Override required — diff control means each motor needs separate characterization
PneumaticMechanism N/A — no continuous actuator

For mechanisms not in the table (your own subclass of CatalystMechanism), override primaryMotorForSysId() to return the motor you want characterized:

@Override
protected CatalystMotor primaryMotorForSysId() {
    return master;
}

Workflow tips

  • Characterize once per season. Gains drift with belt wear and battery age but not enough to matter mid-season.
  • Disable health checks during the run. The over-current and over-temp checks may trip during the dynamic step. Either temporarily raise the limits or expect a clean firing/clearing log.
  • Keep TunableNumber.disableTuning() off during characterization — the live PID tuner can’t hurt anything, the SysId routine doesn’t use PID at all.
  • Use a marker button. Have an operator hit a separate button to call SignalLogger.writeString("Marker", "run-start") so you can find each run easily when you split the data.

Back to top

Built for the FRC community. Go build something awesome.

This site uses Just the Docs, a documentation theme for Jekyll.