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;
006
007import edu.wpi.first.hal.ControlWord;
008import edu.wpi.first.hal.DriverStationJNI;
009import edu.wpi.first.util.WPIUtilJNI;
010import java.util.LinkedHashSet;
011import java.util.Set;
012
013/**
014 * The Motor Safety feature acts as a watchdog timer for an individual motor. It operates by
015 * maintaining a timer that tracks how long it has been since the feed() method has been called for
016 * that actuator. Code in the Driver Station class initiates a comparison of these timers to the
017 * timeout values for any actuator with safety enabled every 5 received packets (100ms nominal).
018 *
019 * <p>The subclass should call feed() whenever the motor value is updated.
020 */
021public abstract class MotorSafety {
022  private static final double kDefaultSafetyExpiration = 0.1;
023
024  private double m_expiration = kDefaultSafetyExpiration;
025  private boolean m_enabled;
026  private double m_stopTime = Timer.getFPGATimestamp();
027  private final Object m_thisMutex = new Object();
028  private static final Set<MotorSafety> m_instanceList = new LinkedHashSet<>();
029  private static final Object m_listMutex = new Object();
030  private static Thread m_safetyThread;
031
032  @SuppressWarnings("PMD.AssignmentInOperand")
033  private static void threadMain() {
034    int event = WPIUtilJNI.createEvent(false, false);
035    DriverStationJNI.provideNewDataEventHandle(event);
036    ControlWord controlWord = new ControlWord();
037
038    int safetyCounter = 0;
039    while (true) {
040      boolean timedOut;
041      try {
042        timedOut = WPIUtilJNI.waitForObjectTimeout(event, 0.1);
043      } catch (InterruptedException e) {
044        DriverStationJNI.removeNewDataEventHandle(event);
045        WPIUtilJNI.destroyEvent(event);
046        Thread.currentThread().interrupt();
047        return;
048      }
049      if (!timedOut) {
050        DriverStationJNI.getControlWord(controlWord);
051        if (!(controlWord.getEnabled() && controlWord.getDSAttached())) {
052          safetyCounter = 0;
053        }
054        if (++safetyCounter >= 4) {
055          checkMotors();
056          safetyCounter = 0;
057        }
058      } else {
059        safetyCounter = 0;
060      }
061    }
062  }
063
064  /** MotorSafety constructor. */
065  @SuppressWarnings("this-escape")
066  public MotorSafety() {
067    synchronized (m_listMutex) {
068      m_instanceList.add(this);
069      if (m_safetyThread == null) {
070        m_safetyThread = new Thread(MotorSafety::threadMain, "MotorSafety Thread");
071        m_safetyThread.setDaemon(true);
072        m_safetyThread.start();
073      }
074    }
075  }
076
077  /**
078   * Feed the motor safety object.
079   *
080   * <p>Resets the timer on this object that is used to do the timeouts.
081   */
082  public void feed() {
083    synchronized (m_thisMutex) {
084      m_stopTime = Timer.getFPGATimestamp() + m_expiration;
085    }
086  }
087
088  /**
089   * Set the expiration time for the corresponding motor safety object.
090   *
091   * @param expirationTime The timeout value in seconds.
092   */
093  public void setExpiration(double expirationTime) {
094    synchronized (m_thisMutex) {
095      m_expiration = expirationTime;
096    }
097  }
098
099  /**
100   * Retrieve the timeout value for the corresponding motor safety object.
101   *
102   * @return the timeout value in seconds.
103   */
104  public double getExpiration() {
105    synchronized (m_thisMutex) {
106      return m_expiration;
107    }
108  }
109
110  /**
111   * Determine of the motor is still operating or has timed out.
112   *
113   * @return a true value if the motor is still operating normally and hasn't timed out.
114   */
115  public boolean isAlive() {
116    synchronized (m_thisMutex) {
117      return !m_enabled || m_stopTime > Timer.getFPGATimestamp();
118    }
119  }
120
121  /**
122   * Check if this motor has exceeded its timeout. This method is called periodically to determine
123   * if this motor has exceeded its timeout value. If it has, the stop method is called, and the
124   * motor is shut down until its value is updated again.
125   */
126  public void check() {
127    boolean enabled;
128    double stopTime;
129
130    synchronized (m_thisMutex) {
131      enabled = m_enabled;
132      stopTime = m_stopTime;
133    }
134
135    if (!enabled || RobotState.isDisabled() || RobotState.isTest()) {
136      return;
137    }
138
139    if (stopTime < Timer.getFPGATimestamp()) {
140      DriverStation.reportError(
141          getDescription()
142              + "... Output not updated often enough. See https://docs.wpilib.org/motorsafety for more information.",
143          false);
144
145      stopMotor();
146    }
147  }
148
149  /**
150   * Enable/disable motor safety for this device.
151   *
152   * <p>Turn on and off the motor safety option for this PWM object.
153   *
154   * @param enabled True if motor safety is enforced for this object
155   */
156  public void setSafetyEnabled(boolean enabled) {
157    synchronized (m_thisMutex) {
158      m_enabled = enabled;
159    }
160  }
161
162  /**
163   * Return the state of the motor safety enabled flag.
164   *
165   * <p>Return if the motor safety is currently enabled for this device.
166   *
167   * @return True if motor safety is enforced for this device
168   */
169  public boolean isSafetyEnabled() {
170    synchronized (m_thisMutex) {
171      return m_enabled;
172    }
173  }
174
175  /**
176   * Check the motors to see if any have timed out.
177   *
178   * <p>This static method is called periodically to poll all the motors and stop any that have
179   * timed out.
180   */
181  public static void checkMotors() {
182    synchronized (m_listMutex) {
183      for (MotorSafety elem : m_instanceList) {
184        elem.check();
185      }
186    }
187  }
188
189  public abstract void stopMotor();
190
191  public abstract String getDescription();
192}