Utilities

FrcCatalyst includes a comprehensive set of utilities commonly needed in FRC programming.

Table of contents

  1. FeedforwardGains
  2. TrapezoidProfileHelper
  3. AlertManager
  4. CharacterizationHelper
  5. MechanismVisualizer
  6. CatalystMath
  7. InterpolatingTable
  8. SlewRateLimiter
  9. MovingAverage
  10. TimedBoolean
  11. HealthCheck + HealthMonitor
  12. HealthHistory
  13. BrownoutMonitor
  14. CAN bus optimization
  15. RobotSafety
  16. MotorType
  17. RobotState
  18. CANRegistry
  19. TunableNumber + TunableGains
  20. Advanced Utilities

FeedforwardGains

Stores and calculates feedforward voltages for different mechanism types. Create them from your SysId results or use manual estimates.

// From SysId results:
FeedforwardGains elevatorFF = FeedforwardGains.elevator(0.12, 2.5, 0.1, 0.35);
FeedforwardGains armFF = FeedforwardGains.arm(0.15, 1.8, 0.05, 0.5);
FeedforwardGains flywheelFF = FeedforwardGains.simple(0.12, 0.11);

// Calculate voltages:
double holdVoltage = elevatorFF.calculateElevator();             // static hold
double moveVoltage = elevatorFF.calculateElevator(1.5);          // at 1.5 m/s
double armHold = armFF.calculateArm(Math.toRadians(45));         // at 45 degrees
double shootVoltage = flywheelFF.calculateSimple(70.0);          // at 70 RPS

TrapezoidProfileHelper

Factory methods for WPILib ProfiledPIDController — saves you from remembering the constraint constructors:

// Linear mechanism profile (meters)
ProfiledPIDController elevatorPID = TrapezoidProfileHelper.createLinear(
    50, 0, 0.5,   // PID gains
    2.0, 4.0      // max velocity (m/s), max accel (m/s^2)
);

// Rotational mechanism profile (degrees)
ProfiledPIDController armPID = TrapezoidProfileHelper.createRotational(
    80, 0, 1.0,   // PID gains
    1.5, 3.0      // max velocity (rot/s), max accel (rot/s^2)
);

// Continuous rotation (turret, 360-degree wrapping)
ProfiledPIDController turretPID = TrapezoidProfileHelper.createContinuousRotational(
    40, 0, 0.5, 2.0, 4.0
);

AlertManager

Centralized fault and warning system. Publishes to NetworkTables under Catalyst/Alerts/ so you can monitor robot health on the dashboard.

AlertManager alerts = AlertManager.getInstance();

// Report faults from anywhere in your code
alerts.error("Elevator", "Motor overtemp cutoff at 85C!");
alerts.warning("Intake", "Stall detected - game piece stuck?");
alerts.info("Vision", "No AprilTags visible");

// Check and clear faults
if (alerts.hasErrors()) {
    // handle errors
}
alerts.clearSubsystem("Elevator");
alerts.clearAll();

CharacterizationHelper

One-line SysId characterization setup. Pass the mechanism and its motor — the helper creates all four SysId routines automatically:

CharacterizationHelper charHelper = new CharacterizationHelper(
    "Elevator",          // name for SysId logging
    elevator,            // subsystem (extends SubsystemBase)
    elevator.getMotor()  // CatalystMotor for voltage control
);

// Four commands for the SysId routine:
SmartDashboard.putData("QS Fwd", charHelper.quasistaticForward());
SmartDashboard.putData("QS Rev", charHelper.quasistaticReverse());
SmartDashboard.putData("Dyn Fwd", charHelper.dynamicForward());
SmartDashboard.putData("Dyn Rev", charHelper.dynamicReverse());

MechanismVisualizer

Dashboard visualization using WPILib’s Mechanism2d. Creates a canvas with elevator and arm visualizations for real-time monitoring:

// Create a canvas (name, width in meters, height in meters)
MechanismVisualizer viz = new MechanismVisualizer("Robot", 1.0, 2.0);

// Add an elevator visualization
// (name, rootX, rootY, maxHeight, color)
var elevatorViz = viz.addElevator("Elevator", 0.5, 0.0, 1.2, Color.kBlue);

// Add an arm on top of the elevator
// (name, rootX, rootY, length, color)
var armViz = viz.addArm("Arm", 0.5, 0.0, 0.5, Color.kRed);

// In periodic: update positions
elevatorViz.setLength(elevator.getPosition());
armViz.setAngle(arm.getAngle());

CatalystMath

Joystick processing, angle math, and physics estimation helpers:

// Complete joystick processing pipeline
double output = CatalystMath.processJoystick(
    joystick.getY(), // raw input
    0.05,            // deadband
    2.0,             // square curve exponent
    0.7              // 70% max speed (slow mode)
);

// Individual operations
double dead = CatalystMath.deadband(0.03, 0.05);   // 0.0 (inside deadband)
double sq = CatalystMath.squareInput(0.5);          // 0.25 (sign preserved)
double cb = CatalystMath.cubeInput(0.5);            // 0.125

// Angle math (all in degrees)
double normalized = CatalystMath.normalizeAngle(370);     // -170 ([-180, 180])
double diff = CatalystMath.angleDifference(90, 270);      // -180
boolean close = CatalystMath.angleWithinTolerance(89, 90, 2); // true

// Physics estimation
double kG = CatalystMath.elevatorGravityFF(5.0, 0.0254, 10.0, 7.09);
double armkG = CatalystMath.armGravityFF(3.0, 0.5, 50.0, 7.09, 45.0);

InterpolatingTable

TreeMap-based linear interpolation for shooter distance tables. Uses method chaining with add():

InterpolatingTable shooterTable = new InterpolatingTable()
    .add(1.0, 3000)   // 1m -> 3000 RPM
    .add(2.0, 3500)   // 2m -> 3500 RPM
    .add(3.0, 4200)   // 3m -> 4200 RPM
    .add(5.0, 5000);  // 5m -> 5000 RPM

double rpm = shooterTable.get(2.5); // interpolates to ~3850 RPM
double clamped = shooterTable.get(0.5); // clamps to 3000 (below min key)

SlewRateLimiter

Asymmetric rate limiter with different acceleration and deceleration profiles. Great for smooth joystick response:

// Different accel vs brake rates
SlewRateLimiter limiter = new SlewRateLimiter(
    3.0,  // max acceleration (units/sec)
    5.0   // max deceleration (units/sec) - brake harder than accelerate
);

// Symmetric rate limit
SlewRateLimiter symmetric = new SlewRateLimiter(4.0);

// In periodic:
double smoothed = limiter.calculate(targetSpeed);
double current = limiter.get(); // read current output

// Reset when needed
limiter.reset(0.0);

MovingAverage

Sliding window average filter for noisy sensor data. Use calculate() to add a sample and get the running average:

MovingAverage filter = new MovingAverage(10); // 10-sample window

// In periodic:
double smooth = filter.calculate(noisySensorValue); // add + get average
double current = filter.get();                       // read current average

// Status checks
boolean full = filter.isFull();     // true when window is filled
int count = filter.getCount();      // number of samples added

// Clear and start over
filter.reset();

TimedBoolean

Debounced boolean with configurable duration threshold. The condition must be sustained for the specified duration before the output goes true:

TimedBoolean stallDetector = new TimedBoolean(0.2); // 200ms threshold

// In periodic:
boolean isStalled = stallDetector.update(current > 30.0);

// Edge detection (great for triggering one-shot actions):
boolean justStalled = stallDetector.risingEdge(current > 30.0);
boolean justCleared = stallDetector.fallingEdge(current > 30.0);

// Manual control
stallDetector.reset();
boolean state = stallDetector.get();

HealthCheck + HealthMonitor

The fault-monitoring kit added in v0.3.3 wires a debounced health check layer into every Catalyst mechanism by default. Every motor-driven mechanism automatically registers OverCurrent, HighTemp, and OverTemp checks; the OverTemp check auto-calls motor.stop() when it fires. Teams add their own with one fluent call:

HealthCheck.builder("Climber", "RopeSlack")
    .severity(HealthCheck.Severity.WARN)
    .description("Climber rope slack detected")
    .when(() -> tensionSensor.getNewtons() < 5)
    .detail(() -> String.format("%.0f N", tensionSensor.getNewtons()))
    .debounce(0.5)
    .clearAfter(2.0)
    .onFire(() -> driver.rumble(0.5))
    .register();

State publishes to /Catalyst/Health/<subsystem>/<id>/... for any NT dashboard, and every fire/clear edge is forwarded to the legacy AlertManager so existing integrations keep working. Full guide: Health Monitoring.

HealthHistory

Ring buffer of recent fire/clear events (default 100). Automatically fed by HealthMonitor and published as a string array at /Catalyst/Health/History for the Health Dashboard’s timeline view. Queryable from team code for post-match triage:

for (HealthHistory.Event e : HealthHistory.snapshot()) {
    System.out.println(e);   // newest first
}

HealthHistory.setCapacity(250);  // bigger buffer for long matches

Each Event carries timestamp, subsystem, id, severity, kind (FIRED / CLEARED), and the live detail string at the moment of transition.

BrownoutMonitor

Predicts an oncoming brownout from I × R_internal so you can act before the radio drops. Passive by default — it estimates and publishes to /Catalyst/Brownout/... but takes no action until you opt in.

// Passive — just watch the prediction:
BrownoutMonitor brownout = BrownoutMonitor.builder()
    .totalCurrent(() -> pdh.getTotalCurrent())
    .batteryInternalResistance(0.020)   // estimate from match logs (ΔV / ΔI)
    .build();
brownout.update();   // outputScale() stays 1.0, nothing trips

The two aggressive behaviours — output throttling and a preemptive RobotSafety trip — are off by default and genuinely aggressive. Throttling cuts your robot’s power mid-match the instant predicted voltage dips; a too-high warnVoltage or too-low R can throttle a healthy robot during a hard accel. Test thoroughly with realistic loads and tune R from your own logs before enabling either.

// Opt in only after testing:
BrownoutMonitor active = BrownoutMonitor.builder()
    .totalCurrent(() -> pdh.getTotalCurrent())
    .batteryInternalResistance(0.020)
    .warnVoltage(7.2).tripVoltage(6.9)
    .enableThrottling()       // outputScale() now ramps below warn
    .tripsRobotSafety(true)   // trips RobotSafety at tripVoltage
    .build();

brownout.update();
double scale = active.outputScale();   // 1.0 healthy → 0 near the floor
drive.driveFieldCentric(vx * scale, vy * scale, omega * scale);

CAN bus optimization

CatalystMotor.Builder.optimizeCanBus() raises only the status signals Catalyst reads (position, velocity, voltage, currents, temp, faults) to a set rate, then stops every other signal on the device from broadcasting — a real bus-utilization win on high-device-count robots. Opt-in (default off) so it never starves a signal you depend on.

CatalystMotor.builder(10)
    .optimizeCanBus()        // 50 Hz on the used signals, the rest silenced
    // or .optimizeCanBus(100)
    .build();

RobotSafety

Opt-in cross-mechanism watchdog driven by HealthMonitor. When too many ERROR-severity (or, optionally, WARN) health checks fire simultaneously the safety layer “trips” — a single boolean teams can read to bail out of teleop / auto. Catalyst never forcibly disables motors itself; the trip is advisory so each team decides what “all-stop” means for their robot.

public void robotInit() {
    RobotSafety.configure(
        RobotSafety.Config.builder()
            .maxConcurrentErrors(2)   // trip when 2+ ERROR checks fire at once
            .debounce(0.25)           // sustained for at least 0.25 s
            .onTrip(() -> {
                drive.stop();
                superstructure.stow();
                leds.fire();
            })
            .build());
}

// Read the signal anywhere:
RobotSafety.trippedTrigger().onTrue(drive.stopCommand());

// Or in any subsystem:
if (RobotSafety.isTripped()) { /* ... */ }

State publishes to /Catalyst/Safety/{Tripped,Reason,ErrorCount,WarnCount} so the Health Dashboard lights up when it fires. Manual RobotSafety.reset() clears the trip; or pass .autoReset(seconds) to the builder.

MotorType

Spec sheet for an FRC motor — stall torque, free speed, current draw — used by simulation models, MotionConstraintCalculator, and gravity feedforward helpers. Pre-shipped presets:

Preset Notes
KRAKEN_X60, KRAKEN_X60_FOC FOC variant gets the proper +30% stall torque
KRAKEN_X44, KRAKEN_X44_FOC Compact Kraken (added in v0.3.3)
FALCON_500, FALCON_500_FOC Phoenix-Pro FOC variant available
NEO, NEO_VORTEX, NEO_550 REV brushless line (sim/physics only — not Phoenix-controllable)
MINION WCP Minion
LinearMechanism.Config.builder()
    .motorType(MotorType.KRAKEN_X60_FOC)
    .follower(14, true)        // second motor — count is leader + followers
    // ...
    .build();

MotorType is a regular final class (not an enum) with a public constructor, so teams can ship their own preset for anything Catalyst doesn’t include:

MotorType custom = new MotorType(
    "Custom Motor",
    3.2,    // stall torque (Nm)
    7200,   // free speed (RPM)
    230,    // stall current (A)
    1.6);   // free current (A)

⚠️ The pre-v0.3.3 FOC presets had the same stall torque as their non-FOC counterparts (Phoenix-6 FOC actually delivers ~30% more torque). If you used KRAKEN_X60_FOC or FALCON_500_FOC for gravity feedforward in 0.3.2 or earlier and hand-tuned kG, re-check the value after upgrading.

RobotState

A singleton view of “what’s the robot doing right now?” — alliance, match time, mode, battery voltage, time since enable. Cached for 5 ms so re-reading from multiple subsystems is free, and exposes ready-to- bind triggers for the common end-game patterns.

if (RobotState.isAutonomous()) { ... }
if (RobotState.isRed())        { mirrorPoseForRed(); }
if (RobotState.matchTimeRemaining() < 10) { climber.runDefault(); }

// As triggers — best for command bindings:
RobotState.lateMatch(20).onTrue(climber.deployCommand());
RobotState.lowBattery(11.0).onTrue(RobotSafety.tripCommand("low battery"));
RobotState.disabled().onTrue(LedSubsystem.idlePattern());
Field Method
Mode isAutonomous(), isTeleop(), isTest(), isDisabled(), isEnabled(), isEStopped()
Alliance alliance(), allianceOpt(), isRed(), isBlue(), stationLocation()
Match matchTimeRemaining(), timeSinceEnable()
Power batteryVoltage()
Triggers autonomous(), teleop(), disabled(), enabled(), lateMatch(s), lowBattery(v)

Refresh is lazy. To force a re-read in a test, call RobotState.refresh().

CANRegistry

A process-wide registry of every CAN device the robot has claimed. Every Catalyst motor (and any CANcoder it fuses / syncs / reads) calls CANRegistry.register(...) automatically when its builder runs. If two devices try to claim the same (bus, canId) with different names, the registry throws a CANConflictException with both sides of the collision named. Identical re-registrations are silently idempotent.

// Manual registration — usually you let mechanisms do it, but useful
// when you control raw hardware outside the library.
CANRegistry.register("ShooterTop", 30, "rio", "Kraken X60");

// Look up what's on a given id:
CANRegistry.lookup(30, "rio").ifPresent(e ->
    System.out.println(e.name() + " (" + e.type() + ")"));

// Get the full plan (sorted by bus then id):
for (var e : CANRegistry.all()) System.out.println(e);

The full plan is also published at /Catalyst/CAN/Devices as a pipe-delimited string array for the Health Dashboard.

The most ergonomic way to populate it is to use the CAN ID Planner and click Generate Catalyst Java. The exported CANIds.java looks like this:

public final class CANIds {
    public static final String CANIVORE = "canivore";
    public static final String RIO      = "";

    public static final int FRONT_LEFT_DRIVE  = 1;
    public static final int FRONT_LEFT_STEER  = 2;
    // ...

    static {
        CANRegistry.register("FrontLeftDrive", FRONT_LEFT_DRIVE, CANIVORE, "Kraken X60");
        CANRegistry.register("FrontLeftSteer", FRONT_LEFT_STEER, CANIVORE, "Kraken X60");
        // ...
    }
    public static void init() {}
}

Call CANIds.init() once from Robot.robotInit() and any wiring mistake — a missing device, a wrong name, a duplicate id — surfaces immediately at boot instead of mid-match.

TunableNumber + TunableGains

Live-tunable doubles backed by NetworkTables. TunableNumber is a single double with a known default; TunableGains bundles every Slot 0 gain + Motion Magic constant for a mechanism. Both are internally cached so reading is cheap. See Live Tuning for the full story.

TunableNumber shooterRPS = new TunableNumber("Catalyst/Tuning/Shooter/TargetRPS", 60.0);
shooter.spinUp(shooterRPS.get());

// For comp: lock everything in one call.
TunableNumber.disableTuning();

Advanced Utilities

FrcCatalyst also includes advanced utilities that successful teams build in-house every season. See the Advanced section for:

Utility Description
StateSpaceController LQR + Kalman filter for optimal mechanism control
MotionConstraintCalculator Physics-based max velocity/acceleration from motor specs
SignalProcessor EMA, median, low-pass, composite filters for sensor data
PoseHistory Temporal pose tracking with interpolation
DynamicAutoBuilder Runtime path generation with PathPlanner

Back to top

Built for the FRC community. Go build something awesome.

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