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