Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe mode #32

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
efc4728
safe mode (child mode)
BrownBear85 Apr 22, 2023
1eba207
child mode refuses arm control
FriedLongJohns Apr 25, 2023
a92d442
Slowmode (dt, arm+wrist, disable X and joysticks)
DriverStationComputer Apr 26, 2023
917c156
fix
FriedLongJohns Apr 29, 2023
6fa4ab0
Use limits instead of multipliers
FriedLongJohns Apr 29, 2023
ddf341b
Add Arm kD value
DriverStationComputer May 17, 2023
20da511
addding encoder connected code and recalibrated
DriverStationComputer Sep 28, 2023
c3afc26
Remove unused forbFlag
FriedLongJohns Sep 29, 2023
4039891
fix baby vel & accel limits
FriedLongJohns Sep 30, 2023
cf37551
merged baby and slow booloean suppliers into slow
TuskAct2 Sep 30, 2023
f510639
Revert "merged baby and slow booloean suppliers into slow"
FriedLongJohns Oct 1, 2023
eec4b4a
Shift RollerMode to Roller
FriedLongJohns Oct 1, 2023
d6b8515
Merge branch 'master' into safe-mode
FriedLongJohns Oct 1, 2023
88cace6
Reorganize lambdas
FriedLongJohns Oct 1, 2023
87c5866
Merge branch 'safe-mode' of https://github.com/DeepBlueRobotics/Robot…
FriedLongJohns Oct 1, 2023
46866e3
copied arm encoder dc logic into wrist
TuskAct2 Oct 1, 2023
6a5b4a7
if encoders are dc then motors will stop
TuskAct2 Oct 1, 2023
6d929d1
shorten functions
FriedLongJohns Oct 1, 2023
e5907cd
fix stupid ifs
FriedLongJohns Oct 1, 2023
9c4f55b
merge driver modes
FriedLongJohns Oct 1, 2023
fe366fe
alt + shift + f, autoformatting everything
ProfessorAtomicManiac Oct 1, 2023
15f0207
added constants together
ProfessorAtomicManiac Oct 1, 2023
1812ca5
documentation of DISCONNECTED_ENCODER_TIMEOUT_SEC
ProfessorAtomicManiac Oct 1, 2023
531783d
format RunRoller and TeleopDrive
ProfessorAtomicManiac Oct 1, 2023
becd19a
remove useless line
ProfessorAtomicManiac Oct 1, 2023
7ff0122
made wrist encoder disconnection detection work
ProfessorAtomicManiac Oct 1, 2023
8955c82
documentation of disconnected encoder code
ProfessorAtomicManiac Oct 1, 2023
feea3e3
Fix logic
FriedLongJohns Oct 1, 2023
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
36 changes: 8 additions & 28 deletions src/main/java/org/carlmontrobotics/robotcode2023/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ public static final class Drivetrain {
public static final double kNormalDriveRotation = 0.5; // Percent Multiplier
public static final double kSlowDriveSpeed = 0.4; // Percent Multiplier
public static final double kSlowDriveRotation = 0.250; // Percent Multiplier
public static final double kBabyDriveSpeed = 0.3; // Percent Multiplier
public static final double kBabyTurnSpeed = 0.2; // Percent Multiplier
public static final double kAlignMultiplier = 1D/3D;
public static final double kAlignForward = 0.6;

Expand Down Expand Up @@ -187,7 +189,7 @@ public static final class Arm {
public static double[] posToleranceRad = { .07, .07 }; // rad
public static double[] velToleranceRadPSec = { 1, 1 }; // rad/s

public static double[] offsetRad = { 0.865, 2.93 + Math.PI / 2 }; // rad
public static double[] offsetRad = { 2.936 + 0.301, 2.93 + Math.PI / 2 }; // rad
public static final double rumbleFullPower = 1;
public static final double rumbleNoPower = 0;

Expand Down Expand Up @@ -222,6 +224,9 @@ public static final class Arm {

// TODO: Determine actual max vel/accel
// public static double[] MAX_FF_VEL = {.25, .25}; // rad / s
//Arm, Wrist
public static double[] MAX_FF_VEL_BABY = {0.675, 0.7};
public static double[] MAX_FF_ACCEL_BABY = {4, 4};
public static double[] MAX_FF_VEL_MANUAL = {1, 3}; // rad / s
public static double[] MAX_FF_VEL_AUTO = {1.25, 5}; // rad / s
public static double[] MAX_FF_ACCEL = {5, 5}; // rad / s^2
ProfessorAtomicManiac marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -233,7 +238,7 @@ public static final class Arm {
//#region Ports

public static final boolean[] motorInverted = { true, false };
public static final boolean[] encoderInverted = { false, true };
public static final boolean[] encoderInverted = { true, true };
public static final double rotationToRad = 2 * Math.PI;

public static final int armMotorPort = 17;
Expand All @@ -250,6 +255,7 @@ public static final class Arm {
public static final int WRIST_CURRENT_LIMIT_AMP = 15;
public static final double ROLLER_COM_CORRECTION_RAD = Units.degreesToRadians(18.3);
public static double ARM_TELEOP_MAX_GOAL_DIFF_FROM_CURRENT_RAD = .5;
public static final double DISCONNECTED_ENCODER_TIMEOUT_SEC = .25;

//#endregion

Expand Down Expand Up @@ -374,32 +380,6 @@ public static final class Roller {
//#endregion

//#region Command Constants
// TODO: Determine actual speeds/timings for roller
public static class RollerMode {
public static RollerMode INTAKE_CONE = new RollerMode(-0.5, .5, GameObject.CONE, conePickupColor);
public static RollerMode INTAKE_CUBE = new RollerMode(0.4, .25, GameObject.CUBE, cubePickupColor);
// The obj indicates which game object the roller is trying to intake
// if obj == NONE, that means it is trying to outtake rather than intake
public static RollerMode OUTTAKE_CONE = new RollerMode(0.5, .5, GameObject.NONE, defaultColor);
public static RollerMode OUTTAKE_CUBE = new RollerMode(-0.5, .5, GameObject.NONE, defaultColor);
public static RollerMode STOP = new RollerMode(0, .1, GameObject.NONE, defaultColor);
public double speed, time;
public GameObject obj;
public Color ledColor;

/**
* @param speed A number between -1 and 1
* @param time Amount of time in seconds to keep the motor running after
* distance sensor has detected an object
* @param intake Whether the roller is outtaking or intaking
*/
public RollerMode(double speed, double time, GameObject obj, Color ledColor) {
this.speed = speed;
this.time = time;
this.obj = obj;
this.ledColor = ledColor;
}
}
Comment on lines -377 to -402
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally Ryan wanted all constants put into Constants.java. Personally, I think constants that are strictly only used in subsystems should stay in their subsystem classes and the only appropriate constants that should be put in Constants.java are constants that are used between subsystems or ports. That way, there is less clutter in Constants.java and it is easier to find certain constants. Seems like you agree with me on this, but do we wanna make it convention that all subsystem specific constants (excluding ports) should stay in subsystems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other solutions include creating completely new files just to store constants to organize them better. I realize that storing goal positions in Arm.java would be extremely annoying. We should probably discuss and agree on how exactly we want to store constants.


//#endregion

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/carlmontrobotics/robotcode2023/Robot.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import edu.wpi.first.wpilibj.DataLogManager;
import edu.wpi.first.wpilibj.DriverStation;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import edu.wpi.first.wpilibj2.command.CommandScheduler;

public class Robot extends TimedRobot {
Expand All @@ -26,6 +27,8 @@ public void robotInit() {
DriverStation.startDataLog(DataLogManager.getLog());
if(!DriverStation.isFMSAttached()) PathPlannerServer.startServer(5811);
robotContainer = new RobotContainer();

SmartDashboard.putBoolean("safeMode", false);
ProfessorAtomicManiac marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
Expand All @@ -40,6 +43,8 @@ public void simulationInit() {
public void robotPeriodic() {
CommandScheduler.getInstance().run();
MotorErrors.printSparkMaxErrorMessages();

RobotContainer.isdriverchild = SmartDashboard.getBoolean("safeMode", false);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.carlmontrobotics.robotcode2023.Constants.GoalPos;
import org.carlmontrobotics.robotcode2023.Constants.OI.Driver;
import org.carlmontrobotics.robotcode2023.Constants.OI.Manipulator;
import org.carlmontrobotics.robotcode2023.Constants.Roller.RollerMode;
import org.carlmontrobotics.robotcode2023.commands.AlignChargingStation;
import org.carlmontrobotics.robotcode2023.commands.ArmTeleop;
import org.carlmontrobotics.robotcode2023.commands.DriveOverChargeStation;
Expand Down Expand Up @@ -51,6 +50,8 @@

public class RobotContainer {

public static boolean isdriverchild = false;

public final GenericHID driverController = new GenericHID(Driver.port);
public final GenericHID manipulatorController = new GenericHID(Manipulator.port);

Expand All @@ -74,7 +75,7 @@ public RobotContainer() {
eventMap.put("Cone High Pos.", new SetArmWristGoalPreset(GoalPos.HIGH, () -> false, () -> false, arm));
// Command fakeArmCommand = new InstantCommand(() -> System.err.println("==============Store================="), arm);
// eventMap.put("Stored Pos.", new SequentialCommandGroup(fakeArmCommand, new WaitCommand(2)));
eventMap.put("Run Cube Intake", new SequentialCommandGroup(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> true, () -> false, arm), new RunRoller(roller, RollerMode.INTAKE_CUBE)));
eventMap.put("Run Cube Intake", new SequentialCommandGroup(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> true, () -> false, arm), new RunRoller(roller, Roller.RollerMode.INTAKE_CUBE)));
eventMap.put("DriveOverChargeStation", new DriveOverChargeStation(drivetrain));
eventMap.put("Extend Arm High Cube", new SetArmWristGoalPreset(GoalPos.HIGH, () -> true, () -> false, arm));
eventMap.put("Extend Arm Mid Cube", new SetArmWristGoalPreset(GoalPos.MID, () -> true, () -> false, arm));
Expand All @@ -85,9 +86,9 @@ public RobotContainer() {
new SetArmWristGoalPreset(GoalPos.HIGH, () -> true, () -> false, arm),
new PrintCommand("================================Cube High Pos. Ended==================================")
));
eventMap.put("Run Cube Outtake", new RunRoller(roller, RollerMode.OUTTAKE_CUBE));
eventMap.put("Run Cone Intake", new SequentialCommandGroup(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, () -> false, arm), new RunRoller(roller, RollerMode.INTAKE_CONE)));
eventMap.put("Run Cone Outtake", new RunRoller(roller, RollerMode.OUTTAKE_CONE));
eventMap.put("Run Cube Outtake", new RunRoller(roller, Roller.RollerMode.OUTTAKE_CUBE));
eventMap.put("Run Cone Intake", new SequentialCommandGroup(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, () -> false, arm), new RunRoller(roller, Roller.RollerMode.INTAKE_CONE)));
eventMap.put("Run Cone Outtake", new RunRoller(roller, Roller.RollerMode.OUTTAKE_CONE));
//eventMap.put("Move Arm Back", new SetArmWristPositionV3((-5*Math.PI)/8, Constants.Arm.WRIST_STOW_POS_RAD, arm));
eventMap.put("Cone Intake Pos.", new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, () -> false, arm));
eventMap.put("Cube Intake Pos.", new SequentialCommandGroup(
Expand Down Expand Up @@ -129,18 +130,20 @@ public RobotContainer() {

drivetrain.setDefaultCommand(new TeleopDrive(
drivetrain,
() -> inputProcessing(getStickValue(driverController, Axis.kLeftY)),
() -> inputProcessing(getStickValue(driverController, Axis.kLeftY)),
() -> inputProcessing(getStickValue(driverController, Axis.kLeftX)),
() -> inputProcessing(getStickValue(driverController, Axis.kRightX)),
() -> driverController.getRawButton(Driver.slowDriveButton)
() -> driverController.getRawButton(Driver.slowDriveButton),
() -> {return isdriverchild;}
));

configureButtonBindingsDriver();
configureButtonBindingsManipulator();
arm.setDefaultCommand(new ArmTeleop(
arm,
() -> inputProcessing(getStickValue(manipulatorController, Axis.kLeftY)),
() -> inputProcessing(getStickValue(manipulatorController, Axis.kRightY))
() -> {return (isdriverchild ? 0 : inputProcessing(getStickValue(manipulatorController, Axis.kLeftY)));},
() -> {return (isdriverchild ? 0 : inputProcessing(getStickValue(manipulatorController, Axis.kRightY)));},
() -> {return isdriverchild;}
));
}
private void configureButtonBindingsDriver() {
Expand All @@ -159,32 +162,31 @@ private void configureButtonBindingsManipulator() {
BooleanSupplier isIntake = () -> !isCube.getAsBoolean();

new JoystickButton(manipulatorController, Manipulator.storePosButton).onTrue(new SetArmWristGoalPreset(GoalPos.STORED, isCube, isFront, arm));
new JoystickButton(manipulatorController, Manipulator.lowPosButton).onTrue(new SetArmWristGoalPreset(GoalPos.LOW, isCube, isFront, arm));
//new JoystickButton(manipulatorController, Manipulator.lowPosButton).onTrue(new SetArmWristGoalPreset(GoalPos.LOW, isCube, isFront, arm));
new JoystickButton(manipulatorController, Manipulator.midPosButton).onTrue(new SetArmWristGoalPreset(GoalPos.MID, isCube, isFront, arm));
new JoystickButton(manipulatorController, Manipulator.highPosButton).onTrue(new SetArmWristGoalPreset(GoalPos.HIGH, isCube, isFront, arm));
new POVButton(manipulatorController, Manipulator.shelfPickupPOV).onTrue(new SetArmWristGoalPreset(GoalPos.SHELF, isCube, isFront, arm));
new POVButton(manipulatorController, Manipulator.intakeConePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, isFront, arm));
new POVButton(manipulatorController, Manipulator.substationPickupPOV).onTrue(new SetArmWristGoalPreset(GoalPos.STORED, isCube, isFront, arm));
// new Trigger(() -> {return arm.getForbFlag();}).onTrue(new InstantCommand(() -> {manipulatorController.setRumble(RumbleType.kBothRumble,rumbleFullPower);}))
// .onFalse(new InstantCommand(() -> {manipulatorController.setRumble(RumbleType.kBothRumble,rumbleNoPower);}));

new POVButton(manipulatorController, Manipulator.intakeCubePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> true, isFront, arm));
new POVButton(manipulatorController, Manipulator.intakeConePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, isFront, arm).andThen(new RunRoller(roller, RollerMode.INTAKE_CONE)));
new POVButton(manipulatorController, Manipulator.substationPickupPOV).onTrue(new SetArmWristGoalPreset(GoalPos.SUBSTATION, isCube, isFront, arm).andThen(new RunRoller(roller, RollerMode.INTAKE_CONE)));
new POVButton(manipulatorController, Manipulator.intakeCubePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> true, isFront, arm).andThen(new RunRoller(roller, RollerMode.INTAKE_CUBE)));
new JoystickButton(manipulatorController, Manipulator.stopRollerButton).onTrue(new RunRoller(roller, RollerMode.STOP));
new POVButton(manipulatorController, Manipulator.intakeConePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> false, isFront, arm).andThen(new RunRoller(roller, Roller.RollerMode.INTAKE_CONE)));
new POVButton(manipulatorController, Manipulator.substationPickupPOV).onTrue(new SetArmWristGoalPreset(GoalPos.SUBSTATION, isCube, isFront, arm).andThen(new RunRoller(roller, Roller.RollerMode.INTAKE_CONE)));
new POVButton(manipulatorController, Manipulator.intakeCubePOV).onTrue(new SetArmWristGoalPreset(GoalPos.INTAKE, () -> true, isFront, arm).andThen(new RunRoller(roller, Roller.RollerMode.INTAKE_CUBE)));
new JoystickButton(manipulatorController, Manipulator.stopRollerButton).onTrue(new RunRoller(roller, Roller.RollerMode.STOP));

// axisTrigger(manipulatorController, Manipulator.rollerIntakeConeButton)
// .onTrue(new RunRoller(roller, RollerMode.INTAKE_CONE, Constants.Roller.conePickupColor));
axisTrigger(manipulatorController, Manipulator.rollerIntakeCubeButton)
.onTrue(new ConditionalCommand(
new RunRoller(roller, RollerMode.INTAKE_CUBE),
new RunRoller(roller, RollerMode.OUTTAKE_CUBE),
new RunRoller(roller, Roller.RollerMode.INTAKE_CUBE),
new RunRoller(roller, Roller.RollerMode.OUTTAKE_CUBE),
isIntake
));
axisTrigger(manipulatorController, Manipulator.rollerIntakeConeButton)
.onTrue(new ConditionalCommand(
new RunRoller(roller, RollerMode.INTAKE_CONE),
new RunRoller(roller, RollerMode.OUTTAKE_CONE),
new RunRoller(roller, Roller.RollerMode.INTAKE_CONE),
new RunRoller(roller, Roller.RollerMode.OUTTAKE_CONE),
isIntake
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.carlmontrobotics.robotcode2023.Constants.Arm.*;

import java.util.function.DoubleSupplier;
import java.util.function.BooleanSupplier;

import org.carlmontrobotics.robotcode2023.Constants;
import org.carlmontrobotics.robotcode2023.subsystems.Arm;
Expand All @@ -21,13 +22,15 @@ public class ArmTeleop extends CommandBase {
private Arm armSubsystem;
private DoubleSupplier arm;
private DoubleSupplier wrist;
private BooleanSupplier baby;//is robot in baby (safe) mode?
private double lastTime = 0;

public ArmTeleop(Arm armSubsystem, DoubleSupplier arm, DoubleSupplier wrist) {
public ArmTeleop(Arm armSubsystem, DoubleSupplier arm, DoubleSupplier wrist, BooleanSupplier baby) {
// Use addRequirements() here to declare subsystem dependencies.
addRequirements(this.armSubsystem = armSubsystem);
this.arm = arm;
this.wrist = wrist;
this.baby = baby;
}

// Called when the command is initially scheduled.
Expand Down Expand Up @@ -67,18 +70,22 @@ public void execute() {

// Copy and pasted from drivetrain, handles input from joysticks
public double[] getRequestedSpeeds() {
double rawArmVel, rawWristVel;
// Sets all values less than or equal to a very small value (determined by the
// Sets all values less than or equal to a very small value (determined by the
// idle joystick state) to zero.

double rawArmVel, rawWristVel;
// prep maximum velocity variable
double[] maxFF = baby.getAsBoolean() ? MAX_FF_VEL_BABY : MAX_FF_VEL_MANUAL;

if (Math.abs(arm.getAsDouble()) <= Constants.OI.JOY_THRESH)
rawArmVel = 0.0;
else
rawArmVel = MAX_FF_VEL_MANUAL[ARM] * arm.getAsDouble();
rawArmVel = maxFF[ARM] * arm.getAsDouble();

if (Math.abs(wrist.getAsDouble()) <= Constants.OI.JOY_THRESH)
rawWristVel = 0.0;
else
rawWristVel = MAX_FF_VEL_MANUAL[WRIST] * wrist.getAsDouble();
rawWristVel = maxFF[WRIST] * wrist.getAsDouble();

return new double[] {rawArmVel, rawWristVel};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package org.carlmontrobotics.robotcode2023.commands;

import org.carlmontrobotics.robotcode2023.Constants.Roller.GameObject;
import org.carlmontrobotics.robotcode2023.Constants.Roller.RollerMode;
import org.carlmontrobotics.robotcode2023.subsystems.Roller;
import org.carlmontrobotics.robotcode2023.Constants.Roller.GameObject;

import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
Expand All @@ -12,9 +11,9 @@ public class RunRoller extends CommandBase {

private final Roller roller;
private final Timer timer = new Timer();
private final RollerMode mode;
private final Roller.RollerMode mode;

public RunRoller(Roller roller, RollerMode mode) {
public RunRoller(Roller roller, Roller.RollerMode mode) {
addRequirements(this.roller = roller);
this.mode = mode;
}
Expand All @@ -38,8 +37,10 @@ public void execute() {}
@Override
public void end(boolean interrupted) {
// keep it running if its a cone
if (mode.obj != GameObject.CONE && !interrupted)
roller.setSpeed(0);
if (mode.obj != GameObject.CONE && !interrupted) {
mode.speed = 0;
roller.setRollerMode(mode);
}
timer.stop();
System.err.println("=============================RunRoller has ended==========================================================");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ public class TeleopDrive extends CommandBase {
private DoubleSupplier str;
private DoubleSupplier rcw;
private BooleanSupplier slow;
private BooleanSupplier baby;
private double currentForwardVel = 0;
private double currentStrafeVel = 0;

/**
* Creates a new TeleopDrive.
*/
public TeleopDrive(Drivetrain drivetrain, DoubleSupplier fwd, DoubleSupplier str, DoubleSupplier rcw, BooleanSupplier slow) {
public TeleopDrive(Drivetrain drivetrain, DoubleSupplier fwd, DoubleSupplier str, DoubleSupplier rcw, BooleanSupplier slow, BooleanSupplier baby) {
TuskAct2 marked this conversation as resolved.
Show resolved Hide resolved
addRequirements(this.drivetrain = drivetrain);
this.fwd = fwd;
this.str = str;
this.rcw = rcw;
this.slow = slow;
this.baby = baby;
}

// Called when the command is initially scheduled.
Expand Down Expand Up @@ -67,8 +69,12 @@ public double[] getRequestedSpeeds() {
if (Math.abs(rotateClockwise) <= Constants.OI.JOY_THRESH) rotateClockwise = 0.0;
else rotateClockwise *= maxRCW;

double driveMultiplier = slow.getAsBoolean() ? kSlowDriveSpeed : kNormalDriveSpeed;
double rotationMultiplier = slow.getAsBoolean() ? kSlowDriveRotation : kNormalDriveRotation;
double driveMultiplier = baby.getAsBoolean() ? kBabyDriveSpeed : (
slow.getAsBoolean() ? kSlowDriveSpeed : kNormalDriveSpeed
);
double rotationMultiplier = baby.getAsBoolean() ? kBabyTurnSpeed : (
slow.getAsBoolean() ? kSlowDriveRotation : kNormalDriveRotation
);

forward *= driveMultiplier;
strafe *= driveMultiplier;
Expand Down
Loading