001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj.simulation;
006
007import edu.wpi.first.math.MathUtil;
008import edu.wpi.first.math.Matrix;
009import edu.wpi.first.math.VecBuilder;
010import edu.wpi.first.math.numbers.N1;
011import edu.wpi.first.math.numbers.N2;
012import edu.wpi.first.math.system.LinearSystem;
013import edu.wpi.first.math.system.NumericalIntegration;
014import edu.wpi.first.math.system.plant.DCMotor;
015import edu.wpi.first.math.system.plant.LinearSystemId;
016import edu.wpi.first.wpilibj.RobotController;
017
018/** Represents a simulated single jointed arm mechanism. */
019public class SingleJointedArmSim extends LinearSystemSim<N2, N1, N2> {
020  // The gearbox for the arm.
021  private final DCMotor m_gearbox;
022
023  // The gearing between the motors and the output.
024  private final double m_gearing;
025
026  // The length of the arm.
027  private final double m_armLenMeters;
028
029  // The minimum angle that the arm is capable of.
030  private final double m_minAngle;
031
032  // The maximum angle that the arm is capable of.
033  private final double m_maxAngle;
034
035  // Whether the simulator should simulate gravity.
036  private final boolean m_simulateGravity;
037
038  /**
039   * Creates a simulated arm mechanism.
040   *
041   * @param plant The linear system that represents the arm. This system can be created with {@link
042   *     edu.wpi.first.math.system.plant.LinearSystemId#createSingleJointedArmSystem(DCMotor,
043   *     double, double)}.
044   * @param gearbox The type of and number of motors in the arm gearbox.
045   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
046   * @param armLengthMeters The length of the arm.
047   * @param minAngleRads The minimum angle that the arm is capable of.
048   * @param maxAngleRads The maximum angle that the arm is capable of.
049   * @param simulateGravity Whether gravity should be simulated or not.
050   * @param startingAngleRads The initial position of the Arm simulation in radians.
051   * @param measurementStdDevs The standard deviations of the measurements. Can be omitted if no
052   *     noise is desired. If present must have 1 element for position.
053   */
054  @SuppressWarnings("this-escape")
055  public SingleJointedArmSim(
056      LinearSystem<N2, N1, N2> plant,
057      DCMotor gearbox,
058      double gearing,
059      double armLengthMeters,
060      double minAngleRads,
061      double maxAngleRads,
062      boolean simulateGravity,
063      double startingAngleRads,
064      double... measurementStdDevs) {
065    super(plant, measurementStdDevs);
066    m_gearbox = gearbox;
067    m_gearing = gearing;
068    m_armLenMeters = armLengthMeters;
069    m_minAngle = minAngleRads;
070    m_maxAngle = maxAngleRads;
071    m_simulateGravity = simulateGravity;
072
073    setState(startingAngleRads, 0.0);
074  }
075
076  /**
077   * Creates a simulated arm mechanism.
078   *
079   * @param gearbox The type of and number of motors in the arm gearbox.
080   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
081   * @param jKgMetersSquared The moment of inertia of the arm; can be calculated from CAD software.
082   * @param armLengthMeters The length of the arm.
083   * @param minAngleRads The minimum angle that the arm is capable of.
084   * @param maxAngleRads The maximum angle that the arm is capable of.
085   * @param simulateGravity Whether gravity should be simulated or not.
086   * @param startingAngleRads The initial position of the Arm simulation in radians.
087   * @param measurementStdDevs The standard deviations of the measurements. Can be omitted if no
088   *     noise is desired. If present must have 1 element for position.
089   */
090  public SingleJointedArmSim(
091      DCMotor gearbox,
092      double gearing,
093      double jKgMetersSquared,
094      double armLengthMeters,
095      double minAngleRads,
096      double maxAngleRads,
097      boolean simulateGravity,
098      double startingAngleRads,
099      double... measurementStdDevs) {
100    this(
101        LinearSystemId.createSingleJointedArmSystem(gearbox, jKgMetersSquared, gearing),
102        gearbox,
103        gearing,
104        armLengthMeters,
105        minAngleRads,
106        maxAngleRads,
107        simulateGravity,
108        startingAngleRads,
109        measurementStdDevs);
110  }
111
112  /**
113   * Sets the arm's state. The new angle will be limited between the minimum and maximum allowed
114   * limits.
115   *
116   * @param angleRadians The new angle in radians.
117   * @param velocityRadPerSec The new angular velocity in radians per second.
118   */
119  public final void setState(double angleRadians, double velocityRadPerSec) {
120    setState(
121        VecBuilder.fill(MathUtil.clamp(angleRadians, m_minAngle, m_maxAngle), velocityRadPerSec));
122  }
123
124  /**
125   * Returns whether the arm would hit the lower limit.
126   *
127   * @param currentAngleRads The current arm height.
128   * @return Whether the arm would hit the lower limit.
129   */
130  public boolean wouldHitLowerLimit(double currentAngleRads) {
131    return currentAngleRads <= this.m_minAngle;
132  }
133
134  /**
135   * Returns whether the arm would hit the upper limit.
136   *
137   * @param currentAngleRads The current arm height.
138   * @return Whether the arm would hit the upper limit.
139   */
140  public boolean wouldHitUpperLimit(double currentAngleRads) {
141    return currentAngleRads >= this.m_maxAngle;
142  }
143
144  /**
145   * Returns whether the arm has hit the lower limit.
146   *
147   * @return Whether the arm has hit the lower limit.
148   */
149  public boolean hasHitLowerLimit() {
150    return wouldHitLowerLimit(getAngleRads());
151  }
152
153  /**
154   * Returns whether the arm has hit the upper limit.
155   *
156   * @return Whether the arm has hit the upper limit.
157   */
158  public boolean hasHitUpperLimit() {
159    return wouldHitUpperLimit(getAngleRads());
160  }
161
162  /**
163   * Returns the current arm angle.
164   *
165   * @return The current arm angle.
166   */
167  public double getAngleRads() {
168    return getOutput(0);
169  }
170
171  /**
172   * Returns the current arm velocity.
173   *
174   * @return The current arm velocity.
175   */
176  public double getVelocityRadPerSec() {
177    return getOutput(1);
178  }
179
180  /**
181   * Returns the arm current draw.
182   *
183   * @return The aram current draw.
184   */
185  public double getCurrentDrawAmps() {
186    // Reductions are greater than 1, so a reduction of 10:1 would mean the motor is
187    // spinning 10x faster than the output
188    var motorVelocity = m_x.get(1, 0) * m_gearing;
189    return m_gearbox.getCurrent(motorVelocity, m_u.get(0, 0)) * Math.signum(m_u.get(0, 0));
190  }
191
192  /**
193   * Sets the input voltage for the arm.
194   *
195   * @param volts The input voltage.
196   */
197  public void setInputVoltage(double volts) {
198    setInput(volts);
199    clampInput(RobotController.getBatteryVoltage());
200  }
201
202  /**
203   * Calculates a rough estimate of the moment of inertia of an arm given its length and mass.
204   *
205   * @param lengthMeters The length of the arm.
206   * @param massKg The mass of the arm.
207   * @return The calculated moment of inertia.
208   */
209  public static double estimateMOI(double lengthMeters, double massKg) {
210    return 1.0 / 3.0 * massKg * lengthMeters * lengthMeters;
211  }
212
213  /**
214   * Updates the state of the arm.
215   *
216   * @param currentXhat The current state estimate.
217   * @param u The system inputs (voltage).
218   * @param dtSeconds The time difference between controller updates.
219   */
220  @Override
221  protected Matrix<N2, N1> updateX(Matrix<N2, N1> currentXhat, Matrix<N1, N1> u, double dtSeconds) {
222    // The torque on the arm is given by τ = F⋅r, where F is the force applied by
223    // gravity and r the distance from pivot to center of mass. Recall from
224    // dynamics that the sum of torques for a rigid body is τ = J⋅α, were τ is
225    // torque on the arm, J is the mass-moment of inertia about the pivot axis,
226    // and α is the angular acceleration in rad/s². Rearranging yields: α = F⋅r/J
227    //
228    // We substitute in F = m⋅g⋅cos(θ), where θ is the angle from horizontal:
229    //
230    //   α = (m⋅g⋅cos(θ))⋅r/J
231    //
232    // Multiply RHS by cos(θ) to account for the arm angle. Further, we know the
233    // arm mass-moment of inertia J of our arm is given by J=1/3 mL², modeled as a
234    // rod rotating about it's end, where L is the overall rod length. The mass
235    // distribution is assumed to be uniform. Substitute r=L/2 to find:
236    //
237    //   α = (m⋅g⋅cos(θ))⋅r/(1/3 mL²)
238    //   α = (m⋅g⋅cos(θ))⋅(L/2)/(1/3 mL²)
239    //   α = 3/2⋅g⋅cos(θ)/L
240    //
241    // This acceleration is next added to the linear system dynamics ẋ=Ax+Bu
242    //
243    //   f(x, u) = Ax + Bu + [0  α]ᵀ
244    //   f(x, u) = Ax + Bu + [0  3/2⋅g⋅cos(θ)/L]ᵀ
245
246    Matrix<N2, N1> updatedXhat =
247        NumericalIntegration.rkdp(
248            (Matrix<N2, N1> x, Matrix<N1, N1> _u) -> {
249              Matrix<N2, N1> xdot = m_plant.getA().times(x).plus(m_plant.getB().times(_u));
250              if (m_simulateGravity) {
251                double alphaGrav = 3.0 / 2.0 * -9.8 * Math.cos(x.get(0, 0)) / m_armLenMeters;
252                xdot = xdot.plus(VecBuilder.fill(0, alphaGrav));
253              }
254              return xdot;
255            },
256            currentXhat,
257            u,
258            dtSeconds);
259
260    // We check for collision after updating xhat
261    if (wouldHitLowerLimit(updatedXhat.get(0, 0))) {
262      return VecBuilder.fill(m_minAngle, 0);
263    }
264    if (wouldHitUpperLimit(updatedXhat.get(0, 0))) {
265      return VecBuilder.fill(m_maxAngle, 0);
266    }
267    return updatedXhat;
268  }
269}