Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Java vendor dependencies are read directly out of `vendordeps/*.json` at configu
time, so the Java and C++ builds always consume the same vendor versions. Adding or
bumping a vendordep JSON is all that is needed — there is no second list to update.

Currently: WPILib New Commands, CTRE Phoenix 6, PhotonLib and maple-sim.
Currently: WPILib New Commands, CTRE Phoenix 6 and PhotonLib.

### Robot projects install these themselves

Expand All @@ -137,8 +137,8 @@ A path following library is the robot's own choice and its own vendordep. Nothin
library needs one, so a robot that follows paths with BLine does not install PathPlanner,
and the other way round.

maple-sim is simply the newest entry on that list, and the one a robot set up before 2026
will not have. Without it, simulation fails immediately. A real robot most likely runs
anyway, because everything touching `org.ironmaple` sits behind a `RobotBase.isSimulation()`
check and the JVM resolves classes lazily — but that is how HotSpot happens to behave
rather than a guarantee, and it has not been tested.
maple-sim used to be on that list and no longer is: the library does not simulate the
drivetrain any more. A simulator that drives the devices themselves, such as WPI Emulator,
runs the unmodified robot code, and a second simulation inside the library would fight it
over the same motor and gyro states. A robot that still wants maple-sim installs it and
wires it up in its own project.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:2.19.2"
implementation 'edu.wpi.first.thirdparty.frc2025.opencv:opencv-java:4.10.0-3'

// WPILibNewCommands, Phoenix 6, PhotonLib and maple-sim
// WPILibNewCommands, Phoenix 6 and PhotonLib
vendordepJava.deps.each {implementation it}

testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
Expand Down
45 changes: 37 additions & 8 deletions documentation/swerve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ them as public methods:
| Somewhere to send speeds | `chassis::setTargetSpeeds` | Robot relative |
| The subsystem it requires | `chassis` | |

**`resetPose` or `resetOdometry`?** A path follower resetting the pose means "the robot is
here now", so `resetPose` also places the simulated robot there. `resetOdometry` only
corrects the estimate, which is what a heading reset or a vision correction wants. Hand a
follower `resetOdometry` and autos still run on the real robot, but in simulation the
physics robot stays where it was while odometry jumps to the start of the auto.
**`resetPose` or `resetOdometry`?** Today they do the same thing, but they mean different
things. A path follower resetting the pose says "the robot is here now", while
`resetOdometry` is also what heading resets and vision corrections go through. Hand a
follower `resetPose`, so that anything that should happen on a placement and not on a
correction has one place to live.

Speeds sent to `setTargetSpeeds` go through the same path as the driver's: an active
`SpeedsHelper` still rewrites them, and they are discretized and desaturated before
Expand Down Expand Up @@ -57,7 +57,7 @@ They were compiled against PathPlannerLib 2026.1.2 and BLine-Lib v0.9.2.

| `settings.json` | Value | Same number in `Chassis.java` |
| --------------- | ----- | ----------------------------- |
| `robotMass` | 61.235 | `withRobotMass(Kilograms.of(61.235))` |
| `robotMass` | 61.235 | None: the chassis does not need the mass. Weigh the robot |
| `driveGearing` | 7.03 | `kDriveGearRatio` |
| `driveWheelRadius` | 0.0508 | `kWheelRadiusInches` (2 in) |
| `maxDriveSpeed` | 4.541 | `getMaxModuleSpeed()` |
Expand Down Expand Up @@ -153,7 +153,36 @@ robot written against those versions needs three changes when it updates:
2. **Add `PathPlannerConfig` and call `PathPlannerConfig.configure(chassis)`.** Without it
`AutoBuilder` is never configured, and PathPlanner refuses to build autos.
3. **Stop relying on `/PathPlanner/ResetPose`.** The library no longer publishes that
NetworkTables topic. It existed for the old external simulator; the built in simulation
is moved by `resetPose` directly.
NetworkTables topic. It existed for the old external simulator.

Robots that never ran an auto need only the first.

## Updating a robot from 2026.4.0-beta-4

2026.4.0-beta-4 simulated the drivetrain with maple-sim. That is gone: a simulator that
drives the devices themselves, such as WPI Emulator, runs the unmodified robot code, and a
second simulation inside the library fought it over the same motor and gyro states. A
robot written against beta-4 needs these changes:

1. **Delete `getSimulationConfig()` and `getSimulationPigeon()` from the chassis,** with
their `org.ironmaple` imports. They no longer override anything, so the `@Override` on
them stops compiling.
2. **Stop calling `getSimulatedPose()` and `resetSimulatedPose()`.** They are gone with the
simulation. `resetPose` stays, and is still the reset to hand a path follower.
3. **Remove the maple-sim vendordep,** unless the robot uses maple-sim on its own.

Outside a simulator the drivetrain no longer moves in simulation, because nothing feeds
the motors' simulated sensors.

Nothing else changes for the robot, but the drivetrain reads its sensors differently. The
chassis refreshes all four modules in one batched call per CAN bus at the start of its
`periodic()`, and a module owned by a chassis no longer reads its sensors again in its own
`periodic()`. A module's `getVoltageDrive()` returns the voltage as of that refresh. X mode
brakes through the drive requests rather than by rewriting the neutral mode config.

Telemetry moved, with no values lost. The per-module `/Swerve/Modules/<name>/State`,
`/TargetState` and `/Distance` topics are gone; the same values are in the
`/Swerve/Modules/States`, `/Swerve/Modules/TargetStates` and `/Swerve/Modules/Positions`
arrays, indexed front left, front right, back left, back right. Point any dashboard or
AdvantageScope layout that used the old names at the arrays. `RequestedVoltage` and
`AppliedVoltage` stay per module.
2 changes: 1 addition & 1 deletion documentation/swerve/examples/BLinePaths.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public Command follow(String pathName) {
* @return the command
*/
public Command resetAndFollow(String pathName) {
// resetPose rather than resetOdometry: it places the simulated robot there as well.
// resetPose rather than resetOdometry: placing the robot is a reset, not a correction.
return builder.withPoseReset(chassis::resetPose).build(new Path(pathName));
}
}
52 changes: 1 addition & 51 deletions documentation/swerve/examples/Chassis.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@

package frc.robot.Subsystems.Chassis;

import static edu.wpi.first.units.Units.Inches;
import static edu.wpi.first.units.Units.KilogramSquareMeters;
import static edu.wpi.first.units.Units.Kilograms;
import static edu.wpi.first.units.Units.Volts;

import com.ctre.phoenix6.CANBus;
import com.ctre.phoenix6.hardware.Pigeon2;
import com.ctre.phoenix6.signals.InvertedValue;
import com.overture.lib.sensors.OverPigeon;
import com.overture.lib.subsystems.swerve.SwerveChassis;
Expand All @@ -20,11 +14,7 @@
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.kinematics.SwerveDriveKinematics;
import edu.wpi.first.math.system.plant.DCMotor;
import edu.wpi.first.math.util.Units;
import org.ironmaple.simulation.drivesims.COTS;
import org.ironmaple.simulation.drivesims.configs.DriveTrainSimulationConfig;
import org.ironmaple.simulation.drivesims.configs.SwerveModuleSimulationConfig;

/**
* Shelby's drivetrain, from FRC-Shelby-2026OS, as an example of a concrete {@link SwerveChassis}.
Expand All @@ -37,7 +27,7 @@ public class Chassis extends SwerveChassis {
/** The CANivore carrying the drivetrain. */
private static final CANBus kCanBus = new CANBus("OverCANivore");

/** Drive reduction, shared by the real module config and the simulated drivetrain below. */
/** Drive reduction, shared by the four module configs below. */
private static final double kDriveGearRatio = 7.03;

/** Steer reduction, likewise shared. */
Expand Down Expand Up @@ -86,46 +76,6 @@ public Chassis() {
resetHeading();
}

/**
* Describes this drivetrain to the physics simulation.
*
* <p>The gearing constants are the same ones the module configs use below. That is the entire
* point: kept anywhere else, a drive ratio can sit at 6.03 against a robot geared 7.03 without
* anyone noticing.
*/
@Override
protected DriveTrainSimulationConfig getSimulationConfig() {
return DriveTrainSimulationConfig.Default()
.withRobotMass(Kilograms.of(61.235))
.withGyro(COTS.ofPigeon2())
.withSwerveModule(
new SwerveModuleSimulationConfig(
DCMotor.getKrakenX60(1),
DCMotor.getFalcon500(1),
kDriveGearRatio,
kTurnGearRatio,
Volts.of(0.01),
Volts.of(0.01),
Inches.of(kWheelRadiusInches),
KilogramSquareMeters.of(0.03),
// Conservative tyre friction. The COTS value was flagged as abnormal by the
// simulation (~2.106); 0.9 is realistic for rubber on carpet.
0.9))
.withCustomModuleTranslations(
new Translation2d[] {
new Translation2d(Inches.of(kTrackXInches), Inches.of(kTrackYInches)),
new Translation2d(Inches.of(kTrackXInches), Inches.of(-kTrackYInches)),
new Translation2d(Inches.of(-kTrackXInches), Inches.of(kTrackYInches)),
new Translation2d(Inches.of(-kTrackXInches), Inches.of(-kTrackYInches))
})
.withBumperSize(Inches.of(30.5), Inches.of(31.5));
}

@Override
protected Pigeon2 getSimulationPigeon() {
return pigeon;
}

@Override
public double getMaxModuleSpeed() {
return 4.541;
Expand Down
2 changes: 1 addition & 1 deletion documentation/swerve/examples/PathPlannerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static void configure(Chassis chassis) {
AutoBuilder.configure(
chassis::getEstimatedPose,
// resetPose rather than resetOdometry. An auto resetting the pose means "the robot is
// here now", and resetPose places the simulated robot there as well.
// here now", which is a placement, not a correction.
chassis::resetPose,
chassis::getCurrentSpeeds,
// Robot relative. The parameter type is spelled out to pick the overload that takes the
Expand Down
45 changes: 25 additions & 20 deletions src/main/java/com/overture/lib/motorcontrollers/OverTalonFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,27 @@ public OverTalonFX(TalonFXConfiguration config, int id, CANBus bus) {
}

/**
* Sets the sensor to mechanism ratio of the TalonFX.
* Sets the sensor to mechanism ratio of the TalonFX. Writes the feedback configs.
*
* @param gearRatio the gear ratio between the sensor and the mechanism
*/
public void setSensorToMechanism(double gearRatio) {
ctreConfig.Feedback.withSensorToMechanismRatio(gearRatio);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.Feedback);
}

/**
* Sets the rotor to sensor ratio of the TalonFX.
* Sets the rotor to sensor ratio of the TalonFX. Writes the feedback configs.
*
* @param gearRatio the gear ratio between the rotor and the sensor
*/
public void setRotorToSensorRatio(double gearRatio) {
ctreConfig.Feedback.withRotorToSensorRatio(gearRatio);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.Feedback);
}

/**
* Uses a remote CANcoder as the feedback source.
* Uses a remote CANcoder as the feedback source. Writes the feedback configs.
*
* @param id the device id of the remote CANcoder
*/
Expand All @@ -73,11 +73,11 @@ public void setRemoteCANCoder(int id) {
.Feedback
.withFeedbackRemoteSensorID(id)
.withFeedbackSensorSource(FeedbackSensorSourceValue.RemoteCANcoder);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.Feedback);
}

/**
* Uses a fused CANcoder as the feedback source.
* Uses a fused CANcoder as the feedback source. Writes the feedback configs.
*
* @param id the device id of the fused CANcoder
*/
Expand All @@ -86,11 +86,11 @@ public void setFusedCANCoder(int id) {
.Feedback
.withFeedbackRemoteSensorID(id)
.withFeedbackSensorSource(FeedbackSensorSourceValue.FusedCANcoder);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.Feedback);
}

/**
* Uses a synced CANcoder as the feedback source.
* Uses a synced CANcoder as the feedback source. Writes the feedback configs.
*
* @param id the device id of the sync CANcoder
*/
Expand All @@ -99,21 +99,21 @@ public void setSyncCANCoder(int id) {
.Feedback
.withFeedbackRemoteSensorID(id)
.withFeedbackSensorSource(FeedbackSensorSourceValue.SyncCANcoder);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.Feedback);
}

/**
* Sets the closed loop torque ramp rate of the TalonFX.
* Sets the closed loop torque ramp rate of the TalonFX. Writes the closed loop ramp configs.
*
* @param rampSeconds the ramp period, in seconds
*/
public void setClosedLoopTorqueRamp(double rampSeconds) {
ctreConfig.ClosedLoopRamps.withTorqueClosedLoopRampPeriod(rampSeconds);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.ClosedLoopRamps);
}

/**
* Sets the torque current limits of the TalonFX.
* Sets the torque current limits of the TalonFX. Writes the torque current configs.
*
* @param peakForward the peak forward torque current, in amps
* @param peakBackward the peak reverse torque current, in amps
Expand All @@ -125,7 +125,7 @@ public void setTorqueCurrentLimit(double peakForward, double peakBackward, doubl
.withPeakForwardTorqueCurrent(peakForward)
.withPeakReverseTorqueCurrent(peakBackward)
.withTorqueNeutralDeadband(deadband);
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.TorqueCurrent);
}

/**
Expand Down Expand Up @@ -163,23 +163,25 @@ public void configureMotionMagic(double cruiseVelocity, double acceleration, dou
.withMotionMagicAcceleration(acceleration)
.withMotionMagicJerk(jerk);

getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.MotionMagic);
}

/**
* Configures the software limit switches of the TalonFX.
* Configures the software limit switches of the TalonFX. Writes the software limit switch
* configs.
*
* @param configs the software limit switch configuration
*/
public void configureSoftwareLimitSwitch(SoftwareLimitSwitchConfigs configs) {
// Cloned for the same reason as the slot 0 gains: the C++ took this parameter
// by value.
ctreConfig.withSoftwareLimitSwitch(configs.clone());
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.SoftwareLimitSwitch);
}

/**
* Sets the neutral mode, keeping the stored configuration in step with the device.
* Sets the neutral mode, keeping the stored configuration in step with the device. Writes the
* motor output configs.
*
* @param mode the neutral mode to apply
*/
Expand All @@ -188,10 +190,13 @@ public void configureNeutralMode(NeutralModeValue mode) {
getConfigurator().apply(ctreConfig.MotorOutput);
}

/** Makes the closed loop take the shortest path across the rotation wrap point. */
/**
* Makes the closed loop take the shortest path across the rotation wrap point. Writes the closed
* loop general configs.
*/
public void setContinuousWrap() {
ctreConfig.ClosedLoopGeneral.ContinuousWrap = true;
getConfigurator().apply(ctreConfig);
getConfigurator().apply(ctreConfig.ClosedLoopGeneral);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@ private StructSubscriber<Pose2d> simulatedDriveTrainPoseEntry() {
/**
* Tells the manager where the simulated robot is.
*
* <p>Pass {@code chassis::getSimulatedPose} when the physics runs in this process. The
* NetworkTables fallback below only exists for an external simulator publishing the AdvantageKit
* field pose, which is what this used to depend on; that path lags by roughly a tenth of a second
* and disappears entirely once the drivetrain simulates itself.
* <p>Pass a supplier of the true pose when something in this process knows it. The NetworkTables
* fallback below only exists for an external simulator publishing the AdvantageKit field pose,
* which is what this used to depend on; that path lags by roughly a tenth of a second.
*
* @param poseSupplier supplies the true robot pose, empty when it is not known yet
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
import edu.wpi.first.units.measure.Angle;
import edu.wpi.first.units.measure.AngularVelocity;
import edu.wpi.first.units.measure.Current;
import edu.wpi.first.wpilibj.RobotController;
import edu.wpi.first.wpilibj.Timer;
import java.util.ArrayList;
import java.util.List;
import org.ironmaple.simulation.motorsims.SimulatedBattery;

/**
* A simulated mechanism bolted to the robot.
Expand Down Expand Up @@ -88,10 +88,9 @@ protected final double getTimeStep() {
*/
protected final double getAppliedVolts() {
TalonFXSimState sim = motor.getSimState();
// maple-sim owns the battery model, and asking it directly works before the arena has stepped
// even once. The arena does mirror its result onto RoboRIO input voltage every sub-tick, but
// only once it is running, and a mechanism can be spinning before then.
sim.setSupplyVoltage(SimulatedBattery.getBatteryVoltage());
// The robot's own battery reading: a steady 12 V in a plain simulation, and the sagging battery
// of whatever simulator drives the HAL, so a mechanism slows with the rest of the robot.
sim.setSupplyVoltage(RobotController.getBatteryVoltage());
return sim.getMotorVoltage();
}

Expand Down
Loading
Loading