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.pneumatic;
006
007import java.util.HashMap;
008import java.util.Map;
009import org.wpilib.hardware.hal.HAL;
010import org.wpilib.hardware.hal.PortsJNI;
011import org.wpilib.hardware.hal.REVPHFaults;
012import org.wpilib.hardware.hal.REVPHJNI;
013import org.wpilib.hardware.hal.REVPHStickyFaults;
014import org.wpilib.hardware.hal.REVPHVersion;
015import org.wpilib.system.SensorUtil;
016
017/** Module class for controlling a REV Robotics Pneumatic Hub. */
018public class PneumaticHub implements PneumaticsBase {
019  private static class DataStore implements AutoCloseable {
020    public final int m_module;
021    public final int m_handle;
022    private final int m_busId;
023    private int m_refCount;
024    private int m_reservedMask;
025    private boolean m_compressorReserved;
026    public final int[] m_oneShotDurMs = new int[PortsJNI.getNumREVPHChannels()];
027    private final Object m_reserveLock = new Object();
028
029    DataStore(int busId, int module) {
030      m_handle = REVPHJNI.initialize(busId, module);
031      m_module = module;
032      m_busId = busId;
033      m_handleMaps[busId].put(module, this);
034
035      final REVPHVersion version = REVPHJNI.getVersion(m_handle);
036      final String fwVersion =
037          version.firmwareMajor + "." + version.firmwareMinor + "." + version.firmwareFix;
038
039      // Check PH firmware version
040      if (version.firmwareMajor > 0 && version.firmwareMajor < 22) {
041        throw new IllegalStateException(
042            "The Pneumatic Hub has firmware version "
043                + fwVersion
044                + ", and must be updated to version 2022.0.0 or later "
045                + "using the REV Hardware Client.");
046      }
047    }
048
049    @Override
050    public void close() {
051      REVPHJNI.free(m_handle);
052      m_handleMaps[m_busId].remove(m_module);
053    }
054
055    public void addRef() {
056      m_refCount++;
057    }
058
059    public void removeRef() {
060      m_refCount--;
061      if (m_refCount == 0) {
062        this.close();
063      }
064    }
065  }
066
067  @SuppressWarnings({"unchecked", "rawtypes"})
068  private static final Map<Integer, DataStore>[] m_handleMaps =
069      (Map<Integer, DataStore>[]) new Map[PortsJNI.getNumCanBuses()];
070
071  private static final Object m_handleLock = new Object();
072
073  private static DataStore getForModule(int busId, int module) {
074    synchronized (m_handleLock) {
075      Map<Integer, DataStore> handleMap = m_handleMaps[busId];
076      if (handleMap == null) {
077        handleMap = new HashMap<>();
078        m_handleMaps[busId] = handleMap;
079      }
080
081      DataStore pcm = handleMap.get(module);
082      if (pcm == null) {
083        pcm = new DataStore(busId, module);
084      }
085      pcm.addRef();
086      return pcm;
087    }
088  }
089
090  private static void freeModule(DataStore store) {
091    synchronized (m_handleLock) {
092      store.removeRef();
093    }
094  }
095
096  /** Converts volts to PSI per the REV Analog Pressure Sensor datasheet. */
097  private static double voltsToPsi(double sensorVoltage, double supplyVoltage) {
098    return 250 * (sensorVoltage / supplyVoltage) - 25;
099  }
100
101  /** Converts PSI to volts per the REV Analog Pressure Sensor datasheet. */
102  private static double psiToVolts(double pressure, double supplyVoltage) {
103    return supplyVoltage * (0.004 * pressure + 0.1);
104  }
105
106  private final DataStore m_dataStore;
107  private final int m_handle;
108
109  /**
110   * Constructs a PneumaticHub with the default ID (1).
111   *
112   * @param busId The bus ID
113   */
114  public PneumaticHub(int busId) {
115    this(busId, SensorUtil.getDefaultREVPHModule());
116  }
117
118  /**
119   * Constructs a PneumaticHub.
120   *
121   * @param busId The bus ID
122   * @param module module number to construct
123   */
124  public PneumaticHub(int busId, int module) {
125    m_dataStore = getForModule(busId, module);
126    m_handle = m_dataStore.m_handle;
127  }
128
129  @Override
130  public void close() {
131    freeModule(m_dataStore);
132  }
133
134  @Override
135  public boolean getCompressor() {
136    return REVPHJNI.getCompressor(m_handle);
137  }
138
139  @Override
140  public CompressorConfigType getCompressorConfigType() {
141    return CompressorConfigType.fromValue(REVPHJNI.getCompressorConfig(m_handle));
142  }
143
144  @Override
145  public boolean getPressureSwitch() {
146    return REVPHJNI.getPressureSwitch(m_handle);
147  }
148
149  @Override
150  public double getCompressorCurrent() {
151    return REVPHJNI.getCompressorCurrent(m_handle);
152  }
153
154  @Override
155  public void setSolenoids(int mask, int values) {
156    REVPHJNI.setSolenoids(m_handle, mask, values);
157  }
158
159  @Override
160  public int getSolenoids() {
161    return REVPHJNI.getSolenoids(m_handle);
162  }
163
164  @Override
165  public int getModuleNumber() {
166    return m_dataStore.m_module;
167  }
168
169  @Override
170  public void fireOneShot(int index) {
171    REVPHJNI.fireOneShot(m_handle, index, m_dataStore.m_oneShotDurMs[index]);
172  }
173
174  @Override
175  public void setOneShotDuration(int index, int durMs) {
176    m_dataStore.m_oneShotDurMs[index] = durMs;
177  }
178
179  @Override
180  public boolean checkSolenoidChannel(int channel) {
181    return REVPHJNI.checkSolenoidChannel(channel);
182  }
183
184  @Override
185  public int checkAndReserveSolenoids(int mask) {
186    synchronized (m_dataStore.m_reserveLock) {
187      if ((m_dataStore.m_reservedMask & mask) != 0) {
188        return m_dataStore.m_reservedMask & mask;
189      }
190      m_dataStore.m_reservedMask |= mask;
191      return 0;
192    }
193  }
194
195  @Override
196  public void unreserveSolenoids(int mask) {
197    synchronized (m_dataStore.m_reserveLock) {
198      m_dataStore.m_reservedMask &= ~mask;
199    }
200  }
201
202  @Override
203  public Solenoid makeSolenoid(int channel) {
204    return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.REVPH, channel);
205  }
206
207  @Override
208  public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
209    return new DoubleSolenoid(
210        m_dataStore.m_module, PneumaticsModuleType.REVPH, forwardChannel, reverseChannel);
211  }
212
213  @Override
214  public Compressor makeCompressor() {
215    return new Compressor(m_dataStore.m_module, PneumaticsModuleType.REVPH);
216  }
217
218  @Override
219  public boolean reserveCompressor() {
220    synchronized (m_dataStore.m_reserveLock) {
221      if (m_dataStore.m_compressorReserved) {
222        return false;
223      }
224      m_dataStore.m_compressorReserved = true;
225      return true;
226    }
227  }
228
229  @Override
230  public void unreserveCompressor() {
231    synchronized (m_dataStore.m_reserveLock) {
232      m_dataStore.m_compressorReserved = false;
233    }
234  }
235
236  @Override
237  public int getSolenoidDisabledList() {
238    return REVPHJNI.getSolenoidDisabledList(m_handle);
239  }
240
241  /**
242   * Disables the compressor. The compressor will not turn on until {@link
243   * #enableCompressorDigital()}, {@link #enableCompressorAnalog(double, double)}, or {@link
244   * #enableCompressorHybrid(double, double)} are called.
245   */
246  @Override
247  public void disableCompressor() {
248    REVPHJNI.setClosedLoopControlDisabled(m_handle);
249  }
250
251  @Override
252  public void enableCompressorDigital() {
253    REVPHJNI.setClosedLoopControlDigital(m_handle);
254  }
255
256  /**
257   * Enables the compressor in analog mode. This mode uses an analog pressure sensor connected to
258   * analog channel 0 to cycle the compressor. The compressor will turn on when the pressure drops
259   * below {@code minPressure} and will turn off when the pressure reaches {@code maxPressure}.
260   *
261   * @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
262   *     drops below this value. Range 0-120 PSI.
263   * @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
264   *     reaches this value. Range 0-120 PSI. Must be larger then minPressure.
265   */
266  @Override
267  public void enableCompressorAnalog(double minPressure, double maxPressure) {
268    if (minPressure >= maxPressure) {
269      throw new IllegalArgumentException("maxPressure must be greater than minPressure");
270    }
271    if (minPressure < 0 || minPressure > 120) {
272      throw new IllegalArgumentException(
273          "minPressure must be between 0 and 120 PSI, got " + minPressure);
274    }
275    if (maxPressure < 0 || maxPressure > 120) {
276      throw new IllegalArgumentException(
277          "maxPressure must be between 0 and 120 PSI, got " + maxPressure);
278    }
279
280    // Send the voltage as it would be if the 5V rail was at exactly 5V.
281    // The firmware will compensate for the real 5V rail voltage, which
282    // can fluctuate somewhat over time.
283    double minAnalogVoltage = psiToVolts(minPressure, 5);
284    double maxAnalogVoltage = psiToVolts(maxPressure, 5);
285    REVPHJNI.setClosedLoopControlAnalog(m_handle, minAnalogVoltage, maxAnalogVoltage);
286  }
287
288  /**
289   * Enables the compressor in hybrid mode. This mode uses both a digital pressure switch and an
290   * analog pressure sensor connected to analog channel 0 to cycle the compressor.
291   *
292   * <p>The compressor will turn on when <i>both</i>:
293   *
294   * <ul>
295   *   <li>The digital pressure switch indicates the system is not full AND
296   *   <li>The analog pressure sensor indicates that the pressure in the system is below the
297   *       specified minimum pressure.
298   * </ul>
299   *
300   * <p>The compressor will turn off when <i>either</i>:
301   *
302   * <ul>
303   *   <li>The digital pressure switch is disconnected or indicates that the system is full OR
304   *   <li>The pressure detected by the analog sensor is greater than the specified maximum
305   *       pressure.
306   * </ul>
307   *
308   * @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
309   *     drops below this value and the pressure switch indicates that the system is not full. Range
310   *     0-120 PSI.
311   * @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
312   *     reaches this value or the pressure switch is disconnected or indicates that the system is
313   *     full. Range 0-120 PSI. Must be larger then minPressure.
314   */
315  @Override
316  public void enableCompressorHybrid(double minPressure, double maxPressure) {
317    if (minPressure >= maxPressure) {
318      throw new IllegalArgumentException("maxPressure must be greater than minPressure");
319    }
320    if (minPressure < 0 || minPressure > 120) {
321      throw new IllegalArgumentException(
322          "minPressure must be between 0 and 120 PSI, got " + minPressure);
323    }
324    if (maxPressure < 0 || maxPressure > 120) {
325      throw new IllegalArgumentException(
326          "maxPressure must be between 0 and 120 PSI, got " + maxPressure);
327    }
328
329    // Send the voltage as it would be if the 5V rail was at exactly 5V.
330    // The firmware will compensate for the real 5V rail voltage, which
331    // can fluctuate somewhat over time.
332    double minAnalogVoltage = psiToVolts(minPressure, 5);
333    double maxAnalogVoltage = psiToVolts(maxPressure, 5);
334    REVPHJNI.setClosedLoopControlHybrid(m_handle, minAnalogVoltage, maxAnalogVoltage);
335  }
336
337  /**
338   * Returns the raw voltage of the specified analog input channel.
339   *
340   * @param channel The analog input channel to read voltage from.
341   * @return The voltage of the specified analog input channel.
342   */
343  @Override
344  public double getAnalogVoltage(int channel) {
345    return REVPHJNI.getAnalogVoltage(m_handle, channel);
346  }
347
348  /**
349   * Returns the pressure read by an analog pressure sensor on the specified analog input channel.
350   *
351   * @param channel The analog input channel to read pressure from.
352   * @return The pressure read by an analog pressure sensor on the specified analog input channel.
353   */
354  @Override
355  public double getPressure(int channel) {
356    double sensorVoltage = REVPHJNI.getAnalogVoltage(m_handle, channel);
357    double supplyVoltage = REVPHJNI.get5VVoltage(m_handle);
358    return voltsToPsi(sensorVoltage, supplyVoltage);
359  }
360
361  /** Clears the sticky faults. */
362  public void clearStickyFaults() {
363    REVPHJNI.clearStickyFaults(m_handle);
364  }
365
366  /**
367   * Returns the hardware and firmware versions of this device.
368   *
369   * @return The hardware and firmware versions.
370   */
371  public REVPHVersion getVersion() {
372    return REVPHJNI.getVersion(m_handle);
373  }
374
375  /**
376   * Returns the faults currently active on this device.
377   *
378   * @return The faults.
379   */
380  public REVPHFaults getFaults() {
381    return REVPHJNI.getFaults(m_handle);
382  }
383
384  /**
385   * Returns the sticky faults currently active on this device.
386   *
387   * @return The sticky faults.
388   */
389  public REVPHStickyFaults getStickyFaults() {
390    return REVPHJNI.getStickyFaults(m_handle);
391  }
392
393  /**
394   * Returns the current input voltage for this device.
395   *
396   * @return The input voltage.
397   */
398  public double getInputVoltage() {
399    return REVPHJNI.getInputVoltage(m_handle);
400  }
401
402  /**
403   * Returns the current voltage of the regulated 5v supply.
404   *
405   * @return The current voltage of the 5v supply.
406   */
407  public double get5VRegulatedVoltage() {
408    return REVPHJNI.get5VVoltage(m_handle);
409  }
410
411  /**
412   * Returns the total current (in amps) drawn by all solenoids.
413   *
414   * @return Total current drawn by all solenoids in amps.
415   */
416  public double getSolenoidsTotalCurrent() {
417    return REVPHJNI.getSolenoidCurrent(m_handle);
418  }
419
420  /**
421   * Returns the current voltage of the solenoid power supply.
422   *
423   * @return The current voltage of the solenoid power supply.
424   */
425  public double getSolenoidsVoltage() {
426    return REVPHJNI.getSolenoidVoltage(m_handle);
427  }
428
429  @Override
430  public void reportUsage(String device, String data) {
431    HAL.reportUsage("PH[" + m_dataStore.m_module + "]/" + device, data);
432  }
433}