Subsystems
FrcCatalyst provides three complex subsystem wrappers that integrate multiple components.
Table of contents
SwerveSubsystem
Wraps CTRE Tuner X generated swerve code. Teams generate their drivetrain using Tuner X, then wrap it with SwerveSubsystem to get:
- Field-centric and robot-centric drive commands
- Heading lock — auto-holds heading when driver isn’t rotating
- Point-at-target — always face a scoring target while translating
- Drive-with-heading — lock to a specific heading angle
- PathPlanner integration — one-line AutoBuilder configuration
- Vision pose estimation —
addVisionMeasurement()bridge - Automatic telemetry — pose, heading, speed to NetworkTables
- Skew correction — pose exponential discretization
- Slew rate limiting — smooth acceleration with asymmetric profiles
- Snap-to-angle — auto-snap heading to predefined angles
- Advanced drive — combined deadband, slew, heading lock, skew correction, snap-to-angle
- Slow mode — toggleable speed multiplier for precision
- Auto-align — drive while auto-rotating to a target heading
SwerveSubsystem drive = new SwerveSubsystem(
TunerConstants.createDrivetrain(),
4.5,
SwerveSubsystem.PathPlannerConfig.builder()
.translationPID(5.0, 0, 0)
.rotationPID(5.0, 0, 0)
.build()
);
// Advanced drive (recommended default command)
// Combines deadband, slew limiting, heading lock, snap-to-angle, and skew correction
drive.setSkewCorrectionEnabled(true);
drive.enableSlewRateLimiting(2.0, 5.0); // accel, decel
drive.setSnapToAngles(List.of(0.0, 90.0, 180.0, 270.0), 5.0);
drive.setDefaultCommand(drive.advancedDrive(
() -> -driver.getLeftY(),
() -> -driver.getLeftX(),
() -> -driver.getRightX(),
0.05
));
// Slow mode for precision alignment
driver.leftBumper().whileTrue(drive.slowModeWhileHeld(0.3));
// Point at speaker while driving
driver.rightBumper().whileTrue(drive.pointAtTarget(
() -> -driver.getLeftY(),
() -> -driver.getLeftX(),
() -> new Translation2d(0.0, 5.55),
0.05
));
See the Advanced Features section for detailed documentation on skew correction, slew rate limiting, snap-to-angle, and auto-align.
PathPlanner support
SwerveSubsystem configures PathPlanner’s AutoBuilder for you when you pass a PathPlannerConfig (as above). It wires:
getPose/resetPose— pose source + resetgetChassisSpeeds— robot-relative speeds (what PathPlanner expects)- a robot-relative
ChassisSpeedsconsumer for path output - a
PPHolonomicDriveControllerfrom your translation/rotation PID RobotConfig.fromGUISettings()— mass, MOI, module config from the PathPlanner GUI- alliance flipping (mirrors paths for the red alliance automatically)
Once configured, named autos load with one call:
autonomousCommand = AutoBuilder.buildAuto("MyAuto");
// or follow a single path:
new PathPlannerAuto("MyAuto").schedule();
Both pathfindToPose(...) (below) and the behavior framework autos build on this. If AutoBuilder isn’t configured (you didn’t pass a PathPlannerConfig), pathfinding commands fall back to PID-only and print a DS error rather than crashing.
Robot- vs field-relative:
getChassisSpeeds()is robot-relative for PathPlanner. For Shoot-On-The-Fly usegetFieldRelativeSpeeds(), which rotates it into the field frame.
pathfindToPose (v0.3.6.1+)
Pathfind to a pose with PathPlanner’s AutoBuilder.pathfindToPose, then hand off to the existing precision-align PID for the last metre.
swerve.pathfindToPose(() -> ScoringPoses.RED_L2); // defaults: kP=4.0, tolerance=2 cm
swerve.pathfindToPose(() -> target, 5.0, 0.015); // tighter tolerance
swerve.pathfindToPose(() -> target, new PathConstraints(3, 2, …)); // your own constraints
If AutoBuilder isn’t configured the command falls back to PID-only align and prints a driver-station error explaining why — no crash.
Choreo paths (v0.8.0+)
Follow Choreo’s time-optimal trajectories — loaded through PathPlanner, so no extra vendordep (Choreo exports, PathPlanner follows). Put the .traj files in src/main/deploy/choreo/:
autonomousCommand = swerve.followChoreoPath("FourPieceFar");
Needs AutoBuilder configured (the PathPlannerConfig constructor). If the trajectory can’t be loaded it reports to the driver station and returns a no-op instead of crashing.
driveToPiece — vision pursuit (v0.8.0+)
The primitive the Autopilot “acquire” action wants. Give it a supplier of the detected piece’s field position (empty when your coprocessor sees nothing) and it drives onto it:
swerve.driveToPiece(() -> vision.nearestFuelPose()); // defaults kP=3, tol=0.2m
swerve.driveToPiece(() -> vision.nearestFuelPose(), 4.0, 0.15);
Stops when it arrives or the piece disappears. vision.nearestFuelPose() is your team-side detection method (Supplier<Optional<Translation2d>>).
WheelRadiusCalibration (v0.8.0+)
Measures the actual wheel radius by spinning the robot — correcting the CAD value, a documented source of autonomous inaccuracy (tread wears and compresses). Compares the gyro arc to the distance odometry thinks each wheel rolled:
test.a().onTrue(WheelRadiusCalibration.builder(swerve)
.currentWheelRadius(0.0508) // your configured radius (m)
.driveBaseRadius(0.42) // center → module distance (m)
.rotations(4)
.build());
The corrected radius + a copy-paste constant publish to /Catalyst/Calibration/WheelRadius/....
SwerveSetpointGenerator (v0.4.0+)
Light chassis-aware accel/skid clamp. Wraps a ChassisSpeeds and returns one limited by max wheel speed, max angular rate, and a per-second delta-v cap.
SwerveSetpointGenerator gen = new SwerveSetpointGenerator(
drive.getMaxSpeedMPS(), drive.getMaxAngularRate(), 8.0); // 8 m/s² accel cap
ChassisSpeeds limited = gen.generate(requestedSpeeds);
drivetrain.setControl(req.withVelocityX(limited.vxMetersPerSecond)
.withVelocityY(limited.vyMetersPerSecond)
.withRotationalRate(limited.omegaRadiansPerSecond));
Catches the most common driver-induced skid (jerking the stick from full-forward to full-right) without doing a full per-wheel feasibility solve. Cheap.
VisionSubsystem
Multi-camera pose estimation with Kalman filter integration. Supports both Limelight (MegaTag2) and PhotonVision cameras simultaneously.
Features:
- Distance-scaled standard deviations — trusts close targets more
- Ambiguity-scaled std devs — higher ambiguity = less trust
- Spin rejection — ignores vision during fast rotation
- High-speed rejection — ignores vision while driving fast
- Heading divergence filtering — rejects single-tag poses that disagree with the gyro
- Kalman innovation tracking — logs innovation norms for tuning
- Latency filtering — rejects stale measurements
- Configurable field bounds — custom field dimensions for bounds checking
- Per-cycle telemetry — see which estimates are accepted/rejected with reasons
VisionSubsystem vision = new VisionSubsystem(
VisionConfig.builder()
.driveSubsystem(drive) // wires pose fusion
.addLimelight("limelight-front", frontCameraPose)
.addPhotonCamera("cam-rear", rearCameraPose, fieldLayout) // Photon needs the tag layout
.singleTagStdDevs(4, 8)
.multiTagStdDevs(0.5, 1)
.xyDistanceScaling(1.0)
.rotDistanceScaling(1.5)
.rejectDuringSpin(2.0)
.rejectDuringHighSpeed(3.0) // reject when > 3 m/s
.maxHeadingDivergence(15.0) // reject if heading disagrees > 15 deg
.fieldDimensions(16.54, 8.21) // custom field bounds
.maxLatency(0.5)
.build()
);
Pass 4+ cameras and they’re fused deterministically — each is filtered independently and added to the pose estimator in timestamp order with quality/index tiebreaks, so the result is reproducible run-to-run.
LimelightTriggers (v0.4.0+)
Wrap a Limelight’s NetworkTables keys as WPILib Triggers. Point at the table name, bind, done.
LimelightTriggers front = new LimelightTriggers("limelight-front");
front.hasTarget().onTrue(leds.solid(Color.kGreen));
front.tagInView(7).whileTrue(swerve.pathfindToPose(() -> SCORE_7));
front.detectorClass("note").onTrue(intake.intakeCommand());
front.horizontalErrorBelow(2.0).onTrue(rumble.fire(Pattern.DOUBLE_TAP, Channel.DRIVER));
front.targetWithinArea(2.0).onTrue(climber.armCommand());
| Trigger | Backed by |
|---|---|
hasTarget() | tv > 0.5 |
tagInView(int) | tv > 0.5 && tid == id |
detectorClass(String) | tv > 0.5 && tclass == name |
horizontalErrorBelow(double) | \|tx\| <= degrees |
targetWithinArea(double) | ta >= percent |
Plus diagnostic readers: tx(), ty(), ta(), tid(), latencyMs() for direct polling when you don’t want a trigger.
LEDSubsystem
Addressable LED pattern controller with 14 pre-built effects.
Basic patterns: solid, blink, rainbow, chase, breathe, alternating
Advanced patterns: fire, gradient, scrolling gradient, strobe, larson scanner, dynamic progress, status indicator, alignment indicator
LEDSubsystem leds = new LEDSubsystem(
LEDConfig.builder()
.port(0)
.length(60)
.build()
);
// Alliance color by default
leds.setDefaultCommand(leds.solid(Color.kBlue));
// Rainbow when scoring
scoring.whileTrue(leds.rainbow());
// Blink green when game piece acquired
intake.hasPieceTrigger().whileTrue(leds.blink(Color.kGreen, 0.1));
// Fire effect for celebration
scoring.whileTrue(leds.fire());
// Alignment indicator for driver (Color + 0..1 progress supplier)
aligning.whileTrue(leds.alignmentIndicator(Color.kGreen, () -> alignProgress));
// Progress bar for elevator height
leds.dynamicProgress(Color.kGreen, () -> elevator.getPosition() / 1.2);
See the Advanced Features section for details on all new LED patterns.