|
| 1 | +package com.team4099.robot2026.commands.drivetrain |
| 2 | + |
| 3 | +import com.team4099.lib.hal.Clock |
| 4 | +import com.team4099.robot2026.RobotContainer |
| 5 | +import com.team4099.robot2026.RobotContainer.hasAligned |
| 6 | +import com.team4099.robot2026.config.constants.DrivetrainConstants |
| 7 | +import com.team4099.robot2026.config.constants.ShooterConstants |
| 8 | +import com.team4099.robot2026.subsystems.drivetrain.Drive |
| 9 | +import com.team4099.robot2026.subsystems.superstructure.Superstructure |
| 10 | +import com.team4099.robot2026.subsystems.superstructure.shooter.Shooter |
| 11 | +import com.team4099.robot2026.subsystems.vision.Vision |
| 12 | +import com.team4099.robot2026.util.CustomLogger |
| 13 | +import edu.wpi.first.units.LinearVelocityUnit |
| 14 | +import edu.wpi.first.units.Units.Degrees |
| 15 | +import edu.wpi.first.units.Units.Meters |
| 16 | +import edu.wpi.first.units.Units.Seconds |
| 17 | +import edu.wpi.first.units.measure.LinearVelocity as WPILinearVelocity |
| 18 | +import edu.wpi.first.wpilibj.DriverStation |
| 19 | +import edu.wpi.first.wpilibj.RobotBase |
| 20 | +import edu.wpi.first.wpilibj2.command.Command |
| 21 | +import kotlin.math.PI |
| 22 | +import org.ironmaple.simulation.SimulatedArena |
| 23 | +import org.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly |
| 24 | +import org.team4099.lib.controller.PIDController |
| 25 | +import org.team4099.lib.geometry.Pose2d |
| 26 | +import org.team4099.lib.geometry.Pose3d |
| 27 | +import org.team4099.lib.kinematics.ChassisSpeeds |
| 28 | +import org.team4099.lib.units.Velocity |
| 29 | +import org.team4099.lib.units.base.inMeters |
| 30 | +import org.team4099.lib.units.base.inSeconds |
| 31 | +import org.team4099.lib.units.base.inches |
| 32 | +import org.team4099.lib.units.base.meters |
| 33 | +import org.team4099.lib.units.base.seconds |
| 34 | +import org.team4099.lib.units.derived.Radian |
| 35 | +import org.team4099.lib.units.derived.degrees |
| 36 | +import org.team4099.lib.units.derived.inDegrees |
| 37 | +import org.team4099.lib.units.derived.inRotation2ds |
| 38 | +import org.team4099.lib.units.derived.radians |
| 39 | +import org.team4099.lib.units.inMetersPerSecond |
| 40 | +import org.team4099.lib.units.perSecond |
| 41 | + |
| 42 | +/** |
| 43 | + * Aim for the HUB or to pass, depending on current position on the field and which option is legal. |
| 44 | + * This command does NOT activate the shooter; that is the responsibility of the superstructure's |
| 45 | + * state machine. |
| 46 | + * |
| 47 | + * Note: This command assumes a flat drivetrain on z = 0. |
| 48 | + * |
| 49 | + * Note: This command never ends. |
| 50 | + * |
| 51 | + * @param drivetrain |
| 52 | + * @see com.team4099.robot2026.subsystems.superstructure.shooter.Shooter.calculateLaunchData |
| 53 | + * @author Nathan Arega, Ryan Chung |
| 54 | + */ |
| 55 | +class AimCommand( |
| 56 | + private val drivetrain: Drive, |
| 57 | + private val vision: Vision, |
| 58 | +) : Command() { |
| 59 | + |
| 60 | + private val thetaPID: PIDController<Radian, Velocity<Radian>> |
| 61 | + |
| 62 | + private var timeout = -1.seconds |
| 63 | + private var startTime = -1.seconds |
| 64 | + |
| 65 | + private var startedInAuto = false |
| 66 | + |
| 67 | + private var convergedPose = Pose3d() |
| 68 | + |
| 69 | + init { |
| 70 | + thetaPID = |
| 71 | + PIDController( |
| 72 | + DrivetrainConstants.PID.TELEOP_THETA_PID_KP, |
| 73 | + DrivetrainConstants.PID.TELEOP_THETA_PID_KI, |
| 74 | + DrivetrainConstants.PID.TELEOP_THETA_PID_KD) |
| 75 | + |
| 76 | + thetaPID.enableContinuousInput(-PI.radians, PI.radians) |
| 77 | + } |
| 78 | + |
| 79 | + override fun initialize() { |
| 80 | + thetaPID.reset() |
| 81 | + |
| 82 | + hasAligned = false |
| 83 | + startTime = Clock.timestamp |
| 84 | + startedInAuto = DriverStation.isAutonomous() |
| 85 | + |
| 86 | + RobotContainer.isAligning = true |
| 87 | + } |
| 88 | + |
| 89 | + override fun execute() { |
| 90 | + val currentlyTrackedPose: Pose3d |
| 91 | + |
| 92 | + val expectedPose = drivetrain.pose + vision.lastTagVisionUpdate.robotTTargetTag |
| 93 | + |
| 94 | + if (convergedPose == Pose3d()) { |
| 95 | + val robotTExpected = drivetrain.pose.relativeTo(expectedPose) |
| 96 | + if (robotTExpected.x.absoluteValue < 3.inches && |
| 97 | + robotTExpected.y.absoluteValue < 3.inches && |
| 98 | + (robotTExpected.rotation.z < 3.degrees || |
| 99 | + robotTExpected.rotation.z > (-180.degrees + 3.degrees))) { |
| 100 | + currentlyTrackedPose = expectedPose |
| 101 | + convergedPose = expectedPose |
| 102 | + } else { |
| 103 | + currentlyTrackedPose = drivetrain.pose |
| 104 | + } |
| 105 | + } else { |
| 106 | + currentlyTrackedPose = convergedPose |
| 107 | + } |
| 108 | + |
| 109 | + CustomLogger.recordOutput("ActiveCommands/FaceHubCommand", true) |
| 110 | + |
| 111 | + val (distanceToHub, launchSpeed, timeOfFlight, wantedRotation) = |
| 112 | + Shooter.calculateLaunchData(currentlyTrackedPose.toPose2d(), ChassisSpeeds()) |
| 113 | + |
| 114 | + val thetaVel = thetaPID.calculate(currentlyTrackedPose.rotation.z, wantedRotation) |
| 115 | + |
| 116 | + CustomLogger.recordOutput("FaceHubCommand/thetaError", thetaPID.error.inDegrees) |
| 117 | + |
| 118 | + CustomLogger.recordOutput( |
| 119 | + "FaceHubCommand/wantedPose", |
| 120 | + Pose2d(currentlyTrackedPose.x, currentlyTrackedPose.y, wantedRotation).pose2d) |
| 121 | + |
| 122 | + hasAligned = thetaPID.error.absoluteValue < 3.degrees |
| 123 | + |
| 124 | + CustomLogger.recordOutput("FaceHubCommand/hasAligned", hasAligned) |
| 125 | + |
| 126 | + if (!hasAligned) { |
| 127 | + drivetrain.runSpeeds( |
| 128 | + ChassisSpeeds.fromFieldRelativeSpeeds( |
| 129 | + 0.meters.perSecond, 0.meters.perSecond, thetaVel, drivetrain.pose.rotation.z)) |
| 130 | + } else { |
| 131 | + drivetrain.stopWithX() |
| 132 | + } |
| 133 | + |
| 134 | + if (RobotBase.isSimulation() && |
| 135 | + (hasAligned && |
| 136 | + Clock.timestamp.inSeconds % 1 < 0.04 && |
| 137 | + RobotContainer.superstructure.currentState == |
| 138 | + Superstructure.Companion.SuperstructureStates.SCORE || |
| 139 | + DriverStation.isAutonomous())) { |
| 140 | + SimulatedArena.getInstance() |
| 141 | + .addGamePieceProjectile( |
| 142 | + RebuiltFuelOnFly( |
| 143 | + drivetrain.pose.translation.toTranslation2d().translation2d, |
| 144 | + ShooterConstants.SHOOTER_OFFSET.translation.translation2d, |
| 145 | + edu.wpi.first.math.kinematics.ChassisSpeeds.fromRobotRelativeSpeeds( |
| 146 | + drivetrain.chassisSpeeds.chassisSpeedsWPILIB, |
| 147 | + drivetrain.rotation.z.inRotation2ds), |
| 148 | + (drivetrain.pose.rotation.z + ShooterConstants.SHOOTER_OFFSET.rotation) |
| 149 | + .inRotation2ds, |
| 150 | + Meters.of(ShooterConstants.SHOOTER_HEIGHT.inMeters), |
| 151 | + WPILinearVelocity.ofBaseUnits( |
| 152 | + launchSpeed.inMetersPerSecond, LinearVelocityUnit.combine(Meters, Seconds)), |
| 153 | + Degrees.of(ShooterConstants.SHOOTER_ANGLE.inDegrees))) |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + override fun isFinished(): Boolean { |
| 158 | + return startedInAuto xor DriverStation.isAutonomous() |
| 159 | + } |
| 160 | + |
| 161 | + override fun end(interrupted: Boolean) { |
| 162 | + CustomLogger.recordOutput("FaceHubCommand/interrupted", interrupted) |
| 163 | + |
| 164 | + drivetrain.runSpeeds(ChassisSpeeds()) |
| 165 | + RobotContainer.isAligning = false |
| 166 | + |
| 167 | + CustomLogger.recordOutput("ActiveCommands/FaceHubCommand", false) |
| 168 | + } |
| 169 | +} |
0 commit comments