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