001/**
002 * Copyright 2024 The Space Cookies : Girl Scout Troop #62868 and FRC Team #1868
003 * Open Source Software; you may modify and/or share it under the terms of
004 * the 3-Clause BSD License found in the root directory of this project.
005 */
006
007package tagalong.subsystems.micro;
008
009import com.ctre.phoenix6.signals.InvertedValue;
010import edu.wpi.first.math.controller.ElevatorFeedforward;
011import edu.wpi.first.math.trajectory.TrapezoidProfile;
012import edu.wpi.first.wpilibj.RobotController;
013import edu.wpi.first.wpilibj.simulation.BatterySim;
014import edu.wpi.first.wpilibj.simulation.ElevatorSim;
015import edu.wpi.first.wpilibj.simulation.RoboRioSim;
016import edu.wpi.first.wpilibj.smartdashboard.Mechanism2d;
017import edu.wpi.first.wpilibj.smartdashboard.MechanismLigament2d;
018import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
019import edu.wpi.first.wpilibj.util.Color;
020import edu.wpi.first.wpilibj.util.Color8Bit;
021import tagalong.TagalongConfiguration;
022import tagalong.math.AlgebraicUtils;
023import tagalong.measurements.Height;
024import tagalong.subsystems.micro.confs.ElevatorConf;
025
026/**
027 * Elevator Microsystem
028 */
029public class Elevator extends Microsystem {
030  /**
031   * Configuration for the elevator
032   */
033  public final ElevatorConf _elevatorConf;
034
035  /* -------- Hardware: motors and sensors -------- */
036
037  /* -------- Control: states and constants -------- */
038  /**
039   * Ratio between motor rotations and drum rotations
040   */
041  protected final double _motorToMechRatio;
042  /**
043   * Minimum height of the elevator in meters,
044   * Maximum height of the elevator in meters
045   */
046  public final double _elevatorMinHeightM, _elevatorMaxHeightM;
047  /**
048   * Maximum velocity of the elevator in meters per second,
049   * Maximum acceleration of the elevator in meters per second squared
050   */
051  public final double _maxVelocityMPS, _maxAccelerationMPS2;
052  /**
053   * Default lower tolerance of the elevator in meters,
054   * Default upper tolerance of the elevator in meters
055   */
056  public final double _defaultElevatorLowerToleranceM, _defaultElevatorUpperToleranceM;
057  /**
058   * Power for zeroing the elevator
059   */
060  public final double _elevatorZeroingPower;
061  /**
062   * Positional tolerance in meters for zeroing the elevator
063   */
064  public final double _elevatorZeroingStallToleranceM;
065  /**
066   * Duration of stalling in seconds necessary for the elevator zero command to finish
067   */
068  public final double _elevatorZeroingDurationS;
069
070  /* -------- Control: controllers and utilities -------- */
071  /**
072   * Feedforward model for the elevator
073   */
074  protected ElevatorFeedforward _elevatorFF;
075
076  /* -------- Sim -------- */
077  /**
078   * Simulation for the elevator
079   */
080  protected ElevatorSim _elevatorSim;
081  /**
082   * Base stage ligament
083   */
084  protected MechanismLigament2d _elevatorBaseStageLigament;
085  /**
086   * Stage 1 ligament
087   */
088  protected MechanismLigament2d _elevatorStage1Ligament;
089  /**
090   * Velocity of the simulated elevator in meters per second
091   */
092  protected double _simVelocityMPS;
093  /**
094   * Whether or not the primary motor is inverted
095   */
096  protected boolean _primaryMotorInverted;
097
098  /**
099   * Constructs an elevator microsystem with the below configurations
100   *
101   * @param conf Configuration for the elevator
102   */
103  public Elevator(ElevatorConf conf) {
104    super(conf);
105    _elevatorConf = conf;
106
107    if (_configuredMicrosystemDisable) {
108      _motorToMechRatio = 1.0;
109      _elevatorMinHeightM = 0.0;
110      _elevatorMaxHeightM = 0.0;
111      _maxVelocityMPS = 0.0;
112      _maxAccelerationMPS2 = 0.0;
113      _defaultElevatorLowerToleranceM = 0.0;
114      _defaultElevatorUpperToleranceM = 0.0;
115      _elevatorZeroingPower = 0.0;
116      _elevatorZeroingStallToleranceM = 0.0;
117      _elevatorZeroingDurationS = 0.0;
118      return;
119    }
120
121    _elevatorFF = _elevatorConf.feedForward;
122    _trapProfile = new TrapezoidProfile(_elevatorConf.trapezoidalLimits);
123    _curState.position = getElevatorHeightM();
124    _motorToMechRatio = _elevatorConf.motorToMechRatio;
125    _elevatorMinHeightM = _elevatorConf.positionalMin;
126    _elevatorMaxHeightM = _elevatorConf.positionalMax;
127    _maxVelocityMPS = _elevatorConf.trapezoidalLimitsVelocity;
128    _maxAccelerationMPS2 = _elevatorConf.trapezoidalLimitsAcceleration;
129    _defaultElevatorLowerToleranceM = _elevatorConf.defaultLowerTolerance;
130    _defaultElevatorUpperToleranceM = _elevatorConf.defaultUpperTolerance;
131    _elevatorZeroingPower = -Math.abs(_elevatorConf.elevatorZeroingPower);
132    _elevatorZeroingStallToleranceM = _elevatorConf.elevatorZeroingStallToleranceM;
133    _elevatorZeroingDurationS = _elevatorConf.elevatorZeroingDurationS;
134
135    configAllDevices();
136  }
137
138  // Override to ensure the position config happens after the devices are configured
139  @Override
140  public void configAllDevices() {
141    super.configAllDevices();
142
143    // FUTURE DEV: Look into if all motors or just the leader need their positions set?
144    // for (var motor : _allMotors) motor.setPosition(0.0);
145    _primaryMotor.setPosition(0.0);
146  }
147
148  /**
149   * Calculates the next state according to the trapezoidal profile and requests the elevator
150   * motor(s) to arrive at the next position with feedforward
151   */
152  public void followLastProfile() {
153    if (_isMicrosystemDisabled) {
154      return;
155    }
156    TrapezoidProfile.State nextState =
157        _trapProfile.calculate(TagalongConfiguration.LOOP_PERIOD_S, _curState, _goalState);
158
159    _primaryMotor.setControl(_requestedPositionVoltage
160                                 .withPosition(metersToMotor(nextState.position))
161                                 // State is in meters, so the FF can handle the units directly
162                                 .withFeedForward(_elevatorFF.calculate(nextState.velocity)));
163
164    if (_isShuffleboardMicro) {
165      _targetPositionEntry.setDouble(nextState.position);
166      _targetVelocityEntry.setDouble(nextState.velocity);
167    }
168
169    _curState = nextState;
170  }
171
172  /**
173   * Creates a new trapezoidal profile for the elevator to follow
174   *
175   * @param goalPosition     goal position in meters
176   */
177  public void setElevatorProfile(Height goalPosition) {
178    setElevatorProfile(goalPosition.getHeightM());
179  }
180
181  /**
182   * Creates a new trapezoidal profile for the elevator to follow
183   *
184   * @param goalPosition     goal position in meters
185   * @param goalVelocityMPS     goal velocity in meters per second
186   */
187  public void setElevatorProfile(Height goalPosition, double goalVelocityMPS) {
188    setElevatorProfile(goalPosition.getHeightM(), goalVelocityMPS);
189  }
190
191  /**
192   * Creates a new trapezoidal profile for the elevator to follow
193   *
194   * @param goalPosition     goal position in meters
195   * @param goalVelocityMPS     goal velocity in meters per second
196   * @param maxVelocityMPS      maximum velocity in meters per second
197   */
198  public void setElevatorProfile(
199      Height goalPosition, double goalVelocityMPS, double maxVelocityMPS
200  ) {
201    setElevatorProfile(goalPosition.getHeightM(), goalVelocityMPS, maxVelocityMPS);
202  }
203
204  /**
205   * Creates a new trapezoidal profile for the elevator to follow
206   *
207   * @param goalPosition     goal position in meters
208   * @param goalVelocityMPS     goal velocity in meters per second
209   * @param maxVelocityMPS      maximum velocity in meters per second
210   * @param maxAccelerationMPS2 maximum acceleration in meters per second squared
211   */
212  public void setElevatorProfile(
213      Height goalPosition, double goalVelocityMPS, double maxVelocityMPS, double maxAccelerationMPS2
214  ) {
215    setElevatorProfile(
216        goalPosition.getHeightM(), goalVelocityMPS, maxVelocityMPS, maxAccelerationMPS2
217    );
218  }
219
220  /**
221   * Creates a new trapezoidal profile for the elevator to follow
222   *
223   * @param goalPosition     goal position in meters
224   * @param goalVelocityMPS     goal velocity in meters per second
225   * @param maxVelocityMPS      maximum velocity in meters per second
226   * @param maxAccelerationMPS2 maximum acceleration in meters per second squared
227   * @param setCurrentState     True if the profiles current state should base itself off sensor
228   *     values rather than continue from the existing state
229   */
230  public void setElevatorProfile(
231      Height goalPosition,
232      double goalVelocityMPS,
233      double maxVelocityMPS,
234      double maxAccelerationMPS2,
235      boolean setCurrentState
236  ) {
237    setElevatorProfile(
238        goalPosition.getHeightM(),
239        goalVelocityMPS,
240        maxVelocityMPS,
241        maxAccelerationMPS2,
242        setCurrentState
243    );
244  }
245
246  /**
247   * Creates a new trapezoidal profile for the elevator to follow
248   *
249   * @param goalPositionM goal position in meters
250   */
251  public void setElevatorProfile(double goalPositionM) {
252    setElevatorProfile(goalPositionM, 0.0);
253  }
254
255  /**
256   * Creates a new trapezoidal profile for the elevator
257   *
258   * @param goalPositionM   The goal position to reach, in meters
259   * @param goalVelocityMPS The goal velocity to reach, in meters per second
260   */
261  public void setElevatorProfile(double goalPositionM, double goalVelocityMPS) {
262    setElevatorProfile(goalPositionM, goalVelocityMPS, _maxVelocityMPS);
263  }
264
265  /**
266   * Creates a new trapezoidal profile for the elevator
267   *
268   * @param goalPositionM   The goal position to reach, in meters
269   * @param goalVelocityMPS The goal velocity to reach, in meters per second
270   * @param maxVelocityMPS  The maximum velocity, in meters per second
271   */
272  public void setElevatorProfile(
273      double goalPositionM, double goalVelocityMPS, double maxVelocityMPS
274  ) {
275    setElevatorProfile(goalPositionM, goalVelocityMPS, maxVelocityMPS, _maxAccelerationMPS2);
276  }
277
278  /**
279   * Creates a new trapezoidal profile for the elevator
280   *
281   * @param goalPositionM   The goal position to reach, in meters
282   * @param goalVelocityMPS The goal velocity to reach, in meters per second
283   * @param maxVelocityMPS  The maximum velocity, in meters per second
284   * @param maxAccelerationMPS2 The maximum acceleration, in meters per second squared
285   */
286  public void setElevatorProfile(
287      double goalPositionM,
288      double goalVelocityMPS,
289      double maxVelocityMPS,
290      double maxAccelerationMPS2
291  ) {
292    setElevatorProfile(goalPositionM, goalVelocityMPS, maxVelocityMPS, maxAccelerationMPS2, true);
293  }
294
295  /**
296   * Creates a new trapezoidal profile for the elevator
297   *
298   * @param goalPositionM       The goal position to reach, in meters
299   * @param goalVelocityMPS     The goal velocity to reach, in meters per second
300   * @param maxVelocityMPS      The maximum velocity, in meters per second
301   * @param maxAccelerationMPS2 The maximum acceleration, in meters per second squared
302   * @param setCurrentState     True if the profiles current state should base itself off sensor
303   *     values rather than continue from the existing state
304   */
305  public void setElevatorProfile(
306      double goalPositionM,
307      double goalVelocityMPS,
308      double maxVelocityMPS,
309      double maxAccelerationMPS2,
310      boolean setCurrentState
311  ) {
312    if (_isMicrosystemDisabled) {
313      return;
314    }
315    setFollowProfile(false);
316
317    if (setCurrentState) {
318      _curState.position = getElevatorHeightM();
319      _curState.velocity = getElevatorVelocityMPS();
320    }
321
322    _goalState.position =
323        AlgebraicUtils.clamp(goalPositionM, _elevatorMinHeightM, _elevatorMaxHeightM);
324    _goalState.velocity = goalVelocityMPS;
325
326    _trapProfile = new TrapezoidProfile(
327        (maxVelocityMPS >= _maxVelocityMPS || maxAccelerationMPS2 >= _maxAccelerationMPS2)
328            ? _elevatorConf.trapezoidalLimits
329            : new TrapezoidProfile.Constraints(maxVelocityMPS, maxAccelerationMPS2)
330    );
331
332    _profileTimer.restart();
333  }
334
335  /**
336   * Gets the height of the elevator in meters
337   *
338   * @return height in meters
339   */
340  public double getElevatorHeightM() {
341    return motorToMeters(getPrimaryMotorPosition());
342  }
343
344  /**
345   * Gets the velocity of the elevator in meters per second
346   *
347   * @return velocity in meters per second
348   */
349  public double getElevatorVelocityMPS() {
350    return motorToMeters(getPrimaryMotorVelocity());
351  }
352
353  /**
354   * Sets the velocity of the elevator in MPS
355   *
356   * @param mps    Desired velocity in meters per second
357   * @param withFF with feedforward
358   */
359  public void setElevatorVelocity(double mps, boolean withFF) {
360    if (_isMicrosystemDisabled) {
361      return;
362    }
363
364    setFollowProfile(false);
365    _primaryMotor.setControl(_requestedVelocityVoltage.withVelocity(metersToMotor(mps))
366                                 .withFeedForward(withFF ? _elevatorFF.calculate(mps) : 0.0));
367  }
368
369  /**
370   * Clamps the elevator position to be within height limits
371   *
372   * @param target goal position of the elevator in meters
373   * @return clamped goal position in meters
374   */
375  public double clampElevatorPosition(double target) {
376    return AlgebraicUtils.clamp(target, _elevatorMinHeightM, _elevatorMaxHeightM);
377  }
378
379  @Override
380  public void onEnable() {
381    if (_isMicrosystemDisabled) {
382      return;
383    }
384    super.onEnable();
385
386    if (_isFFTuningMicro) {
387      _elevatorFF = new ElevatorFeedforward(
388          _KSEntry.getDouble(_elevatorFF.getKs()),
389          _KGEntry.getDouble(_elevatorFF.getKg()),
390          _KVEntry.getDouble(_elevatorFF.getKv()),
391          _KAEntry.getDouble(_elevatorFF.getKa())
392      );
393      _primaryMotor.setControl(_requestedPositionVoltage.withFeedForward(_elevatorFF.getKs()));
394    }
395  }
396
397  @Override
398  public void onDisable() {
399    if (_isMicrosystemDisabled) {
400      return;
401    }
402
403    super.onDisable();
404  }
405
406  @Override
407  public void periodic() {
408    if (_isMicrosystemDisabled) {
409      return;
410    } else if (motorResetConfig()) {
411      setFollowProfile(false);
412      setElevatorProfile(getElevatorHeightM(), 0.0);
413      _primaryMotor.set(0.0);
414    }
415    if (_followProfile) {
416      followLastProfile();
417    }
418  }
419
420  @Override
421  public void simulationInit() {
422    if (_isMicrosystemDisabled) {
423      return;
424    }
425    _elevatorSim = new ElevatorSim(
426        _elevatorConf.motorTypes[0].simSupplier.apply(_elevatorConf.numMotors),
427        _motorToMechRatio,
428        _elevatorConf.carriageMassValue,
429        _elevatorConf.drumDiameter / 2.0,
430        _elevatorMinHeightM,
431        _elevatorMaxHeightM,
432        true,
433        0
434    );
435    _primaryMotorSim = _primaryMotor.getSimState();
436    _mechanism = new Mechanism2d(_elevatorConf.mech2dDim, _elevatorConf.mech2dDim);
437    _root = _mechanism.getRoot(_elevatorConf.rootName, _elevatorConf.rootX, _elevatorConf.rootY);
438    _elevatorBaseStageLigament = new MechanismLigament2d(
439        "BaseStage",
440        _elevatorConf.lineLength,
441        _elevatorConf.angle,
442        6.0,
443        new Color8Bit(Color.kAliceBlue)
444    );
445    _elevatorStage1Ligament = new MechanismLigament2d(
446        "Stage1",
447        _elevatorConf.lineLength,
448        _elevatorConf.angle,
449        6.0,
450        new Color8Bit(Color.kLightSalmon)
451    );
452    _root.append(_elevatorBaseStageLigament);
453    _root.append(_elevatorStage1Ligament);
454    SmartDashboard.putData("SIM: " + _conf.name, _mechanism);
455    _primaryMotorInverted = _conf.motorDirection[0] == InvertedValue.Clockwise_Positive;
456  }
457
458  @Override
459  public void simulationPeriodic() {
460    if (_isMicrosystemDisabled) {
461      return;
462    }
463    _elevatorSim.setInputVoltage(_primaryMotor.getMotorVoltage().getValueAsDouble());
464    _elevatorSim.update(TagalongConfiguration.LOOP_PERIOD_S);
465
466    double simAccelMPS2 = (_elevatorSim.getVelocityMetersPerSecond() - _simVelocityMPS)
467        / TagalongConfiguration.LOOP_PERIOD_S;
468    _simVelocityMPS = _elevatorSim.getVelocityMetersPerSecond();
469
470    _primaryMotorSim.setRawRotorPosition(metersToMotor(
471        _primaryMotorInverted ? (-1 * _elevatorSim.getPositionMeters())
472                              : _elevatorSim.getPositionMeters()
473    ));
474    _primaryMotorSim.setRotorVelocity(metersToMotor(
475        _primaryMotorInverted ? (-1 * _elevatorSim.getVelocityMetersPerSecond())
476                              : _elevatorSim.getVelocityMetersPerSecond()
477    ));
478    _primaryMotorSim.setRotorAcceleration(
479        metersToMotor(_primaryMotorInverted ? (-1 * simAccelMPS2) : simAccelMPS2)
480    );
481    _elevatorStage1Ligament.setLength(_elevatorSim.getPositionMeters());
482    _primaryMotorSim.setSupplyVoltage(RobotController.getBatteryVoltage());
483    RoboRioSim.setVInVoltage(
484        BatterySim.calculateDefaultBatteryLoadedVoltage(_elevatorSim.getCurrentDrawAmps())
485    );
486  }
487
488  @Override
489  public void updateShuffleboard() {
490    if (_isMicrosystemDisabled) {
491      return;
492    }
493    if (_isShuffleboardMicro) {
494      _currentPositionEntry.setDouble(getElevatorHeightM());
495      _currentVelocityEntry.setDouble(getElevatorVelocityMPS());
496    }
497  }
498
499  /**
500   * Configure shuffleboard for the elevator
501   */
502  @Override
503  public void configShuffleboard() {
504    if (_isMicrosystemDisabled) {
505      return;
506    }
507    super.configShuffleboard();
508  }
509
510  /**
511   * Converts motor rotations to elevator height
512   *
513   * @param rotation motor position in rotations
514   * @return elevator position in meters
515   */
516  public double motorToMeters(double rotation) {
517    return _isMicrosystemDisabled
518        ? 0.0
519        : (rotation * _elevatorConf.drumCircumference) / _motorToMechRatio;
520  }
521
522  /**
523   * Converts elevator height to motor rotations
524   *
525   * @param meter elevator position in meters
526   * @return motor position in rotations
527   */
528  public double metersToMotor(double meter) {
529    return _isMicrosystemDisabled ? 0.0
530                                  : (meter / _elevatorConf.drumCircumference) * _motorToMechRatio;
531  }
532
533  /**
534   * Checks whether or not it's safe for the elevator to move
535   *
536   * @return whether or not it's safe to move
537   */
538  public boolean isSafeToMove() {
539    return true;
540  }
541
542  /**
543   * Bounds checking function that uses the current elevator position
544   *
545   * @param lowerBound minimum of acceptable range
546   * @param upperBound maximum of acceptable range
547   * @return if the current position is in specified acceptable range
548   */
549  public boolean isElevatorInTolerance(double lowerBound, double upperBound) {
550    if (_isMicrosystemDisabled) {
551      return true;
552    }
553    return AlgebraicUtils.inTolerance(getElevatorHeightM(), lowerBound, upperBound);
554  }
555
556  /**
557   * Sets the position of the elevator in meters
558   *
559   * @param height goal position in meters
560   */
561  public void setElevatorHeight(double height) {
562    if (_isMicrosystemDisabled) {
563      return;
564    }
565    _primaryMotor.setPosition(metersToMotor(height));
566  }
567
568  @Override
569  public void holdCurrentPosition() {
570    setElevatorProfile(getElevatorHeightM(), 0.0);
571    setFollowProfile(true);
572  }
573
574  /**
575   * Retrieves base stage ligament attached to elevator Mechanism2d
576   *
577   * @return elevator base stage ligament
578   */
579  public MechanismLigament2d getElevatorBaseStageLigament() {
580    return _elevatorBaseStageLigament;
581  }
582
583  /**
584   * Retrieves stage 1 ligament attached to elevator Mechanism2d
585   *
586   * @return elevator stage 1 ligament
587   */
588  public MechanismLigament2d getElevatorStage1Ligament() {
589    return _elevatorStage1Ligament;
590  }
591}