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