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 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      Integer moduleBoxed = module;
100      DataStore pcm = m_handleMap.get(moduleBoxed);
101      if (pcm == null) {
102        pcm = new DataStore(module);
103      }
104      pcm.addRef();
105      return pcm;
106    }
107  }
108
109  private static void freeModule(DataStore store) {
110    synchronized (m_handleLock) {
111      store.removeRef();
112    }
113  }
114
115  /** Converts volts to PSI per the REV Analog Pressure Sensor datasheet. */
116  private static double voltsToPsi(double sensorVoltage, double supplyVoltage) {
117    return 250 * (sensorVoltage / supplyVoltage) - 25;
118  }
119
120  /** Converts PSI to volts per the REV Analog Pressure Sensor datasheet. */
121  private static double psiToVolts(double pressure, double supplyVoltage) {
122    return supplyVoltage * (0.004 * pressure + 0.1);
123  }
124
125  private final DataStore m_dataStore;
126  private final int m_handle;
127
128  /** Constructs a PneumaticHub with the default ID (1). */
129  public PneumaticHub() {
130    this(SensorUtil.getDefaultREVPHModule());
131  }
132
133  /**
134   * Constructs a PneumaticHub.
135   *
136   * @param module module number to construct
137   */
138  public PneumaticHub(int module) {
139    m_dataStore = getForModule(module);
140    m_handle = m_dataStore.m_handle;
141  }
142
143  @Override
144  public void close() {
145    freeModule(m_dataStore);
146  }
147
148  @Override
149  public boolean getCompressor() {
150    return REVPHJNI.getCompressor(m_handle);
151  }
152
153  @Override
154  public CompressorConfigType getCompressorConfigType() {
155    return CompressorConfigType.fromValue(REVPHJNI.getCompressorConfig(m_handle));
156  }
157
158  @Override
159  public boolean getPressureSwitch() {
160    return REVPHJNI.getPressureSwitch(m_handle);
161  }
162
163  @Override
164  public double getCompressorCurrent() {
165    return REVPHJNI.getCompressorCurrent(m_handle);
166  }
167
168  @Override
169  public void setSolenoids(int mask, int values) {
170    REVPHJNI.setSolenoids(m_handle, mask, values);
171  }
172
173  @Override
174  public int getSolenoids() {
175    return REVPHJNI.getSolenoids(m_handle);
176  }
177
178  @Override
179  public int getModuleNumber() {
180    return m_dataStore.m_module;
181  }
182
183  @Override
184  public void fireOneShot(int index) {
185    REVPHJNI.fireOneShot(m_handle, index, m_dataStore.m_oneShotDurMs[index]);
186  }
187
188  @Override
189  public void setOneShotDuration(int index, int durMs) {
190    m_dataStore.m_oneShotDurMs[index] = durMs;
191  }
192
193  @Override
194  public boolean checkSolenoidChannel(int channel) {
195    return REVPHJNI.checkSolenoidChannel(channel);
196  }
197
198  @Override
199  public int checkAndReserveSolenoids(int mask) {
200    synchronized (m_dataStore.m_reserveLock) {
201      if ((m_dataStore.m_reservedMask & mask) != 0) {
202        return m_dataStore.m_reservedMask & mask;
203      }
204      m_dataStore.m_reservedMask |= mask;
205      return 0;
206    }
207  }
208
209  @Override
210  public void unreserveSolenoids(int mask) {
211    synchronized (m_dataStore.m_reserveLock) {
212      m_dataStore.m_reservedMask &= ~mask;
213    }
214  }
215
216  @Override
217  public Solenoid makeSolenoid(int channel) {
218    return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.REVPH, channel);
219  }
220
221  @Override
222  public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
223    return new DoubleSolenoid(
224        m_dataStore.m_module, PneumaticsModuleType.REVPH, forwardChannel, reverseChannel);
225  }
226
227  @Override
228  public Compressor makeCompressor() {
229    return new Compressor(m_dataStore.m_module, PneumaticsModuleType.REVPH);
230  }
231
232  @Override
233  public boolean reserveCompressor() {
234    synchronized (m_dataStore.m_reserveLock) {
235      if (m_dataStore.m_compressorReserved) {
236        return false;
237      }
238      m_dataStore.m_compressorReserved = true;
239      return true;
240    }
241  }
242
243  @Override
244  public void unreserveCompressor() {
245    synchronized (m_dataStore.m_reserveLock) {
246      m_dataStore.m_compressorReserved = false;
247    }
248  }
249
250  @Override
251  public int getSolenoidDisabledList() {
252    int raw = REVPHJNI.getStickyFaultsNative(m_handle);
253    return raw & 0xFFFF;
254  }
255
256  /**
257   * Disables the compressor. The compressor will not turn on until {@link
258   * #enableCompressorDigital()}, {@link #enableCompressorAnalog(double, double)}, or {@link
259   * #enableCompressorHybrid(double, double)} are called.
260   */
261  @Override
262  public void disableCompressor() {
263    REVPHJNI.setClosedLoopControlDisabled(m_handle);
264  }
265
266  @Override
267  public void enableCompressorDigital() {
268    REVPHJNI.setClosedLoopControlDigital(m_handle);
269  }
270
271  /**
272   * Enables the compressor in analog mode. This mode uses an analog pressure sensor connected to
273   * analog channel 0 to cycle the compressor. The compressor will turn on when the pressure drops
274   * below {@code minPressure} and will turn off when the pressure reaches {@code maxPressure}.
275   *
276   * @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
277   *     drops below this value. Range 0-120 PSI.
278   * @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
279   *     reaches this value. Range 0-120 PSI. Must be larger then minPressure.
280   */
281  @Override
282  public void enableCompressorAnalog(double minPressure, double maxPressure) {
283    if (minPressure >= maxPressure) {
284      throw new IllegalArgumentException("maxPressure must be greater than minPressure");
285    }
286    if (minPressure < 0 || minPressure > 120) {
287      throw new IllegalArgumentException(
288          "minPressure must be between 0 and 120 PSI, got " + minPressure);
289    }
290    if (maxPressure < 0 || maxPressure > 120) {
291      throw new IllegalArgumentException(
292          "maxPressure must be between 0 and 120 PSI, got " + maxPressure);
293    }
294
295    // Send the voltage as it would be if the 5V rail was at exactly 5V.
296    // The firmware will compensate for the real 5V rail voltage, which
297    // can fluctuate somewhat over time.
298    double minAnalogVoltage = psiToVolts(minPressure, 5);
299    double maxAnalogVoltage = psiToVolts(maxPressure, 5);
300    REVPHJNI.setClosedLoopControlAnalog(m_handle, minAnalogVoltage, maxAnalogVoltage);
301  }
302
303  /**
304   * Enables the compressor in hybrid mode. This mode uses both a digital pressure switch and an
305   * analog pressure sensor connected to analog channel 0 to cycle the compressor.
306   *
307   * <p>The compressor will turn on when <i>both</i>:
308   *
309   * <ul>
310   *   <li>The digital pressure switch indicates the system is not full AND
311   *   <li>The analog pressure sensor indicates that the pressure in the system is below the
312   *       specified minimum pressure.
313   * </ul>
314   *
315   * <p>The compressor will turn off when <i>either</i>:
316   *
317   * <ul>
318   *   <li>The digital pressure switch is disconnected or indicates that the system is full OR
319   *   <li>The pressure detected by the analog sensor is greater than the specified maximum
320   *       pressure.
321   * </ul>
322   *
323   * @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
324   *     drops below this value and the pressure switch indicates that the system is not full. Range
325   *     0-120 PSI.
326   * @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
327   *     reaches this value or the pressure switch is disconnected or indicates that the system is
328   *     full. Range 0-120 PSI. Must be larger then minPressure.
329   */
330  @Override
331  public void enableCompressorHybrid(double minPressure, double maxPressure) {
332    if (minPressure >= maxPressure) {
333      throw new IllegalArgumentException("maxPressure must be greater than minPressure");
334    }
335    if (minPressure < 0 || minPressure > 120) {
336      throw new IllegalArgumentException(
337          "minPressure must be between 0 and 120 PSI, got " + minPressure);
338    }
339    if (maxPressure < 0 || maxPressure > 120) {
340      throw new IllegalArgumentException(
341          "maxPressure must be between 0 and 120 PSI, got " + maxPressure);
342    }
343
344    // Send the voltage as it would be if the 5V rail was at exactly 5V.
345    // The firmware will compensate for the real 5V rail voltage, which
346    // can fluctuate somewhat over time.
347    double minAnalogVoltage = psiToVolts(minPressure, 5);
348    double maxAnalogVoltage = psiToVolts(maxPressure, 5);
349    REVPHJNI.setClosedLoopControlHybrid(m_handle, minAnalogVoltage, maxAnalogVoltage);
350  }
351
352  /**
353   * Returns the raw voltage of the specified analog input channel.
354   *
355   * @param channel The analog input channel to read voltage from.
356   * @return The voltage of the specified analog input channel.
357   */
358  @Override
359  public double getAnalogVoltage(int channel) {
360    return REVPHJNI.getAnalogVoltage(m_handle, channel);
361  }
362
363  /**
364   * Returns the pressure read by an analog pressure sensor on the specified analog input channel.
365   *
366   * @param channel The analog input channel to read pressure from.
367   * @return The pressure read by an analog pressure sensor on the specified analog input channel.
368   */
369  @Override
370  public double getPressure(int channel) {
371    double sensorVoltage = REVPHJNI.getAnalogVoltage(m_handle, channel);
372    double supplyVoltage = REVPHJNI.get5VVoltage(m_handle);
373    return voltsToPsi(sensorVoltage, supplyVoltage);
374  }
375
376  /** Clears the sticky faults. */
377  public void clearStickyFaults() {
378    REVPHJNI.clearStickyFaults(m_handle);
379  }
380
381  /**
382   * Returns the hardware and firmware versions of this device.
383   *
384   * @return The hardware and firmware versions.
385   */
386  public REVPHVersion getVersion() {
387    return REVPHJNI.getVersion(m_handle);
388  }
389
390  /**
391   * Returns the faults currently active on this device.
392   *
393   * @return The faults.
394   */
395  public REVPHFaults getFaults() {
396    return REVPHJNI.getFaults(m_handle);
397  }
398
399  /**
400   * Returns the sticky faults currently active on this device.
401   *
402   * @return The sticky faults.
403   */
404  public REVPHStickyFaults getStickyFaults() {
405    return REVPHJNI.getStickyFaults(m_handle);
406  }
407
408  /**
409   * Returns the current input voltage for this device.
410   *
411   * @return The input voltage.
412   */
413  public double getInputVoltage() {
414    return REVPHJNI.getInputVoltage(m_handle);
415  }
416
417  /**
418   * Returns the current voltage of the regulated 5v supply.
419   *
420   * @return The current voltage of the 5v supply.
421   */
422  public double get5VRegulatedVoltage() {
423    return REVPHJNI.get5VVoltage(m_handle);
424  }
425
426  /**
427   * Returns the total current (in amps) drawn by all solenoids.
428   *
429   * @return Total current drawn by all solenoids in amps.
430   */
431  public double getSolenoidsTotalCurrent() {
432    return REVPHJNI.getSolenoidCurrent(m_handle);
433  }
434
435  /**
436   * Returns the current voltage of the solenoid power supply.
437   *
438   * @return The current voltage of the solenoid power supply.
439   */
440  public double getSolenoidsVoltage() {
441    return REVPHJNI.getSolenoidVoltage(m_handle);
442  }
443}