SCRIPT Move java files

This commit is contained in:
PJ Reiniger
2025-11-07 19:55:40 -05:00
committed by Peter Johnson
parent 7ca1be9bae
commit c350c5f112
1486 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,285 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.hal.SimDouble;
import edu.wpi.first.hal.SimEnum;
import edu.wpi.first.networktables.DoublePublisher;
import edu.wpi.first.networktables.DoubleTopic;
import edu.wpi.first.networktables.NTSendable;
import edu.wpi.first.networktables.NTSendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/** ADXL345 I2C Accelerometer. */
@SuppressWarnings("TypeName")
public class ADXL345_I2C implements NTSendable, AutoCloseable {
/** Default I2C device address. */
public static final byte kAddress = 0x1D;
private static final byte kPowerCtlRegister = 0x2D;
private static final byte kDataFormatRegister = 0x31;
private static final byte kDataRegister = 0x32;
private static final double kGsPerLSB = 0.00390625;
// private static final byte kPowerCtl_Link = 0x20;
// private static final byte kPowerCtl_AutoSleep = 0x10;
private static final byte kPowerCtl_Measure = 0x08;
// private static final byte kPowerCtl_Sleep = 0x04;
// private static final byte kDataFormat_SelfTest = (byte) 0x80;
// private static final byte kDataFormat_SPI = 0x40;
// private static final byte kDataFormat_IntInvert = 0x20;
private static final byte kDataFormat_FullRes = 0x08;
// private static final byte kDataFormat_Justify = 0x04;
/** Accelerometer range. */
public enum Range {
/** 2 Gs max. */
k2G,
/** 4 Gs max. */
k4G,
/** 8 Gs max. */
k8G,
/** 16 Gs max. */
k16G
}
/** Accelerometer axes. */
public enum Axes {
/** X axis. */
kX((byte) 0x00),
/** Y axis. */
kY((byte) 0x02),
/** Z axis. */
kZ((byte) 0x04);
/** The integer value representing this enumeration. */
public final byte value;
Axes(byte value) {
this.value = value;
}
}
/** Container type for accelerations from all axes. */
@SuppressWarnings("MemberName")
public static class AllAxes {
/** Acceleration along the X axis in g-forces. */
public double XAxis;
/** Acceleration along the Y axis in g-forces. */
public double YAxis;
/** Acceleration along the Z axis in g-forces. */
public double ZAxis;
/** Default constructor. */
public AllAxes() {}
}
private I2C m_i2c;
private SimDevice m_simDevice;
private SimEnum m_simRange;
private SimDouble m_simX;
private SimDouble m_simY;
private SimDouble m_simZ;
/**
* Constructs the ADXL345 Accelerometer with I2C address 0x1D.
*
* @param port The I2C port the accelerometer is attached to
* @param range The range (+ or -) that the accelerometer will measure.
*/
public ADXL345_I2C(I2C.Port port, Range range) {
this(port, range, kAddress);
}
/**
* Constructs the ADXL345 Accelerometer over I2C.
*
* @param port The I2C port the accelerometer is attached to
* @param range The range (+ or -) that the accelerometer will measure.
* @param deviceAddress I2C address of the accelerometer (0x1D or 0x53)
*/
@SuppressWarnings("this-escape")
public ADXL345_I2C(I2C.Port port, Range range, int deviceAddress) {
m_i2c = new I2C(port, deviceAddress);
// simulation
m_simDevice = SimDevice.create("Accel:ADXL345_I2C", port.value, deviceAddress);
if (m_simDevice != null) {
m_simRange =
m_simDevice.createEnumDouble(
"range",
SimDevice.Direction.kOutput,
new String[] {"2G", "4G", "8G", "16G"},
new double[] {2.0, 4.0, 8.0, 16.0},
0);
m_simX = m_simDevice.createDouble("x", SimDevice.Direction.kInput, 0.0);
m_simY = m_simDevice.createDouble("y", SimDevice.Direction.kInput, 0.0);
m_simZ = m_simDevice.createDouble("z", SimDevice.Direction.kInput, 0.0);
}
// Turn on the measurements
m_i2c.write(kPowerCtlRegister, kPowerCtl_Measure);
setRange(range);
HAL.reportUsage("I2C[" + port.value + "][" + deviceAddress + "]", "ADXL345");
SendableRegistry.add(this, "ADXL345_I2C", port.value);
}
/**
* Returns the I2C port.
*
* @return The I2C port.
*/
public int getPort() {
return m_i2c.getPort();
}
/**
* Returns the I2C device address.
*
* @return The I2C device address.
*/
public int getDeviceAddress() {
return m_i2c.getDeviceAddress();
}
@Override
public void close() {
SendableRegistry.remove(this);
if (m_i2c != null) {
m_i2c.close();
m_i2c = null;
}
if (m_simDevice != null) {
m_simDevice.close();
m_simDevice = null;
}
}
/**
* Set the measuring range of the accelerometer.
*
* @param range The maximum acceleration, positive or negative, that the accelerometer will
* measure.
*/
public final void setRange(Range range) {
final byte value =
switch (range) {
case k2G -> 0;
case k4G -> 1;
case k8G -> 2;
case k16G -> 3;
};
// Specify the data format to read
m_i2c.write(kDataFormatRegister, kDataFormat_FullRes | value);
if (m_simRange != null) {
m_simRange.set(value);
}
}
/**
* Returns the acceleration along the X axis in g-forces.
*
* @return The acceleration along the X axis in g-forces.
*/
public double getX() {
return getAcceleration(Axes.kX);
}
/**
* Returns the acceleration along the Y axis in g-forces.
*
* @return The acceleration along the Y axis in g-forces.
*/
public double getY() {
return getAcceleration(Axes.kY);
}
/**
* Returns the acceleration along the Z axis in g-forces.
*
* @return The acceleration along the Z axis in g-forces.
*/
public double getZ() {
return getAcceleration(Axes.kZ);
}
/**
* Get the acceleration of one axis in Gs.
*
* @param axis The axis to read from.
* @return Acceleration of the ADXL345 in Gs.
*/
public double getAcceleration(Axes axis) {
if (axis == Axes.kX && m_simX != null) {
return m_simX.get();
}
if (axis == Axes.kY && m_simY != null) {
return m_simY.get();
}
if (axis == Axes.kZ && m_simZ != null) {
return m_simZ.get();
}
ByteBuffer rawAccel = ByteBuffer.allocate(2);
m_i2c.read(kDataRegister + axis.value, 2, rawAccel);
// Sensor is little endian... swap bytes
rawAccel.order(ByteOrder.LITTLE_ENDIAN);
return rawAccel.getShort(0) * kGsPerLSB;
}
/**
* Get the acceleration of all axes in Gs.
*
* @return An object containing the acceleration measured on each axis of the ADXL345 in Gs.
*/
public AllAxes getAccelerations() {
AllAxes data = new AllAxes();
if (m_simX != null && m_simY != null && m_simZ != null) {
data.XAxis = m_simX.get();
data.YAxis = m_simY.get();
data.ZAxis = m_simZ.get();
return data;
}
ByteBuffer rawData = ByteBuffer.allocate(6);
m_i2c.read(kDataRegister, 6, rawData);
// Sensor is little endian... swap bytes
rawData.order(ByteOrder.LITTLE_ENDIAN);
data.XAxis = rawData.getShort(0) * kGsPerLSB;
data.YAxis = rawData.getShort(2) * kGsPerLSB;
data.ZAxis = rawData.getShort(4) * kGsPerLSB;
return data;
}
@Override
public void initSendable(NTSendableBuilder builder) {
builder.setSmartDashboardType("3AxisAccelerometer");
DoublePublisher pubX = new DoubleTopic(builder.getTopic("X")).publish();
DoublePublisher pubY = new DoubleTopic(builder.getTopic("Y")).publish();
DoublePublisher pubZ = new DoubleTopic(builder.getTopic("Z")).publish();
builder.addCloseable(pubX);
builder.addCloseable(pubY);
builder.addCloseable(pubZ);
builder.setUpdateTable(
() -> {
AllAxes data = getAccelerations();
pubX.set(data.XAxis);
pubY.set(data.YAxis);
pubZ.set(data.ZAxis);
});
}
}

View File

@@ -0,0 +1,117 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Handle operation of an analog accelerometer. The accelerometer reads acceleration directly
* through the sensor. Many sensors have multiple axis and can be treated as multiple devices. Each
* is calibrated by finding the center value over a period of time.
*/
public class AnalogAccelerometer implements Sendable, AutoCloseable {
private AnalogInput m_analogChannel;
private double m_voltsPerG = 1.0;
private double m_zeroGVoltage = 2.5;
private final boolean m_allocatedChannel;
/** Common initialization. */
private void initAccelerometer() {
HAL.reportUsage("IO", m_analogChannel.getChannel(), "Accelerometer");
SendableRegistry.add(this, "Accelerometer", m_analogChannel.getChannel());
}
/**
* Create a new instance of an accelerometer.
*
* <p>The constructor allocates desired analog channel.
*
* @param channel The channel number for the analog input the accelerometer is connected to
*/
@SuppressWarnings("this-escape")
public AnalogAccelerometer(final int channel) {
this(new AnalogInput(channel), true);
SendableRegistry.addChild(this, m_analogChannel);
}
/**
* Create a new instance of Accelerometer from an existing AnalogChannel. Make a new instance of
* accelerometer given an AnalogChannel. This is particularly useful if the port is going to be
* read as an analog channel as well as through the Accelerometer class.
*
* @param channel The existing AnalogInput object for the analog input the accelerometer is
* connected to
*/
@SuppressWarnings("this-escape")
public AnalogAccelerometer(final AnalogInput channel) {
this(channel, false);
}
@SuppressWarnings("this-escape")
private AnalogAccelerometer(final AnalogInput channel, final boolean allocatedChannel) {
requireNonNullParam(channel, "channel", "AnalogAccelerometer");
m_allocatedChannel = allocatedChannel;
m_analogChannel = channel;
initAccelerometer();
}
/** Delete the analog components used for the accelerometer. */
@Override
public void close() {
SendableRegistry.remove(this);
if (m_analogChannel != null && m_allocatedChannel) {
m_analogChannel.close();
}
m_analogChannel = null;
}
/**
* Return the acceleration in Gs.
*
* <p>The acceleration is returned units of Gs.
*
* @return The current acceleration of the sensor in Gs.
*/
public double getAcceleration() {
if (m_analogChannel == null) {
return 0.0;
}
return (m_analogChannel.getVoltage() - m_zeroGVoltage) / m_voltsPerG;
}
/**
* Set the accelerometer sensitivity.
*
* <p>This sets the sensitivity of the accelerometer used for calculating the acceleration. The
* sensitivity varies by accelerometer model.
*
* @param sensitivity The sensitivity of accelerometer in Volts per G.
*/
public void setSensitivity(double sensitivity) {
m_voltsPerG = sensitivity;
}
/**
* Set the voltage that corresponds to 0 G.
*
* <p>The zero G voltage varies by accelerometer model.
*
* @param zero The zero G voltage.
*/
public void setZero(double zero) {
m_zeroGVoltage = zero;
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Accelerometer");
builder.addDoubleProperty("Value", this::getAcceleration, null);
}
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.CANAPIJNI;
import edu.wpi.first.hal.CANAPITypes;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.can.CANReceiveMessage;
import java.io.Closeable;
/**
* High level class for interfacing with CAN devices conforming to the standard CAN spec.
*
* <p>No packets that can be sent gets blocked by the RoboRIO, so all methods work identically in
* all robot modes.
*
* <p>All methods are thread safe, however the CANData object passed into the read methods and the
* byte[] passed into the write methods need to not be modified for the duration of their respective
* calls.
*/
public class CAN implements Closeable {
/** Team manufacturer. */
public static final int kTeamManufacturer = CANAPITypes.CANManufacturer.kTeamUse.id;
/** Team device type. */
public static final int kTeamDeviceType = CANAPITypes.CANDeviceType.kMiscellaneous.id;
private final int m_handle;
/**
* Create a new CAN communication interface with the specific device ID. This uses the team
* manufacturer and device types. The device ID is 6 bits (0-63).
*
* @param busId The bus ID
* @param deviceId The device id
*/
public CAN(int busId, int deviceId) {
this(busId, deviceId, kTeamManufacturer, kTeamDeviceType);
}
/**
* Create a new CAN communication interface with a specific device ID, manufacturer and device
* type. The device ID is 6 bits, the manufacturer is 8 bits, and the device type is 5 bits.
*
* @param busId The bus ID
* @param deviceId The device ID
* @param deviceManufacturer The device manufacturer
* @param deviceType The device type
*/
public CAN(int busId, int deviceId, int deviceManufacturer, int deviceType) {
m_handle = CANAPIJNI.initializeCAN(busId, deviceManufacturer, deviceId, deviceType);
HAL.reportUsage("CAN[" + deviceType + "][" + deviceManufacturer + "][" + deviceId + "]", "");
}
/** Closes the CAN communication. */
@Override
public void close() {
if (m_handle != 0) {
CANAPIJNI.cleanCAN(m_handle);
}
}
/**
* Write a packet to the CAN device with a specific ID. This ID is 10 bits.
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
*/
public void writePacket(int apiId, byte[] data, int dataLength, int flags) {
CANAPIJNI.writeCANPacket(m_handle, apiId, data, dataLength, flags);
}
/**
* Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits. The RoboRIO
* will automatically repeat the packet at the specified interval
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
* @param repeatMs The period to repeat the packet at.
*/
public void writePacketRepeating(
int apiId, byte[] data, int dataLength, int flags, int repeatMs) {
CANAPIJNI.writeCANPacketRepeating(m_handle, apiId, data, dataLength, flags, repeatMs);
}
/**
* Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits. The length by spec
* must match what is returned by the responding device
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
*/
public void writeRTRFrame(int apiId, byte[] data, int dataLength, int flags) {
CANAPIJNI.writeCANRTRFrame(m_handle, apiId, data, dataLength, flags);
}
/**
* Write a packet to the CAN device with a specific ID. This ID is 10 bits.
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
* @return TODO
*/
public int writePacketNoThrow(int apiId, byte[] data, int dataLength, int flags) {
return CANAPIJNI.writeCANPacketNoThrow(m_handle, apiId, data, dataLength, flags);
}
/**
* Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits. The RoboRIO
* will automatically repeat the packet at the specified interval
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
* @param repeatMs The period to repeat the packet at.
* @return TODO
*/
public int writePacketRepeatingNoThrow(
int apiId, byte[] data, int dataLength, int flags, int repeatMs) {
return CANAPIJNI.writeCANPacketRepeatingNoThrow(
m_handle, apiId, data, dataLength, flags, repeatMs);
}
/**
* Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits. The length by spec
* must match what is returned by the responding device
*
* @param apiId The API ID to write.
* @param data The data to write
* @param dataLength The data length
* @param flags The flags
* @return TODO
*/
public int writeRTRFrameNoThrow(int apiId, byte[] data, int dataLength, int flags) {
return CANAPIJNI.writeCANRTRFrameNoThrow(m_handle, apiId, data, dataLength, flags);
}
/**
* Stop a repeating packet with a specific ID. This ID is 10 bits.
*
* @param apiId The API ID to stop repeating
*/
public void stopPacketRepeating(int apiId) {
CANAPIJNI.stopCANPacketRepeating(m_handle, apiId);
}
/**
* Read a new CAN packet. This will only return properly once per packet received. Multiple calls
* without receiving another packet will return false.
*
* @param apiId The API ID to read.
* @param data Storage for the received data.
* @return True if the data is valid, otherwise false.
*/
public boolean readPacketNew(int apiId, CANReceiveMessage data) {
return CANAPIJNI.readCANPacketNew(m_handle, apiId, data);
}
/**
* Read a CAN packet. This will continuously return the last packet received, without accounting
* for packet age.
*
* @param apiId The API ID to read.
* @param data Storage for the received data.
* @return True if the data is valid, otherwise false.
*/
public boolean readPacketLatest(int apiId, CANReceiveMessage data) {
return CANAPIJNI.readCANPacketLatest(m_handle, apiId, data);
}
/**
* Read a CAN packet. This will return the last packet received until the packet is older than the
* requested timeout. Then it will return false.
*
* @param apiId The API ID to read.
* @param timeoutMs The timeout time for the packet
* @param data Storage for the received data.
* @return True if the data is valid, otherwise false.
*/
public boolean readPacketTimeout(int apiId, CANReceiveMessage data, int timeoutMs) {
return CANAPIJNI.readCANPacketTimeout(m_handle, apiId, data, timeoutMs);
}
}

View File

@@ -0,0 +1,390 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.I2CJNI;
import edu.wpi.first.hal.util.BoundaryException;
import java.nio.ByteBuffer;
/**
* I2C bus interface class.
*
* <p>This class is intended to be used by sensor (and other I2C device) drivers. It probably should
* not be used directly.
*/
public class I2C implements AutoCloseable {
/** I2C connection ports. */
public enum Port {
/** I2C Port 0. */
kPort0(0),
/** I2C Port 1. */
kPort1(1);
/** Port value. */
public final int value;
Port(int value) {
this.value = value;
}
}
private final int m_port;
private final int m_deviceAddress;
private ByteBuffer m_readDataToSendBuffer;
/**
* Constructor.
*
* @param port The I2C port the device is connected to.
* @param deviceAddress The address of the device on the I2C bus.
*/
public I2C(Port port, int deviceAddress) {
m_port = port.value;
m_deviceAddress = deviceAddress;
I2CJNI.i2CInitialize((byte) port.value);
HAL.reportUsage("I2C[" + port.value + "][" + deviceAddress + "]", "");
}
/**
* Returns I2C port.
*
* @return I2C port.
*/
public int getPort() {
return m_port;
}
/**
* Returns I2C device address.
*
* @return I2C device address.
*/
public int getDeviceAddress() {
return m_deviceAddress;
}
@Override
public void close() {
I2CJNI.i2CClose(m_port);
}
/**
* Generic transaction.
*
* <p>This is a lower-level interface to the I2C hardware giving you more control over each
* transaction. If you intend to write multiple bytes in the same transaction and do not plan to
* receive anything back, use writeBulk() instead. Calling this with a receiveSize of 0 will
* result in an error.
*
* @param dataToSend Buffer of data to send as part of the transaction.
* @param sendSize Number of bytes to send as part of the transaction.
* @param dataReceived Buffer to read data into.
* @param receiveSize Number of bytes to read from the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean transaction(
byte[] dataToSend, int sendSize, byte[] dataReceived, int receiveSize) {
if (dataToSend.length < sendSize) {
throw new IllegalArgumentException("dataToSend is too small, must be at least " + sendSize);
}
if (dataReceived.length < receiveSize) {
throw new IllegalArgumentException(
"dataReceived is too small, must be at least " + receiveSize);
}
return I2CJNI.i2CTransactionB(
m_port,
(byte) m_deviceAddress,
dataToSend,
(byte) sendSize,
dataReceived,
(byte) receiveSize)
< 0;
}
/**
* Generic transaction.
*
* <p>This is a lower-level interface to the I2C hardware giving you more control over each
* transaction.
*
* @param dataToSend Buffer of data to send as part of the transaction.
* @param sendSize Number of bytes to send as part of the transaction.
* @param dataReceived Buffer to read data into.
* @param receiveSize Number of bytes to read from the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean transaction(
ByteBuffer dataToSend, int sendSize, ByteBuffer dataReceived, int receiveSize) {
if (dataToSend.hasArray() && dataReceived.hasArray()) {
return transaction(dataToSend.array(), sendSize, dataReceived.array(), receiveSize);
}
if (!dataToSend.isDirect()) {
throw new IllegalArgumentException("dataToSend must be a direct buffer");
}
if (dataToSend.capacity() < sendSize) {
throw new IllegalArgumentException("dataToSend is too small, must be at least " + sendSize);
}
if (!dataReceived.isDirect()) {
throw new IllegalArgumentException("dataReceived must be a direct buffer");
}
if (dataReceived.capacity() < receiveSize) {
throw new IllegalArgumentException(
"dataReceived is too small, must be at least " + receiveSize);
}
return I2CJNI.i2CTransaction(
m_port,
(byte) m_deviceAddress,
dataToSend,
(byte) sendSize,
dataReceived,
(byte) receiveSize)
< 0;
}
/**
* Attempt to address a device on the I2C bus.
*
* <p>This allows you to figure out if there is a device on the I2C bus that responds to the
* address specified in the constructor.
*
* @return Transfer Aborted... false for success, true for aborted.
*/
public boolean addressOnly() {
return transaction(new byte[0], (byte) 0, new byte[0], (byte) 0);
}
/**
* Execute a write transaction with the device.
*
* <p>Write a single byte to a register on a device and wait until the transaction is complete.
*
* @param registerAddress The address of the register on the device to be written.
* @param data The byte to write to the register on the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean write(int registerAddress, int data) {
byte[] buffer = new byte[2];
buffer[0] = (byte) registerAddress;
buffer[1] = (byte) data;
return I2CJNI.i2CWriteB(m_port, (byte) m_deviceAddress, buffer, (byte) buffer.length) < 0;
}
/**
* Execute a write transaction with the device.
*
* <p>Write multiple bytes to a register on a device and wait until the transaction is complete.
*
* @param data The data to write to the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean writeBulk(byte[] data) {
return writeBulk(data, data.length);
}
/**
* Execute a write transaction with the device.
*
* <p>Write multiple bytes to a register on a device and wait until the transaction is complete.
*
* @param data The data to write to the device.
* @param size The number of data bytes to write.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean writeBulk(byte[] data, int size) {
if (data.length < size) {
throw new IllegalArgumentException("buffer is too small, must be at least " + size);
}
return I2CJNI.i2CWriteB(m_port, (byte) m_deviceAddress, data, (byte) size) < 0;
}
/**
* Execute a write transaction with the device.
*
* <p>Write multiple bytes to a register on a device and wait until the transaction is complete.
*
* @param data The data to write to the device.
* @param size The number of data bytes to write.
* @return Transfer Aborted... false for success, true for aborted.
*/
public synchronized boolean writeBulk(ByteBuffer data, int size) {
if (data.hasArray()) {
return writeBulk(data.array(), size);
}
if (!data.isDirect()) {
throw new IllegalArgumentException("must be a direct buffer");
}
if (data.capacity() < size) {
throw new IllegalArgumentException("buffer is too small, must be at least " + size);
}
return I2CJNI.i2CWrite(m_port, (byte) m_deviceAddress, data, (byte) size) < 0;
}
/**
* Execute a read transaction with the device.
*
* <p>Read bytes from a device. Most I2C devices will auto-increment the register pointer
* internally allowing you to read consecutive registers on a device in a single transaction.
*
* @param registerAddress The register to read first in the transaction.
* @param count The number of bytes to read in the transaction.
* @param buffer A pointer to the array of bytes to store the data read from the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public boolean read(int registerAddress, int count, byte[] buffer) {
requireNonNullParam(buffer, "buffer", "read");
if (count < 1) {
throw new BoundaryException("Value must be at least 1, " + count + " given");
}
if (buffer.length < count) {
throw new IllegalArgumentException("buffer is too small, must be at least " + count);
}
byte[] registerAddressArray = new byte[1];
registerAddressArray[0] = (byte) registerAddress;
return transaction(registerAddressArray, registerAddressArray.length, buffer, count);
}
/**
* Execute a read transaction with the device.
*
* <p>Read bytes from a device. Most I2C devices will auto-increment the register pointer
* internally allowing you to read consecutive registers on a device in a single transaction.
*
* @param registerAddress The register to read first in the transaction.
* @param count The number of bytes to read in the transaction.
* @param buffer A buffer to store the data read from the device.
* @return Transfer Aborted... false for success, true for aborted.
*/
public boolean read(int registerAddress, int count, ByteBuffer buffer) {
if (count < 1) {
throw new BoundaryException("Value must be at least 1, " + count + " given");
}
if (buffer.hasArray()) {
return read(registerAddress, count, buffer.array());
}
if (!buffer.isDirect()) {
throw new IllegalArgumentException("must be a direct buffer");
}
if (buffer.capacity() < count) {
throw new IllegalArgumentException("buffer is too small, must be at least " + count);
}
synchronized (this) {
if (m_readDataToSendBuffer == null) {
m_readDataToSendBuffer = ByteBuffer.allocateDirect(1);
}
m_readDataToSendBuffer.put(0, (byte) registerAddress);
return transaction(m_readDataToSendBuffer, 1, buffer, count);
}
}
/**
* Execute a read only transaction with the device.
*
* <p>Read bytes from a device. This method does not write any data to prompt the device.
*
* @param buffer A pointer to the array of bytes to store the data read from the device.
* @param count The number of bytes to read in the transaction.
* @return Transfer Aborted... false for success, true for aborted.
*/
public boolean readOnly(byte[] buffer, int count) {
requireNonNullParam(buffer, "buffer", "readOnly");
if (count < 1) {
throw new BoundaryException("Value must be at least 1, " + count + " given");
}
if (buffer.length < count) {
throw new IllegalArgumentException("buffer is too small, must be at least " + count);
}
return I2CJNI.i2CReadB(m_port, (byte) m_deviceAddress, buffer, (byte) count) < 0;
}
/**
* Execute a read only transaction with the device.
*
* <p>Read bytes from a device. This method does not write any data to prompt the device.
*
* @param buffer A pointer to the array of bytes to store the data read from the device.
* @param count The number of bytes to read in the transaction.
* @return Transfer Aborted... false for success, true for aborted.
*/
public boolean readOnly(ByteBuffer buffer, int count) {
if (count < 1) {
throw new BoundaryException("Value must be at least 1, " + count + " given");
}
if (buffer.hasArray()) {
return readOnly(buffer.array(), count);
}
if (!buffer.isDirect()) {
throw new IllegalArgumentException("must be a direct buffer");
}
if (buffer.capacity() < count) {
throw new IllegalArgumentException("buffer is too small, must be at least " + count);
}
return I2CJNI.i2CRead(m_port, (byte) m_deviceAddress, buffer, (byte) count) < 0;
}
/*
* Send a broadcast write to all devices on the I2C bus.
*
* <p>This is not currently implemented!
*
* @param registerAddress The register to write on all devices on the bus.
* @param data The value to write to the devices.
*/
// public void broadcast(int registerAddress, int data) {
// }
/**
* Verify that a device's registers contain expected values.
*
* <p>Most devices will have a set of registers that contain a known value that can be used to
* identify them. This allows an I2C device driver to easily verify that the device contains the
* expected value.
*
* @param registerAddress The base register to start reading from the device.
* @param count The size of the field to be verified.
* @param expected A buffer containing the values expected from the device.
* @return true if the sensor was verified to be connected
* @pre The device must support and be configured to use register auto-increment.
*/
public boolean verifySensor(int registerAddress, int count, byte[] expected) {
// TODO: Make use of all 7 read bytes
byte[] dataToSend = new byte[1];
byte[] deviceData = new byte[4];
for (int i = 0; i < count; i += 4) {
int toRead = Math.min(count - i, 4);
// Read the chunk of data. Return false if the sensor does not
// respond.
dataToSend[0] = (byte) (registerAddress + i);
if (transaction(dataToSend, 1, deviceData, toRead)) {
return false;
}
for (byte j = 0; j < toRead; j++) {
if (deviceData[j] != expected[i + j]) {
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,358 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SerialPortJNI;
import java.nio.charset.StandardCharsets;
/** Driver for the serial ports (USB, MXP, Onboard) on the roboRIO. */
public class SerialPort implements AutoCloseable {
private int m_portHandle;
/** Serial port. */
public enum Port {
/** Onboard serial port on the roboRIO. */
kOnboard(0),
/** MXP (roboRIO MXP) serial port. */
kMXP(1),
/** USB serial port (same as kUSB1). */
kUSB(2),
/** USB serial port 1. */
kUSB1(2),
/** USB serial port 2. */
kUSB2(3);
/** Port value. */
public final int value;
Port(int value) {
this.value = value;
}
}
/** Represents the parity to use for serial communications. */
public enum Parity {
/** No parity. */
kNone(0),
/** Odd parity. */
kOdd(1),
/** Even parity. */
kEven(2),
/** Parity bit always on. */
kMark(3),
/** Parity bit always off. */
kSpace(4);
/** Parity value. */
public final int value;
Parity(int value) {
this.value = value;
}
}
/** Represents the number of stop bits to use for Serial Communication. */
public enum StopBits {
/** One stop bit. */
kOne(10),
/** One and a half stop bits. */
kOnePointFive(15),
/** Two stop bits. */
kTwo(20);
/** StopBits value. */
public final int value;
StopBits(int value) {
this.value = value;
}
}
/** Represents what type of flow control to use for serial communication. */
public enum FlowControl {
/** No flow control. */
kNone(0),
/** XON/XOFF flow control. */
kXonXoff(1),
/** RTS/CTS flow control. */
kRtsCts(2),
/** DTS/DSR flow control. */
kDtsDsr(4);
/** FlowControl value. */
public final int value;
FlowControl(int value) {
this.value = value;
}
}
/** Represents which type of buffer mode to use when writing to a serial port. */
public enum WriteBufferMode {
/** Flush the buffer on each access. */
kFlushOnAccess(1),
/** Flush the buffer when it is full. */
kFlushWhenFull(2);
/** WriteBufferMode value. */
public final int value;
WriteBufferMode(int value) {
this.value = value;
}
}
/**
* Create an instance of a Serial Port class.
*
* @param baudRate The baud rate to configure the serial port.
* @param port The Serial port to use
* @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
* @param parity Select the type of parity checking to use.
* @param stopBits The number of stop bits to use as defined by the enum StopBits.
*/
public SerialPort(
final int baudRate, Port port, final int dataBits, Parity parity, StopBits stopBits) {
m_portHandle = SerialPortJNI.serialInitializePort((byte) port.value);
SerialPortJNI.serialSetBaudRate(m_portHandle, baudRate);
SerialPortJNI.serialSetDataBits(m_portHandle, (byte) dataBits);
SerialPortJNI.serialSetParity(m_portHandle, (byte) parity.value);
SerialPortJNI.serialSetStopBits(m_portHandle, (byte) stopBits.value);
// Set the default read buffer size to 1 to return bytes immediately
setReadBufferSize(1);
// Set the default timeout to 5 seconds.
setTimeout(5.0);
// Don't wait until the buffer is full to transmit.
setWriteBufferMode(WriteBufferMode.kFlushOnAccess);
disableTermination();
HAL.reportUsage("SerialPort", port.value, "");
}
/**
* Create an instance of a Serial Port class. Defaults to one stop bit.
*
* @param baudRate The baud rate to configure the serial port.
* @param port The serial port to use.
* @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
* @param parity Select the type of parity checking to use.
*/
public SerialPort(final int baudRate, Port port, final int dataBits, Parity parity) {
this(baudRate, port, dataBits, parity, StopBits.kOne);
}
/**
* Create an instance of a Serial Port class. Defaults to no parity and one stop bit.
*
* @param baudRate The baud rate to configure the serial port.
* @param port The serial port to use.
* @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
*/
public SerialPort(final int baudRate, Port port, final int dataBits) {
this(baudRate, port, dataBits, Parity.kNone, StopBits.kOne);
}
/**
* Create an instance of a Serial Port class. Defaults to 8 databits, no parity, and one stop bit.
*
* @param baudRate The baud rate to configure the serial port.
* @param port The serial port to use.
*/
public SerialPort(final int baudRate, Port port) {
this(baudRate, port, 8, Parity.kNone, StopBits.kOne);
}
@Override
public void close() {
SerialPortJNI.serialClose(m_portHandle);
}
/**
* Set the type of flow control to enable on this port.
*
* <p>By default, flow control is disabled.
*
* @param flowControl the FlowControl m_value to use
*/
public void setFlowControl(FlowControl flowControl) {
SerialPortJNI.serialSetFlowControl(m_portHandle, (byte) flowControl.value);
}
/**
* Enable termination and specify the termination character.
*
* <p>Termination is currently only implemented for receive. When the terminator is received, the
* read() or readString() will return fewer bytes than requested, stopping after the terminator.
*
* @param terminator The character to use for termination.
*/
public void enableTermination(char terminator) {
SerialPortJNI.serialEnableTermination(m_portHandle, terminator);
}
/**
* Enable termination with the default terminator '\n'
*
* <p>Termination is currently only implemented for receive. When the terminator is received, the
* read() or readString() will return fewer bytes than requested, stopping after the terminator.
*
* <p>The default terminator is '\n'
*/
public void enableTermination() {
enableTermination('\n');
}
/** Disable termination behavior. */
public final void disableTermination() {
SerialPortJNI.serialDisableTermination(m_portHandle);
}
/**
* Get the number of bytes currently available to read from the serial port.
*
* @return The number of bytes available to read.
*/
public int getBytesReceived() {
return SerialPortJNI.serialGetBytesReceived(m_portHandle);
}
/**
* Read a string out of the buffer. Reads the entire contents of the buffer
*
* @return The read string
*/
public String readString() {
return readString(getBytesReceived());
}
/**
* Read a string out of the buffer. Reads the entire contents of the buffer
*
* @param count the number of characters to read into the string
* @return The read string
*/
public String readString(int count) {
byte[] out = read(count);
return new String(out, StandardCharsets.US_ASCII);
}
/**
* Read raw bytes out of the buffer.
*
* @param count The maximum number of bytes to read.
* @return An array of the read bytes
*/
public byte[] read(final int count) {
byte[] dataReceivedBuffer = new byte[count];
int gotten = SerialPortJNI.serialRead(m_portHandle, dataReceivedBuffer, count);
if (gotten == count) {
return dataReceivedBuffer;
}
byte[] retVal = new byte[gotten];
System.arraycopy(dataReceivedBuffer, 0, retVal, 0, gotten);
return retVal;
}
/**
* Write raw bytes to the serial port.
*
* @param buffer The buffer of bytes to write.
* @param count The maximum number of bytes to write.
* @return The number of bytes actually written into the port.
*/
public int write(byte[] buffer, int count) {
if (buffer.length < count) {
throw new IllegalArgumentException("buffer is too small, must be at least " + count);
}
return SerialPortJNI.serialWrite(m_portHandle, buffer, count);
}
/**
* Write a string to the serial port.
*
* @param data The string to write to the serial port.
* @return The number of bytes actually written into the port.
*/
public int writeString(String data) {
return write(data.getBytes(StandardCharsets.UTF_8), data.length());
}
/**
* Configure the timeout of the serial m_port.
*
* <p>This defines the timeout for transactions with the hardware. It will affect reads if less
* bytes are available than the read buffer size (defaults to 1) and very large writes.
*
* @param timeout The number of seconds to wait for I/O.
*/
public final void setTimeout(double timeout) {
SerialPortJNI.serialSetTimeout(m_portHandle, timeout);
}
/**
* Specify the size of the input buffer.
*
* <p>Specify the amount of data that can be stored before data from the device is returned to
* Read. If you want data that is received to be returned immediately, set this to 1.
*
* <p>It the buffer is not filled before the read timeout expires, all data that has been received
* so far will be returned.
*
* @param size The read buffer size.
*/
public final void setReadBufferSize(int size) {
SerialPortJNI.serialSetReadBufferSize(m_portHandle, size);
}
/**
* Specify the size of the output buffer.
*
* <p>Specify the amount of data that can be stored before being transmitted to the device.
*
* @param size The write buffer size.
*/
public void setWriteBufferSize(int size) {
SerialPortJNI.serialSetWriteBufferSize(m_portHandle, size);
}
/**
* Specify the flushing behavior of the output buffer.
*
* <p>When set to kFlushOnAccess, data is synchronously written to the serial port after each call
* to either print() or write().
*
* <p>When set to kFlushWhenFull, data will only be written to the serial port when the buffer is
* full or when flush() is called.
*
* @param mode The write buffer mode.
*/
public final void setWriteBufferMode(WriteBufferMode mode) {
SerialPortJNI.serialSetWriteMode(m_portHandle, (byte) mode.value);
}
/**
* Force the output buffer to be written to the port.
*
* <p>This is used when setWriteBufferMode() is set to kFlushWhenFull to force a flush before the
* buffer is full.
*/
public void flush() {
SerialPortJNI.serialFlush(m_portHandle);
}
/**
* Reset the serial port driver to a known state.
*
* <p>Empty the transmit and receive buffers in the device and formatted I/O.
*/
public void reset() {
SerialPortJNI.serialClear(m_portHandle);
}
}

View File

@@ -0,0 +1,213 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.AnalogJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Analog channel class.
*
* <p>Each analog channel is read from hardware as a 12-bit number representing 0V to 3.3V.
*
* <p>Connected to each analog channel is an averaging and oversampling engine. This engine
* accumulates the specified ( by setAverageBits() and setOversampleBits() ) number of samples
* before returning a new value. This is not a sliding window average. The only difference between
* the oversampled samples and the averaged samples is that the oversampled samples are simply
* accumulated effectively increasing the resolution, while the averaged samples are divided by the
* number of samples to retain the resolution, but get more stable values.
*/
public class AnalogInput implements Sendable, AutoCloseable {
int m_port; // explicit no modifier, private and package accessible.
private int m_channel;
/**
* Construct an analog channel.
*
* @param channel The channel number to represent. 0-3 are on-board 4-7 are on the MXP port.
*/
@SuppressWarnings("this-escape")
public AnalogInput(final int channel) {
AnalogJNI.checkAnalogInputChannel(channel);
m_channel = channel;
m_port = AnalogJNI.initializeAnalogInputPort(channel);
HAL.reportUsage("IO", channel, "AnalogInput");
SendableRegistry.add(this, "AnalogInput", channel);
}
@Override
public void close() {
SendableRegistry.remove(this);
AnalogJNI.freeAnalogInputPort(m_port);
m_port = 0;
m_channel = 0;
}
/**
* Get a sample straight from this channel. The sample is a 12-bit value representing the 0V to
* 3.3V range of the A/D converter. The units are in A/D converter codes. Use GetVoltage() to get
* the analog value in calibrated units.
*
* @return A sample straight from this channel.
*/
public int getValue() {
return AnalogJNI.getAnalogValue(m_port);
}
/**
* Get a sample from the output of the oversample and average engine for this channel. The sample
* is 12-bit + the bits configured in SetOversampleBits(). The value configured in
* setAverageBits() will cause this value to be averaged 2^bits number of samples. This is not a
* sliding window. The sample will not change until 2^(OversampleBits + AverageBits) samples have
* been acquired from this channel. Use getAverageVoltage() to get the analog value in calibrated
* units.
*
* @return A sample from the oversample and average engine for this channel.
*/
public int getAverageValue() {
return AnalogJNI.getAnalogAverageValue(m_port);
}
/**
* Get a scaled sample straight from this channel. The value is scaled to units of Volts using the
* calibrated scaling data from getLSBWeight() and getOffset().
*
* @return A scaled sample straight from this channel.
*/
public double getVoltage() {
return AnalogJNI.getAnalogVoltage(m_port);
}
/**
* Get a scaled sample from the output of the oversample and average engine for this channel. The
* value is scaled to units of Volts using the calibrated scaling data from getLSBWeight() and
* getOffset(). Using oversampling will cause this value to be higher resolution, but it will
* update more slowly. Using averaging will cause this value to be more stable, but it will update
* more slowly.
*
* @return A scaled sample from the output of the oversample and average engine for this channel.
*/
public double getAverageVoltage() {
return AnalogJNI.getAnalogAverageVoltage(m_port);
}
/**
* Get the factory scaling the least significant bit weight constant. The least significant bit
* weight constant for the channel that was calibrated in manufacturing and stored in an eeprom.
*
* <p>Volts = ((LSB_Weight * 1e-9) * raw) - (Offset * 1e-9)
*
* @return Least significant bit weight.
*/
public long getLSBWeight() {
return AnalogJNI.getAnalogLSBWeight(m_port);
}
/**
* Get the factory scaling offset constant. The offset constant for the channel that was
* calibrated in manufacturing and stored in an eeprom.
*
* <p>Volts = ((LSB_Weight * 1e-9) * raw) - (Offset * 1e-9)
*
* @return Offset constant.
*/
public int getOffset() {
return AnalogJNI.getAnalogOffset(m_port);
}
/**
* Get the channel number.
*
* @return The channel number.
*/
public int getChannel() {
return m_channel;
}
/**
* Set the number of averaging bits. This sets the number of averaging bits. The actual number of
* averaged samples is 2^bits. The averaging is done automatically in the FPGA.
*
* @param bits The number of averaging bits.
*/
public void setAverageBits(final int bits) {
AnalogJNI.setAnalogAverageBits(m_port, bits);
}
/**
* Get the number of averaging bits. This gets the number of averaging bits from the FPGA. The
* actual number of averaged samples is 2^bits. The averaging is done automatically in the FPGA.
*
* @return The number of averaging bits.
*/
public int getAverageBits() {
return AnalogJNI.getAnalogAverageBits(m_port);
}
/**
* Set the number of oversample bits. This sets the number of oversample bits. The actual number
* of oversampled values is 2^bits. The oversampling is done automatically in the FPGA.
*
* @param bits The number of oversample bits.
*/
public void setOversampleBits(final int bits) {
AnalogJNI.setAnalogOversampleBits(m_port, bits);
}
/**
* Get the number of oversample bits. This gets the number of oversample bits from the FPGA. The
* actual number of oversampled values is 2^bits. The oversampling is done automatically in the
* FPGA.
*
* @return The number of oversample bits.
*/
public int getOversampleBits() {
return AnalogJNI.getAnalogOversampleBits(m_port);
}
/**
* Set the sample rate per channel.
*
* <p>This is a global setting for all channels. The maximum rate is 500kS/s divided by the number
* of channels in use. This is 62500 samples/s per channel if all 8 channels are used.
*
* @param samplesPerSecond The number of samples per second.
*/
public static void setGlobalSampleRate(final double samplesPerSecond) {
AnalogJNI.setAnalogSampleRate(samplesPerSecond);
}
/**
* Get the current sample rate.
*
* <p>This assumes one entry in the scan list. This is a global setting for all channels.
*
* @return Sample rate.
*/
public static double getGlobalSampleRate() {
return AnalogJNI.getAnalogSampleRate();
}
/**
* Indicates this input is used by a simulated device.
*
* @param device simulated device handle
*/
public void setSimDevice(SimDevice device) {
AnalogJNI.setAnalogInputSimDevice(m_port, device.getNativeHandle());
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Analog Input");
builder.addDoubleProperty("Value", this::getAverageVoltage, null);
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
/**
* Interface for counting the number of ticks on a digital input channel. Encoders, Gear tooth
* sensors, and counters should all subclass this in order to build more advanced classes for
* control and driving.
*
* <p>All counters will immediately start counting - reset() them if you need them to be zeroed
* before use.
*/
public interface CounterBase {
/** The number of edges for the CounterBase to increment or decrement on. */
enum EncodingType {
/** Count only the rising edge. */
k1X(0),
/** Count both the rising and falling edge. */
k2X(1),
/** Count rising and falling on both channels. */
k4X(2);
/** EncodingType value. */
public final int value;
EncodingType(int value) {
this.value = value;
}
}
/**
* Get the count.
*
* @return the count
*/
int get();
/** Reset the count to zero. */
void reset();
/**
* Get the time between the last two edges counted.
*
* @return the time between the last two ticks in seconds
*/
double getPeriod();
/**
* Set the maximum time between edges to be considered stalled.
*
* @param maxPeriod the maximum period in seconds
*/
void setMaxPeriod(double maxPeriod);
/**
* Determine if the counter is not moving.
*
* @return true if the counter has not changed for the max period
*/
boolean getStopped();
/**
* Determine which direction the counter is going.
*
* @return true for one direction, false for the other
*/
boolean getDirection();
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.DIOJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class to read a digital input. This class will read digital inputs and return the current value
* on the channel. Other devices such as encoders, gear tooth sensors, etc. that are implemented
* elsewhere will automatically allocate digital inputs and outputs as required. This class is only
* for devices like switches etc. that aren't implemented anywhere else.
*/
public class DigitalInput implements AutoCloseable, Sendable {
private final int m_channel;
private int m_handle;
/**
* Create an instance of a Digital Input class. Creates a digital input given a channel.
*
* @param channel the DIO channel for the digital input 0-9 are on-board, 10-25 are on the MXP
*/
@SuppressWarnings("this-escape")
public DigitalInput(int channel) {
SensorUtil.checkDigitalChannel(channel);
m_channel = channel;
m_handle = DIOJNI.initializeDIOPort(channel, true);
HAL.reportUsage("IO", channel, "DigitalInput");
SendableRegistry.add(this, "DigitalInput", channel);
}
@Override
public void close() {
SendableRegistry.remove(this);
DIOJNI.freeDIOPort(m_handle);
m_handle = 0;
}
/**
* Get the value from a digital input channel. Retrieve the value of a single digital input
* channel from the FPGA.
*
* @return the status of the digital input
*/
public boolean get() {
return DIOJNI.getDIO(m_handle);
}
/**
* Get the channel of the digital input.
*
* @return The GPIO channel number that this object represents.
*/
public int getChannel() {
return m_channel;
}
/**
* Indicates this input is used by a simulated device.
*
* @param device simulated device handle
*/
public void setSimDevice(SimDevice device) {
DIOJNI.setDIOSimDevice(m_handle, device.getNativeHandle());
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Digital Input");
builder.addBooleanProperty("Value", this::get, null);
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.DIOJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class to write digital outputs. This class will write digital outputs. Other devices that are
* implemented elsewhere will automatically allocate digital inputs and outputs as required.
*/
public class DigitalOutput implements AutoCloseable, Sendable {
private static final int invalidPwmGenerator = 0;
private int m_pwmGenerator = invalidPwmGenerator;
private final int m_channel;
private int m_handle;
/**
* Create an instance of a digital output. Create an instance of a digital output given a channel.
*
* @param channel the DIO channel to use for the digital output. 0-9 are on-board, 10-25 are on
* the MXP
*/
@SuppressWarnings("this-escape")
public DigitalOutput(int channel) {
SensorUtil.checkDigitalChannel(channel);
m_channel = channel;
m_handle = DIOJNI.initializeDIOPort(channel, false);
HAL.reportUsage("IO", channel, "DigitalOutput");
SendableRegistry.add(this, "DigitalOutput", channel);
}
@Override
public void close() {
SendableRegistry.remove(this);
// Disable the pwm only if we have allocated it
if (m_pwmGenerator != invalidPwmGenerator) {
disablePWM();
}
DIOJNI.freeDIOPort(m_handle);
m_handle = 0;
}
/**
* Set the value of a digital output.
*
* @param value true is on, off is false
*/
public void set(boolean value) {
DIOJNI.setDIO(m_handle, value);
}
/**
* Gets the value being output from the Digital Output.
*
* @return the state of the digital output.
*/
public boolean get() {
return DIOJNI.getDIO(m_handle);
}
/**
* Get the GPIO channel number that this object represents.
*
* @return The GPIO channel number.
*/
public int getChannel() {
return m_channel;
}
/**
* Output a single pulse on the digital output line.
*
* <p>Send a single pulse on the digital output line where the pulse duration is specified in
* seconds. Maximum of 65535 microseconds.
*
* @param pulseLength The pulse length in seconds
*/
public void pulse(final double pulseLength) {
DIOJNI.pulse(m_handle, pulseLength);
}
/**
* Determine if the pulse is still going. Determine if a previously started pulse is still going.
*
* @return true if pulsing
*/
public boolean isPulsing() {
return DIOJNI.isPulsing(m_handle);
}
/**
* Change the PWM frequency of the PWM output on a Digital Output line.
*
* <p>The valid range is from 0.6 Hz to 19 kHz. The frequency resolution is logarithmic.
*
* <p>There is only one PWM frequency for all channels.
*
* @param rate The frequency to output all digital output PWM signals.
*/
public void setPWMRate(double rate) {
DIOJNI.setDigitalPWMRate(rate);
}
/**
* Enable a PWM PPS (Pulse Per Second) Output on this line.
*
* <p>Allocate one of the 6 DO PWM generator resources.
*
* <p>Supply the duty-cycle to output.
*
* <p>The resolution of the duty cycle is 8-bit.
*
* @param dutyCycle The duty-cycle to start generating. [0..1]
*/
public void enablePPS(double dutyCycle) {
if (m_pwmGenerator != invalidPwmGenerator) {
return;
}
m_pwmGenerator = DIOJNI.allocateDigitalPWM();
DIOJNI.setDigitalPWMPPS(m_pwmGenerator, dutyCycle);
DIOJNI.setDigitalPWMOutputChannel(m_pwmGenerator, m_channel);
}
/**
* Enable a PWM Output on this line.
*
* <p>Allocate one of the 6 DO PWM generator resources.
*
* <p>Supply the initial duty-cycle to output in order to avoid a glitch when first starting.
*
* <p>The resolution of the duty cycle is 8-bit for low frequencies (1kHz or less) but is reduced
* the higher the frequency of the PWM signal is.
*
* @param initialDutyCycle The duty-cycle to start generating. [0..1]
*/
public void enablePWM(double initialDutyCycle) {
if (m_pwmGenerator != invalidPwmGenerator) {
return;
}
m_pwmGenerator = DIOJNI.allocateDigitalPWM();
DIOJNI.setDigitalPWMDutyCycle(m_pwmGenerator, initialDutyCycle);
DIOJNI.setDigitalPWMOutputChannel(m_pwmGenerator, m_channel);
}
/**
* Change this line from a PWM output back to a static Digital Output line.
*
* <p>Free up one of the 6 DO PWM generator resources that were in use.
*/
public void disablePWM() {
if (m_pwmGenerator == invalidPwmGenerator) {
return;
}
// Disable the output by routing to a dead bit.
DIOJNI.setDigitalPWMOutputChannel(m_pwmGenerator, SensorUtil.kDigitalChannels);
DIOJNI.freeDigitalPWM(m_pwmGenerator);
m_pwmGenerator = invalidPwmGenerator;
}
/**
* Change the duty-cycle that is being generated on the line.
*
* <p>The resolution of the duty cycle is 8-bit for low frequencies (1kHz or less) but is reduced
* the higher the frequency of the PWM signal is.
*
* @param dutyCycle The duty-cycle to change to. [0..1]
*/
public void updateDutyCycle(double dutyCycle) {
if (m_pwmGenerator == invalidPwmGenerator) {
return;
}
DIOJNI.setDigitalPWMDutyCycle(m_pwmGenerator, dutyCycle);
}
/**
* Indicates this input is used by a simulated device.
*
* @param device simulated device handle
*/
public void setSimDevice(SimDevice device) {
DIOJNI.setDIOSimDevice(m_handle, device.getNativeHandle());
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Digital Output");
builder.addBooleanProperty("Value", this::get, this::set);
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.PWMJNI;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class implements the PWM generation in the FPGA.
*
* <p>The values supplied as arguments for PWM outputs range from -1.0 to 1.0. They are mapped to
* the microseconds to keep the pulse high, with a range of 0 (off) to 4096. Changes are immediately
* sent to the FPGA, and the update occurs at the next FPGA cycle (5.05ms). There is no delay.
*/
public class PWM implements Sendable, AutoCloseable {
/** Represents the output period in microseconds. */
public enum OutputPeriod {
/** Pulse every 5ms. */
k5Ms,
/** Pulse every 10ms. */
k10Ms,
/** Pulse every 20ms. */
k20Ms
}
private final int m_channel;
private int m_handle;
/**
* Allocate a PWM given a channel.
*
* <p>Checks channel value range and allocates the appropriate channel. The allocation is only
* done to help users ensure that they don't double assign channels.
*
* <p>By default, adds itself to SendableRegistry.
*
* @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
*/
public PWM(final int channel) {
this(channel, true);
}
/**
* Allocate a PWM given a channel.
*
* @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
* @param registerSendable If true, adds this instance to SendableRegistry
*/
@SuppressWarnings("this-escape")
public PWM(final int channel, final boolean registerSendable) {
SensorUtil.checkPWMChannel(channel);
m_channel = channel;
m_handle = PWMJNI.initializePWMPort(channel);
setDisabled();
HAL.reportUsage("IO", channel, "PWM");
if (registerSendable) {
SendableRegistry.add(this, "PWM", channel);
}
}
/** Free the resource associated with the PWM channel and set the value to 0. */
@Override
public void close() {
SendableRegistry.remove(this);
if (m_handle == 0) {
return;
}
setDisabled();
PWMJNI.freePWMPort(m_handle);
m_handle = 0;
}
/**
* Gets the channel number associated with the PWM Object.
*
* @return The channel number.
*/
public int getChannel() {
return m_channel;
}
/**
* Set the PWM value directly to the hardware.
*
* <p>Write a microsecond pulse value to a PWM channel.
*
* @param microsecondPulseTime Microsecond pulse PWM value. Range 0 - 4096.
*/
public void setPulseTimeMicroseconds(int microsecondPulseTime) {
PWMJNI.setPulseTimeMicroseconds(m_handle, microsecondPulseTime);
}
/**
* Get the PWM value directly from the hardware.
*
* <p>Read a raw value from a PWM channel.
*
* @return Microsecond pulse PWM control value. Range: 0 - 4096.
*/
public int getPulseTimeMicroseconds() {
return PWMJNI.getPulseTimeMicroseconds(m_handle);
}
/** Temporarily disables the PWM output. The next set call will re-enable the output. */
public final void setDisabled() {
setPulseTimeMicroseconds(0);
}
/**
* Sets the PWM output period.
*
* @param mult The output period to apply to this channel
*/
public void setOutputPeriod(OutputPeriod mult) {
int scale =
switch (mult) {
case k20Ms -> 3;
case k10Ms -> 1;
case k5Ms -> 0;
};
PWMJNI.setPWMOutputPeriod(m_handle, scale);
}
/**
* Get the underlying handle.
*
* @return Underlying PWM handle
*/
public int getHandle() {
return m_handle;
}
/**
* Indicates this input is used by a simulated device.
*
* @param device simulated device handle
*/
public void setSimDevice(SimDevice device) {
PWMJNI.setPWMSimDevice(m_handle, device.getNativeHandle());
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("PWM");
builder.setActuator(true);
builder.addDoubleProperty(
"Value", this::getPulseTimeMicroseconds, value -> setPulseTimeMicroseconds((int) value));
}
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.IMUJNI;
import edu.wpi.first.math.geometry.Quaternion;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Rotation3d;
/** SystemCore onboard IMU. */
public class OnboardIMU {
/** A mount orientation of SystemCore. */
public enum MountOrientation {
/** Flat (mounted parallel to the ground). */
kFlat,
/** Landscape (vertically mounted with long edge of SystemCore parallel to the ground). */
kLandscape,
/** Portrait (vertically mounted with the short edge of SystemCore parallel to the ground). */
kPortrait
}
/**
* Constructs a handle to the SystemCore onboard IMU.
*
* @param mountOrientation the mount orientation of SystemCore to determine yaw.
*/
public OnboardIMU(MountOrientation mountOrientation) {
m_mountOrientation = mountOrientation;
}
/**
* Get the yaw value in radians.
*
* @return yaw value in radians
*/
public double getYawRadians() {
return getYawNoOffset() - m_yawOffset;
}
/**
* Reset the current yaw value to 0. Future reads of the yaw value will be relative to the current
* orientation.
*/
public void resetYaw() {
m_yawOffset = getYawNoOffset();
}
/**
* Get the yaw as a Rotation2d.
*
* @return yaw
*/
public Rotation2d getRotation2d() {
return new Rotation2d(getYawRadians());
}
/**
* Get the 3D orientation as a Rotation3d.
*
* @return 3D orientation
*/
public Rotation3d getRotation3d() {
return new Rotation3d(getQuaternion());
}
/**
* Get the 3D orientation as a Quaternion.
*
* @return 3D orientation
*/
public Quaternion getQuaternion() {
double[] quatRaw = new double[4];
IMUJNI.getIMUQuaternion(quatRaw);
return new Quaternion(quatRaw[0], quatRaw[1], quatRaw[2], quatRaw[3]);
}
/**
* Get the angle about the X axis of the IMU in radians.
*
* @return angle about the X axis in radians
*/
public double getAngleX() {
return getRawEulerAngles()[0];
}
/**
* Get the angle about the Y axis of the IMU in radians.
*
* @return angle about the Y axis in radians
*/
public double getAngleY() {
return getRawEulerAngles()[1];
}
/**
* Get the angle about the Z axis of the IMU in radians.
*
* @return angle about the Z axis in radians
*/
public double getAngleZ() {
return getRawEulerAngles()[2];
}
/**
* Get the angular rate about the X axis of the IMU in radians per second.
*
* @return angular rate about the X axis in radians per second
*/
public double getGyroRateX() {
return getRawGyroRates()[0];
}
/**
* Get the angular rate about the Y axis of the IMU in radians per second.
*
* @return angular rate about the Y axis in radians per second
*/
public double getGyroRateY() {
return getRawGyroRates()[1];
}
/**
* Get the angular rate about the Z axis of the IMU in radians per second.
*
* @return angular rate about the Z axis in radians per second
*/
public double getGyroRateZ() {
return getRawGyroRates()[2];
}
/**
* Get the acceleration along the X axis of the IMU in meters per second squared.
*
* @return acceleration along the X axis in meters per second squared
*/
public double getAccelX() {
return getRawAccels()[0];
}
/**
* Get the acceleration along the X axis of the IMU in meters per second squared.
*
* @return acceleration along the X axis in meters per second squared
*/
public double getAccelY() {
return getRawAccels()[1];
}
/**
* Get the acceleration along the X axis of the IMU in meters per second squared.
*
* @return acceleration along the X axis in meters per second squared
*/
public double getAccelZ() {
return getRawAccels()[2];
}
private double[] getRawEulerAngles() {
double[] anglesRaw = new double[3];
switch (m_mountOrientation) {
case kFlat -> IMUJNI.getIMUEulerAnglesFlat(anglesRaw);
case kLandscape -> IMUJNI.getIMUEulerAnglesLandscape(anglesRaw);
case kPortrait -> IMUJNI.getIMUEulerAnglesPortrait(anglesRaw);
default -> {
// NOP
}
}
return anglesRaw;
}
private double[] getRawGyroRates() {
double[] ratesRaw = new double[3];
IMUJNI.getIMUGyroRates(ratesRaw);
return ratesRaw;
}
private double[] getRawAccels() {
double[] accelsRaw = new double[3];
IMUJNI.getIMUAcceleration(accelsRaw);
return accelsRaw;
}
private double getYawNoOffset() {
return switch (m_mountOrientation) {
case kFlat -> IMUJNI.getIMUYawFlat();
case kLandscape -> IMUJNI.getIMUYawLandscape();
case kPortrait -> IMUJNI.getIMUYawPortrait();
default -> 0;
};
}
private final MountOrientation m_mountOrientation;
private double m_yawOffset;
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.AddressableLEDJNI;
import edu.wpi.first.hal.HAL;
/**
* A class for driving addressable LEDs, such as WS2812B, WS2815, and NeoPixels.
*
* <p>Some LEDs use a different color order than the default GRB. The color order is configurable
* using {@link #setColorOrder(ColorOrder)}.
*
* <p>Up to 1024 LEDs may be controlled in total across all AddressableLED instances. A single
* global buffer is used for all instances. The start position used for LED data for the output is
* set via SetStart() and the length of the strip is set via SetLength(). Both of these default to
* zero, so multiple instances will access the same pixel data unless SetStart() is called to adjust
* the starting point.
*/
public class AddressableLED implements AutoCloseable {
/** Order that color data is sent over the wire. */
public enum ColorOrder {
/** RGB order. */
kRGB(AddressableLEDJNI.COLOR_ORDER_RGB),
/** RBG order. */
kRBG(AddressableLEDJNI.COLOR_ORDER_RBG),
/** BGR order. */
kBGR(AddressableLEDJNI.COLOR_ORDER_BGR),
/** BRG order. */
kBRG(AddressableLEDJNI.COLOR_ORDER_BRG),
/** GBR order. */
kGBR(AddressableLEDJNI.COLOR_ORDER_GBR),
/** GRB order. This is the default order. */
kGRB(AddressableLEDJNI.COLOR_ORDER_GRB);
/** The native value for this ColorOrder. */
public final int value;
ColorOrder(int value) {
this.value = value;
}
/**
* Gets a color order from an int value.
*
* @param value int value
* @return color order
*/
public ColorOrder fromValue(int value) {
return switch (value) {
case AddressableLEDJNI.COLOR_ORDER_RBG -> kRBG;
case AddressableLEDJNI.COLOR_ORDER_BGR -> kBGR;
case AddressableLEDJNI.COLOR_ORDER_BRG -> kBRG;
case AddressableLEDJNI.COLOR_ORDER_GRB -> kGRB;
case AddressableLEDJNI.COLOR_ORDER_GBR -> kGBR;
case AddressableLEDJNI.COLOR_ORDER_RGB -> kRGB;
default -> kGRB;
};
}
}
private final int m_channel;
private final int m_handle;
private int m_start;
private int m_length;
private ColorOrder m_colorOrder = ColorOrder.kGRB;
/**
* Constructs a new driver for a specific channel.
*
* @param channel the output channel to use
*/
public AddressableLED(int channel) {
m_channel = channel;
m_handle = AddressableLEDJNI.initialize(channel);
HAL.reportUsage("IO", channel, "AddressableLED");
}
@Override
public void close() {
if (m_handle != 0) {
AddressableLEDJNI.free(m_handle);
}
}
/**
* Gets the output channel.
*
* @return the output channel
*/
public int getChannel() {
return m_channel;
}
/**
* Sets the color order for this AddressableLED. The default order is GRB.
*
* <p>This will take effect on the next call to {@link #setData(AddressableLEDBuffer)}.
*
* @param order the color order
*/
public void setColorOrder(ColorOrder order) {
m_colorOrder = order;
}
/**
* Sets the display start of the LED strip in the global buffer.
*
* @param start the strip start, in LEDs
*/
public void setStart(int start) {
m_start = start;
AddressableLEDJNI.setStart(m_handle, start);
}
/**
* Gets the display start of the LED strip in the global buffer.
*
* @return the strip start, in LEDs
*/
public int getStart() {
return m_start;
}
/**
* Sets the length of the LED strip.
*
* @param length the strip length, in LEDs
*/
public void setLength(int length) {
m_length = length;
AddressableLEDJNI.setLength(m_handle, length);
}
/**
* Sets the LED output data.
*
* <p>This will write to the global buffer starting at the location set by setStart() and up to
* the length set by setLength().
*
* @param buffer the buffer to write
*/
public void setData(AddressableLEDBuffer buffer) {
AddressableLEDJNI.setData(
m_start,
m_colorOrder.value,
buffer.m_buffer,
0,
3 * Math.min(m_length, buffer.getLength()));
}
/**
* Sets the LED output data at an arbitrary location in the global buffer.
*
* @param start the start location, in LEDs
* @param colorOrder the color order
* @param buffer the buffer to write
*/
public static void setGlobalData(int start, ColorOrder colorOrder, AddressableLEDBuffer buffer) {
AddressableLEDJNI.setData(start, colorOrder.value, buffer.m_buffer);
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
/** Buffer storage for Addressable LEDs. */
public class AddressableLEDBuffer implements LEDReader, LEDWriter {
byte[] m_buffer;
/**
* Constructs a new LED buffer with the specified length.
*
* @param length The length of the buffer in pixels
*/
public AddressableLEDBuffer(int length) {
m_buffer = new byte[length * 3];
}
/**
* Sets a specific led in the buffer.
*
* @param index the index to write
* @param r the r value [0-255]
* @param g the g value [0-255]
* @param b the b value [0-255]
*/
@Override
public void setRGB(int index, int r, int g, int b) {
m_buffer[index * 3] = (byte) r;
m_buffer[(index * 3) + 1] = (byte) g;
m_buffer[(index * 3) + 2] = (byte) b;
}
/**
* Gets the buffer length.
*
* @return the buffer length
*/
@Override
public int getLength() {
return m_buffer.length / 3;
}
/**
* Gets the red channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the red channel, from [0, 255]
*/
@Override
public int getRed(int index) {
return m_buffer[index * 3] & 0xFF;
}
/**
* Gets the green channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the green channel, from [0, 255]
*/
@Override
public int getGreen(int index) {
return m_buffer[index * 3 + 1] & 0xFF;
}
/**
* Gets the blue channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the blue channel, from [0, 255]
*/
@Override
public int getBlue(int index) {
return m_buffer[index * 3 + 2] & 0xFF;
}
/**
* Creates a view of a subsection of this data buffer, starting from (and including) {@code
* startingIndex} and ending on (and including) {@code endingIndex}. Views cannot be written
* directly to an {@link AddressableLED}, but are useful tools for logically separating different
* sections of an LED strip for independent control.
*
* @param startingIndex the first index in this buffer that the view should encompass (inclusive)
* @param endingIndex the last index in this buffer that the view should encompass (inclusive)
* @return the view object
*/
public AddressableLEDBufferView createView(int startingIndex, int endingIndex) {
return new AddressableLEDBufferView(this, startingIndex, endingIndex);
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.wpilibj.util.Color;
import edu.wpi.first.wpilibj.util.Color8Bit;
/**
* A view of another addressable LED buffer. Views CANNOT be written directly to an LED strip; the
* backing buffer must be written instead. However, views provide an easy way to split a large LED
* strip into smaller sections (which may be reversed from the orientation of the LED strip as a
* whole) that can be animated individually without modifying LEDs outside those sections.
*/
public class AddressableLEDBufferView implements LEDReader, LEDWriter {
private final LEDReader m_backingReader;
private final LEDWriter m_backingWriter;
private final int m_startingIndex;
private final int m_endingIndex;
private final int m_length;
/**
* Creates a new view of a buffer. A view will be reversed if the starting index is after the
* ending index; writing front-to-back in the view will write in the back-to-front direction on
* the underlying buffer.
*
* @param backingBuffer the backing buffer to view
* @param startingIndex the index of the LED in the backing buffer that the view should start from
* @param endingIndex the index of the LED in the backing buffer that the view should end on
* @param <B> the type of the buffer object to create a view for
*/
public <B extends LEDReader & LEDWriter> AddressableLEDBufferView(
B backingBuffer, int startingIndex, int endingIndex) {
this(
requireNonNullParam(backingBuffer, "backingBuffer", "AddressableLEDBufferView"),
backingBuffer,
startingIndex,
endingIndex);
}
/**
* Creates a new view of a buffer. A view will be reversed if the starting index is after the
* ending index; writing front-to-back in the view will write in the back-to-front direction on
* the underlying buffer.
*
* @param backingReader the backing LED data reader
* @param backingWriter the backing LED data writer
* @param startingIndex the index of the LED in the backing buffer that the view should start from
* @param endingIndex the index of the LED in the backing buffer that the view should end on
*/
public AddressableLEDBufferView(
LEDReader backingReader, LEDWriter backingWriter, int startingIndex, int endingIndex) {
requireNonNullParam(backingReader, "backingReader", "AddressableLEDBufferView");
requireNonNullParam(backingWriter, "backingWriter", "AddressableLEDBufferView");
if (startingIndex < 0 || startingIndex >= backingReader.getLength()) {
throw new IndexOutOfBoundsException("Start index out of range: " + startingIndex);
}
if (endingIndex < 0 || endingIndex >= backingReader.getLength()) {
throw new IndexOutOfBoundsException("End index out of range: " + endingIndex);
}
m_backingReader = backingReader;
m_backingWriter = backingWriter;
m_startingIndex = startingIndex;
m_endingIndex = endingIndex;
m_length = Math.abs(endingIndex - startingIndex) + 1;
}
/**
* Creates a view that operates on the same range as this one, but goes in reverse order. This is
* useful for serpentine runs of LED strips connected front-to-end; simply reverse the view for
* reversed sections and animations will move in the same physical direction along both strips.
*
* @return the reversed view
*/
public AddressableLEDBufferView reversed() {
return new AddressableLEDBufferView(this, m_length - 1, 0);
}
@Override
public int getLength() {
return m_length;
}
@Override
public void setRGB(int index, int r, int g, int b) {
m_backingWriter.setRGB(nativeIndex(index), r, g, b);
}
@Override
public Color getLED(int index) {
// override to delegate to the backing buffer to avoid 3x native index lookups & bounds checks
return m_backingReader.getLED(nativeIndex(index));
}
@Override
public Color8Bit getLED8Bit(int index) {
// override to delegate to the backing buffer to avoid 3x native index lookups & bounds checks
return m_backingReader.getLED8Bit(nativeIndex(index));
}
@Override
public int getRed(int index) {
return m_backingReader.getRed(nativeIndex(index));
}
@Override
public int getGreen(int index) {
return m_backingReader.getGreen(nativeIndex(index));
}
@Override
public int getBlue(int index) {
return m_backingReader.getBlue(nativeIndex(index));
}
/**
* Checks if this view is reversed with respect to its backing buffer.
*
* @return true if the view is reversed, false otherwise
*/
public boolean isReversed() {
return m_endingIndex < m_startingIndex;
}
/**
* Converts a view-local index in the range [start, end] to a global index in the range [0,
* length].
*
* @param viewIndex the view-local index
* @return the corresponding global index
* @throws IndexOutOfBoundsException if the view index is not contained within the bounds of this
* view
*/
private int nativeIndex(int viewIndex) {
if (isReversed()) {
// 0 1 2 3 4 5 6 7 8 9 10
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// [_, _, _, _, (d, c, b, a), _, _, _]
// ↑ ↑ ↑ ↑
// 3 2 1 0
if (viewIndex < 0 || viewIndex > m_startingIndex) {
throw new IndexOutOfBoundsException(viewIndex);
}
return m_startingIndex - viewIndex;
} else {
// 0 1 2 3 4 5 6 7 8 9 10
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// [_, _, _, _, (a, b, c, d), _, _, _]
// ↑ ↑ ↑ ↑
// 0 1 2 3
if (viewIndex < 0 || viewIndex > m_endingIndex) {
throw new IndexOutOfBoundsException(viewIndex);
}
return m_startingIndex + viewIndex;
}
}
}

View File

@@ -0,0 +1,689 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.units.Units.Meters;
import static edu.wpi.first.units.Units.Microsecond;
import static edu.wpi.first.units.Units.Microseconds;
import static edu.wpi.first.units.Units.Value;
import edu.wpi.first.units.collections.LongToObjectHashMap;
import edu.wpi.first.units.measure.Dimensionless;
import edu.wpi.first.units.measure.Distance;
import edu.wpi.first.units.measure.Frequency;
import edu.wpi.first.units.measure.LinearVelocity;
import edu.wpi.first.units.measure.Time;
import edu.wpi.first.util.WPIUtilJNI;
import edu.wpi.first.wpilibj.util.Color;
import java.util.Map;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
/**
* An LED pattern controls lights on an LED strip to command patterns of color that may change over
* time. Dynamic patterns should synchronize on an external clock for timed-based animations ({@link
* WPIUtilJNI#now()} is recommended, since it can be mocked in simulation and unit tests), or on
* some other dynamic input (see {@link #synchronizedBlink(BooleanSupplier)}, for example).
*
* <p>Patterns should be updated periodically in order for animations to play smoothly. For example,
* a hypothetical LED subsystem could create a {@code Command} that will continuously apply the
* pattern to its LED data buffer as part of the main periodic loop.
*
* <pre><code>
* public class LEDs extends SubsystemBase {
* private final AddressableLED m_led = new AddressableLED(0);
* private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(120);
*
* public LEDs() {
* m_led.setLength(120);
* m_led.start();
* }
*
* {@literal @}Override
* public void periodic() {
* m_led.writeData(m_ledData);
* }
*
* public Command runPattern(LEDPattern pattern) {
* return run(() -> pattern.applyTo(m_ledData));
* }
* }
* </code></pre>
*
* <p>LED patterns are stateless, and as such can be applied to multiple LED strips (or different
* sections of the same LED strip, since the roboRIO can only drive a single LED strip). In this
* example, we split the single buffer into two views - one for the section of the LED strip on the
* left side of a robot, and another view for the section of LEDs on the right side. The same
* pattern is able to be applied to both sides.
*
* <pre><code>
* public class LEDs extends SubsystemBase {
* private final AddressableLED m_led = new AddressableLED(0);
* private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(60);
* private final AddressableLEDBufferView m_leftData = m_ledData.createView(0, 29);
* private final AddressableLEDBufferView m_rightData = m_ledData.createView(30, 59).reversed();
*
* public LEDs() {
* m_led.setLength(60);
* m_led.start();
* }
*
* {@literal @}Override
* public void periodic() {
* m_led.writeData(m_ledData);
* }
*
* public Command runPattern(LEDPattern pattern) {
* // Use the single input pattern to drive both sides
* return runSplitPatterns(pattern, pattern);
* }
*
* public Command runSplitPatterns(LEDPattern left, LEDPattern right) {
* return run(() -> {
* left.applyTo(m_leftData);
* right.applyTo(m_rightData);
* });
* }
* }
* </code></pre>
*/
@FunctionalInterface
public interface LEDPattern {
/** A functional interface for index mapping functions. */
@FunctionalInterface
interface IndexMapper {
/**
* Maps the index.
*
* @param bufLen Length of the buffer
* @param index The index to map
* @return The mapped index
*/
int apply(int bufLen, int index);
}
/**
* Writes the pattern to an LED buffer. Dynamic animations should be called periodically (such as
* with a command or with a periodic method) to refresh the buffer over time.
*
* <p>This method is intentionally designed to use separate objects for reading and writing data.
* By splitting them up, we can easily modify the behavior of some base pattern to make it {@link
* #scrollAtRelativeSpeed(Frequency) scroll}, {@link #blink(Time, Time) blink}, or {@link
* #breathe(Time) breathe} by intercepting the data writes to transform their behavior to whatever
* we like.
*
* @param reader data reader for accessing buffer length and current colors
* @param writer data writer for setting new LED colors on the buffer
*/
void applyTo(LEDReader reader, LEDWriter writer);
/**
* Convenience for {@link #applyTo(LEDReader, LEDWriter)} when one object provides both a read and
* a write interface. This is most helpful for playing an animated pattern directly on an {@link
* AddressableLEDBuffer} for the sake of code clarity.
*
* <pre><code>
* AddressableLEDBuffer data = new AddressableLEDBuffer(120);
* LEDPattern pattern = ...
*
* void periodic() {
* pattern.applyTo(data);
* }
* </code></pre>
*
* @param readWriter the object to use for both reading and writing to a set of LEDs
* @param <T> the type of the object that can both read and write LED data
*/
default <T extends LEDReader & LEDWriter> void applyTo(T readWriter) {
applyTo(readWriter, readWriter);
}
/**
* Creates a pattern with remapped indices.
*
* @param indexMapper the index mapper
* @return the mapped pattern
*/
default LEDPattern mapIndex(IndexMapper indexMapper) {
return (reader, writer) -> {
int bufLen = reader.getLength();
applyTo(
new LEDReader() {
@Override
public int getLength() {
return reader.getLength();
}
@Override
public int getRed(int index) {
return reader.getRed(indexMapper.apply(bufLen, index));
}
@Override
public int getGreen(int index) {
return reader.getGreen(indexMapper.apply(bufLen, index));
}
@Override
public int getBlue(int index) {
return reader.getBlue(indexMapper.apply(bufLen, index));
}
},
(i, r, g, b) -> writer.setRGB(indexMapper.apply(bufLen, i), r, g, b));
};
}
/**
* Creates a pattern that displays this one in reverse. Scrolling patterns will scroll in the
* opposite direction (but at the same speed). It will treat the end of an LED strip as the start,
* and the start of the strip as the end. This can be useful for making ping-pong patterns that
* travel from one end of an LED strip to the other, then reverse direction and move back to the
* start. This can also be useful when working with LED strips connected in a serpentine pattern
* (where the start of one strip is connected to the end of the previous one); however, consider
* using a {@link AddressableLEDBufferView#reversed() reversed view} of the overall buffer for
* that segment rather than reversing patterns.
*
* @return the reverse pattern
* @see AddressableLEDBufferView#reversed()
*/
default LEDPattern reversed() {
return mapIndex((length, index) -> length - 1 - index);
}
/**
* Creates a pattern that plays this one, but offset by a certain number of LEDs. The offset
* pattern will wrap around, if necessary.
*
* @param offset how many LEDs to offset by
* @return the offset pattern
*/
default LEDPattern offsetBy(int offset) {
return mapIndex((length, index) -> Math.floorMod(index + offset, length));
}
/**
* Creates a pattern that plays this one scrolling up the buffer. The velocity controls how fast
* the pattern returns back to its original position, and is in terms of the length of the LED
* strip; scrolling across a segment that is 10 LEDs long will travel twice as fast as on a
* segment that's only 5 LEDs long (assuming equal LED density on both segments).
*
* <p>For example, scrolling a pattern by one quarter of any LED strip's length per second,
* regardless of the total number of LEDs on that strip:
*
* <pre>
* LEDPattern rainbow = LEDPattern.rainbow(255, 255);
* LEDPattern scrollingRainbow = rainbow.scrollAtRelativeSpeed(Percent.per(Second).of(25));
* </pre>
*
* @param velocity how fast the pattern should move, in terms of how long it takes to do a full
* scroll along the length of LEDs and return back to the starting position
* @return the scrolling pattern
*/
default LEDPattern scrollAtRelativeSpeed(Frequency velocity) {
final double periodMicros = velocity.asPeriod().in(Microseconds);
return mapIndex(
(bufLen, index) -> {
long now = RobotController.getTime();
// index should move by (buf.length) / (period)
double t = (now % (long) periodMicros) / periodMicros;
int offset = (int) (t * bufLen);
return Math.floorMod(index + offset, bufLen);
});
}
/**
* Creates a pattern that plays this one scrolling up an LED strip. A negative velocity makes the
* pattern play in reverse.
*
* <p>For example, scrolling a pattern at 4 inches per second along an LED strip with 60 LEDs per
* meter:
*
* <pre>
* // LEDs per meter, a known value taken from the spec sheet of our particular LED strip
* Distance LED_SPACING = Meters.of(1.0 / 60);
*
* LEDPattern rainbow = LEDPattern.rainbow();
* LEDPattern scrollingRainbow =
* rainbow.scrollAtAbsoluteSpeed(InchesPerSecond.of(4), LED_SPACING);
* </pre>
*
* <p>Note that this pattern will scroll <i>faster</i> if applied to a less dense LED strip (such
* as 30 LEDs per meter), or <i>slower</i> if applied to a denser LED strip (such as 120 or 144
* LEDs per meter).
*
* @param velocity how fast the pattern should move along a physical LED strip
* @param ledSpacing the distance between adjacent LEDs on the physical LED strip
* @return the scrolling pattern
*/
default LEDPattern scrollAtAbsoluteSpeed(LinearVelocity velocity, Distance ledSpacing) {
// eg velocity = 10 m/s, spacing = 0.01m
// meters per micro = 1e-5 m/us
// micros per LED = 1e-2 m / (1e-5 m/us) = 1e-3 us
var metersPerMicro = velocity.in(Meters.per(Microsecond));
var microsPerLED = (int) (ledSpacing.in(Meters) / metersPerMicro);
return mapIndex(
(bufLen, index) -> {
long now = RobotController.getTime();
// every step in time that's a multiple of microsPerLED will increment the offset by 1
var offset = (int) (now / microsPerLED);
// floorMod so if the offset is negative, we still get positive outputs
return Math.floorMod(index + offset, bufLen);
});
}
/**
* Creates a pattern that switches between playing this pattern and turning the entire LED strip
* off.
*
* @param onTime how long the pattern should play for, per cycle
* @param offTime how long the pattern should be turned off for, per cycle
* @return the blinking pattern
*/
default LEDPattern blink(Time onTime, Time offTime) {
final long totalTimeMicros = (long) (onTime.in(Microseconds) + offTime.in(Microseconds));
final long onTimeMicros = (long) onTime.in(Microseconds);
return (reader, writer) -> {
if (RobotController.getTime() % totalTimeMicros < onTimeMicros) {
applyTo(reader, writer);
} else {
kOff.applyTo(reader, writer);
}
};
}
/**
* Like {@link #blink(Time, Time) blink(onTime, offTime)}, but where the "off" time is exactly
* equal to the "on" time.
*
* @param onTime how long the pattern should play for (and be turned off for), per cycle
* @return the blinking pattern
*/
default LEDPattern blink(Time onTime) {
return blink(onTime, onTime);
}
/**
* Creates a pattern that blinks this one on and off in sync with a true/false signal. The pattern
* will play while the signal outputs {@code true}, and will turn off while the signal outputs
* {@code false}.
*
* @param signal the signal to synchronize with
* @return the blinking pattern
*/
default LEDPattern synchronizedBlink(BooleanSupplier signal) {
return (reader, writer) -> {
if (signal.getAsBoolean()) {
applyTo(reader, writer);
} else {
kOff.applyTo(reader, writer);
}
};
}
/**
* Creates a pattern that brightens and dims this one over time. Brightness follows a sinusoidal
* pattern.
*
* @param period how fast the breathing pattern should complete a single cycle
* @return the breathing pattern
*/
default LEDPattern breathe(Time period) {
final long periodMicros = (long) period.in(Microseconds);
return (reader, writer) -> {
applyTo(
reader,
(i, r, g, b) -> {
// How far we are in the cycle, in the range [0, 1)
double t = (RobotController.getTime() % periodMicros) / (double) periodMicros;
double phase = t * 2 * Math.PI;
// Apply the cosine function and shift its output from [-1, 1] to [0, 1]
// Use cosine so the period starts at 100% brightness
double dim = (Math.cos(phase) + 1) / 2.0;
int output = Color.lerpRGB(0, 0, 0, r, g, b, dim);
writer.setRGB(
i,
Color.unpackRGB(output, Color.RGBChannel.kRed),
Color.unpackRGB(output, Color.RGBChannel.kGreen),
Color.unpackRGB(output, Color.RGBChannel.kBlue));
});
};
}
/**
* Creates a pattern that plays this pattern overlaid on another. Anywhere this pattern sets an
* LED to off (or {@link Color#kBlack}), the base pattern will be displayed instead.
*
* @param base the base pattern to overlay on top of
* @return the combined overlay pattern
*/
default LEDPattern overlayOn(LEDPattern base) {
return (reader, writer) -> {
// write the base pattern down first...
base.applyTo(reader, writer);
// ... then, overwrite with the illuminated LEDs from the overlay
applyTo(
reader,
(i, r, g, b) -> {
if (r != 0 || g != 0 || b != 0) {
writer.setRGB(i, r, g, b);
}
});
};
}
/**
* Creates a pattern that displays outputs as a combination of this pattern and another. Color
* values are calculated as the average color of both patterns; if both patterns set the same LED
* to the same color, then it is set to that color, but if one pattern sets to one color and the
* other pattern sets it to off, then it will show the color of the first pattern but at
* approximately half brightness. This is different from {@link #overlayOn}, which will show the
* base pattern at full brightness if the overlay is set to off at that position.
*
* @param other the pattern to blend with
* @return the blended pattern
*/
default LEDPattern blend(LEDPattern other) {
return (reader, writer) -> {
applyTo(reader, writer);
other.applyTo(
reader,
(i, r, g, b) -> {
int blendedRGB =
Color.lerpRGB(
reader.getRed(i), reader.getGreen(i), reader.getBlue(i), r, g, b, 0.5);
writer.setRGB(
i,
Color.unpackRGB(blendedRGB, Color.RGBChannel.kRed),
Color.unpackRGB(blendedRGB, Color.RGBChannel.kGreen),
Color.unpackRGB(blendedRGB, Color.RGBChannel.kBlue));
});
};
}
/**
* Similar to {@link #blend(LEDPattern)}, but performs a bitwise mask on each color channel rather
* than averaging the colors for each LED. This can be helpful for displaying only a portion of
* the base pattern by applying a mask that sets the desired area to white, and all other areas to
* black. However, it can also be used to display only certain color channels or hues; for
* example, masking with {@code LEDPattern.color(Color.kRed)} will turn off the green and blue
* channels on the output pattern, leaving only the red LEDs to be illuminated.
*
* @param mask the mask to apply
* @return the masked pattern
*/
default LEDPattern mask(LEDPattern mask) {
return (reader, writer) -> {
// Apply the current pattern down as normal...
applyTo(reader, writer);
mask.applyTo(
reader,
(i, r, g, b) -> {
// ... then perform a bitwise AND operation on each channel to apply the mask
writer.setRGB(i, r & reader.getRed(i), g & reader.getGreen(i), b & reader.getBlue(i));
});
};
}
/**
* Creates a pattern that plays this one, but at a different brightness. Brightness multipliers
* are applied per-channel in the RGB space; no HSL or HSV conversions are applied. Multipliers
* are also uncapped, which may result in the original colors washing out and appearing less
* saturated or even just a bright white.
*
* <p>This method is predominantly intended for dimming LEDs to avoid painfully bright or
* distracting patterns from playing (apologies to the 2024 NE Greater Boston field staff).
*
* <p>For example, dimming can be done simply by adding a call to `atBrightness` at the end of a
* pattern:
*
* <pre>
* // Solid red, but at 50% brightness
* LEDPattern.solid(Color.kRed).atBrightness(Percent.of(50));
*
* // Solid white, but at only 10% (i.e. ~0.5V)
* LEDPattern.solid(Color.kWhite).atBrightness(Percent.of(10));
* </pre>
*
* @param relativeBrightness the multiplier to apply to all channels to modify brightness
* @return the input pattern, displayed at
*/
default LEDPattern atBrightness(Dimensionless relativeBrightness) {
double multiplier = relativeBrightness.in(Value);
return (reader, writer) -> {
applyTo(
reader,
(i, r, g, b) -> {
// Clamp RGB values to keep them in the range [0, 255].
// Otherwise, the casts to byte would result in values like 256 wrapping to 0
writer.setRGB(
i,
(int) Math.clamp(r * multiplier, 0, 255),
(int) Math.clamp(g * multiplier, 0, 255),
(int) Math.clamp(b * multiplier, 0, 255));
});
};
}
/** A pattern that turns off all LEDs. */
LEDPattern kOff = solid(Color.kBlack);
/**
* Creates a pattern that displays a single static color along the entire length of the LED strip.
*
* @param color the color to display
* @return the pattern
*/
static LEDPattern solid(Color color) {
return (reader, writer) -> {
int bufLen = reader.getLength();
for (int led = 0; led < bufLen; led++) {
writer.setLED(led, color);
}
};
}
/**
* Creates a pattern that works as a mask layer for {@link #mask(LEDPattern)} that illuminates
* only the portion of the LED strip corresponding with some progress. The mask pattern will start
* from the base and set LEDs to white at a proportion equal to the progress returned by the
* function. Some usages for this could be for displaying progress of a flywheel to its target
* velocity, progress of a complex autonomous sequence, or the height of an elevator.
*
* <p>For example, creating a mask for displaying a red-to-blue gradient, starting from the red
* end, based on where an elevator is in its range of travel.
*
* <pre>
* LEDPattern basePattern = gradient(Color.kRed, Color.kBlue);
* LEDPattern progressPattern =
* basePattern.mask(progressMaskLayer(() -> elevator.getHeight() / elevator.maxHeight());
* </pre>
*
* @param progressSupplier the function to call to determine the progress. This should return
* values in the range [0, 1]; any values outside that range will be clamped.
* @return the mask pattern
*/
static LEDPattern progressMaskLayer(DoubleSupplier progressSupplier) {
return (reader, writer) -> {
double progress = Math.clamp(progressSupplier.getAsDouble(), 0, 1);
int bufLen = reader.getLength();
int max = (int) (bufLen * progress);
for (int led = 0; led < max; led++) {
writer.setLED(led, Color.kWhite);
}
for (int led = max; led < bufLen; led++) {
writer.setLED(led, Color.kBlack);
}
};
}
/**
* Display a set of colors in steps across the length of the LED strip. No interpolation is done
* between colors. Colors are specified by the first LED on the strip to show that color. The last
* color in the map will be displayed all the way to the end of the strip. LEDs positioned before
* the first specified step will be turned off (you can think of this as if there's a 0 -> black
* step by default)
*
* <pre>
* // Display red from 0-33%, white from 33% - 67%, and blue from 67% to 100%
* steps(Map.of(0.00, Color.kRed, 0.33, Color.kWhite, 0.67, Color.kBlue))
*
* // Half off, half on
* steps(Map.of(0.5, Color.kWhite))
* </pre>
*
* @param steps a map of progress to the color to start displaying at that position along the LED
* strip
* @return a motionless step pattern
*/
static LEDPattern steps(Map<? extends Number, Color> steps) {
if (steps.isEmpty()) {
// no colors specified
DriverStation.reportWarning("Creating LED steps with no colors!", false);
return kOff;
}
if (steps.size() == 1 && steps.keySet().iterator().next().doubleValue() == 0) {
// only one color specified, just show a static color
DriverStation.reportWarning("Creating LED steps with only one color!", false);
return solid(steps.values().iterator().next());
}
return (reader, writer) -> {
int bufLen = reader.getLength();
// precompute relevant positions for this buffer so we don't need to do a check
// on every single LED index
var stopPositions = new LongToObjectHashMap<Color>();
steps.forEach(
(progress, color) -> {
stopPositions.put((int) Math.floor(progress.doubleValue() * bufLen), color);
});
Color currentColor = Color.kBlack;
for (int led = 0; led < bufLen; led++) {
currentColor = Objects.requireNonNullElse(stopPositions.get(led), currentColor);
writer.setLED(led, currentColor);
}
};
}
/** Types of gradients. */
enum GradientType {
/**
* A continuous gradient, where the gradient wraps around to allow for seamless scrolling
* effects.
*/
kContinuous,
/**
* A discontinuous gradient, where the first pixel is set to the first color of the gradient and
* the final pixel is set to the last color of the gradient. There is no wrapping effect, so
* scrolling effects will display an obvious seam.
*/
kDiscontinuous
}
/**
* Creates a pattern that displays a non-animated gradient of colors across the entire length of
* the LED strip. Colors are evenly distributed along the full length of the LED strip. The
* gradient type is configured with the {@code type} parameter, allowing the gradient to be either
* continuous (no seams, good for scrolling effects) or discontinuous (a clear seam is visible,
* but the gradient applies to the full length of the LED strip without needing to use some space
* for wrapping).
*
* @param type the type of gradient (continuous or discontinuous)
* @param colors the colors to display in the gradient
* @return a motionless gradient pattern
*/
static LEDPattern gradient(GradientType type, Color... colors) {
if (colors.length == 0) {
// Nothing to display
DriverStation.reportWarning("Creating a gradient with no colors!", false);
return kOff;
}
if (colors.length == 1) {
// No gradients with one color
DriverStation.reportWarning("Creating a gradient with only one color!", false);
return solid(colors[0]);
}
final int numSegments = colors.length;
return (reader, writer) -> {
int bufLen = reader.getLength();
int ledsPerSegment =
switch (type) {
case kContinuous -> bufLen / numSegments;
case kDiscontinuous -> (bufLen - 1) / (numSegments - 1);
};
for (int led = 0; led < bufLen; led++) {
int colorIndex = (led / ledsPerSegment) % numSegments;
int nextColorIndex = (colorIndex + 1) % numSegments;
double t = (led / (double) ledsPerSegment) % 1;
Color color = colors[colorIndex];
Color nextColor = colors[nextColorIndex];
int gradientColor =
Color.lerpRGB(
color.red,
color.green,
color.blue,
nextColor.red,
nextColor.green,
nextColor.blue,
t);
writer.setRGB(
led,
Color.unpackRGB(gradientColor, Color.RGBChannel.kRed),
Color.unpackRGB(gradientColor, Color.RGBChannel.kGreen),
Color.unpackRGB(gradientColor, Color.RGBChannel.kBlue));
}
};
}
/**
* Creates an LED pattern that displays a rainbow across the color wheel. The rainbow pattern will
* stretch across the entire length of the LED strip.
*
* @param saturation the saturation of the HSV colors, in [0, 255]
* @param value the value of the HSV colors, in [0, 255]
* @return the rainbow pattern
*/
static LEDPattern rainbow(int saturation, int value) {
return (reader, writer) -> {
int bufLen = reader.getLength();
for (int i = 0; i < bufLen; i++) {
int hue = ((i * 180) / bufLen) % 180;
writer.setHSV(i, hue, saturation, value);
}
};
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.wpilibj.util.Color;
import edu.wpi.first.wpilibj.util.Color8Bit;
/** Generic interface for reading data from an LED buffer. */
public interface LEDReader {
/**
* Gets the length of the buffer.
*
* @return the buffer length
*/
int getLength();
/**
* Gets the most recently written color for a particular LED in the buffer.
*
* @param index the index of the LED
* @return the LED color
* @throws IndexOutOfBoundsException if the index is negative or greater than {@link #getLength()}
*/
default Color getLED(int index) {
return new Color(getRed(index) / 255.0, getGreen(index) / 255.0, getBlue(index) / 255.0);
}
/**
* Gets the most recently written color for a particular LED in the buffer.
*
* @param index the index of the LED
* @return the LED color
* @throws IndexOutOfBoundsException if the index is negative or greater than {@link #getLength()}
*/
default Color8Bit getLED8Bit(int index) {
return new Color8Bit(getRed(index), getGreen(index), getBlue(index));
}
/**
* Gets the red channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the red channel, from [0, 255]
*/
int getRed(int index);
/**
* Gets the green channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the green channel, from [0, 255]
*/
int getGreen(int index);
/**
* Gets the blue channel of the color at the specified index.
*
* @param index the index of the LED to read
* @return the value of the blue channel, from [0, 255]
*/
int getBlue(int index);
/**
* A functional interface that allows for iteration over an LED buffer without manually writing an
* indexed for-loop.
*/
@FunctionalInterface
interface IndexedColorIterator {
/**
* Accepts an index of an LED in the buffer and the red, green, and blue components of the
* currently stored color for that LED.
*
* @param index the index of the LED in the buffer that the red, green, and blue channels
* corresponds to
* @param r the value of the red channel of the color currently in the buffer at index {@code i}
* @param g the value of the green channel of the color currently in the buffer at index {@code
* i}
* @param b the value of the blue channel of the color currently in the buffer at index {@code
* i}
*/
void accept(int index, int r, int g, int b);
}
/**
* Iterates over the LEDs in the buffer, starting from index 0. The iterator function is passed
* the current index of iteration, along with the values for the red, green, and blue components
* of the color written to the LED at that index.
*
* @param iterator the iterator function to call for each LED in the buffer.
*/
default void forEach(IndexedColorIterator iterator) {
int bufLen = getLength();
for (int i = 0; i < bufLen; i++) {
iterator.accept(i, getRed(i), getGreen(i), getBlue(i));
}
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.wpilibj.util.Color;
import edu.wpi.first.wpilibj.util.Color8Bit;
/** Generic interface for writing data to an LED buffer. */
@FunctionalInterface
public interface LEDWriter {
/**
* Sets the RGB value for an LED at a specific index on a LED buffer.
*
* @param index the index of the LED to write to
* @param r the value of the red channel, in [0, 255]
* @param g the value of the green channel, in [0, 255]
* @param b the value of the blue channel, in [0, 255]
*/
void setRGB(int index, int r, int g, int b);
/**
* Sets a specific led in the buffer.
*
* @param index the index to write
* @param h the h value [0-180)
* @param s the s value [0-255]
* @param v the v value [0-255]
*/
default void setHSV(int index, int h, int s, int v) {
if (s == 0) {
setRGB(index, v, v, v);
return;
}
int packedRGB = Color.hsvToRgb(h, s, v);
setRGB(
index,
Color.unpackRGB(packedRGB, Color.RGBChannel.kRed),
Color.unpackRGB(packedRGB, Color.RGBChannel.kGreen),
Color.unpackRGB(packedRGB, Color.RGBChannel.kBlue));
}
/**
* Sets the RGB value for an LED at a specific index on a LED buffer.
*
* @param index the index of the LED to write to
* @param color the color to set
*/
default void setLED(int index, Color color) {
setRGB(index, (int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255));
}
/**
* Sets the RGB value for an LED at a specific index on a LED buffer.
*
* @param index the index of the LED to write to
* @param color the color to set
*/
default void setLED(int index, Color8Bit color) {
setRGB(index, color.red, color.green, color.blue);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.motorcontrol;
import static edu.wpi.first.units.Units.Volts;
import edu.wpi.first.units.measure.Voltage;
import edu.wpi.first.wpilibj.RobotController;
/** Interface for motor controlling devices. */
public interface MotorController {
/**
* Common interface for setting the speed of a motor controller.
*
* @param speed The speed to set. Value should be between -1.0 and 1.0.
*/
void set(double speed);
/**
* Sets the voltage output of the MotorController. Compensates for the current bus voltage to
* ensure that the desired voltage is output even if the battery voltage is below 12V - highly
* useful when the voltage outputs are "meaningful" (e.g. they come from a feedforward
* calculation).
*
* <p>NOTE: This function *must* be called regularly in order for voltage compensation to work
* properly - unlike the ordinary set function, it is not "set it and forget it."
*
* @param outputVolts The voltage to output, in Volts.
*/
default void setVoltage(double outputVolts) {
set(outputVolts / RobotController.getBatteryVoltage());
}
/**
* Sets the voltage output of the MotorController. Compensates for the current bus voltage to
* ensure that the desired voltage is output even if the battery voltage is below 12V - highly
* useful when the voltage outputs are "meaningful" (e.g. they come from a feedforward
* calculation).
*
* <p>NOTE: This function *must* be called regularly in order for voltage compensation to work
* properly - unlike the ordinary set function, it is not "set it and forget it."
*
* @param outputVoltage The voltage to output.
*/
default void setVoltage(Voltage outputVoltage) {
setVoltage(outputVoltage.in(Volts));
}
/**
* Common interface for getting the current set speed of a motor controller.
*
* @return The current set speed. Value is between -1.0 and 1.0.
*/
double get();
/**
* Common interface for inverting direction of a motor controller.
*
* @param isInverted The state of inversion true is inverted.
*/
void setInverted(boolean isInverted);
/**
* Common interface for returning if a motor controller is in the inverted state or not.
*
* @return isInverted The state of the inversion true is inverted.
*/
boolean getInverted();
/** Disable the motor controller. */
void disable();
/**
* Stops motor movement. Motor can be moved again by calling set without having to re-enable the
* motor.
*/
void stopMotor();
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.motorcontrol;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
import java.util.Arrays;
/**
* Allows multiple {@link MotorController} objects to be linked together.
*
* @deprecated Use {@link PWMMotorController#addFollower(PWMMotorController)} or if using CAN motor
* controllers, use their method of following.
*/
@SuppressWarnings("removal")
@Deprecated(forRemoval = true, since = "2024")
public class MotorControllerGroup implements MotorController, Sendable, AutoCloseable {
private boolean m_isInverted;
private final MotorController[] m_motorControllers;
private static int instances;
/**
* Create a new MotorControllerGroup with the provided MotorControllers.
*
* @param motorController The first MotorController to add
* @param motorControllers The MotorControllers to add
*/
@SuppressWarnings("this-escape")
public MotorControllerGroup(
MotorController motorController, MotorController... motorControllers) {
m_motorControllers = new MotorController[motorControllers.length + 1];
m_motorControllers[0] = motorController;
System.arraycopy(motorControllers, 0, m_motorControllers, 1, motorControllers.length);
init();
}
/**
* Create a new MotorControllerGroup with the provided MotorControllers.
*
* @param motorControllers The MotorControllers to add.
*/
@SuppressWarnings("this-escape")
public MotorControllerGroup(MotorController[] motorControllers) {
m_motorControllers = Arrays.copyOf(motorControllers, motorControllers.length);
init();
}
private void init() {
for (MotorController controller : m_motorControllers) {
SendableRegistry.addChild(this, controller);
}
instances++;
SendableRegistry.add(this, "MotorControllerGroup", instances);
}
@Override
public void close() {
SendableRegistry.remove(this);
}
@Override
public void set(double speed) {
for (MotorController motorController : m_motorControllers) {
motorController.set(m_isInverted ? -speed : speed);
}
}
@Override
public void setVoltage(double outputVolts) {
for (MotorController motorController : m_motorControllers) {
motorController.setVoltage(m_isInverted ? -outputVolts : outputVolts);
}
}
@Override
public double get() {
if (m_motorControllers.length > 0) {
return m_motorControllers[0].get() * (m_isInverted ? -1 : 1);
}
return 0.0;
}
@Override
public void setInverted(boolean isInverted) {
m_isInverted = isInverted;
}
@Override
public boolean getInverted() {
return m_isInverted;
}
@Override
public void disable() {
for (MotorController motorController : m_motorControllers) {
motorController.disable();
}
}
@Override
public void stopMotor() {
for (MotorController motorController : m_motorControllers) {
motorController.stopMotor();
}
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Motor Controller");
builder.setActuator(true);
builder.addDoubleProperty("Value", this::get, this::set);
}
}

View File

@@ -0,0 +1,198 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.ControlWord;
import edu.wpi.first.hal.DriverStationJNI;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* The Motor Safety feature acts as a watchdog timer for an individual motor. It operates by
* maintaining a timer that tracks how long it has been since the feed() method has been called for
* that actuator. Code in the Driver Station class initiates a comparison of these timers to the
* timeout values for any actuator with safety enabled every 5 received packets (100ms nominal).
*
* <p>The subclass should call feed() whenever the motor value is updated.
*/
public abstract class MotorSafety {
private static final double kDefaultSafetyExpiration = 0.1;
private double m_expiration = kDefaultSafetyExpiration;
private boolean m_enabled;
private double m_stopTime = Timer.getFPGATimestamp();
private final Object m_thisMutex = new Object();
private static final Set<MotorSafety> m_instanceList = new LinkedHashSet<>();
private static final Object m_listMutex = new Object();
private static Thread m_safetyThread;
@SuppressWarnings("PMD.AssignmentInOperand")
private static void threadMain() {
int event = WPIUtilJNI.createEvent(false, false);
DriverStationJNI.provideNewDataEventHandle(event);
ControlWord controlWord = new ControlWord();
int safetyCounter = 0;
while (true) {
boolean timedOut;
try {
timedOut = WPIUtilJNI.waitForObjectTimeout(event, 0.1);
} catch (InterruptedException e) {
DriverStationJNI.removeNewDataEventHandle(event);
WPIUtilJNI.destroyEvent(event);
Thread.currentThread().interrupt();
return;
}
if (!timedOut) {
DriverStationJNI.getControlWord(controlWord);
if (!(controlWord.getEnabled() && controlWord.getDSAttached())) {
safetyCounter = 0;
}
if (++safetyCounter >= 4) {
checkMotors();
safetyCounter = 0;
}
} else {
safetyCounter = 0;
}
}
}
/** MotorSafety constructor. */
@SuppressWarnings("this-escape")
public MotorSafety() {
synchronized (m_listMutex) {
m_instanceList.add(this);
if (m_safetyThread == null) {
m_safetyThread = new Thread(MotorSafety::threadMain, "MotorSafety Thread");
m_safetyThread.setDaemon(true);
m_safetyThread.start();
}
}
}
/**
* Feed the motor safety object.
*
* <p>Resets the timer on this object that is used to do the timeouts.
*/
public void feed() {
synchronized (m_thisMutex) {
m_stopTime = Timer.getFPGATimestamp() + m_expiration;
}
}
/**
* Set the expiration time for the corresponding motor safety object.
*
* @param expirationTime The timeout value in seconds.
*/
public void setExpiration(double expirationTime) {
synchronized (m_thisMutex) {
m_expiration = expirationTime;
}
}
/**
* Retrieve the timeout value for the corresponding motor safety object.
*
* @return the timeout value in seconds.
*/
public double getExpiration() {
synchronized (m_thisMutex) {
return m_expiration;
}
}
/**
* Determine of the motor is still operating or has timed out.
*
* @return a true value if the motor is still operating normally and hasn't timed out.
*/
public boolean isAlive() {
synchronized (m_thisMutex) {
return !m_enabled || m_stopTime > Timer.getFPGATimestamp();
}
}
/**
* Check if this motor has exceeded its timeout. This method is called periodically to determine
* if this motor has exceeded its timeout value. If it has, the stop method is called, and the
* motor is shut down until its value is updated again.
*/
public void check() {
boolean enabled;
double stopTime;
synchronized (m_thisMutex) {
enabled = m_enabled;
stopTime = m_stopTime;
}
if (!enabled || RobotState.isDisabled() || RobotState.isTest()) {
return;
}
if (stopTime < Timer.getFPGATimestamp()) {
DriverStation.reportError(
getDescription()
+ "... Output not updated often enough. See https://docs.wpilib.org/motorsafety for more information.",
false);
stopMotor();
}
}
/**
* Enable/disable motor safety for this device.
*
* <p>Turn on and off the motor safety option for this PWM object.
*
* @param enabled True if motor safety is enforced for this object
*/
public void setSafetyEnabled(boolean enabled) {
synchronized (m_thisMutex) {
m_enabled = enabled;
}
}
/**
* Return the state of the motor safety enabled flag.
*
* <p>Return if the motor safety is currently enabled for this device.
*
* @return True if motor safety is enforced for this device
*/
public boolean isSafetyEnabled() {
synchronized (m_thisMutex) {
return m_enabled;
}
}
/**
* Check the motors to see if any have timed out.
*
* <p>This static method is called periodically to poll all the motors and stop any that have
* timed out.
*/
public static void checkMotors() {
synchronized (m_listMutex) {
for (MotorSafety elem : m_instanceList) {
elem.check();
}
}
}
/** Called to stop the motor when the timeout expires. */
public abstract void stopMotor();
/**
* Returns a description to print when an error occurs.
*
* @return Description to print when an error occurs.
*/
public abstract String getDescription();
}

View File

@@ -0,0 +1,286 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj.motorcontrol;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.hal.SimDevice.Direction;
import edu.wpi.first.hal.SimDouble;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
import edu.wpi.first.wpilibj.MotorSafety;
import edu.wpi.first.wpilibj.PWM;
import edu.wpi.first.wpilibj.RobotController;
import java.util.ArrayList;
/** Common base class for all PWM Motor Controllers. */
@SuppressWarnings("removal")
public abstract class PWMMotorController extends MotorSafety
implements MotorController, Sendable, AutoCloseable {
private boolean m_isInverted;
private final ArrayList<PWMMotorController> m_followers = new ArrayList<>();
/** PWM instances for motor controller. */
protected PWM m_pwm;
private SimDevice m_simDevice;
private SimDouble m_simSpeed;
private boolean m_eliminateDeadband;
private int m_minPwm;
private int m_deadbandMinPwm;
private int m_centerPwm;
private int m_deadbandMaxPwm;
private int m_maxPwm;
/**
* Constructor.
*
* @param name Name to use for SendableRegistry
* @param channel The PWM channel that the controller is attached to. 0-9 are on-board, 10-19 are
* on the MXP port
*/
@SuppressWarnings("this-escape")
protected PWMMotorController(final String name, final int channel) {
m_pwm = new PWM(channel, false);
SendableRegistry.add(this, name, channel);
m_simDevice = SimDevice.create("PWMMotorController", channel);
if (m_simDevice != null) {
m_simSpeed = m_simDevice.createDouble("Speed", Direction.kOutput, 0.0);
m_pwm.setSimDevice(m_simDevice);
}
}
/** Free the resource associated with the PWM channel and set the value to 0. */
@Override
public void close() {
SendableRegistry.remove(this);
m_pwm.close();
if (m_simDevice != null) {
m_simDevice.close();
m_simDevice = null;
m_simSpeed = null;
}
}
private int getMinPositivePwm() {
if (m_eliminateDeadband) {
return m_deadbandMaxPwm;
} else {
return m_centerPwm + 1;
}
}
private int getMaxNegativePwm() {
if (m_eliminateDeadband) {
return m_deadbandMinPwm;
} else {
return m_centerPwm - 1;
}
}
private int getPositiveScaleFactor() {
return m_maxPwm - getMinPositivePwm();
}
private int getNegativeScaleFactor() {
return getMaxNegativePwm() - m_minPwm;
}
/**
* Takes a speed from -1 to 1, and outputs it in the microsecond format.
*
* @param speed the speed to output
*/
protected final void setSpeed(double speed) {
if (Double.isFinite(speed)) {
speed = Math.clamp(speed, -1.0, 1.0);
} else {
speed = 0.0;
}
if (m_simSpeed != null) {
m_simSpeed.set(speed);
}
int rawValue;
if (speed == 0.0) {
rawValue = m_centerPwm;
} else if (speed > 0.0) {
rawValue = (int) Math.round(speed * getPositiveScaleFactor()) + getMinPositivePwm();
} else {
rawValue = (int) Math.round(speed * getNegativeScaleFactor()) + getMaxNegativePwm();
}
m_pwm.setPulseTimeMicroseconds(rawValue);
}
/**
* Gets the speed from -1 to 1, from the currently set pulse time.
*
* @return motor controller speed
*/
protected final double getSpeed() {
int rawValue = m_pwm.getPulseTimeMicroseconds();
if (rawValue == 0) {
return 0.0;
} else if (rawValue > m_maxPwm) {
return 1.0;
} else if (rawValue < m_minPwm) {
return -1.0;
} else if (rawValue > getMinPositivePwm()) {
return (rawValue - getMinPositivePwm()) / (double) getPositiveScaleFactor();
} else if (rawValue < getMaxNegativePwm()) {
return (rawValue - getMaxNegativePwm()) / (double) getNegativeScaleFactor();
} else {
return 0.0;
}
}
/**
* Sets the bounds in microseconds for the controller.
*
* @param maxPwm maximum
* @param deadbandMaxPwm deadband max
* @param centerPwm center
* @param deadbandMinPwm deadmand min
* @param minPwm minimum
*/
protected final void setBoundsMicroseconds(
int maxPwm, int deadbandMaxPwm, int centerPwm, int deadbandMinPwm, int minPwm) {
m_maxPwm = maxPwm;
m_deadbandMaxPwm = deadbandMaxPwm;
m_centerPwm = centerPwm;
m_deadbandMinPwm = deadbandMinPwm;
m_minPwm = minPwm;
}
/**
* Set the PWM value.
*
* <p>The PWM value is set using a range of -1.0 to 1.0, appropriately scaling the value for the
* FPGA.
*
* @param speed The speed value between -1.0 and 1.0 to set.
*/
@Override
public void set(double speed) {
if (m_isInverted) {
speed = -speed;
}
setSpeed(speed);
for (var follower : m_followers) {
follower.set(speed);
}
feed();
}
/**
* Get the recently set value of the PWM. This value is affected by the inversion property.
*
* @return The most recently set value for the PWM between -1.0 and 1.0.
*/
@Override
public double get() {
return getSpeed() * (m_isInverted ? -1.0 : 1.0);
}
/**
* Gets the voltage output of the motor controller, nominally between -12 V and 12 V.
*
* @return The voltage of the motor controller, nominally between -12 V and 12 V.
*/
public double getVoltage() {
return get() * RobotController.getBatteryVoltage();
}
@Override
public void setInverted(boolean isInverted) {
m_isInverted = isInverted;
}
@Override
public boolean getInverted() {
return m_isInverted;
}
@Override
public void disable() {
m_pwm.setDisabled();
if (m_simSpeed != null) {
m_simSpeed.set(0.0);
}
for (var follower : m_followers) {
follower.disable();
}
}
@Override
public void stopMotor() {
// Don't use set(0) as that will feed the watch kitty
m_pwm.setPulseTimeMicroseconds(0);
for (var follower : m_followers) {
follower.stopMotor();
}
}
@Override
public String getDescription() {
return "PWM " + getChannel();
}
/**
* Gets the backing PWM handle.
*
* @return The pwm handle.
*/
public int getPwmHandle() {
return m_pwm.getHandle();
}
/**
* Gets the PWM channel number.
*
* @return The channel number.
*/
public int getChannel() {
return m_pwm.getChannel();
}
/**
* Optionally eliminate the deadband from a motor controller.
*
* @param eliminateDeadband If true, set the motor curve for the motor controller to eliminate the
* deadband in the middle of the range. Otherwise, keep the full range without modifying any
* values.
*/
public void enableDeadbandElimination(boolean eliminateDeadband) {
m_eliminateDeadband = eliminateDeadband;
}
/**
* Make the given PWM motor controller follow the output of this one.
*
* @param follower The motor controller follower.
*/
public void addFollower(PWMMotorController follower) {
m_followers.add(follower);
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Motor Controller");
builder.setActuator(true);
builder.addDoubleProperty("Value", this::get, this::set);
}
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.util.AllocationException;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class for operating a compressor connected to a pneumatics module. The module will automatically
* run in closed loop mode by default whenever a {@link Solenoid} object is created. For most cases,
* a Compressor object does not need to be instantiated or used in a robot program. This class is
* only required in cases where the robot program needs a more detailed status of the compressor or
* to enable/disable closed loop control.
*
* <p>Note: you cannot operate the compressor directly from this class as doing so would circumvent
* the safety provided by using the pressure switch and closed loop control. You can only turn off
* closed loop control, thereby stopping the compressor from operating.
*/
public class Compressor implements Sendable, AutoCloseable {
private PneumaticsBase m_module;
private PneumaticsModuleType m_moduleType;
/**
* Constructs a compressor for a specified module and type.
*
* @param busId The bus ID
* @param module The module ID to use.
* @param moduleType The module type to use.
*/
@SuppressWarnings("this-escape")
public Compressor(int busId, int module, PneumaticsModuleType moduleType) {
m_module = PneumaticsBase.getForType(busId, module, moduleType);
m_moduleType = moduleType;
if (!m_module.reserveCompressor()) {
m_module.close();
throw new AllocationException("Compressor already allocated");
}
m_module.enableCompressorDigital();
m_module.reportUsage("Compressor", "");
SendableRegistry.add(this, "Compressor", module);
}
/**
* Constructs a compressor for a default module and specified type.
*
* @param busId The bus ID
* @param moduleType The module type to use.
*/
public Compressor(int busId, PneumaticsModuleType moduleType) {
this(busId, PneumaticsBase.getDefaultForType(moduleType), moduleType);
}
@Override
public void close() {
SendableRegistry.remove(this);
m_module.unreserveCompressor();
m_module.close();
m_module = null;
}
/**
* Returns whether the compressor is active or not.
*
* @return true if the compressor is on - otherwise false.
*/
public boolean isEnabled() {
return m_module.getCompressor();
}
/**
* Returns the state of the pressure switch.
*
* @return True if pressure switch indicates that the system is not full, otherwise false.
*/
public boolean getPressureSwitchValue() {
return m_module.getPressureSwitch();
}
/**
* Get the current drawn by the compressor.
*
* @return Current drawn by the compressor in amps.
*/
public double getCurrent() {
return m_module.getCompressorCurrent();
}
/**
* If supported by the device, returns the analog input voltage (on channel 0).
*
* <p>This function is only supported by the REV PH. On CTRE PCM, this will return 0.
*
* @return The analog input voltage, in volts.
*/
public double getAnalogVoltage() {
return m_module.getAnalogVoltage(0);
}
/**
* If supported by the device, returns the pressure (in PSI) read by the analog pressure sensor
* (on channel 0).
*
* <p>This function is only supported by the REV PH with the REV Analog Pressure Sensor. On CTRE
* PCM, this will return 0.
*
* @return The pressure (in PSI) read by the analog pressure sensor.
*/
public double getPressure() {
return m_module.getPressure(0);
}
/** Disable the compressor. */
public void disable() {
m_module.disableCompressor();
}
/**
* Enables the compressor in digital mode using the digital pressure switch. The compressor will
* turn on when the pressure switch indicates that the system is not full, and will turn off when
* the pressure switch indicates that the system is full.
*/
public void enableDigital() {
m_module.enableCompressorDigital();
}
/**
* If supported by the device, enables the compressor in analog mode. This mode uses an analog
* pressure sensor connected to analog channel 0 to cycle the compressor. The compressor will turn
* on when the pressure drops below {@code minPressure} and will turn off when the pressure
* reaches {@code maxPressure}. This mode is only supported by the REV PH with the REV Analog
* Pressure Sensor connected to analog channel 0.
*
* <p>On CTRE PCM, this will enable digital control.
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value.
*/
public void enableAnalog(double minPressure, double maxPressure) {
m_module.enableCompressorAnalog(minPressure, maxPressure);
}
/**
* If supported by the device, enables the compressor in hybrid mode. This mode uses both a
* digital pressure switch and an analog pressure sensor connected to analog channel 0 to cycle
* the compressor. This mode is only supported by the REV PH with the REV Analog Pressure Sensor
* connected to analog channel 0.
*
* <p>The compressor will turn on when <i>both</i>:
*
* <ul>
* <li>The digital pressure switch indicates the system is not full AND
* <li>The analog pressure sensor indicates that the pressure in the system is below the
* specified minimum pressure.
* </ul>
*
* <p>The compressor will turn off when <i>either</i>:
*
* <ul>
* <li>The digital pressure switch is disconnected or indicates that the system is full OR
* <li>The pressure detected by the analog sensor is greater than the specified maximum
* pressure.
* </ul>
*
* <p>On CTRE PCM, this will enable digital control.
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value and the pressure switch indicates that the system is not full.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value or the pressure switch is disconnected or indicates that the system is
* full.
*/
public void enableHybrid(double minPressure, double maxPressure) {
m_module.enableCompressorHybrid(minPressure, maxPressure);
}
/**
* Returns the active compressor configuration.
*
* @return The active compressor configuration.
*/
public CompressorConfigType getConfigType() {
return m_module.getCompressorConfigType();
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Compressor");
builder.addBooleanProperty("Enabled", this::isEnabled, null);
builder.addBooleanProperty("Pressure switch", this::getPressureSwitchValue, null);
builder.addDoubleProperty("Current (A)", this::getCurrent, null);
if (m_moduleType == PneumaticsModuleType.REVPH) { // These are not supported by the CTRE PCM
builder.addDoubleProperty("Analog Voltage", this::getAnalogVoltage, null);
builder.addDoubleProperty("Pressure (PSI)", this::getPressure, null);
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.REVPHJNI;
/** Compressor config type. */
public enum CompressorConfigType {
/** Disabled. */
Disabled(REVPHJNI.COMPRESSOR_CONFIG_TYPE_DISABLED),
/** Digital. */
Digital(REVPHJNI.COMPRESSOR_CONFIG_TYPE_DIGITAL),
/** Analog. */
Analog(REVPHJNI.COMPRESSOR_CONFIG_TYPE_ANALOG),
/** Hybrid. */
Hybrid(REVPHJNI.COMPRESSOR_CONFIG_TYPE_HYBRID);
/** CompressorConfigType value. */
public final int value;
CompressorConfigType(int value) {
this.value = value;
}
/**
* Gets a type from an int value.
*
* @param value int value
* @return type
*/
public static CompressorConfigType fromValue(int value) {
return switch (value) {
case REVPHJNI.COMPRESSOR_CONFIG_TYPE_HYBRID -> Hybrid;
case REVPHJNI.COMPRESSOR_CONFIG_TYPE_ANALOG -> Analog;
case REVPHJNI.COMPRESSOR_CONFIG_TYPE_DIGITAL -> Digital;
default -> Disabled;
};
}
/**
* Returns the CompressorConfigType's value.
*
* @return The CompressorConfigType's value.
*/
public int getValue() {
return value;
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.util.AllocationException;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* DoubleSolenoid class for running 2 channels of high voltage Digital Output on the pneumatics
* module.
*
* <p>The DoubleSolenoid class is typically used for pneumatics solenoids that have two positions
* controlled by two separate channels.
*/
public class DoubleSolenoid implements Sendable, AutoCloseable {
/** Possible values for a DoubleSolenoid. */
public enum Value {
/** Off position. */
kOff,
/** Forward position. */
kForward,
/** Reverse position. */
kReverse
}
private final int m_forwardMask; // The mask for the forward channel.
private final int m_reverseMask; // The mask for the reverse channel.
private final int m_mask; // The channel mask
private PneumaticsBase m_module;
private final int m_forwardChannel;
private final int m_reverseChannel;
/**
* Constructs a double solenoid for a default module of a specific module type.
*
* @param busId The bus ID
* @param moduleType The module type to use.
* @param forwardChannel The forward channel on the module to control.
* @param reverseChannel The reverse channel on the module to control.
*/
public DoubleSolenoid(
final int busId,
final PneumaticsModuleType moduleType,
final int forwardChannel,
final int reverseChannel) {
this(
busId,
PneumaticsBase.getDefaultForType(moduleType),
moduleType,
forwardChannel,
reverseChannel);
}
/**
* Constructs a double solenoid for a specified module of a specific module type.
*
* @param busId The bus ID
* @param module The module of the solenoid module to use.
* @param moduleType The module type to use.
* @param forwardChannel The forward channel on the module to control.
* @param reverseChannel The reverse channel on the module to control.
*/
@SuppressWarnings({"PMD.UseTryWithResources", "this-escape"})
public DoubleSolenoid(
final int busId,
final int module,
final PneumaticsModuleType moduleType,
final int forwardChannel,
final int reverseChannel) {
m_module = PneumaticsBase.getForType(busId, module, moduleType);
boolean allocatedSolenoids = false;
boolean successfulCompletion = false;
m_forwardChannel = forwardChannel;
m_reverseChannel = reverseChannel;
m_forwardMask = 1 << forwardChannel;
m_reverseMask = 1 << reverseChannel;
m_mask = m_forwardMask | m_reverseMask;
try {
if (!m_module.checkSolenoidChannel(forwardChannel)) {
throw new IllegalArgumentException("Channel " + forwardChannel + " out of range");
}
if (!m_module.checkSolenoidChannel(reverseChannel)) {
throw new IllegalArgumentException("Channel " + reverseChannel + " out of range");
}
int allocMask = m_module.checkAndReserveSolenoids(m_mask);
if (allocMask != 0) {
if (allocMask == m_mask) {
throw new AllocationException(
"Channels " + forwardChannel + " and " + reverseChannel + " already allocated");
} else if (allocMask == m_forwardMask) {
throw new AllocationException("Channel " + forwardChannel + " already allocated");
} else {
throw new AllocationException("Channel " + reverseChannel + " already allocated");
}
}
allocatedSolenoids = true;
m_module.reportUsage(
"Solenoid[" + forwardChannel + "," + reverseChannel + "]", "DoubleSolenoid");
SendableRegistry.add(this, "DoubleSolenoid", m_module.getModuleNumber(), forwardChannel);
successfulCompletion = true;
} finally {
if (!successfulCompletion) {
if (allocatedSolenoids) {
m_module.unreserveSolenoids(m_mask);
}
m_module.close();
}
}
}
@Override
public synchronized void close() {
SendableRegistry.remove(this);
m_module.unreserveSolenoids(m_mask);
m_module.close();
m_module = null;
}
/**
* Set the value of a solenoid.
*
* @param value The value to set (Off, Forward, Reverse)
*/
public void set(final Value value) {
int setValue =
switch (value) {
case kOff -> 0;
case kForward -> m_forwardMask;
case kReverse -> m_reverseMask;
};
m_module.setSolenoids(m_mask, setValue);
}
/**
* Read the current value of the solenoid.
*
* @return The current value of the solenoid.
*/
public Value get() {
int values = m_module.getSolenoids();
if ((values & m_forwardMask) != 0) {
return Value.kForward;
} else if ((values & m_reverseMask) != 0) {
return Value.kReverse;
} else {
return Value.kOff;
}
}
/**
* Toggle the value of the solenoid.
*
* <p>If the solenoid is set to forward, it'll be set to reverse. If the solenoid is set to
* reverse, it'll be set to forward. If the solenoid is set to off, nothing happens.
*/
public void toggle() {
Value value = get();
if (value == Value.kForward) {
set(Value.kReverse);
} else if (value == Value.kReverse) {
set(Value.kForward);
}
}
/**
* Get the forward channel.
*
* @return the forward channel.
*/
public int getFwdChannel() {
return m_forwardChannel;
}
/**
* Get the reverse channel.
*
* @return the reverse channel.
*/
public int getRevChannel() {
return m_reverseChannel;
}
/**
* Check if the forward solenoid is Disabled. If a solenoid is shorted, it is added to the
* DisabledList and disabled until power cycle, or until faults are cleared.
*
* @return If solenoid is disabled due to short.
*/
public boolean isFwdSolenoidDisabled() {
return (m_module.getSolenoidDisabledList() & m_forwardMask) != 0;
}
/**
* Check if the reverse solenoid is Disabled. If a solenoid is shorted, it is added to the
* DisabledList and disabled until power cycle, or until faults are cleared.
*
* @return If solenoid is disabled due to short.
*/
public boolean isRevSolenoidDisabled() {
return (m_module.getSolenoidDisabledList() & m_reverseMask) != 0;
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Double Solenoid");
builder.setActuator(true);
builder.addStringProperty(
"Value",
() -> get().name().substring(1),
value -> {
if ("Forward".equals(value)) {
set(Value.kForward);
} else if ("Reverse".equals(value)) {
set(Value.kReverse);
} else {
set(Value.kOff);
}
});
}
}

View File

@@ -0,0 +1,432 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.PortsJNI;
import edu.wpi.first.hal.REVPHFaults;
import edu.wpi.first.hal.REVPHJNI;
import edu.wpi.first.hal.REVPHStickyFaults;
import edu.wpi.first.hal.REVPHVersion;
import java.util.HashMap;
import java.util.Map;
/** Module class for controlling a REV Robotics Pneumatic Hub. */
public class PneumaticHub implements PneumaticsBase {
private static class DataStore implements AutoCloseable {
public final int m_module;
public final int m_handle;
private final int m_busId;
private int m_refCount;
private int m_reservedMask;
private boolean m_compressorReserved;
public final int[] m_oneShotDurMs = new int[PortsJNI.getNumREVPHChannels()];
private final Object m_reserveLock = new Object();
DataStore(int busId, int module) {
m_handle = REVPHJNI.initialize(busId, module);
m_module = module;
m_busId = busId;
m_handleMaps[busId].put(module, this);
final REVPHVersion version = REVPHJNI.getVersion(m_handle);
final String fwVersion =
version.firmwareMajor + "." + version.firmwareMinor + "." + version.firmwareFix;
// Check PH firmware version
if (version.firmwareMajor > 0 && version.firmwareMajor < 22) {
throw new IllegalStateException(
"The Pneumatic Hub has firmware version "
+ fwVersion
+ ", and must be updated to version 2022.0.0 or later "
+ "using the REV Hardware Client.");
}
}
@Override
public void close() {
REVPHJNI.free(m_handle);
m_handleMaps[m_busId].remove(m_module);
}
public void addRef() {
m_refCount++;
}
public void removeRef() {
m_refCount--;
if (m_refCount == 0) {
this.close();
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static final Map<Integer, DataStore>[] m_handleMaps =
(Map<Integer, DataStore>[]) new Map[PortsJNI.getNumCanBuses()];
private static final Object m_handleLock = new Object();
private static DataStore getForModule(int busId, int module) {
synchronized (m_handleLock) {
Map<Integer, DataStore> handleMap = m_handleMaps[busId];
if (handleMap == null) {
handleMap = new HashMap<>();
m_handleMaps[busId] = handleMap;
}
DataStore pcm = handleMap.get(module);
if (pcm == null) {
pcm = new DataStore(busId, module);
}
pcm.addRef();
return pcm;
}
}
private static void freeModule(DataStore store) {
synchronized (m_handleLock) {
store.removeRef();
}
}
/** Converts volts to PSI per the REV Analog Pressure Sensor datasheet. */
private static double voltsToPsi(double sensorVoltage, double supplyVoltage) {
return 250 * (sensorVoltage / supplyVoltage) - 25;
}
/** Converts PSI to volts per the REV Analog Pressure Sensor datasheet. */
private static double psiToVolts(double pressure, double supplyVoltage) {
return supplyVoltage * (0.004 * pressure + 0.1);
}
private final DataStore m_dataStore;
private final int m_handle;
/**
* Constructs a PneumaticHub with the default ID (1).
*
* @param busId The bus ID
*/
public PneumaticHub(int busId) {
this(busId, SensorUtil.getDefaultREVPHModule());
}
/**
* Constructs a PneumaticHub.
*
* @param busId The bus ID
* @param module module number to construct
*/
public PneumaticHub(int busId, int module) {
m_dataStore = getForModule(busId, module);
m_handle = m_dataStore.m_handle;
}
@Override
public void close() {
freeModule(m_dataStore);
}
@Override
public boolean getCompressor() {
return REVPHJNI.getCompressor(m_handle);
}
@Override
public CompressorConfigType getCompressorConfigType() {
return CompressorConfigType.fromValue(REVPHJNI.getCompressorConfig(m_handle));
}
@Override
public boolean getPressureSwitch() {
return REVPHJNI.getPressureSwitch(m_handle);
}
@Override
public double getCompressorCurrent() {
return REVPHJNI.getCompressorCurrent(m_handle);
}
@Override
public void setSolenoids(int mask, int values) {
REVPHJNI.setSolenoids(m_handle, mask, values);
}
@Override
public int getSolenoids() {
return REVPHJNI.getSolenoids(m_handle);
}
@Override
public int getModuleNumber() {
return m_dataStore.m_module;
}
@Override
public void fireOneShot(int index) {
REVPHJNI.fireOneShot(m_handle, index, m_dataStore.m_oneShotDurMs[index]);
}
@Override
public void setOneShotDuration(int index, int durMs) {
m_dataStore.m_oneShotDurMs[index] = durMs;
}
@Override
public boolean checkSolenoidChannel(int channel) {
return REVPHJNI.checkSolenoidChannel(channel);
}
@Override
public int checkAndReserveSolenoids(int mask) {
synchronized (m_dataStore.m_reserveLock) {
if ((m_dataStore.m_reservedMask & mask) != 0) {
return m_dataStore.m_reservedMask & mask;
}
m_dataStore.m_reservedMask |= mask;
return 0;
}
}
@Override
public void unreserveSolenoids(int mask) {
synchronized (m_dataStore.m_reserveLock) {
m_dataStore.m_reservedMask &= ~mask;
}
}
@Override
public Solenoid makeSolenoid(int channel) {
return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.REVPH, channel);
}
@Override
public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
return new DoubleSolenoid(
m_dataStore.m_module, PneumaticsModuleType.REVPH, forwardChannel, reverseChannel);
}
@Override
public Compressor makeCompressor() {
return new Compressor(m_dataStore.m_module, PneumaticsModuleType.REVPH);
}
@Override
public boolean reserveCompressor() {
synchronized (m_dataStore.m_reserveLock) {
if (m_dataStore.m_compressorReserved) {
return false;
}
m_dataStore.m_compressorReserved = true;
return true;
}
}
@Override
public void unreserveCompressor() {
synchronized (m_dataStore.m_reserveLock) {
m_dataStore.m_compressorReserved = false;
}
}
@Override
public int getSolenoidDisabledList() {
return REVPHJNI.getSolenoidDisabledList(m_handle);
}
/**
* Disables the compressor. The compressor will not turn on until {@link
* #enableCompressorDigital()}, {@link #enableCompressorAnalog(double, double)}, or {@link
* #enableCompressorHybrid(double, double)} are called.
*/
@Override
public void disableCompressor() {
REVPHJNI.setClosedLoopControlDisabled(m_handle);
}
@Override
public void enableCompressorDigital() {
REVPHJNI.setClosedLoopControlDigital(m_handle);
}
/**
* Enables the compressor in analog mode. This mode uses an analog pressure sensor connected to
* analog channel 0 to cycle the compressor. The compressor will turn on when the pressure drops
* below {@code minPressure} and will turn off when the pressure reaches {@code maxPressure}.
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value. Range 0-120 PSI.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value. Range 0-120 PSI. Must be larger then minPressure.
*/
@Override
public void enableCompressorAnalog(double minPressure, double maxPressure) {
if (minPressure >= maxPressure) {
throw new IllegalArgumentException("maxPressure must be greater than minPressure");
}
if (minPressure < 0 || minPressure > 120) {
throw new IllegalArgumentException(
"minPressure must be between 0 and 120 PSI, got " + minPressure);
}
if (maxPressure < 0 || maxPressure > 120) {
throw new IllegalArgumentException(
"maxPressure must be between 0 and 120 PSI, got " + maxPressure);
}
// Send the voltage as it would be if the 5V rail was at exactly 5V.
// The firmware will compensate for the real 5V rail voltage, which
// can fluctuate somewhat over time.
double minAnalogVoltage = psiToVolts(minPressure, 5);
double maxAnalogVoltage = psiToVolts(maxPressure, 5);
REVPHJNI.setClosedLoopControlAnalog(m_handle, minAnalogVoltage, maxAnalogVoltage);
}
/**
* Enables the compressor in hybrid mode. This mode uses both a digital pressure switch and an
* analog pressure sensor connected to analog channel 0 to cycle the compressor.
*
* <p>The compressor will turn on when <i>both</i>:
*
* <ul>
* <li>The digital pressure switch indicates the system is not full AND
* <li>The analog pressure sensor indicates that the pressure in the system is below the
* specified minimum pressure.
* </ul>
*
* <p>The compressor will turn off when <i>either</i>:
*
* <ul>
* <li>The digital pressure switch is disconnected or indicates that the system is full OR
* <li>The pressure detected by the analog sensor is greater than the specified maximum
* pressure.
* </ul>
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value and the pressure switch indicates that the system is not full. Range
* 0-120 PSI.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value or the pressure switch is disconnected or indicates that the system is
* full. Range 0-120 PSI. Must be larger then minPressure.
*/
@Override
public void enableCompressorHybrid(double minPressure, double maxPressure) {
if (minPressure >= maxPressure) {
throw new IllegalArgumentException("maxPressure must be greater than minPressure");
}
if (minPressure < 0 || minPressure > 120) {
throw new IllegalArgumentException(
"minPressure must be between 0 and 120 PSI, got " + minPressure);
}
if (maxPressure < 0 || maxPressure > 120) {
throw new IllegalArgumentException(
"maxPressure must be between 0 and 120 PSI, got " + maxPressure);
}
// Send the voltage as it would be if the 5V rail was at exactly 5V.
// The firmware will compensate for the real 5V rail voltage, which
// can fluctuate somewhat over time.
double minAnalogVoltage = psiToVolts(minPressure, 5);
double maxAnalogVoltage = psiToVolts(maxPressure, 5);
REVPHJNI.setClosedLoopControlHybrid(m_handle, minAnalogVoltage, maxAnalogVoltage);
}
/**
* Returns the raw voltage of the specified analog input channel.
*
* @param channel The analog input channel to read voltage from.
* @return The voltage of the specified analog input channel.
*/
@Override
public double getAnalogVoltage(int channel) {
return REVPHJNI.getAnalogVoltage(m_handle, channel);
}
/**
* Returns the pressure read by an analog pressure sensor on the specified analog input channel.
*
* @param channel The analog input channel to read pressure from.
* @return The pressure read by an analog pressure sensor on the specified analog input channel.
*/
@Override
public double getPressure(int channel) {
double sensorVoltage = REVPHJNI.getAnalogVoltage(m_handle, channel);
double supplyVoltage = REVPHJNI.get5VVoltage(m_handle);
return voltsToPsi(sensorVoltage, supplyVoltage);
}
/** Clears the sticky faults. */
public void clearStickyFaults() {
REVPHJNI.clearStickyFaults(m_handle);
}
/**
* Returns the hardware and firmware versions of this device.
*
* @return The hardware and firmware versions.
*/
public REVPHVersion getVersion() {
return REVPHJNI.getVersion(m_handle);
}
/**
* Returns the faults currently active on this device.
*
* @return The faults.
*/
public REVPHFaults getFaults() {
return REVPHJNI.getFaults(m_handle);
}
/**
* Returns the sticky faults currently active on this device.
*
* @return The sticky faults.
*/
public REVPHStickyFaults getStickyFaults() {
return REVPHJNI.getStickyFaults(m_handle);
}
/**
* Returns the current input voltage for this device.
*
* @return The input voltage.
*/
public double getInputVoltage() {
return REVPHJNI.getInputVoltage(m_handle);
}
/**
* Returns the current voltage of the regulated 5v supply.
*
* @return The current voltage of the 5v supply.
*/
public double get5VRegulatedVoltage() {
return REVPHJNI.get5VVoltage(m_handle);
}
/**
* Returns the total current (in amps) drawn by all solenoids.
*
* @return Total current drawn by all solenoids in amps.
*/
public double getSolenoidsTotalCurrent() {
return REVPHJNI.getSolenoidCurrent(m_handle);
}
/**
* Returns the current voltage of the solenoid power supply.
*
* @return The current voltage of the solenoid power supply.
*/
public double getSolenoidsVoltage() {
return REVPHJNI.getSolenoidVoltage(m_handle);
}
@Override
public void reportUsage(String device, String data) {
HAL.reportUsage("PH[" + m_dataStore.m_module + "]/" + device, data);
}
}

View File

@@ -0,0 +1,257 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
/** Interface for pneumatics devices. */
public interface PneumaticsBase extends AutoCloseable {
/**
* For internal use to get a module for a specific type.
*
* @param busId The bus ID
* @param module module number
* @param type module type
* @return module
*/
static PneumaticsBase getForType(int busId, int module, PneumaticsModuleType type) {
return switch (type) {
case CTREPCM -> new PneumaticsControlModule(busId, module);
case REVPH -> new PneumaticHub(busId, module);
};
}
/**
* For internal use to get the default for a specific type.
*
* @param type module type
* @return module default
*/
static int getDefaultForType(PneumaticsModuleType type) {
return switch (type) {
case CTREPCM -> SensorUtil.getDefaultCTREPCMModule();
case REVPH -> SensorUtil.getDefaultREVPHModule();
};
}
/**
* Sets solenoids on a pneumatics module.
*
* @param mask Bitmask indicating which solenoids to set. The LSB represents solenoid 0.
* @param values Bitmask indicating the desired states of the solenoids. The LSB represents
* solenoid 0.
*/
void setSolenoids(int mask, int values);
/**
* Gets a bitmask of solenoid values.
*
* @return Bitmask containing the state of the solenoids. The LSB represents solenoid 0.
*/
int getSolenoids();
/**
* Get module number for this module.
*
* @return module number
*/
int getModuleNumber();
/**
* Get a bitmask of disabled solenoids.
*
* @return Bitmask indicating disabled solenoids. The LSB represents solenoid 0.
*/
int getSolenoidDisabledList();
/**
* Fire a single solenoid shot.
*
* @param index solenoid index
*/
void fireOneShot(int index);
/**
* Set the duration for a single solenoid shot.
*
* @param index solenoid index
* @param durMs shot duration
*/
void setOneShotDuration(int index, int durMs);
/**
* Returns whether the compressor is active or not.
*
* @return True if the compressor is on - otherwise false.
*/
boolean getCompressor();
/**
* Returns the state of the pressure switch.
*
* @return True if pressure switch indicates that the system is not full, otherwise false.
*/
boolean getPressureSwitch();
/**
* Returns the current drawn by the compressor in amps.
*
* @return The current drawn by the compressor.
*/
double getCompressorCurrent();
/** Disables the compressor. */
void disableCompressor();
/**
* Enables the compressor in digital mode using the digital pressure switch. The compressor will
* turn on when the pressure switch indicates that the system is not full, and will turn off when
* the pressure switch indicates that the system is full.
*/
void enableCompressorDigital();
/**
* If supported by the device, enables the compressor in analog mode. This mode uses an analog
* pressure sensor connected to analog channel 0 to cycle the compressor. The compressor will turn
* on when the pressure drops below {@code minPressure} and will turn off when the pressure
* reaches {@code maxPressure}. This mode is only supported by the REV PH with the REV Analog
* Pressure Sensor connected to analog channel 0.
*
* <p>On CTRE PCM, this will enable digital control.
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value.
*/
void enableCompressorAnalog(double minPressure, double maxPressure);
/**
* If supported by the device, enables the compressor in hybrid mode. This mode uses both a
* digital pressure switch and an analog pressure sensor connected to analog channel 0 to cycle
* the compressor. This mode is only supported by the REV PH with the REV Analog Pressure Sensor
* connected to analog channel 0.
*
* <p>The compressor will turn on when <i>both</i>:
*
* <ul>
* <li>The digital pressure switch indicates the system is not full AND
* <li>The analog pressure sensor indicates that the pressure in the system is below the
* specified minimum pressure.
* </ul>
*
* <p>The compressor will turn off when <i>either</i>:
*
* <ul>
* <li>The digital pressure switch is disconnected or indicates that the system is full OR
* <li>The pressure detected by the analog sensor is greater than the specified maximum
* pressure.
* </ul>
*
* <p>On CTRE PCM, this will enable digital control.
*
* @param minPressure The minimum pressure in PSI. The compressor will turn on when the pressure
* drops below this value and the pressure switch indicates that the system is not full.
* @param maxPressure The maximum pressure in PSI. The compressor will turn off when the pressure
* reaches this value or the pressure switch is disconnected or indicates that the system is
* full.
*/
void enableCompressorHybrid(double minPressure, double maxPressure);
/**
* If supported by the device, returns the raw voltage of the specified analog input channel.
*
* <p>This function is only supported by the REV PH. On CTRE PCM, this will return 0.
*
* @param channel The analog input channel to read voltage from.
* @return The voltage of the specified analog input channel.
*/
double getAnalogVoltage(int channel);
/**
* If supported by the device, returns the pressure (in PSI) read by an analog pressure sensor on
* the specified analog input channel.
*
* <p>This function is only supported by the REV PH. On CTRE PCM, this will return 0.
*
* @param channel The analog input channel to read pressure from.
* @return The pressure (in PSI) read by an analog pressure sensor on the specified analog input
* channel.
*/
double getPressure(int channel);
/**
* Returns the active compressor configuration.
*
* @return The active compressor configuration.
*/
CompressorConfigType getCompressorConfigType();
/**
* Check if a solenoid channel is valid.
*
* @param channel Channel to check
* @return True if channel exists
*/
boolean checkSolenoidChannel(int channel);
/**
* Check to see if the solenoids marked in the bitmask can be reserved, and if so, reserve them.
*
* @param mask The bitmask of solenoids to reserve. The LSB represents solenoid 0.
* @return 0 if successful; mask of solenoids that couldn't be allocated otherwise
*/
int checkAndReserveSolenoids(int mask);
/**
* Unreserve the solenoids marked in the bitmask.
*
* @param mask The bitmask of solenoids to unreserve. The LSB represents solenoid 0.
*/
void unreserveSolenoids(int mask);
/**
* Reserve the compressor.
*
* @return true if successful; false if compressor already reserved
*/
boolean reserveCompressor();
/** Unreserve the compressor. */
void unreserveCompressor();
@Override
void close();
/**
* Create a solenoid object for the specified channel.
*
* @param channel solenoid channel
* @return Solenoid object
*/
Solenoid makeSolenoid(int channel);
/**
* Create a double solenoid object for the specified channels.
*
* @param forwardChannel solenoid channel for forward
* @param reverseChannel solenoid channel for reverse
* @return DoubleSolenoid object
*/
DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel);
/**
* Create a compressor object.
*
* @return Compressor object
*/
Compressor makeCompressor();
/**
* Report usage.
*
* @param device device and channel as appropriate
* @param data arbitrary usage data
*/
void reportUsage(String device, String data);
}

View File

@@ -0,0 +1,370 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.CTREPCMJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.PortsJNI;
import java.util.HashMap;
import java.util.Map;
/** Module class for controlling a Cross The Road Electronics Pneumatics Control Module. */
public class PneumaticsControlModule implements PneumaticsBase {
private static class DataStore implements AutoCloseable {
public final int m_module;
public final int m_handle;
private final int m_busId;
private int m_refCount;
private int m_reservedMask;
private boolean m_compressorReserved;
private final Object m_reserveLock = new Object();
DataStore(int busId, int module) {
m_handle = CTREPCMJNI.initialize(busId, module);
m_module = module;
m_busId = busId;
m_handleMaps[busId].put(module, this);
}
@Override
public void close() {
CTREPCMJNI.free(m_handle);
m_handleMaps[m_busId].remove(m_module);
}
public void addRef() {
m_refCount++;
}
public void removeRef() {
m_refCount--;
if (m_refCount == 0) {
this.close();
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static final Map<Integer, DataStore>[] m_handleMaps =
(Map<Integer, DataStore>[]) new Map[PortsJNI.getNumCanBuses()];
private static final Object m_handleLock = new Object();
private static DataStore getForModule(int busId, int module) {
synchronized (m_handleLock) {
Map<Integer, DataStore> handleMap = m_handleMaps[busId];
if (handleMap == null) {
handleMap = new HashMap<>();
m_handleMaps[busId] = handleMap;
}
DataStore pcm = handleMap.get(module);
if (pcm == null) {
pcm = new DataStore(busId, module);
}
pcm.addRef();
return pcm;
}
}
private static void freeModule(DataStore store) {
synchronized (m_handleLock) {
store.removeRef();
}
}
private final DataStore m_dataStore;
private final int m_handle;
/**
* Constructs a PneumaticsControlModule with the default ID (0).
*
* @param busId The bus ID
*/
public PneumaticsControlModule(int busId) {
this(busId, SensorUtil.getDefaultCTREPCMModule());
}
/**
* Constructs a PneumaticsControlModule.
*
* @param busId The bus ID
* @param module module number to construct
*/
public PneumaticsControlModule(int busId, int module) {
m_dataStore = getForModule(busId, module);
m_handle = m_dataStore.m_handle;
}
@Override
public void close() {
freeModule(m_dataStore);
}
@Override
public boolean getCompressor() {
return CTREPCMJNI.getCompressor(m_handle);
}
@Override
public boolean getPressureSwitch() {
return CTREPCMJNI.getPressureSwitch(m_handle);
}
@Override
public double getCompressorCurrent() {
return CTREPCMJNI.getCompressorCurrent(m_handle);
}
/**
* Return whether the compressor current is currently too high.
*
* @return True if the compressor current is too high, otherwise false.
* @see #getCompressorCurrentTooHighStickyFault()
*/
public boolean getCompressorCurrentTooHighFault() {
return CTREPCMJNI.getCompressorCurrentTooHighFault(m_handle);
}
/**
* Returns whether the compressor current has been too high since sticky faults were last cleared.
* This fault is persistent and can be cleared by {@link #clearAllStickyFaults()}
*
* @return True if the compressor current has been too high since sticky faults were last cleared.
* @see #getCompressorCurrentTooHighFault()
*/
public boolean getCompressorCurrentTooHighStickyFault() {
return CTREPCMJNI.getCompressorCurrentTooHighStickyFault(m_handle);
}
/**
* Returns whether the compressor is currently shorted.
*
* @return True if the compressor is currently shorted, otherwise false.
* @see #getCompressorShortedStickyFault()
*/
public boolean getCompressorShortedFault() {
return CTREPCMJNI.getCompressorShortedFault(m_handle);
}
/**
* Returns whether the compressor has been shorted since sticky faults were last cleared. This
* fault is persistent and can be cleared by {@link #clearAllStickyFaults()}
*
* @return True if the compressor has been shorted since sticky faults were last cleared,
* otherwise false.
* @see #getCompressorShortedFault()
*/
public boolean getCompressorShortedStickyFault() {
return CTREPCMJNI.getCompressorShortedStickyFault(m_handle);
}
/**
* Returns whether the compressor is currently disconnected.
*
* @return True if compressor is currently disconnected, otherwise false.
* @see #getCompressorNotConnectedStickyFault()
*/
public boolean getCompressorNotConnectedFault() {
return CTREPCMJNI.getCompressorNotConnectedFault(m_handle);
}
/**
* Returns whether the compressor has been disconnected since sticky faults were last cleared.
* This fault is persistent and can be cleared by {@link #clearAllStickyFaults()}
*
* @return True if the compressor has been disconnected since sticky faults were last cleared,
* otherwise false.
* @see #getCompressorNotConnectedFault()
*/
public boolean getCompressorNotConnectedStickyFault() {
return CTREPCMJNI.getCompressorNotConnectedStickyFault(m_handle);
}
@Override
public void setSolenoids(int mask, int values) {
CTREPCMJNI.setSolenoids(m_handle, mask, values);
}
@Override
public int getSolenoids() {
return CTREPCMJNI.getSolenoids(m_handle);
}
@Override
public int getModuleNumber() {
return m_dataStore.m_module;
}
@Override
public int getSolenoidDisabledList() {
return CTREPCMJNI.getSolenoidDisabledList(m_handle);
}
/**
* Returns whether the solenoid is currently reporting a voltage fault.
*
* @return True if solenoid is reporting a fault, otherwise false.
* @see #getSolenoidVoltageStickyFault()
*/
public boolean getSolenoidVoltageFault() {
return CTREPCMJNI.getSolenoidVoltageFault(m_handle);
}
/**
* Returns whether the solenoid has reported a voltage fault since sticky faults were last
* cleared. This fault is persistent and can be cleared by ClearAllStickyFaults()
*
* @return True if solenoid is reporting a fault, otherwise false.
* @see #getSolenoidVoltageFault()
*/
public boolean getSolenoidVoltageStickyFault() {
return CTREPCMJNI.getSolenoidVoltageStickyFault(m_handle);
}
/** Clears all sticky faults on this device. */
public void clearAllStickyFaults() {
CTREPCMJNI.clearAllStickyFaults(m_handle);
}
@Override
public void fireOneShot(int index) {
CTREPCMJNI.fireOneShot(m_handle, index);
}
@Override
public void setOneShotDuration(int index, int durMs) {
CTREPCMJNI.setOneShotDuration(m_handle, index, durMs);
}
@Override
public boolean checkSolenoidChannel(int channel) {
return CTREPCMJNI.checkSolenoidChannel(channel);
}
@Override
public int checkAndReserveSolenoids(int mask) {
synchronized (m_dataStore.m_reserveLock) {
if ((m_dataStore.m_reservedMask & mask) != 0) {
return m_dataStore.m_reservedMask & mask;
}
m_dataStore.m_reservedMask |= mask;
return 0;
}
}
@Override
public void unreserveSolenoids(int mask) {
synchronized (m_dataStore.m_reserveLock) {
m_dataStore.m_reservedMask &= ~mask;
}
}
@Override
public Solenoid makeSolenoid(int channel) {
return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.CTREPCM, channel);
}
@Override
public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
return new DoubleSolenoid(
m_dataStore.m_module, PneumaticsModuleType.CTREPCM, forwardChannel, reverseChannel);
}
@Override
public Compressor makeCompressor() {
return new Compressor(m_dataStore.m_module, PneumaticsModuleType.CTREPCM);
}
@Override
public boolean reserveCompressor() {
synchronized (m_dataStore.m_reserveLock) {
if (m_dataStore.m_compressorReserved) {
return false;
}
m_dataStore.m_compressorReserved = true;
return true;
}
}
@Override
public void unreserveCompressor() {
synchronized (m_dataStore.m_reserveLock) {
m_dataStore.m_compressorReserved = false;
}
}
/**
* Disables the compressor. The compressor will not turn on until {@link
* #enableCompressorDigital()} is called.
*/
@Override
public void disableCompressor() {
CTREPCMJNI.setClosedLoopControl(m_handle, false);
}
@Override
public void enableCompressorDigital() {
CTREPCMJNI.setClosedLoopControl(m_handle, true);
}
/**
* Enables the compressor in digital mode. Analog mode is unsupported by the CTRE PCM.
*
* @param minPressure Unsupported.
* @param maxPressure Unsupported.
* @see #enableCompressorDigital()
*/
@Override
public void enableCompressorAnalog(double minPressure, double maxPressure) {
CTREPCMJNI.setClosedLoopControl(m_handle, false);
}
/**
* Enables the compressor in digital mode. Hybrid mode is unsupported by the CTRE PCM.
*
* @param minPressure Unsupported.
* @param maxPressure Unsupported.
* @see #enableCompressorDigital()
*/
@Override
public void enableCompressorHybrid(double minPressure, double maxPressure) {
CTREPCMJNI.setClosedLoopControl(m_handle, false);
}
@Override
public CompressorConfigType getCompressorConfigType() {
return CTREPCMJNI.getClosedLoopControl(m_handle)
? CompressorConfigType.Digital
: CompressorConfigType.Disabled;
}
/**
* Unsupported by the CTRE PCM.
*
* @param channel Unsupported.
* @return 0
*/
@Override
public double getAnalogVoltage(int channel) {
return 0;
}
/**
* Unsupported by the CTRE PCM.
*
* @param channel Unsupported.
* @return 0
*/
@Override
public double getPressure(int channel) {
return 0;
}
@Override
public void reportUsage(String device, String data) {
HAL.reportUsage("PCM[" + m_dataStore.m_module + "]/" + device, data);
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
/** Pneumatics module type. */
public enum PneumaticsModuleType {
/** CTRE PCM. */
CTREPCM,
/** REV PH. */
REVPH
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.util.AllocationException;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Solenoid class for running high voltage Digital Output on a pneumatics module.
*
* <p>The Solenoid class is typically used for pneumatic solenoids, but could be used for any device
* within the current spec of the module.
*/
public class Solenoid implements Sendable, AutoCloseable {
private final int m_mask; // The channel mask
private final int m_channel;
private PneumaticsBase m_module;
/**
* Constructs a solenoid for a default module and specified type.
*
* @param busId The bus ID
* @param moduleType The module type to use.
* @param channel The channel the solenoid is on.
*/
public Solenoid(final int busId, final PneumaticsModuleType moduleType, final int channel) {
this(busId, PneumaticsBase.getDefaultForType(moduleType), moduleType, channel);
}
/**
* Constructs a solenoid for a specified module and type.
*
* @param busId The bus ID
* @param module The module ID to use.
* @param moduleType The module type to use.
* @param channel The channel the solenoid is on.
*/
@SuppressWarnings("this-escape")
public Solenoid(
final int busId, final int module, final PneumaticsModuleType moduleType, final int channel) {
m_module = PneumaticsBase.getForType(busId, module, moduleType);
m_mask = 1 << channel;
m_channel = channel;
if (!m_module.checkSolenoidChannel(channel)) {
m_module.close();
throw new IllegalArgumentException("Channel " + channel + " out of range");
}
if (m_module.checkAndReserveSolenoids(m_mask) != 0) {
m_module.close();
throw new AllocationException("Solenoid already allocated");
}
m_module.reportUsage("Solenoid[" + channel + "]", "Solenoid");
SendableRegistry.add(this, "Solenoid", m_module.getModuleNumber(), channel);
}
@Override
public void close() {
SendableRegistry.remove(this);
m_module.unreserveSolenoids(m_mask);
m_module.close();
m_module = null;
}
/**
* Set the value of a solenoid.
*
* @param on True will turn the solenoid output on. False will turn the solenoid output off.
*/
public void set(boolean on) {
int value = on ? (0xFFFF & m_mask) : 0;
m_module.setSolenoids(m_mask, value);
}
/**
* Read the current value of the solenoid.
*
* @return True if the solenoid output is on or false if the solenoid output is off.
*/
public boolean get() {
int currentAll = m_module.getSolenoids();
return (currentAll & m_mask) != 0;
}
/**
* Toggle the value of the solenoid.
*
* <p>If the solenoid is set to on, it'll be turned off. If the solenoid is set to off, it'll be
* turned on.
*/
public void toggle() {
set(!get());
}
/**
* Get the channel this solenoid is connected to.
*
* @return The channel this solenoid is connected to.
*/
public int getChannel() {
return m_channel;
}
/**
* Check if solenoid is DisabledListed. If a solenoid is shorted, it is added to the Disabled List
* and disabled until power cycle, or until faults are cleared.
*
* @return If solenoid is disabled due to short.
*/
public boolean isDisabled() {
return (m_module.getSolenoidDisabledList() & m_mask) != 0;
}
/**
* Set the pulse duration in the pneumatics module. This is used in conjunction with the
* startPulse method to allow the pneumatics module to control the timing of a pulse.
*
* <p>On the PCM, the timing can be controlled in 0.01 second increments, with a maximum of 2.55
* seconds.
*
* <p>On the PH, the timing can be controlled in 0.001 second increments, with a maximum of 65.534
* seconds.
*
* @param duration The duration of the pulse in seconds.
* @see #startPulse()
*/
public void setPulseDuration(double duration) {
long durationMS = (long) (duration * 1000);
m_module.setOneShotDuration(m_channel, (int) durationMS);
}
/**
* Trigger the pneumatics module to generate a pulse of the duration set in setPulseDuration.
*
* @see #setPulseDuration(double)
*/
public void startPulse() {
m_module.fireOneShot(m_channel);
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Solenoid");
builder.setActuator(true);
builder.addBooleanProperty("Value", this::get, this::set);
}
}

View File

@@ -0,0 +1,274 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.PowerDistributionFaults;
import edu.wpi.first.hal.PowerDistributionJNI;
import edu.wpi.first.hal.PowerDistributionStickyFaults;
import edu.wpi.first.hal.PowerDistributionVersion;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class for getting voltage, current, temperature, power and energy from the CTRE Power
* Distribution Panel (PDP) or REV Power Distribution Hub (PDH) over CAN.
*/
public class PowerDistribution implements Sendable, AutoCloseable {
private final int m_handle;
private final int m_module;
/** Default module number. */
public static final int kDefaultModule = PowerDistributionJNI.DEFAULT_MODULE;
/** Power distribution module type. */
public enum ModuleType {
/** CTRE (Cross The Road Electronics) Power Distribution Panel (PDP). */
kCTRE(PowerDistributionJNI.CTRE_TYPE),
/** REV Power Distribution Hub (PDH). */
kRev(PowerDistributionJNI.REV_TYPE);
/** ModuleType value. */
public final int value;
ModuleType(int value) {
this.value = value;
}
}
/**
* Constructs a PowerDistribution object.
*
* @param busId The bus ID
* @param module The CAN ID of the PDP/PDH.
* @param moduleType Module type (CTRE or REV).
*/
@SuppressWarnings("this-escape")
public PowerDistribution(int busId, int module, ModuleType moduleType) {
m_handle = PowerDistributionJNI.initialize(busId, module, moduleType.value);
m_module = PowerDistributionJNI.getModuleNumber(m_handle);
if (moduleType == ModuleType.kCTRE) {
HAL.reportUsage("PDP", m_module, "");
} else {
HAL.reportUsage("PDH", m_module, "");
}
SendableRegistry.add(this, "PowerDistribution", m_module);
}
/**
* Constructs a PowerDistribution object.
*
* <p>Detects the connected PDP/PDH using the default CAN ID (0 for CTRE and 1 for REV).
*
* @param busId The bus ID
*/
@SuppressWarnings("this-escape")
public PowerDistribution(int busId) {
m_handle =
PowerDistributionJNI.initialize(busId, kDefaultModule, PowerDistributionJNI.AUTOMATIC_TYPE);
m_module = PowerDistributionJNI.getModuleNumber(m_handle);
if (PowerDistributionJNI.getType(m_handle) == PowerDistributionJNI.CTRE_TYPE) {
HAL.reportUsage("PowerDistribution", m_module, "CTRE");
} else {
HAL.reportUsage("PowerDistribution", m_module, "Rev");
}
SendableRegistry.add(this, "PowerDistribution", m_module);
}
@Override
public void close() {
SendableRegistry.remove(this);
}
/**
* Gets the number of channels for this power distribution object.
*
* @return Number of output channels (16 for PDP, 24 for PDH).
*/
public int getNumChannels() {
return PowerDistributionJNI.getNumChannels(m_handle);
}
/**
* Query the input voltage of the PDP/PDH.
*
* @return The voltage in volts
*/
public double getVoltage() {
return PowerDistributionJNI.getVoltage(m_handle);
}
/**
* Query the temperature of the PDP.
*
* <p>Not supported on the Rev PDH and returns 0.
*
* @return The temperature in degrees Celsius
*/
public double getTemperature() {
return PowerDistributionJNI.getTemperature(m_handle);
}
/**
* Query the current of a single channel of the PDP/PDH.
*
* @param channel The channel (0-15 for PDP, 0-23 for PDH) to query
* @return The current of the channel in Amperes
*/
public double getCurrent(int channel) {
return PowerDistributionJNI.getChannelCurrent(m_handle, channel);
}
/**
* Query all currents of the PDP.
*
* @return The current of each channel in Amperes
*/
public double[] getAllCurrents() {
double[] currents = new double[getNumChannels()];
PowerDistributionJNI.getAllCurrents(m_handle, currents);
return currents;
}
/**
* Query the current of all monitored channels.
*
* @return The current of all the channels in Amperes
*/
public double getTotalCurrent() {
return PowerDistributionJNI.getTotalCurrent(m_handle);
}
/**
* Query the total power drawn from the monitored channels of the PDP.
*
* <p>Not supported on the Rev PDH and returns 0.
*
* @return the total power in Watts
*/
public double getTotalPower() {
return PowerDistributionJNI.getTotalPower(m_handle);
}
/**
* Query the total energy drawn from the monitored channels of the PDP.
*
* <p>Not supported on the Rev PDH and returns 0.
*
* @return the total energy in Joules
*/
public double getTotalEnergy() {
return PowerDistributionJNI.getTotalEnergy(m_handle);
}
/**
* Reset the total energy to 0 of the PDP.
*
* <p>Not supported on the Rev PDH and does nothing.
*/
public void resetTotalEnergy() {
PowerDistributionJNI.resetTotalEnergy(m_handle);
}
/** Clear all PDP/PDH sticky faults. */
public void clearStickyFaults() {
PowerDistributionJNI.clearStickyFaults(m_handle);
}
/**
* Gets module number (CAN ID).
*
* @return The module number (CAN ID).
*/
public int getModule() {
return m_module;
}
/**
* Gets the module type for this power distribution object.
*
* @return The module type
*/
public ModuleType getType() {
int type = PowerDistributionJNI.getType(m_handle);
if (type == PowerDistributionJNI.REV_TYPE) {
return ModuleType.kRev;
} else {
return ModuleType.kCTRE;
}
}
/**
* Gets whether the PDH switchable channel is turned on or off. Returns false with the CTRE PDP.
*
* @return The output state of the PDH switchable channel
*/
public boolean getSwitchableChannel() {
return PowerDistributionJNI.getSwitchableChannel(m_handle);
}
/**
* Sets the PDH switchable channel on or off. Does nothing with the CTRE PDP.
*
* @param enabled Whether to turn the PDH switchable channel on or off
*/
public void setSwitchableChannel(boolean enabled) {
PowerDistributionJNI.setSwitchableChannel(m_handle, enabled);
}
/**
* Returns the power distribution version number.
*
* @return The power distribution version number.
*/
public PowerDistributionVersion getVersion() {
return PowerDistributionJNI.getVersion(m_handle);
}
/**
* Returns the power distribution faults.
*
* <p>On a CTRE PDP, this will return an object with no faults active.
*
* @return The power distribution faults.
*/
public PowerDistributionFaults getFaults() {
return PowerDistributionJNI.getFaults(m_handle);
}
/**
* Returns the power distribution sticky faults.
*
* <p>On a CTRE PDP, this will return an object with no faults active.
*
* @return The power distribution sticky faults.
*/
public PowerDistributionStickyFaults getStickyFaults() {
return PowerDistributionJNI.getStickyFaults(m_handle);
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("PowerDistribution");
int numChannels = getNumChannels();
for (int i = 0; i < numChannels; ++i) {
final int chan = i;
builder.addDoubleProperty(
"Chan" + i, () -> PowerDistributionJNI.getChannelCurrentNoError(m_handle, chan), null);
}
builder.addDoubleProperty(
"Voltage", () -> PowerDistributionJNI.getVoltageNoError(m_handle), null);
builder.addDoubleProperty(
"TotalCurrent", () -> PowerDistributionJNI.getTotalCurrent(m_handle), null);
builder.addBooleanProperty(
"SwitchableChannel",
() -> PowerDistributionJNI.getSwitchableChannelNoError(m_handle),
value -> PowerDistributionJNI.setSwitchableChannel(m_handle, value));
}
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.hal.SimDevice.Direction;
import edu.wpi.first.hal.SimDouble;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* SharpIR analog distance sensor class. These distance measuring sensors output an analog voltage
* corresponding to the detection distance.
*
* <p>Teams are advised that the case of these sensors are conductive and grounded, so they should
* not be mounted on a metallic surface on an FRC robot.
*/
@SuppressWarnings("MethodName")
public class SharpIR implements Sendable, AutoCloseable {
private AnalogInput m_sensor;
private SimDevice m_simDevice;
private SimDouble m_simRange;
private final double m_A;
private final double m_B;
private final double m_min; // m
private final double m_max; // m
/**
* Sharp GP2Y0A02YK0F is an analog IR sensor capable of measuring distances from 20 cm to 150 cm.
*
* @param channel Analog input channel the sensor is connected to
* @return sensor object
*/
public static SharpIR GP2Y0A02YK0F(int channel) {
return new SharpIR(channel, 62.28, -1.092, 0.2, 1.5);
}
/**
* Sharp GP2Y0A21YK0F is an analog IR sensor capable of measuring distances from 10 cm to 80 cm.
*
* @param channel Analog input channel the sensor is connected to
* @return sensor object
*/
public static SharpIR GP2Y0A21YK0F(int channel) {
return new SharpIR(channel, 26.449, -1.226, 0.1, 0.8);
}
/**
* Sharp GP2Y0A41SK0F is an analog IR sensor capable of measuring distances from 4 cm to 30 cm.
*
* @param channel Analog input channel the sensor is connected to
* @return sensor object
*/
public static SharpIR GP2Y0A41SK0F(int channel) {
return new SharpIR(channel, 12.354, -1.07, 0.04, 0.3);
}
/**
* Sharp GP2Y0A51SK0F is an analog IR sensor capable of measuring distances from 2 cm to 15 cm.
*
* @param channel Analog input channel the sensor is connected to
* @return sensor object
*/
public static SharpIR GP2Y0A51SK0F(int channel) {
return new SharpIR(channel, 5.2819, -1.161, 0.02, 0.15);
}
/**
* Manually construct a SharpIR object. The distance is computed using this formula: A*v ^ B.
* Prefer to use one of the static factories to create this device instead.
*
* @param channel AnalogInput channel
* @param a Constant A
* @param b Constant B
* @param min Minimum distance to report in meters
* @param max Maximum distance to report in meters
*/
@SuppressWarnings("this-escape")
public SharpIR(int channel, double a, double b, double min, double max) {
m_sensor = new AnalogInput(channel);
m_A = a;
m_B = b;
m_min = min;
m_max = max;
HAL.reportUsage("IO", channel, "SharpIR");
SendableRegistry.add(this, "SharpIR", channel);
m_simDevice = SimDevice.create("SharpIR", m_sensor.getChannel());
if (m_simDevice != null) {
m_simRange = m_simDevice.createDouble("Range (m)", Direction.kInput, 0.0);
m_sensor.setSimDevice(m_simDevice);
}
}
@Override
public void close() {
SendableRegistry.remove(this);
m_sensor.close();
m_sensor = null;
if (m_simDevice != null) {
m_simDevice.close();
m_simDevice = null;
m_simRange = null;
}
}
/**
* Get the analog input channel number.
*
* @return analog input channel
*/
public int getChannel() {
return m_sensor.getChannel();
}
/**
* Get the range in meters from the distance sensor.
*
* @return range in meters of the target returned by the sensor
*/
public double getRange() {
if (m_simRange != null) {
return Math.clamp(m_simRange.get(), m_min, m_max);
} else {
// Don't allow zero/negative values
var v = Math.max(m_sensor.getVoltage(), 0.00001);
return Math.clamp(m_A * Math.pow(v, m_B) * 1e-2, m_min, m_max);
}
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Ultrasonic");
builder.addDoubleProperty("Value", this::getRange, null);
}
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.hal.SimDevice.Direction;
import edu.wpi.first.hal.SimDouble;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/** Class for supporting continuous analog encoders, such as the US Digital MA3. */
public class AnalogEncoder implements Sendable, AutoCloseable {
private final AnalogInput m_analogInput;
private boolean m_ownsAnalogInput;
private double m_fullRange;
private double m_expectedZero;
private double m_sensorMin;
private double m_sensorMax = 1.0;
private boolean m_isInverted;
private SimDevice m_simDevice;
private SimDouble m_simPosition;
/**
* Construct a new AnalogEncoder attached to a specific AnalogIn channel.
*
* @param channel the analog input channel to attach to
* @param fullRange the value to report at maximum travel
* @param expectedZero the reading where you would expect a 0 from get()
*/
public AnalogEncoder(int channel, double fullRange, double expectedZero) {
this(new AnalogInput(channel), fullRange, expectedZero);
m_ownsAnalogInput = true;
}
/**
* Construct a new AnalogEncoder attached to a specific AnalogInput.
*
* @param analogInput the analog input to attach to
* @param fullRange the value to report at maximum travel
* @param expectedZero the reading where you would expect a 0 from get()
*/
@SuppressWarnings("this-escape")
public AnalogEncoder(AnalogInput analogInput, double fullRange, double expectedZero) {
m_analogInput = analogInput;
init(fullRange, expectedZero);
}
/**
* Construct a new AnalogEncoder attached to a specific AnalogIn channel.
*
* <p>This has a fullRange of 1 and an expectedZero of 0.
*
* @param channel the analog input channel to attach to
*/
public AnalogEncoder(int channel) {
this(channel, 1.0, 0.0);
}
/**
* Construct a new AnalogEncoder attached to a specific AnalogInput.
*
* <p>This has a fullRange of 1 and an expectedZero of 0.
*
* @param analogInput the analog input to attach to
*/
@SuppressWarnings("this-escape")
public AnalogEncoder(AnalogInput analogInput) {
this(analogInput, 1.0, 0.0);
}
private void init(double fullRange, double expectedZero) {
m_simDevice = SimDevice.create("AnalogEncoder", m_analogInput.getChannel());
if (m_simDevice != null) {
m_simPosition = m_simDevice.createDouble("Position", Direction.kInput, 0.0);
}
m_fullRange = fullRange;
m_expectedZero = expectedZero;
HAL.reportUsage("IO", m_analogInput.getChannel(), "AnalogEncoder");
SendableRegistry.add(this, "Analog Encoder", m_analogInput.getChannel());
}
private double mapSensorRange(double pos) {
// map sensor range
if (pos < m_sensorMin) {
pos = m_sensorMin;
}
if (pos > m_sensorMax) {
pos = m_sensorMax;
}
pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
return pos;
}
/**
* Get the encoder value.
*
* @return the encoder value scaled by the full range input
*/
public double get() {
if (m_simPosition != null) {
return m_simPosition.get();
}
double analog = m_analogInput.getVoltage();
double pos = analog / RobotController.getVoltage3V3();
// Map sensor range if range isn't full
pos = mapSensorRange(pos);
// Compute full range and offset
pos = pos * m_fullRange - m_expectedZero;
// Map from 0 - Full Range
double result = MathUtil.inputModulus(pos, 0, m_fullRange);
// Invert if necessary
if (m_isInverted) {
return m_fullRange - result;
}
return result;
}
/**
* Set the encoder voltage percentage range. Analog sensors are not always fully stable at the end
* of their travel ranges. Shrinking this range down can help mitigate issues with that.
*
* @param min minimum voltage percentage (0-1 range)
* @param max maximum voltage percentage (0-1 range)
*/
public void setVoltagePercentageRange(double min, double max) {
m_sensorMin = Math.clamp(min, 0.0, 1.0);
m_sensorMax = Math.clamp(max, 0.0, 1.0);
}
/**
* Set if this encoder is inverted.
*
* @param inverted true to invert the encoder, false otherwise
*/
public void setInverted(boolean inverted) {
m_isInverted = inverted;
}
/**
* Get the channel number.
*
* @return The channel number.
*/
public int getChannel() {
return m_analogInput.getChannel();
}
@Override
public void close() {
if (m_ownsAnalogInput) {
m_analogInput.close();
}
if (m_simDevice != null) {
m_simDevice.close();
}
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("AbsoluteEncoder");
builder.addDoubleProperty("Position", this::get, null);
}
}

View File

@@ -0,0 +1,148 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class for reading analog potentiometers. Analog potentiometers read in an analog voltage that
* corresponds to a position. The position is in whichever units you choose, by way of the scaling
* and offset constants passed to the constructor.
*/
public class AnalogPotentiometer implements Sendable, AutoCloseable {
private AnalogInput m_analogInput;
private boolean m_initAnalogInput;
private double m_fullRange;
private double m_offset;
/**
* AnalogPotentiometer constructor.
*
* <p>Use the fullRange and offset values so that the output produces meaningful values. I.E: you
* have a 270 degree potentiometer, and you want the output to be degrees with the halfway point
* as 0 degrees. The fullRange value is 270.0(degrees) and the offset is -135.0 since the halfway
* point after scaling is 135 degrees. This will calculate the result from the fullRange times the
* fraction of the supply voltage, plus the offset.
*
* @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
* and 4-7 are on the MXP port.
* @param fullRange The scaling to multiply the fraction by to get a meaningful unit.
* @param offset The offset to add to the scaled value for controlling the zero value
*/
@SuppressWarnings("this-escape")
public AnalogPotentiometer(final int channel, double fullRange, double offset) {
this(new AnalogInput(channel), fullRange, offset);
m_initAnalogInput = true;
SendableRegistry.addChild(this, m_analogInput);
}
/**
* AnalogPotentiometer constructor.
*
* <p>Use the fullRange and offset values so that the output produces meaningful values. I.E: you
* have a 270 degree potentiometer, and you want the output to be degrees with the halfway point
* as 0 degrees. The fullRange value is 270.0(degrees) and the offset is -135.0 since the halfway
* point after scaling is 135 degrees. This will calculate the result from the fullRange times the
* fraction of the supply voltage, plus the offset.
*
* @param input The {@link AnalogInput} this potentiometer is plugged into.
* @param fullRange The angular value (in desired units) representing the full 0-3.3V range of the
* input.
* @param offset The angular value (in desired units) representing the angular output at 0V.
*/
@SuppressWarnings("this-escape")
public AnalogPotentiometer(final AnalogInput input, double fullRange, double offset) {
SendableRegistry.add(this, "AnalogPotentiometer", input.getChannel());
m_analogInput = input;
m_initAnalogInput = false;
m_fullRange = fullRange;
m_offset = offset;
}
/**
* AnalogPotentiometer constructor.
*
* <p>Use the scale value so that the output produces meaningful values. I.E: you have a 270
* degree potentiometer, and you want the output to be degrees with the starting point as 0
* degrees. The scale value is 270.0(degrees).
*
* @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
* and 4-7 are on the MXP port.
* @param scale The scaling to multiply the voltage by to get a meaningful unit.
*/
public AnalogPotentiometer(final int channel, double scale) {
this(channel, scale, 0);
}
/**
* AnalogPotentiometer constructor.
*
* <p>Use the fullRange and offset values so that the output produces meaningful values. I.E: you
* have a 270 degree potentiometer, and you want the output to be degrees with the starting point
* as 0 degrees. The scale value is 270.0(degrees).
*
* @param input The {@link AnalogInput} this potentiometer is plugged into.
* @param scale The scaling to multiply the voltage by to get a meaningful unit.
*/
public AnalogPotentiometer(final AnalogInput input, double scale) {
this(input, scale, 0);
}
/**
* AnalogPotentiometer constructor.
*
* <p>The potentiometer will return a value between 0 and 1.0.
*
* @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
* and 4-7 are on the MXP port.
*/
public AnalogPotentiometer(final int channel) {
this(channel, 1, 0);
}
/**
* AnalogPotentiometer constructor.
*
* <p>The potentiometer will return a value between 0 and 1.0.
*
* @param input The {@link AnalogInput} this potentiometer is plugged into.
*/
public AnalogPotentiometer(final AnalogInput input) {
this(input, 1, 0);
}
/**
* Get the current reading of the potentiometer.
*
* @return The current position of the potentiometer.
*/
public double get() {
if (m_analogInput == null) {
return m_offset;
}
return (m_analogInput.getVoltage() / RobotController.getVoltage3V3()) * m_fullRange + m_offset;
}
@Override
public void initSendable(SendableBuilder builder) {
if (m_analogInput != null) {
builder.setSmartDashboardType("Analog Input");
builder.addDoubleProperty("Value", this::get, null);
}
}
@Override
public void close() {
SendableRegistry.remove(this);
if (m_initAnalogInput) {
m_analogInput.close();
m_analogInput = null;
m_initAnalogInput = false;
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.DutyCycleJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class to read a duty cycle PWM input.
*
* <p>PWM input signals are specified with a frequency and a ratio of high to low in that frequency.
* There are 8 of these in the roboRIO, and they can be attached to any SmartIO Channel.
*
* <p>These can be combined as the input of an AnalogTrigger to a Counter in order to implement
* rollover checking.
*/
public class DutyCycle implements Sendable, AutoCloseable {
// Explicitly package private
final int m_handle;
private final int m_channel;
/**
* Constructs a DutyCycle input from a smartio channel.
*
* @param channel The channel to use.
*/
@SuppressWarnings("this-escape")
public DutyCycle(int channel) {
m_handle = DutyCycleJNI.initialize(channel);
m_channel = channel;
HAL.reportUsage("IO", channel, "DutyCycle");
SendableRegistry.add(this, "Duty Cycle", channel);
}
/** Close the DutyCycle and free all resources. */
@Override
public void close() {
SendableRegistry.remove(this);
DutyCycleJNI.free(m_handle);
}
/**
* Get the frequency of the duty cycle signal.
*
* @return frequency in Hertz
*/
public double getFrequency() {
return DutyCycleJNI.getFrequency(m_handle);
}
/**
* Get the output ratio of the duty cycle signal.
*
* <p>0 means always low, 1 means always high.
*
* @return output ratio between 0 and 1
*/
public double getOutput() {
return DutyCycleJNI.getOutput(m_handle);
}
/**
* Get the raw high time of the duty cycle signal.
*
* @return high time of last pulse in nanoseconds
*/
public int getHighTimeNanoseconds() {
return DutyCycleJNI.getHighTime(m_handle);
}
/**
* Get the channel of the source.
*
* @return the source channel
*/
public int getSourceChannel() {
return m_channel;
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("Duty Cycle");
builder.addDoubleProperty("Frequency", this::getFrequency, null);
builder.addDoubleProperty("Output", this::getOutput, null);
}
}

View File

@@ -0,0 +1,253 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import edu.wpi.first.hal.SimBoolean;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.hal.SimDouble;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class for supporting duty cycle/PWM encoders, such as the US Digital MA3 with PWM Output, the
* CTRE Mag Encoder, the Rev Hex Encoder, and the AM Mag Encoder.
*/
public class DutyCycleEncoder implements Sendable, AutoCloseable {
private final DutyCycle m_dutyCycle;
private boolean m_ownsDutyCycle;
private double m_frequencyThreshold = 100;
private double m_fullRange;
private double m_expectedZero;
private double m_periodNanos;
private double m_sensorMin;
private double m_sensorMax = 1.0;
private boolean m_isInverted;
private SimDevice m_simDevice;
private SimDouble m_simPosition;
private SimBoolean m_simIsConnected;
/**
* Construct a new DutyCycleEncoder on a specific channel.
*
* @param channel the channel to attach to
* @param fullRange the value to report at maximum travel
* @param expectedZero the reading where you would expect a 0 from get()
*/
@SuppressWarnings("this-escape")
public DutyCycleEncoder(int channel, double fullRange, double expectedZero) {
m_ownsDutyCycle = true;
m_dutyCycle = new DutyCycle(channel);
init(fullRange, expectedZero);
}
/**
* Construct a new DutyCycleEncoder attached to an existing DutyCycle object.
*
* @param dutyCycle the duty cycle to attach to
* @param fullRange the value to report at maximum travel
* @param expectedZero the reading where you would expect a 0 from get()
*/
@SuppressWarnings("this-escape")
public DutyCycleEncoder(DutyCycle dutyCycle, double fullRange, double expectedZero) {
m_dutyCycle = dutyCycle;
init(fullRange, expectedZero);
}
/**
* Construct a new DutyCycleEncoder on a specific channel.
*
* <p>This has a fullRange of 1 and an expectedZero of 0.
*
* @param channel the channel to attach to
*/
@SuppressWarnings("this-escape")
public DutyCycleEncoder(int channel) {
this(channel, 1.0, 0.0);
}
/**
* Construct a new DutyCycleEncoder attached to an existing DutyCycle object.
*
* <p>This has a fullRange of 1 and an expectedZero of 0.
*
* @param dutyCycle the duty cycle to attach to
*/
@SuppressWarnings("this-escape")
public DutyCycleEncoder(DutyCycle dutyCycle) {
this(dutyCycle, 1.0, 0.0);
}
private void init(double fullRange, double expectedZero) {
m_simDevice = SimDevice.create("DutyCycle:DutyCycleEncoder", m_dutyCycle.getSourceChannel());
if (m_simDevice != null) {
m_simPosition = m_simDevice.createDouble("Position", SimDevice.Direction.kInput, 0.0);
m_simIsConnected = m_simDevice.createBoolean("Connected", SimDevice.Direction.kInput, true);
}
m_fullRange = fullRange;
m_expectedZero = expectedZero;
SendableRegistry.add(this, "DutyCycle Encoder", m_dutyCycle.getSourceChannel());
}
private double mapSensorRange(double pos) {
// map sensor range
if (pos < m_sensorMin) {
pos = m_sensorMin;
}
if (pos > m_sensorMax) {
pos = m_sensorMax;
}
pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
return pos;
}
/**
* Get the encoder value since the last reset.
*
* <p>This is reported in rotations since the last reset.
*
* @return the encoder value in rotations
*/
public double get() {
if (m_simPosition != null) {
return m_simPosition.get();
}
double pos;
// Compute output percentage (0-1)
if (m_periodNanos == 0.0) {
pos = m_dutyCycle.getOutput();
} else {
int highTime = m_dutyCycle.getHighTimeNanoseconds();
pos = highTime / m_periodNanos;
}
// Map sensor range if range isn't full
pos = mapSensorRange(pos);
// Compute full range and offset
pos = pos * m_fullRange - m_expectedZero;
// Map from 0 - Full Range
double result = MathUtil.inputModulus(pos, 0, m_fullRange);
// Invert if necessary
if (m_isInverted) {
return m_fullRange - result;
}
return result;
}
/**
* Set the encoder duty cycle range. As the encoder needs to maintain a duty cycle, the duty cycle
* cannot go all the way to 0% or all the way to 100%. For example, an encoder with a 4096 us
* period might have a minimum duty cycle of 1 us / 4096 us and a maximum duty cycle of 4095 /
* 4096 us. Setting the range will result in an encoder duty cycle less than or equal to the
* minimum being output as 0 rotation, the duty cycle greater than or equal to the maximum being
* output as 1 rotation, and values in between linearly scaled from 0 to 1.
*
* @param min minimum duty cycle (0-1 range)
* @param max maximum duty cycle (0-1 range)
*/
public void setDutyCycleRange(double min, double max) {
m_sensorMin = Math.clamp(min, 0.0, 1.0);
m_sensorMax = Math.clamp(max, 0.0, 1.0);
}
/**
* Get the frequency in Hz of the duty cycle signal from the encoder.
*
* @return duty cycle frequency in Hz
*/
public double getFrequency() {
return m_dutyCycle.getFrequency();
}
/**
* Get if the sensor is connected
*
* <p>This uses the duty cycle frequency to determine if the sensor is connected. By default, a
* value of 100 Hz is used as the threshold, and this value can be changed with {@link
* #setConnectedFrequencyThreshold(int)}.
*
* @return true if the sensor is connected
*/
public boolean isConnected() {
if (m_simIsConnected != null) {
return m_simIsConnected.get();
}
return getFrequency() > m_frequencyThreshold;
}
/**
* Change the frequency threshold for detecting connection used by {@link #isConnected()}.
*
* @param frequency the minimum frequency in Hz.
*/
public void setConnectedFrequencyThreshold(double frequency) {
if (frequency < 0) {
frequency = 0;
}
m_frequencyThreshold = frequency;
}
/**
* Sets the assumed frequency of the connected device.
*
* <p>By default, the DutyCycle engine has to compute the frequency of the input signal. This can
* result in both delayed readings and jumpy readings. To solve this, you can pass the expected
* frequency of the sensor to this function. This will use that frequency to compute the DutyCycle
* percentage, rather than the computed frequency.
*
* @param frequency the assumed frequency of the sensor
*/
public void setAssumedFrequency(double frequency) {
if (frequency == 0.0) {
m_periodNanos = 0.0;
} else {
m_periodNanos = 1000000000 / frequency;
}
}
/**
* Set if this encoder is inverted.
*
* @param inverted true to invert the encoder, false otherwise
*/
public void setInverted(boolean inverted) {
m_isInverted = inverted;
}
@Override
public void close() {
if (m_ownsDutyCycle) {
m_dutyCycle.close();
}
if (m_simDevice != null) {
m_simDevice.close();
}
}
/**
* Get the channel of the source.
*
* @return the source channel
*/
public int getSourceChannel() {
return m_dutyCycle.getSourceChannel();
}
@Override
public void initSendable(SendableBuilder builder) {
builder.setSmartDashboardType("AbsoluteEncoder");
builder.addDoubleProperty("Position", this::get, null);
builder.addBooleanProperty("Is Connected", this::isConnected, null);
}
}

View File

@@ -0,0 +1,357 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.hal.EncoderJNI;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.hal.SimDevice;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.util.sendable.SendableRegistry;
/**
* Class to read quadrature encoders.
*
* <p>Quadrature encoders are devices that count shaft rotation and can sense direction. The output
* of the Encoder class is an integer that can count either up or down, and can go negative for
* reverse direction counting. When creating Encoders, a direction can be supplied that inverts the
* sense of the output to make code more readable if the encoder is mounted such that forward
* movement generates negative values. Quadrature encoders have two digital outputs, an A Channel
* and a B Channel, that are out of phase with each other for direction sensing.
*
* <p>All encoders will immediately start counting - reset() them if you need them to be zeroed
* before use.
*/
public class Encoder implements CounterBase, Sendable, AutoCloseable {
private final EncodingType m_encodingType;
int m_encoder; // the HAL encoder object
/**
* Common initialization code for Encoders. This code allocates resources for Encoders and is
* common to all constructors.
*
* <p>The encoder will start counting immediately.
*
* @param aChannel The a channel.
* @param bChannel The b channel.
* @param reverseDirection If true, counts down instead of up (this is all relative)
*/
private void initEncoder(
int aChannel, int bChannel, boolean reverseDirection, final EncodingType type) {
m_encoder = EncoderJNI.initializeEncoder(aChannel, bChannel, reverseDirection, type.value);
String typeStr =
switch (type) {
case k1X -> "Encoder:1x";
case k2X -> "Encoder:2x";
case k4X -> "Encoder:4x";
default -> "Encoder";
};
HAL.reportUsage("IO[" + aChannel + "," + bChannel + "]", typeStr);
int fpgaIndex = getFPGAIndex();
SendableRegistry.add(this, "Encoder", fpgaIndex);
}
/**
* Encoder constructor. Construct a Encoder given a and b channels.
*
* <p>The encoder will start counting immediately.
*
* @param channelA The 'a' channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
* @param channelB The 'b' channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
* @param reverseDirection represents the orientation of the encoder and inverts the output values
* if necessary so forward represents positive values.
*/
public Encoder(final int channelA, final int channelB, boolean reverseDirection) {
this(channelA, channelB, reverseDirection, EncodingType.k4X);
}
/**
* Encoder constructor. Construct an Encoder given a and b channels.
*
* <p>The encoder will start counting immediately.
*
* @param channelA The a channel digital input channel.
* @param channelB The b channel digital input channel.
*/
public Encoder(final int channelA, final int channelB) {
this(channelA, channelB, false);
}
/**
* Encoder constructor. Construct an Encoder given a and b channels.
*
* <p>The encoder will start counting immediately.
*
* @param channelA The a channel digital input channel.
* @param channelB The b channel digital input channel.
* @param reverseDirection represents the orientation of the encoder and inverts the output values
* if necessary so forward represents positive values.
* @param encodingType either k1X, k2X, or k4X to indicate 1X, 2X or 4X decoding. If 4X is
* selected, then an encoder FPGA object is used and the returned counts will be 4x the
* encoder spec'd value since all rising and falling edges are counted. If 1X or 2X are
* selected, then a counter object will be used and the returned value will either exactly
* match the spec'd count or be double (2x) the spec'd count.
*/
@SuppressWarnings("this-escape")
public Encoder(
final int channelA,
final int channelB,
boolean reverseDirection,
final EncodingType encodingType) {
requireNonNullParam(encodingType, "encodingType", "Encoder");
m_encodingType = encodingType;
// SendableRegistry.addChild(this, m_aSource);
// SendableRegistry.addChild(this, m_bSource);
initEncoder(channelA, channelB, reverseDirection, encodingType);
}
/**
* Get the FPGA index of the encoder.
*
* @return The Encoder's FPGA index.
*/
public int getFPGAIndex() {
return EncoderJNI.getEncoderFPGAIndex(m_encoder);
}
/**
* Used to divide raw edge counts down to spec'd counts.
*
* @return The encoding scale factor 1x, 2x, or 4x, per the requested encoding type.
*/
public int getEncodingScale() {
return EncoderJNI.getEncoderEncodingScale(m_encoder);
}
@Override
public void close() {
SendableRegistry.remove(this);
// if (m_aSource != null && m_allocatedA) {
// m_aSource.close();
// m_allocatedA = false;
// }
// if (m_bSource != null && m_allocatedB) {
// m_bSource.close();
// m_allocatedB = false;
// }
// if (m_indexSource != null && m_allocatedI) {
// m_indexSource.close();
// m_allocatedI = false;
// }
// m_aSource = null;
// m_bSource = null;
// m_indexSource = null;
EncoderJNI.freeEncoder(m_encoder);
m_encoder = 0;
}
/**
* Gets the raw value from the encoder. The raw value is the actual count unscaled by the 1x, 2x,
* or 4x scale factor.
*
* @return Current raw count from the encoder
*/
public int getRaw() {
return EncoderJNI.getEncoderRaw(m_encoder);
}
/**
* Gets the current count. Returns the current count on the Encoder. This method compensates for
* the decoding type.
*
* @return Current count from the Encoder adjusted for the 1x, 2x, or 4x scale factor.
*/
@Override
public int get() {
return EncoderJNI.getEncoder(m_encoder);
}
/** Reset the Encoder distance to zero. Resets the current count to zero on the encoder. */
@Override
public void reset() {
EncoderJNI.resetEncoder(m_encoder);
}
/**
* Returns the period of the most recent pulse. Returns the period of the most recent Encoder
* pulse in seconds. This method compensates for the decoding type.
*
* <p><b>Warning:</b> This returns unscaled periods. Use getRate() for rates that are scaled using
* the value from setDistancePerPulse().
*
* @return Period in seconds of the most recent pulse.
* @deprecated Use getRate() in favor of this method.
*/
@Override
@Deprecated
public double getPeriod() {
return EncoderJNI.getEncoderPeriod(m_encoder);
}
/**
* Sets the maximum period for stopped detection. Sets the value that represents the maximum
* period of the Encoder before it will assume that the attached device is stopped. This timeout
* allows users to determine if the wheels or other shaft has stopped rotating. This method
* compensates for the decoding type.
*
* @param maxPeriod The maximum time between rising and falling edges before the FPGA will report
* the device stopped. This is expressed in seconds.
* @deprecated Use setMinRate() in favor of this method. This takes unscaled periods and
* setMinRate() scales using value from setDistancePerPulse().
*/
@Override
@Deprecated
public void setMaxPeriod(double maxPeriod) {
EncoderJNI.setEncoderMaxPeriod(m_encoder, maxPeriod);
}
/**
* Determine if the encoder is stopped. Using the MaxPeriod value, a boolean is returned that is
* true if the encoder is considered stopped and false if it is still moving. A stopped encoder is
* one where the most recent pulse width exceeds the MaxPeriod.
*
* @return True if the encoder is considered stopped.
*/
@Override
public boolean getStopped() {
return EncoderJNI.getEncoderStopped(m_encoder);
}
/**
* The last direction the encoder value changed.
*
* @return The last direction the encoder value changed.
*/
@Override
public boolean getDirection() {
return EncoderJNI.getEncoderDirection(m_encoder);
}
/**
* Get the distance the robot has driven since the last reset as scaled by the value from {@link
* #setDistancePerPulse(double)}.
*
* @return The distance driven since the last reset
*/
public double getDistance() {
return EncoderJNI.getEncoderDistance(m_encoder);
}
/**
* Get the current rate of the encoder. Units are distance per second as scaled by the value from
* setDistancePerPulse().
*
* @return The current rate of the encoder.
*/
public double getRate() {
return EncoderJNI.getEncoderRate(m_encoder);
}
/**
* Set the minimum rate of the device before the hardware reports it stopped.
*
* @param minRate The minimum rate. The units are in distance per second as scaled by the value
* from setDistancePerPulse().
*/
public void setMinRate(double minRate) {
EncoderJNI.setEncoderMinRate(m_encoder, minRate);
}
/**
* Set the distance per pulse for this encoder. This sets the multiplier used to determine the
* distance driven based on the count value from the encoder. Do not include the decoding type in
* this scale. The library already compensates for the decoding type. Set this value based on the
* encoder's rated Pulses per Revolution and factor in gearing reductions following the encoder
* shaft. This distance can be in any units you like, linear or angular.
*
* @param distancePerPulse The scale factor that will be used to convert pulses to useful units.
*/
public void setDistancePerPulse(double distancePerPulse) {
EncoderJNI.setEncoderDistancePerPulse(m_encoder, distancePerPulse);
}
/**
* Get the distance per pulse for this encoder.
*
* @return The scale factor that will be used to convert pulses to useful units.
*/
public double getDistancePerPulse() {
return EncoderJNI.getEncoderDistancePerPulse(m_encoder);
}
/**
* Set the direction sensing for this encoder. This sets the direction sensing on the encoder so
* that it could count in the correct software direction regardless of the mounting.
*
* @param reverseDirection true if the encoder direction should be reversed
*/
public void setReverseDirection(boolean reverseDirection) {
EncoderJNI.setEncoderReverseDirection(m_encoder, reverseDirection);
}
/**
* Set the Samples to Average which specifies the number of samples of the timer to average when
* calculating the period. Perform averaging to account for mechanical imperfections or as
* oversampling to increase resolution.
*
* @param samplesToAverage The number of samples to average from 1 to 127.
*/
public void setSamplesToAverage(int samplesToAverage) {
EncoderJNI.setEncoderSamplesToAverage(m_encoder, samplesToAverage);
}
/**
* Get the Samples to Average which specifies the number of samples of the timer to average when
* calculating the period. Perform averaging to account for mechanical imperfections or as
* oversampling to increase resolution.
*
* @return SamplesToAverage The number of samples being averaged (from 1 to 127)
*/
public int getSamplesToAverage() {
return EncoderJNI.getEncoderSamplesToAverage(m_encoder);
}
/**
* Indicates this input is used by a simulated device.
*
* @param device simulated device handle
*/
public void setSimDevice(SimDevice device) {
EncoderJNI.setEncoderSimDevice(m_encoder, device.getNativeHandle());
}
/**
* Gets the decoding scale factor for scaling raw values to full counts.
*
* @return decoding scale factor
*/
public double getDecodingScaleFactor() {
return switch (m_encodingType) {
case k1X -> 1.0;
case k2X -> 0.5;
case k4X -> 0.25;
};
}
@Override
public void initSendable(SendableBuilder builder) {
if (EncoderJNI.getEncoderEncodingType(m_encoder) == EncodingType.k4X.value) {
builder.setSmartDashboardType("Quadrature Encoder");
} else {
builder.setSmartDashboardType("Encoder");
}
builder.addDoubleProperty("Speed", this::getRate, null);
builder.addDoubleProperty("Distance", this::getDistance, null);
builder.addDoubleProperty("Distance per Tick", this::getDistancePerPulse, null);
}
}