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}