diff --git a/drivers/src/main/java/org/wpilib/drivers/range/RevColorSensorV2.java b/drivers/src/main/java/org/wpilib/drivers/range/RevColorSensorV2.java
new file mode 100644
index 00000000000..55a46d2e471
--- /dev/null
+++ b/drivers/src/main/java/org/wpilib/drivers/range/RevColorSensorV2.java
@@ -0,0 +1,883 @@
+// 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.
+
+//
+// This file is based on the FTC SDK drivers for the AMS TMD3782 family of color
+// sensors, which were made available under the BSD 3-Clause License.
+// Copyright (c) 2016-2017 Robert Atkinson and Steve Geffner.
+
+package org.wpilib.drivers.range;
+
+import static org.wpilib.units.Units.Meters;
+import static org.wpilib.units.Units.Milliseconds;
+
+import org.wpilib.hardware.bus.I2C;
+import org.wpilib.units.measure.Distance;
+import org.wpilib.units.measure.Time;
+import org.wpilib.util.Color;
+import org.wpilib.util.ErrorMessages;
+import org.wpilib.util.UsageReporting;
+
+/**
+ * Driver for the REV Robotics Color Sensor V2.
+ *
+ *
The sensor is an AMS TMD37821, which combines an RGBC color sensor with an infrared proximity
+ * sensor. The proximity channel is exposed both as a calibrated distance and as a raw reflected
+ * light level.
+ *
+ *
Call {@link #update()} once per robot loop. All accessors return the values cached by the most
+ * recent update; they do not access the device.
+ *
+ *
Aborted I2C transactions, an unexpected device ID, and data the sensor reports as not yet
+ * valid are reported through {@link DeviceStatus} instead of throwing an exception, and the
+ * previously cached measurements are preserved. Unless otherwise noted, methods that access the
+ * device throw {@link IllegalStateException} after this driver is closed.
+ */
+public class RevColorSensorV2 implements AutoCloseable {
+ /** Default 7-bit I2C address of the Color Sensor V2. */
+ public static final int DEFAULT_ADDRESS = 0x39;
+
+ /** Every register access is prefixed with the command bit. */
+ private static final int COMMAND_BIT = 0x80;
+
+ /**
+ * Command type selecting the auto-increment protocol. Without it the sensor uses the
+ * repeated-byte protocol, which returns the addressed register once per byte instead of advancing
+ * through consecutive registers, so a multi-byte read would return the same register repeatedly.
+ */
+ private static final int COMMAND_TYPE_AUTO_INCREMENT = 0x01 << 5;
+
+ /** Register address prefix used for every read and write. */
+ private static final int COMMAND_AUTO_INCREMENT = COMMAND_BIT | COMMAND_TYPE_AUTO_INCREMENT;
+
+ private static final byte TMD37821_DEVICE_ID = 0x60;
+ private static final byte TMD37823_DEVICE_ID = 0x69;
+
+ private static final int ENABLE_POWER_ON = 0x01;
+ private static final int ENABLE_COLOR = 0x02;
+ private static final int ENABLE_PROXIMITY = 0x04;
+ private static final int ENABLE_WAIT = 0x08;
+ private static final int ENABLE_WRITABLE_MASK =
+ ENABLE_POWER_ON | ENABLE_COLOR | ENABLE_PROXIMITY | ENABLE_WAIT;
+
+ private static final int STATUS_COLOR_VALID = 0x01;
+ private static final int STATUS_PROXIMITY_VALID = 0x02;
+
+ /**
+ * CONTROL proximity diode select field (bits 5:4), set to the value that measures proximity with
+ * the infrared diode. The other values of this field are reserved.
+ */
+ private static final int CONTROL_PDIODE_IR = 0x02 << 4;
+
+ /** STATUS through the high byte of the proximity data, read as one block. */
+ private static final int BULK_READ_LENGTH = 11;
+
+ private static final int CLEAR_OFFSET = 1;
+ private static final int RED_OFFSET = 3;
+ private static final int GREEN_OFFSET = 5;
+ private static final int BLUE_OFFSET = 7;
+ private static final int PROXIMITY_OFFSET = 9;
+
+ private static final double INTEGRATION_CYCLE_MILLISECONDS = 2.4;
+ private static final int MAX_INTEGRATION_CYCLES = 256;
+ private static final int COUNTS_PER_INTEGRATION_CYCLE = 1024;
+ private static final int MAX_RAW_COLOR_VALUE = 65535;
+ private static final int PROXIMITY_SATURATION = 1023;
+
+ /** The datasheet specifies a 2.4 ms warm-up delay after power on. */
+ private static final int POWER_ON_DELAY_MILLISECONDS = 3;
+
+ private static final int ENABLE_SETTLE_DELAY_MILLISECONDS = 5;
+
+ private static final double DEFAULT_INTEGRATION_TIME_MILLISECONDS = 24.0;
+ private static final int DEFAULT_PROXIMITY_PULSE_COUNT = 8;
+
+ private static final double DEFAULT_A_PARAM = 186.347;
+ private static final double DEFAULT_B_PARAM = 30403.5;
+ private static final double DEFAULT_C_PARAM = 0.576649;
+
+ /** Register map for the TMD37821. */
+ public enum Register {
+ /** Enables states and interrupts. */
+ ENABLE(0x00),
+ /** RGBC integration time. */
+ ATIME(0x01),
+ /** Wait time. */
+ WTIME(0x03),
+ /** Clear channel interrupt low threshold. */
+ AILT(0x04),
+ /** Clear channel interrupt high threshold. */
+ AIHT(0x06),
+ /** Interrupt persistence filter. */
+ PERS(0x0C),
+ /** Configuration. */
+ CONFIGURATION(0x0D),
+ /** Proximity LED pulse count. */
+ PPULSE(0x0E),
+ /** Gain and proximity LED drive control. */
+ CONTROL(0x0F),
+ /** Device identifier. */
+ DEVICE_ID(0x12),
+ /** Device status. */
+ STATUS(0x13),
+ /** Clear channel data. */
+ CLEAR(0x14),
+ /** Red channel data. */
+ RED(0x16),
+ /** Green channel data. */
+ GREEN(0x18),
+ /** Blue channel data. */
+ BLUE(0x1A),
+ /** Proximity data. */
+ PROXIMITY(0x1C);
+
+ private final int m_address;
+
+ Register(int address) {
+ m_address = address;
+ }
+
+ /**
+ * Returns the address of this register.
+ *
+ * @return register address
+ */
+ public int getAddress() {
+ return m_address;
+ }
+ }
+
+ /** Analog gain applied to the color channels by the sensor. */
+ public enum Gain {
+ /** 1x gain. */
+ GAIN_1(0x00),
+ /** 4x gain. */
+ GAIN_4(0x01),
+ /** 16x gain. */
+ GAIN_16(0x02),
+ /** 60x gain. */
+ GAIN_60(0x03);
+
+ private final int m_value;
+
+ Gain(int value) {
+ m_value = value;
+ }
+ }
+
+ /** Nominal drive current of the proximity LED. */
+ public enum LedDrive {
+ /** 100% drive current. */
+ PERCENT_100(0x00 << 6),
+ /** 50% drive current. */
+ PERCENT_50(0x01 << 6),
+ /** 25% drive current. */
+ PERCENT_25(0x02 << 6),
+ /** 12.5% drive current. */
+ PERCENT_12_5(0x03 << 6);
+
+ private final int m_value;
+
+ LedDrive(int value) {
+ m_value = value;
+ }
+ }
+
+ /** Sensor status, including device-reported faults and local read failures. */
+ public enum DeviceStatus {
+ /** The sensor has not been configured successfully yet. */
+ NOT_INITIALIZED,
+ /** The sensor is operating normally. */
+ READY,
+ /** The device ID register did not report a TMD3782 family part. */
+ FAULT_UNEXPECTED_DEVICE_ID,
+ /** An I2C transaction was aborted, or the sensor reported that its data was not valid. */
+ FAULT_BAD_READ
+ }
+
+ /** Reason the driver rejected data or an I2C transaction. */
+ public enum FailureReason {
+ /** The I2C controller aborted a read transaction. */
+ I2C_READ_ABORTED,
+ /** The I2C controller aborted a write transaction. */
+ I2C_WRITE_ABORTED,
+ /** The device ID register did not report a TMD3782 family part. */
+ UNEXPECTED_DEVICE_ID,
+ /** The sensor had not completed an RGBC cycle since the color channels were enabled. */
+ COLOR_DATA_NOT_VALID,
+ /** The sensor had not completed a proximity cycle since the proximity channel was enabled. */
+ PROXIMITY_DATA_NOT_VALID
+ }
+
+ private I2C m_i2c;
+
+ private Gain m_gain = Gain.GAIN_4;
+ private LedDrive m_ledDrive = LedDrive.PERCENT_50;
+ private int m_integrationTimeRegister =
+ integrationTimeRegister(DEFAULT_INTEGRATION_TIME_MILLISECONDS);
+ private int m_proximityPulseCount = DEFAULT_PROXIMITY_PULSE_COUNT;
+ private double m_softwareGain = 1.0;
+
+ private double m_aParam = DEFAULT_A_PARAM;
+ private double m_bParam = DEFAULT_B_PARAM;
+ private double m_cParam = DEFAULT_C_PARAM;
+
+ private boolean m_initialized;
+ private byte m_deviceId;
+ private DeviceStatus m_deviceStatus = DeviceStatus.NOT_INITIALIZED;
+ private FailureReason m_lastFailureReason;
+ private long m_failureCount;
+
+ private int m_rawClear;
+ private int m_rawRed;
+ private int m_rawGreen;
+ private int m_rawBlue;
+ private int m_rawProximity;
+
+ /**
+ * Constructs a Color Sensor V2 on its default I2C address and configures it.
+ *
+ * @param port I2C port to which the sensor is connected
+ * @throws NullPointerException if {@code port} is null
+ */
+ public RevColorSensorV2(I2C.Port port) {
+ this(port, DEFAULT_ADDRESS);
+ }
+
+ /**
+ * Constructs a Color Sensor V2 and configures it.
+ *
+ *
Configuration failures are reported through {@link #getDeviceStatus()}; {@link #update()}
+ * retries configuration until it succeeds.
+ *
+ * @param port I2C port to which the sensor is connected
+ * @param deviceAddress 7-bit I2C address
+ * @throws NullPointerException if {@code port} is null
+ * @throws IllegalArgumentException if {@code deviceAddress} is outside the 7-bit address range
+ */
+ public RevColorSensorV2(I2C.Port port, int deviceAddress) {
+ ErrorMessages.requireNonNullParam(port, "port", "RevColorSensorV2");
+ if (deviceAddress < 0 || deviceAddress > 0x7f) {
+ throw new IllegalArgumentException("deviceAddress must be a 7-bit I2C address");
+ }
+
+ m_i2c = new I2C(port, deviceAddress);
+ UsageReporting.reportUsage("I2C[" + port.value + "]", deviceAddress, "RevColorSensorV2");
+ initialize();
+ }
+
+ RevColorSensorV2(I2C i2c) {
+ m_i2c = ErrorMessages.requireNonNullParam(i2c, "i2c", "RevColorSensorV2");
+ initialize();
+ }
+
+ /**
+ * Returns the I2C port.
+ *
+ * @return I2C port
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public I2C.Port getPort() {
+ return requireOpen().getPort();
+ }
+
+ /**
+ * Returns the I2C address.
+ *
+ * @return 7-bit I2C address
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getDeviceAddress() {
+ return requireOpen().getDeviceAddress();
+ }
+
+ /** Closes the I2C connection. Calling this method more than once has no effect. */
+ @Override
+ public void close() {
+ if (m_i2c != null) {
+ m_i2c.close();
+ m_i2c = null;
+ }
+ }
+
+ /**
+ * Reads the color and proximity channels and updates the cached measurements. Call this once per
+ * robot loop.
+ *
+ *
If the sensor has not been configured successfully, this retries configuration first, which
+ * blocks for approximately 8 ms.
+ *
+ *
An aborted I2C transaction or data the sensor reports as not yet valid does not throw; it
+ * sets the device status to {@link DeviceStatus#FAULT_BAD_READ} and preserves the affected
+ * measurements.
+ *
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public void update() {
+ requireOpen();
+ if (!m_initialized && !initialize()) {
+ return;
+ }
+
+ m_deviceStatus = DeviceStatus.READY;
+ byte[] data = readRegister(Register.STATUS, BULK_READ_LENGTH);
+ if (data.length == 0) {
+ return;
+ }
+
+ int status = data[0] & 0xFF;
+ if ((status & STATUS_COLOR_VALID) == 0) {
+ recordFailure(FailureReason.COLOR_DATA_NOT_VALID);
+ } else {
+ m_rawClear = decodeUnsignedShort(data, CLEAR_OFFSET);
+ m_rawRed = decodeUnsignedShort(data, RED_OFFSET);
+ m_rawGreen = decodeUnsignedShort(data, GREEN_OFFSET);
+ m_rawBlue = decodeUnsignedShort(data, BLUE_OFFSET);
+ }
+
+ if ((status & STATUS_PROXIMITY_VALID) == 0) {
+ recordFailure(FailureReason.PROXIMITY_DATA_NOT_VALID);
+ } else {
+ m_rawProximity = decodeUnsignedShort(data, PROXIMITY_OFFSET);
+ }
+ }
+
+ /**
+ * Sets the analog gain applied to the color channels.
+ *
+ *
The sensor only accepts gain changes while its integrator is off, so this briefly disables
+ * and reenables the sensor, blocking for approximately 8 ms. If the sensor has not been
+ * configured successfully, the gain is applied the next time configuration succeeds.
+ *
+ * @param gain color channel gain
+ * @throws NullPointerException if {@code gain} is null
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public void setGain(Gain gain) {
+ m_gain = ErrorMessages.requireNonNullParam(gain, "gain", "setGain");
+ reconfigure();
+ }
+
+ /**
+ * Returns the configured analog gain.
+ *
+ * @return color channel gain
+ */
+ public Gain getGain() {
+ return m_gain;
+ }
+
+ /**
+ * Sets the nominal drive current of the proximity LED.
+ *
+ *
The distance calibration returned by {@link #getDistanceMeters()} was fitted at {@link
+ * LedDrive#PERCENT_50}, the default. Changing the drive current changes the reflected light level
+ * for a given distance, so {@link #setDistanceCalibration(double, double, double)} must be used
+ * to refit the calibration.
+ *
+ *
This briefly disables and reenables the sensor, blocking for approximately 8 ms. If the
+ * sensor has not been configured successfully, the drive current is applied the next time
+ * configuration succeeds.
+ *
+ * @param ledDrive proximity LED drive current
+ * @throws NullPointerException if {@code ledDrive} is null
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public void setLedDrive(LedDrive ledDrive) {
+ m_ledDrive = ErrorMessages.requireNonNullParam(ledDrive, "ledDrive", "setLedDrive");
+ reconfigure();
+ }
+
+ /**
+ * Returns the configured proximity LED drive current.
+ *
+ * @return proximity LED drive current
+ */
+ public LedDrive getLedDrive() {
+ return m_ledDrive;
+ }
+
+ /**
+ * Sets the color channel integration time.
+ *
+ *
The sensor integrates in 2.4 ms cycles, so the time is rounded up to the next whole cycle,
+ * up to a maximum of 614.4 ms. A longer integration time raises {@link
+ * #getMaximumRawColorValue()} and therefore the resolution of the color channels.
+ *
+ *
The sensor only accepts integration time changes while its integrator is off, so this
+ * briefly disables and reenables the sensor, blocking for approximately 8 ms. If the sensor has
+ * not been configured successfully, the integration time is applied the next time configuration
+ * succeeds.
+ *
+ * @param integrationTime color channel integration time
+ * @throws NullPointerException if {@code integrationTime} is null
+ * @throws IllegalArgumentException if {@code integrationTime} is nonfinite or not positive
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public void setIntegrationTime(Time integrationTime) {
+ ErrorMessages.requireNonNullParam(integrationTime, "integrationTime", "setIntegrationTime");
+ m_integrationTimeRegister = integrationTimeRegister(integrationTime.in(Milliseconds));
+ reconfigure();
+ }
+
+ /**
+ * Returns the configured color channel integration time.
+ *
+ * @return color channel integration time
+ */
+ public Time getIntegrationTime() {
+ return Milliseconds.of(integrationCycles() * INTEGRATION_CYCLE_MILLISECONDS);
+ }
+
+ /**
+ * Sets the number of times the proximity LED is pulsed during each proximity cycle. More pulses
+ * raise the reflected light level for a given distance.
+ *
+ *
The distance calibration returned by {@link #getDistanceMeters()} was fitted at the default
+ * of 8 pulses, so changing the pulse count requires refitting the calibration with {@link
+ * #setDistanceCalibration(double, double, double)}.
+ *
+ *
This briefly disables and reenables the sensor, blocking for approximately 8 ms. If the
+ * sensor has not been configured successfully, the pulse count is applied the next time
+ * configuration succeeds.
+ *
+ * @param proximityPulseCount number of LED pulses per proximity cycle, from 1 to 255
+ * @throws IllegalArgumentException if {@code proximityPulseCount} is outside 1 to 255
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public void setProximityPulseCount(int proximityPulseCount) {
+ if (proximityPulseCount < 1 || proximityPulseCount > 255) {
+ throw new IllegalArgumentException("proximityPulseCount must be between 1 and 255");
+ }
+ m_proximityPulseCount = proximityPulseCount;
+ reconfigure();
+ }
+
+ /**
+ * Returns the configured number of proximity LED pulses per proximity cycle.
+ *
+ * @return number of LED pulses per proximity cycle
+ */
+ public int getProximityPulseCount() {
+ return m_proximityPulseCount;
+ }
+
+ /**
+ * Sets a scale factor applied by this driver to the normalized color channels. This is applied in
+ * software after the sensor's own gain; it does not change the raw channel values.
+ *
+ * @param softwareGain scale factor applied to the normalized color channels
+ * @throws IllegalArgumentException if {@code softwareGain} is nonfinite or not positive
+ */
+ public void setSoftwareGain(double softwareGain) {
+ if (!Double.isFinite(softwareGain) || softwareGain <= 0.0) {
+ throw new IllegalArgumentException("softwareGain must be finite and greater than zero");
+ }
+ m_softwareGain = softwareGain;
+ }
+
+ /**
+ * Returns the scale factor applied by this driver to the normalized color channels.
+ *
+ * @return scale factor applied to the normalized color channels
+ */
+ public double getSoftwareGain() {
+ return m_softwareGain;
+ }
+
+ /**
+ * Sets the parameters of the curve that converts the raw proximity reading into a distance.
+ *
+ *
The raw proximity signal follows an inverse square law, and the parameters fit it to a
+ * linear measure of distance in centimeters:
+ *
+ *
rawProximity = a + b * (cm + c)^-2
+ *
+ * The default parameters were fitted for this sensor at the default LED drive current and
+ * proximity pulse count. Because the fit is affected by the infrared reflectivity of the target
+ * surface, the linearity it produces is usually preserved on other surfaces even when the scale
+ * is not, so a simple multiplicative correction is often enough to retune it.
+ *
+ * @param a the {@code a} parameter of the fitted curve
+ * @param b the {@code b} parameter of the fitted curve
+ * @param c the {@code c} parameter of the fitted curve
+ * @throws IllegalArgumentException if any parameter is nonfinite
+ */
+ public void setDistanceCalibration(double a, double b, double c) {
+ if (!Double.isFinite(a) || !Double.isFinite(b) || !Double.isFinite(c)) {
+ throw new IllegalArgumentException("Distance calibration parameters must be finite");
+ }
+ m_aParam = a;
+ m_bParam = b;
+ m_cParam = c;
+ }
+
+ /**
+ * Returns the distance to the target measured by the infrared proximity channel, from the plastic
+ * housing at the front of the sensor.
+ *
+ *
Readings are most accurate perpendicular to the target surface; a cosine correction is
+ * usually appropriate otherwise. The usable range is roughly 1 to 10 cm, and this returns {@link
+ * Double#NaN} when the reflected light level is too low for the calibration curve, which is the
+ * case when the target is beyond that range.
+ *
+ * @return distance in meters, or {@link Double#NaN} if the target is out of range
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getDistanceMeters() {
+ requireOpen();
+ if (m_rawProximity <= m_aParam) {
+ return Double.NaN;
+ }
+ double centimeters =
+ (-m_aParam * m_cParam
+ + m_cParam * m_rawProximity
+ - Math.sqrt(-m_aParam * m_bParam + m_bParam * m_rawProximity))
+ / (m_aParam - m_rawProximity);
+ return centimeters / 100.0;
+ }
+
+ /**
+ * Returns the distance to the target measured by the infrared proximity channel, from the plastic
+ * housing at the front of the sensor.
+ *
+ * @return distance, or a measure of {@link Double#NaN} if the target is out of range
+ * @throws IllegalStateException if this driver has been closed
+ * @see #getDistanceMeters()
+ */
+ public Distance getDistance() {
+ return Meters.of(getDistanceMeters());
+ }
+
+ /**
+ * Returns the raw reflected light level measured by the infrared proximity channel. The value
+ * rises as the target gets closer.
+ *
+ * @return raw proximity reading, from 0 to {@link #getMaximumRawProximityValue()}
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getRawProximity() {
+ requireOpen();
+ return m_rawProximity;
+ }
+
+ /**
+ * Returns the reflected light level measured by the infrared proximity channel, normalized
+ * against its saturation value. The value rises as the target gets closer.
+ *
+ * @return proximity reading, from 0 to 1
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getProximity() {
+ requireOpen();
+ return Math.clamp(m_rawProximity / (double) PROXIMITY_SATURATION, 0.0, 1.0);
+ }
+
+ /**
+ * Returns the raw proximity reading at which the proximity channel saturates.
+ *
+ * @return maximum raw proximity reading
+ */
+ public int getMaximumRawProximityValue() {
+ return PROXIMITY_SATURATION;
+ }
+
+ /**
+ * Returns the color measured by the sensor, with each channel normalized against {@link
+ * #getMaximumRawColorValue()} and scaled by the software gain.
+ *
+ * @return measured color
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public Color getColor() {
+ return new Color(getRed(), getGreen(), getBlue());
+ }
+
+ /**
+ * Returns the normalized red channel.
+ *
+ * @return red channel, from 0 to 1
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getRed() {
+ return normalize(getRawRed());
+ }
+
+ /**
+ * Returns the normalized green channel.
+ *
+ * @return green channel, from 0 to 1
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getGreen() {
+ return normalize(getRawGreen());
+ }
+
+ /**
+ * Returns the normalized blue channel.
+ *
+ * @return blue channel, from 0 to 1
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getBlue() {
+ return normalize(getRawBlue());
+ }
+
+ /**
+ * Returns the normalized clear channel, which measures unfiltered light and is a useful measure
+ * of overall brightness.
+ *
+ * @return clear channel, from 0 to 1
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public double getClear() {
+ return normalize(getRawClear());
+ }
+
+ /**
+ * Returns the raw red channel count.
+ *
+ * @return red channel, from 0 to {@link #getMaximumRawColorValue()}
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getRawRed() {
+ requireOpen();
+ return m_rawRed;
+ }
+
+ /**
+ * Returns the raw green channel count.
+ *
+ * @return green channel, from 0 to {@link #getMaximumRawColorValue()}
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getRawGreen() {
+ requireOpen();
+ return m_rawGreen;
+ }
+
+ /**
+ * Returns the raw blue channel count.
+ *
+ * @return blue channel, from 0 to {@link #getMaximumRawColorValue()}
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getRawBlue() {
+ requireOpen();
+ return m_rawBlue;
+ }
+
+ /**
+ * Returns the raw clear channel count.
+ *
+ * @return clear channel, from 0 to {@link #getMaximumRawColorValue()}
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public int getRawClear() {
+ requireOpen();
+ return m_rawClear;
+ }
+
+ /**
+ * Returns the highest raw color channel count reachable with the configured integration time.
+ *
+ * @return maximum raw color channel count
+ */
+ public int getMaximumRawColorValue() {
+ return Math.min(MAX_RAW_COLOR_VALUE, COUNTS_PER_INTEGRATION_CYCLE * integrationCycles());
+ }
+
+ /**
+ * Returns the device identifier reported by the sensor.
+ *
+ * @return device identifier, or zero if it has not been read successfully
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public byte getDeviceId() {
+ requireOpen();
+ return m_deviceId;
+ }
+
+ /**
+ * Returns the current device or read status.
+ *
+ * @return device status
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public DeviceStatus getDeviceStatus() {
+ requireOpen();
+ return m_deviceStatus;
+ }
+
+ /**
+ * Returns the reason for the most recent driver-detected failure.
+ *
+ * @return failure reason, or null if the driver has not detected a failure
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public FailureReason getLastFailureReason() {
+ requireOpen();
+ return m_lastFailureReason;
+ }
+
+ /**
+ * Returns the total number of failures detected by this driver instance.
+ *
+ * @return total failure count
+ * @throws IllegalStateException if this driver has been closed
+ */
+ public long getFailureCount() {
+ requireOpen();
+ return m_failureCount;
+ }
+
+ private I2C requireOpen() {
+ if (m_i2c == null) {
+ throw new IllegalStateException("RevColorSensorV2 has been closed");
+ }
+ return m_i2c;
+ }
+
+ private boolean initialize() {
+ m_initialized = false;
+ m_deviceStatus = DeviceStatus.NOT_INITIALIZED;
+
+ byte[] data = readRegister(Register.DEVICE_ID, 1);
+ if (data.length == 0) {
+ return false;
+ }
+ m_deviceId = data[0];
+ if (m_deviceId != TMD37821_DEVICE_ID && m_deviceId != TMD37823_DEVICE_ID) {
+ recordFailure(FailureReason.UNEXPECTED_DEVICE_ID);
+ return false;
+ }
+ return configure();
+ }
+
+ private void reconfigure() {
+ requireOpen();
+ if (m_initialized) {
+ configure();
+ }
+ }
+
+ /**
+ * Writes the cached configuration to the sensor. The integrator must be off while the gain and
+ * integration time are written, so the sensor is disabled first and reenabled afterwards.
+ *
+ * @return whether every transaction succeeded
+ */
+ private boolean configure() {
+ m_initialized = false;
+ return disable()
+ && writeRegister(Register.ATIME, m_integrationTimeRegister)
+ && writeControl()
+ && writeRegister(Register.PPULSE, m_proximityPulseCount)
+ && enable();
+ }
+
+ /**
+ * Writes every CONTROL field explicitly rather than preserving the register's current contents.
+ * Disabling the sensor does not reset CONTROL, so a value retained from an earlier configuration
+ * could leave a reserved proximity diode or proximity gain selection in place, which changes
+ * every proximity reading and invalidates the distance calibration.
+ *
+ *
This covers all eight bits: the LED drive occupies bits 7:6, the proximity diode select bits
+ * 5:4, the proximity gain bits 3:2, and the color gain bits 1:0. The proximity gain is left as
+ * zero, selecting the 1x gain the distance calibration was fitted at and the only value this part
+ * defines.
+ *
+ * @return whether the write succeeded
+ */
+ private boolean writeControl() {
+ int control = m_ledDrive.m_value | CONTROL_PDIODE_IR | m_gain.m_value;
+ return writeRegister(Register.CONTROL, control);
+ }
+
+ private boolean enable() {
+ if (!writeEnable(ENABLE_POWER_ON)) {
+ return false;
+ }
+ delay(POWER_ON_DELAY_MILLISECONDS);
+
+ if (!writeEnable(ENABLE_POWER_ON | ENABLE_COLOR | ENABLE_PROXIMITY)) {
+ return false;
+ }
+ delay(ENABLE_SETTLE_DELAY_MILLISECONDS);
+
+ m_initialized = true;
+ m_deviceStatus = DeviceStatus.READY;
+ return true;
+ }
+
+ private boolean disable() {
+ return writeEnable(0);
+ }
+
+ private boolean writeEnable(int value) {
+ // The interrupt enables are not used, and the reserved high bits must be written as zero.
+ return writeRegister(Register.ENABLE, value & ENABLE_WRITABLE_MASK);
+ }
+
+ private byte[] readRegister(Register register, int count) {
+ byte[] data = new byte[count];
+ if (requireOpen().read(register.m_address | COMMAND_AUTO_INCREMENT, count, data)) {
+ recordFailure(FailureReason.I2C_READ_ABORTED);
+ return new byte[0];
+ }
+ return data;
+ }
+
+ private boolean writeRegister(Register register, int value) {
+ if (requireOpen().write(register.m_address | COMMAND_AUTO_INCREMENT, value)) {
+ recordFailure(FailureReason.I2C_WRITE_ABORTED);
+ return false;
+ }
+ return true;
+ }
+
+ private void recordFailure(FailureReason reason) {
+ m_lastFailureReason = reason;
+ m_failureCount++;
+ m_deviceStatus =
+ reason == FailureReason.UNEXPECTED_DEVICE_ID
+ ? DeviceStatus.FAULT_UNEXPECTED_DEVICE_ID
+ : DeviceStatus.FAULT_BAD_READ;
+ }
+
+ private double normalize(int rawValue) {
+ return Math.clamp(m_softwareGain * rawValue / getMaximumRawColorValue(), 0.0, 1.0);
+ }
+
+ private int integrationCycles() {
+ return MAX_INTEGRATION_CYCLES - m_integrationTimeRegister;
+ }
+
+ /**
+ * Returns the ATIME register value that integrates for at least the given duration. ATIME is the
+ * two's complement of the number of 2.4 ms integration cycles.
+ *
+ * @param milliseconds requested integration time
+ * @return ATIME register value
+ */
+ private static int integrationTimeRegister(double milliseconds) {
+ if (!Double.isFinite(milliseconds) || milliseconds <= 0.0) {
+ throw new IllegalArgumentException("integrationTime must be finite and greater than zero");
+ }
+ int cycles = (int) Math.ceil(milliseconds / INTEGRATION_CYCLE_MILLISECONDS);
+ return MAX_INTEGRATION_CYCLES - Math.min(cycles, MAX_INTEGRATION_CYCLES);
+ }
+
+ private static int decodeUnsignedShort(byte[] data, int offset) {
+ return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
+ }
+
+ private static void delay(int milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/drivers/src/main/java/org/wpilib/drivers/range/package-info.java b/drivers/src/main/java/org/wpilib/drivers/range/package-info.java
new file mode 100644
index 00000000000..4732a1f225b
--- /dev/null
+++ b/drivers/src/main/java/org/wpilib/drivers/range/package-info.java
@@ -0,0 +1,6 @@
+// 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.
+
+/** WPILib range and color sensor drivers. */
+package org.wpilib.drivers.range;
diff --git a/drivers/src/main/native/cpp/range/RevColorSensorV2.cpp b/drivers/src/main/native/cpp/range/RevColorSensorV2.cpp
new file mode 100644
index 00000000000..a80a703fff1
--- /dev/null
+++ b/drivers/src/main/native/cpp/range/RevColorSensorV2.cpp
@@ -0,0 +1,377 @@
+// 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.
+
+//
+// This file is based on the FTC SDK drivers for the AMS TMD3782 family of color
+// sensors, which were made available under the BSD 3-Clause License.
+// Copyright (c) 2016-2017 Robert Atkinson and Steve Geffner.
+
+#include "wpi/drivers/range/RevColorSensorV2.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "wpi/util/UsageReporting.hpp"
+
+using namespace wpi;
+
+namespace {
+
+void Delay(int milliseconds) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
+}
+
+int DecodeUnsignedShort(const std::vector& data, std::size_t offset) {
+ return data[offset] | (data[offset + 1] << 8);
+}
+
+} // namespace
+
+RevColorSensorV2::RevColorSensorV2(I2C::Port port, int deviceAddress)
+ : m_i2c{port, ValidateAddress(deviceAddress)} {
+ wpi::util::ReportUsage(std::format("I2C[{}]", static_cast(port)),
+ deviceAddress, "RevColorSensorV2");
+ Initialize();
+}
+
+I2C::Port RevColorSensorV2::GetPort() const {
+ return m_i2c.GetPort();
+}
+
+int RevColorSensorV2::GetDeviceAddress() const {
+ return m_i2c.GetDeviceAddress();
+}
+
+void RevColorSensorV2::Update() {
+ if (!m_initialized && !Initialize()) {
+ return;
+ }
+
+ m_deviceStatus = DeviceStatus::READY;
+ std::vector data = ReadRegister(Register::STATUS, BULK_READ_LENGTH);
+ if (data.empty()) {
+ return;
+ }
+
+ int status = data[0];
+ if ((status & STATUS_COLOR_VALID) == 0) {
+ RecordFailure(FailureReason::COLOR_DATA_NOT_VALID);
+ } else {
+ m_rawClear = DecodeUnsignedShort(data, CLEAR_OFFSET);
+ m_rawRed = DecodeUnsignedShort(data, RED_OFFSET);
+ m_rawGreen = DecodeUnsignedShort(data, GREEN_OFFSET);
+ m_rawBlue = DecodeUnsignedShort(data, BLUE_OFFSET);
+ }
+
+ if ((status & STATUS_PROXIMITY_VALID) == 0) {
+ RecordFailure(FailureReason::PROXIMITY_DATA_NOT_VALID);
+ } else {
+ m_rawProximity = DecodeUnsignedShort(data, PROXIMITY_OFFSET);
+ }
+}
+
+void RevColorSensorV2::SetGain(Gain gain) {
+ switch (gain) {
+ case Gain::GAIN_1:
+ case Gain::GAIN_4:
+ case Gain::GAIN_16:
+ case Gain::GAIN_60:
+ break;
+ default:
+ throw std::invalid_argument("Invalid gain");
+ }
+ m_gain = gain;
+ Reconfigure();
+}
+
+RevColorSensorV2::Gain RevColorSensorV2::GetGain() const {
+ return m_gain;
+}
+
+void RevColorSensorV2::SetLedDrive(LedDrive ledDrive) {
+ switch (ledDrive) {
+ case LedDrive::PERCENT_100:
+ case LedDrive::PERCENT_50:
+ case LedDrive::PERCENT_25:
+ case LedDrive::PERCENT_12_5:
+ break;
+ default:
+ throw std::invalid_argument("Invalid LED drive");
+ }
+ m_ledDrive = ledDrive;
+ Reconfigure();
+}
+
+RevColorSensorV2::LedDrive RevColorSensorV2::GetLedDrive() const {
+ return m_ledDrive;
+}
+
+void RevColorSensorV2::SetIntegrationTime(
+ wpi::units::millisecond_t integrationTime) {
+ m_integrationTimeRegister = IntegrationTimeRegister(integrationTime);
+ Reconfigure();
+}
+
+wpi::units::millisecond_t RevColorSensorV2::GetIntegrationTime() const {
+ return wpi::units::millisecond_t{IntegrationCycles() *
+ INTEGRATION_CYCLE_MILLISECONDS};
+}
+
+void RevColorSensorV2::SetProximityPulseCount(int proximityPulseCount) {
+ if (proximityPulseCount < 1 || proximityPulseCount > 255) {
+ throw std::invalid_argument(
+ "proximityPulseCount must be between 1 and 255");
+ }
+ m_proximityPulseCount = proximityPulseCount;
+ Reconfigure();
+}
+
+int RevColorSensorV2::GetProximityPulseCount() const {
+ return m_proximityPulseCount;
+}
+
+void RevColorSensorV2::SetSoftwareGain(double softwareGain) {
+ if (!std::isfinite(softwareGain) || softwareGain <= 0.0) {
+ throw std::invalid_argument(
+ "softwareGain must be finite and greater than zero");
+ }
+ m_softwareGain = softwareGain;
+}
+
+double RevColorSensorV2::GetSoftwareGain() const {
+ return m_softwareGain;
+}
+
+void RevColorSensorV2::SetDistanceCalibration(double a, double b, double c) {
+ if (!std::isfinite(a) || !std::isfinite(b) || !std::isfinite(c)) {
+ throw std::invalid_argument(
+ "Distance calibration parameters must be finite");
+ }
+ m_aParam = a;
+ m_bParam = b;
+ m_cParam = c;
+}
+
+wpi::units::meter_t RevColorSensorV2::GetDistance() const {
+ if (m_rawProximity <= m_aParam) {
+ return wpi::units::meter_t{std::numeric_limits::quiet_NaN()};
+ }
+ double centimeters =
+ (-m_aParam * m_cParam + m_cParam * m_rawProximity -
+ std::sqrt(-m_aParam * m_bParam + m_bParam * m_rawProximity)) /
+ (m_aParam - m_rawProximity);
+ return wpi::units::centimeter_t{centimeters};
+}
+
+int RevColorSensorV2::GetRawProximity() const {
+ return m_rawProximity;
+}
+
+double RevColorSensorV2::GetProximity() const {
+ return std::clamp(m_rawProximity / static_cast(PROXIMITY_SATURATION),
+ 0.0, 1.0);
+}
+
+int RevColorSensorV2::GetMaximumRawProximityValue() const {
+ return PROXIMITY_SATURATION;
+}
+
+wpi::util::Color RevColorSensorV2::GetColor() const {
+ return wpi::util::Color{GetRed(), GetGreen(), GetBlue()};
+}
+
+double RevColorSensorV2::GetRed() const {
+ return Normalize(m_rawRed);
+}
+
+double RevColorSensorV2::GetGreen() const {
+ return Normalize(m_rawGreen);
+}
+
+double RevColorSensorV2::GetBlue() const {
+ return Normalize(m_rawBlue);
+}
+
+double RevColorSensorV2::GetClear() const {
+ return Normalize(m_rawClear);
+}
+
+int RevColorSensorV2::GetRawRed() const {
+ return m_rawRed;
+}
+
+int RevColorSensorV2::GetRawGreen() const {
+ return m_rawGreen;
+}
+
+int RevColorSensorV2::GetRawBlue() const {
+ return m_rawBlue;
+}
+
+int RevColorSensorV2::GetRawClear() const {
+ return m_rawClear;
+}
+
+int RevColorSensorV2::GetMaximumRawColorValue() const {
+ return std::min(MAX_RAW_COLOR_VALUE,
+ COUNTS_PER_INTEGRATION_CYCLE * IntegrationCycles());
+}
+
+uint8_t RevColorSensorV2::GetDeviceId() const {
+ return m_deviceId;
+}
+
+RevColorSensorV2::DeviceStatus RevColorSensorV2::GetDeviceStatus() const {
+ return m_deviceStatus;
+}
+
+std::optional
+RevColorSensorV2::GetLastFailureReason() const {
+ return m_lastFailureReason;
+}
+
+uint64_t RevColorSensorV2::GetFailureCount() const {
+ return m_failureCount;
+}
+
+int RevColorSensorV2::ValidateAddress(int deviceAddress) {
+ if (deviceAddress < 0 || deviceAddress > 0x7f) {
+ throw std::invalid_argument("deviceAddress must be a 7-bit I2C address");
+ }
+ return deviceAddress;
+}
+
+int RevColorSensorV2::IntegrationTimeRegister(
+ wpi::units::millisecond_t integrationTime) {
+ double milliseconds = integrationTime.value();
+ if (!std::isfinite(milliseconds) || milliseconds <= 0.0) {
+ throw std::invalid_argument(
+ "integrationTime must be finite and greater than zero");
+ }
+ // The cycle count is clamped while it is still floating point. A duration
+ // longer than the sensor can integrate for produces a value beyond the range
+ // of int, and converting that to int is undefined behavior, so clamping after
+ // the conversion would be too late to saturate at the maximum.
+ double cycles = std::ceil(milliseconds / INTEGRATION_CYCLE_MILLISECONDS);
+ int clampedCycles = static_cast(
+ std::min(cycles, static_cast(MAX_INTEGRATION_CYCLES)));
+ return MAX_INTEGRATION_CYCLES - clampedCycles;
+}
+
+bool RevColorSensorV2::Initialize() {
+ m_initialized = false;
+ m_deviceStatus = DeviceStatus::NOT_INITIALIZED;
+
+ std::vector data = ReadRegister(Register::DEVICE_ID, 1);
+ if (data.empty()) {
+ return false;
+ }
+ m_deviceId = data[0];
+ if (m_deviceId != TMD37821_DEVICE_ID && m_deviceId != TMD37823_DEVICE_ID) {
+ RecordFailure(FailureReason::UNEXPECTED_DEVICE_ID);
+ return false;
+ }
+ return Configure();
+}
+
+void RevColorSensorV2::Reconfigure() {
+ if (m_initialized) {
+ Configure();
+ }
+}
+
+bool RevColorSensorV2::Configure() {
+ // The integrator must be off while the gain and integration time are
+ // written, so the sensor is disabled first and reenabled afterwards.
+ m_initialized = false;
+ return Disable() &&
+ WriteRegister(Register::ATIME, m_integrationTimeRegister) &&
+ WriteControl() &&
+ WriteRegister(Register::PPULSE, m_proximityPulseCount) && Enable();
+}
+
+bool RevColorSensorV2::WriteControl() {
+ // Every CONTROL field is written explicitly rather than preserving the
+ // register's current contents. Disabling the sensor does not reset CONTROL,
+ // so a value retained from an earlier configuration could leave a reserved
+ // proximity diode or proximity gain selection in place, which changes every
+ // proximity reading and invalidates the distance calibration.
+ //
+ // This covers all eight bits: the LED drive occupies bits 7:6, the proximity
+ // diode select bits 5:4, the proximity gain bits 3:2, and the color gain bits
+ // 1:0. The proximity gain is left as zero, selecting the 1x gain the distance
+ // calibration was fitted at and the only value this part defines.
+ int control = static_cast(m_ledDrive) | CONTROL_PDIODE_IR |
+ static_cast(m_gain);
+ return WriteRegister(Register::CONTROL, control);
+}
+
+bool RevColorSensorV2::Enable() {
+ if (!WriteEnable(ENABLE_POWER_ON)) {
+ return false;
+ }
+ Delay(POWER_ON_DELAY_MILLISECONDS);
+
+ if (!WriteEnable(ENABLE_POWER_ON | ENABLE_COLOR | ENABLE_PROXIMITY)) {
+ return false;
+ }
+ Delay(ENABLE_SETTLE_DELAY_MILLISECONDS);
+
+ m_initialized = true;
+ m_deviceStatus = DeviceStatus::READY;
+ return true;
+}
+
+bool RevColorSensorV2::Disable() {
+ return WriteEnable(0);
+}
+
+bool RevColorSensorV2::WriteEnable(int value) {
+ // The interrupt enables are not used, and the reserved high bits must be
+ // written as zero.
+ return WriteRegister(Register::ENABLE, value & ENABLE_WRITABLE_MASK);
+}
+
+std::vector RevColorSensorV2::ReadRegister(Register reg, int count) {
+ std::vector data(count);
+ if (m_i2c.Read(static_cast(reg) | COMMAND_AUTO_INCREMENT, count,
+ data.data())) {
+ RecordFailure(FailureReason::I2C_READ_ABORTED);
+ return {};
+ }
+ return data;
+}
+
+bool RevColorSensorV2::WriteRegister(Register reg, int value) {
+ if (m_i2c.Write(static_cast(reg) | COMMAND_AUTO_INCREMENT,
+ static_cast(value))) {
+ RecordFailure(FailureReason::I2C_WRITE_ABORTED);
+ return false;
+ }
+ return true;
+}
+
+void RevColorSensorV2::RecordFailure(FailureReason reason) {
+ m_lastFailureReason = reason;
+ ++m_failureCount;
+ m_deviceStatus = reason == FailureReason::UNEXPECTED_DEVICE_ID
+ ? DeviceStatus::FAULT_UNEXPECTED_DEVICE_ID
+ : DeviceStatus::FAULT_BAD_READ;
+}
+
+double RevColorSensorV2::Normalize(int rawValue) const {
+ return std::clamp(m_softwareGain * rawValue / GetMaximumRawColorValue(), 0.0,
+ 1.0);
+}
+
+int RevColorSensorV2::IntegrationCycles() const {
+ return MAX_INTEGRATION_CYCLES - m_integrationTimeRegister;
+}
diff --git a/drivers/src/main/native/include/wpi/drivers/range/RevColorSensorV2.hpp b/drivers/src/main/native/include/wpi/drivers/range/RevColorSensorV2.hpp
new file mode 100644
index 00000000000..53199e9ac91
--- /dev/null
+++ b/drivers/src/main/native/include/wpi/drivers/range/RevColorSensorV2.hpp
@@ -0,0 +1,488 @@
+// 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.
+
+//
+// This file is based on the FTC SDK drivers for the AMS TMD3782 family of color
+// sensors, which were made available under the BSD 3-Clause License.
+// Copyright (c) 2016-2017 Robert Atkinson and Steve Geffner.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "wpi/hardware/bus/I2C.hpp"
+#include "wpi/units/length.hpp"
+#include "wpi/units/time.hpp"
+#include "wpi/util/Color.hpp"
+
+namespace wpi {
+
+/**
+ * Driver for the REV Robotics Color Sensor V2.
+ *
+ * The sensor is an AMS TMD37821, which combines an RGBC color sensor with an
+ * infrared proximity sensor. The proximity channel is exposed both as a
+ * calibrated distance and as a raw reflected light level.
+ *
+ * Call Update() once per robot loop. All accessors return the values cached by
+ * the most recent update; they do not access the device.
+ *
+ * Aborted I2C transactions, an unexpected device ID, and data the sensor
+ * reports as not yet valid are reported through DeviceStatus instead of
+ * throwing an exception, and the previously cached measurements are preserved.
+ */
+class RevColorSensorV2 {
+ public:
+ /// Default 7-bit I2C address of the Color Sensor V2.
+ static constexpr int DEFAULT_ADDRESS = 0x39;
+
+ /** Register map for the TMD37821. */
+ enum class Register {
+ /// Enables states and interrupts.
+ ENABLE = 0x00,
+ /// RGBC integration time.
+ ATIME = 0x01,
+ /// Wait time.
+ WTIME = 0x03,
+ /// Clear channel interrupt low threshold.
+ AILT = 0x04,
+ /// Clear channel interrupt high threshold.
+ AIHT = 0x06,
+ /// Interrupt persistence filter.
+ PERS = 0x0C,
+ /// Configuration.
+ CONFIGURATION = 0x0D,
+ /// Proximity LED pulse count.
+ PPULSE = 0x0E,
+ /// Gain and proximity LED drive control.
+ CONTROL = 0x0F,
+ /// Device identifier.
+ DEVICE_ID = 0x12,
+ /// Device status.
+ STATUS = 0x13,
+ /// Clear channel data.
+ CLEAR = 0x14,
+ /// Red channel data.
+ RED = 0x16,
+ /// Green channel data.
+ GREEN = 0x18,
+ /// Blue channel data.
+ BLUE = 0x1A,
+ /// Proximity data.
+ PROXIMITY = 0x1C
+ };
+
+ /** Analog gain applied to the color channels by the sensor. */
+ enum class Gain {
+ /// 1x gain.
+ GAIN_1 = 0x00,
+ /// 4x gain.
+ GAIN_4 = 0x01,
+ /// 16x gain.
+ GAIN_16 = 0x02,
+ /// 60x gain.
+ GAIN_60 = 0x03
+ };
+
+ /** Nominal drive current of the proximity LED. */
+ enum class LedDrive {
+ /// 100% drive current.
+ PERCENT_100 = 0x00 << 6,
+ /// 50% drive current.
+ PERCENT_50 = 0x01 << 6,
+ /// 25% drive current.
+ PERCENT_25 = 0x02 << 6,
+ /// 12.5% drive current.
+ PERCENT_12_5 = 0x03 << 6
+ };
+
+ /** Sensor status, including device-reported faults and local read failures.
+ */
+ enum class DeviceStatus {
+ /// The sensor has not been configured successfully yet.
+ NOT_INITIALIZED,
+ /// The sensor is operating normally.
+ READY,
+ /// The device ID register did not report a TMD3782 family part.
+ FAULT_UNEXPECTED_DEVICE_ID,
+ /// An I2C transaction was aborted, or the sensor reported that its data was
+ /// not valid.
+ FAULT_BAD_READ
+ };
+
+ /** Reason the driver rejected data or an I2C transaction. */
+ enum class FailureReason {
+ /// The I2C controller aborted a read transaction.
+ I2C_READ_ABORTED,
+ /// The I2C controller aborted a write transaction.
+ I2C_WRITE_ABORTED,
+ /// The device ID register did not report a TMD3782 family part.
+ UNEXPECTED_DEVICE_ID,
+ /// The sensor had not completed an RGBC cycle since the color channels were
+ /// enabled.
+ COLOR_DATA_NOT_VALID,
+ /// The sensor had not completed a proximity cycle since the proximity
+ /// channel was enabled.
+ PROXIMITY_DATA_NOT_VALID
+ };
+
+ /**
+ * Constructs a Color Sensor V2 and configures it.
+ *
+ * Configuration failures are reported through GetDeviceStatus(); Update()
+ * retries configuration until it succeeds.
+ *
+ * @param port I2C port to which the sensor is connected.
+ * @param deviceAddress 7-bit I2C address.
+ * @throws std::invalid_argument if deviceAddress is outside the 7-bit range.
+ */
+ explicit RevColorSensorV2(I2C::Port port,
+ int deviceAddress = DEFAULT_ADDRESS);
+
+ RevColorSensorV2(RevColorSensorV2&&) = default;
+ RevColorSensorV2& operator=(RevColorSensorV2&&) = default;
+
+ /**
+ * Returns the I2C port.
+ *
+ * @return I2C port.
+ */
+ I2C::Port GetPort() const;
+
+ /**
+ * Returns the I2C address.
+ *
+ * @return 7-bit I2C address.
+ */
+ int GetDeviceAddress() const;
+
+ /**
+ * Reads the color and proximity channels and updates the cached
+ * measurements. Call this once per robot loop.
+ *
+ * If the sensor has not been configured successfully, this retries
+ * configuration first, which blocks for approximately 8 ms.
+ *
+ * An aborted I2C transaction or data the sensor reports as not yet valid
+ * does not throw; it sets the device status to DeviceStatus::FAULT_BAD_READ
+ * and preserves the affected measurements.
+ */
+ void Update();
+
+ /**
+ * Sets the analog gain applied to the color channels.
+ *
+ * The sensor only accepts gain changes while its integrator is off, so this
+ * briefly disables and reenables the sensor, blocking for approximately 8 ms.
+ * If the sensor has not been configured successfully, the gain is applied the
+ * next time configuration succeeds.
+ *
+ * @param gain Color channel gain.
+ * @throws std::invalid_argument if gain is invalid.
+ */
+ void SetGain(Gain gain);
+
+ /** @return Configured analog gain. */
+ Gain GetGain() const;
+
+ /**
+ * Sets the nominal drive current of the proximity LED.
+ *
+ * The distance calibration returned by GetDistance() was fitted at
+ * LedDrive::PERCENT_50, the default. Changing the drive current changes the
+ * reflected light level for a given distance, so SetDistanceCalibration()
+ * must be used to refit the calibration.
+ *
+ * This briefly disables and reenables the sensor, blocking for approximately
+ * 8 ms. If the sensor has not been configured successfully, the drive current
+ * is applied the next time configuration succeeds.
+ *
+ * @param ledDrive Proximity LED drive current.
+ * @throws std::invalid_argument if ledDrive is invalid.
+ */
+ void SetLedDrive(LedDrive ledDrive);
+
+ /** @return Configured proximity LED drive current. */
+ LedDrive GetLedDrive() const;
+
+ /**
+ * Sets the color channel integration time.
+ *
+ * The sensor integrates in 2.4 ms cycles, so the time is rounded up to the
+ * next whole cycle, up to a maximum of 614.4 ms. A longer integration time
+ * raises GetMaximumRawColorValue() and therefore the resolution of the color
+ * channels.
+ *
+ * The sensor only accepts integration time changes while its integrator is
+ * off, so this briefly disables and reenables the sensor, blocking for
+ * approximately 8 ms. If the sensor has not been configured successfully, the
+ * integration time is applied the next time configuration succeeds.
+ *
+ * @param integrationTime Color channel integration time.
+ * @throws std::invalid_argument if integrationTime is nonfinite or not
+ * positive.
+ */
+ void SetIntegrationTime(wpi::units::millisecond_t integrationTime);
+
+ /** @return Configured color channel integration time. */
+ wpi::units::millisecond_t GetIntegrationTime() const;
+
+ /**
+ * Sets the number of times the proximity LED is pulsed during each proximity
+ * cycle. More pulses raise the reflected light level for a given distance.
+ *
+ * The distance calibration returned by GetDistance() was fitted at the
+ * default of 8 pulses, so changing the pulse count requires refitting the
+ * calibration with SetDistanceCalibration().
+ *
+ * This briefly disables and reenables the sensor, blocking for approximately
+ * 8 ms. If the sensor has not been configured successfully, the pulse count
+ * is applied the next time configuration succeeds.
+ *
+ * @param proximityPulseCount Number of LED pulses per proximity cycle, from
+ * 1 to 255.
+ * @throws std::invalid_argument if proximityPulseCount is outside 1 to 255.
+ */
+ void SetProximityPulseCount(int proximityPulseCount);
+
+ /** @return Configured number of proximity LED pulses per proximity cycle. */
+ int GetProximityPulseCount() const;
+
+ /**
+ * Sets a scale factor applied by this driver to the normalized color
+ * channels. This is applied in software after the sensor's own gain; it does
+ * not change the raw channel values.
+ *
+ * @param softwareGain Scale factor applied to the normalized color channels.
+ * @throws std::invalid_argument if softwareGain is nonfinite or not positive.
+ */
+ void SetSoftwareGain(double softwareGain);
+
+ /** @return Scale factor applied to the normalized color channels. */
+ double GetSoftwareGain() const;
+
+ /**
+ * Sets the parameters of the curve that converts the raw proximity reading
+ * into a distance.
+ *
+ * The raw proximity signal follows an inverse square law, and the parameters
+ * fit it to a linear measure of distance in centimeters:
+ *
+ * rawProximity = a + b * (cm + c)^-2
+ *
+ * The default parameters were fitted for this sensor at the default LED drive
+ * current and proximity pulse count. Because the fit is affected by the
+ * infrared reflectivity of the target surface, the linearity it produces is
+ * usually preserved on other surfaces even when the scale is not, so a simple
+ * multiplicative correction is often enough to retune it.
+ *
+ * @param a The a parameter of the fitted curve.
+ * @param b The b parameter of the fitted curve.
+ * @param c The c parameter of the fitted curve.
+ * @throws std::invalid_argument if any parameter is nonfinite.
+ */
+ void SetDistanceCalibration(double a, double b, double c);
+
+ /**
+ * Returns the distance to the target measured by the infrared proximity
+ * channel, from the plastic housing at the front of the sensor.
+ *
+ * Readings are most accurate perpendicular to the target surface; a cosine
+ * correction is usually appropriate otherwise. The usable range is roughly
+ * 1 to 10 cm, and this returns NaN when the reflected light level is too low
+ * for the calibration curve, which is the case when the target is beyond that
+ * range.
+ *
+ * @return Distance, or NaN if the target is out of range.
+ */
+ wpi::units::meter_t GetDistance() const;
+
+ /**
+ * Returns the raw reflected light level measured by the infrared proximity
+ * channel. The value rises as the target gets closer.
+ *
+ * @return Raw proximity reading, from 0 to GetMaximumRawProximityValue().
+ */
+ int GetRawProximity() const;
+
+ /**
+ * Returns the reflected light level measured by the infrared proximity
+ * channel, normalized against its saturation value. The value rises as the
+ * target gets closer.
+ *
+ * @return Proximity reading, from 0 to 1.
+ */
+ double GetProximity() const;
+
+ /** @return Raw proximity reading at which the proximity channel saturates. */
+ int GetMaximumRawProximityValue() const;
+
+ /**
+ * Returns the color measured by the sensor, with each channel normalized
+ * against GetMaximumRawColorValue() and scaled by the software gain.
+ *
+ * @return Measured color.
+ */
+ wpi::util::Color GetColor() const;
+
+ /** @return Normalized red channel, from 0 to 1. */
+ double GetRed() const;
+
+ /** @return Normalized green channel, from 0 to 1. */
+ double GetGreen() const;
+
+ /** @return Normalized blue channel, from 0 to 1. */
+ double GetBlue() const;
+
+ /**
+ * Returns the normalized clear channel, which measures unfiltered light and
+ * is a useful measure of overall brightness.
+ *
+ * @return Clear channel, from 0 to 1.
+ */
+ double GetClear() const;
+
+ /** @return Raw red channel count, from 0 to GetMaximumRawColorValue(). */
+ int GetRawRed() const;
+
+ /** @return Raw green channel count, from 0 to GetMaximumRawColorValue(). */
+ int GetRawGreen() const;
+
+ /** @return Raw blue channel count, from 0 to GetMaximumRawColorValue(). */
+ int GetRawBlue() const;
+
+ /** @return Raw clear channel count, from 0 to GetMaximumRawColorValue(). */
+ int GetRawClear() const;
+
+ /**
+ * Returns the highest raw color channel count reachable with the configured
+ * integration time.
+ *
+ * @return Maximum raw color channel count.
+ */
+ int GetMaximumRawColorValue() const;
+
+ /**
+ * Returns the device identifier reported by the sensor.
+ *
+ * @return Device identifier, or zero if it has not been read successfully.
+ */
+ uint8_t GetDeviceId() const;
+
+ /** @return Current device or read status. */
+ DeviceStatus GetDeviceStatus() const;
+
+ /**
+ * @return Reason for the most recent driver-detected failure, or std::nullopt
+ * if none has occurred.
+ */
+ std::optional GetLastFailureReason() const;
+
+ /** @return Total number of failures detected by this driver instance. */
+ uint64_t GetFailureCount() const;
+
+ private:
+ /// Every register access is prefixed with the command bit.
+ static constexpr int COMMAND_BIT = 0x80;
+
+ /// Command type selecting the auto-increment protocol. Without it the sensor
+ /// uses the repeated-byte protocol, which returns the addressed register once
+ /// per byte instead of advancing through consecutive registers, so a
+ /// multi-byte read would return the same register repeatedly.
+ static constexpr int COMMAND_TYPE_AUTO_INCREMENT = 0x01 << 5;
+
+ /// Register address prefix used for every read and write.
+ static constexpr int COMMAND_AUTO_INCREMENT =
+ COMMAND_BIT | COMMAND_TYPE_AUTO_INCREMENT;
+
+ static constexpr uint8_t TMD37821_DEVICE_ID = 0x60;
+ static constexpr uint8_t TMD37823_DEVICE_ID = 0x69;
+
+ static constexpr int ENABLE_POWER_ON = 0x01;
+ static constexpr int ENABLE_COLOR = 0x02;
+ static constexpr int ENABLE_PROXIMITY = 0x04;
+ static constexpr int ENABLE_WAIT = 0x08;
+ static constexpr int ENABLE_WRITABLE_MASK =
+ ENABLE_POWER_ON | ENABLE_COLOR | ENABLE_PROXIMITY | ENABLE_WAIT;
+
+ static constexpr int STATUS_COLOR_VALID = 0x01;
+ static constexpr int STATUS_PROXIMITY_VALID = 0x02;
+
+ /// CONTROL proximity diode select field (bits 5:4), set to the value that
+ /// measures proximity with the infrared diode. The other values of this field
+ /// are reserved.
+ static constexpr int CONTROL_PDIODE_IR = 0x02 << 4;
+
+ /// STATUS through the high byte of the proximity data, read as one block.
+ static constexpr int BULK_READ_LENGTH = 11;
+
+ static constexpr std::size_t CLEAR_OFFSET = 1;
+ static constexpr std::size_t RED_OFFSET = 3;
+ static constexpr std::size_t GREEN_OFFSET = 5;
+ static constexpr std::size_t BLUE_OFFSET = 7;
+ static constexpr std::size_t PROXIMITY_OFFSET = 9;
+
+ static constexpr double INTEGRATION_CYCLE_MILLISECONDS = 2.4;
+ static constexpr int MAX_INTEGRATION_CYCLES = 256;
+ static constexpr int COUNTS_PER_INTEGRATION_CYCLE = 1024;
+ static constexpr int MAX_RAW_COLOR_VALUE = 65535;
+ static constexpr int PROXIMITY_SATURATION = 1023;
+
+ /// The datasheet specifies a 2.4 ms warm-up delay after power on.
+ static constexpr int POWER_ON_DELAY_MILLISECONDS = 3;
+
+ static constexpr int ENABLE_SETTLE_DELAY_MILLISECONDS = 5;
+
+ static constexpr wpi::units::millisecond_t DEFAULT_INTEGRATION_TIME{24.0};
+ static constexpr int DEFAULT_PROXIMITY_PULSE_COUNT = 8;
+
+ static constexpr double DEFAULT_A_PARAM = 186.347;
+ static constexpr double DEFAULT_B_PARAM = 30403.5;
+ static constexpr double DEFAULT_C_PARAM = 0.576649;
+
+ static int ValidateAddress(int deviceAddress);
+ static int IntegrationTimeRegister(wpi::units::millisecond_t integrationTime);
+
+ bool Initialize();
+ void Reconfigure();
+ bool Configure();
+ bool WriteControl();
+ bool Enable();
+ bool Disable();
+ bool WriteEnable(int value);
+ std::vector ReadRegister(Register reg, int count);
+ bool WriteRegister(Register reg, int value);
+ void RecordFailure(FailureReason reason);
+ double Normalize(int rawValue) const;
+ int IntegrationCycles() const;
+
+ I2C m_i2c;
+
+ Gain m_gain = Gain::GAIN_4;
+ LedDrive m_ledDrive = LedDrive::PERCENT_50;
+ int m_integrationTimeRegister =
+ IntegrationTimeRegister(DEFAULT_INTEGRATION_TIME);
+ int m_proximityPulseCount = DEFAULT_PROXIMITY_PULSE_COUNT;
+ double m_softwareGain = 1.0;
+
+ double m_aParam = DEFAULT_A_PARAM;
+ double m_bParam = DEFAULT_B_PARAM;
+ double m_cParam = DEFAULT_C_PARAM;
+
+ bool m_initialized = false;
+ uint8_t m_deviceId = 0;
+ DeviceStatus m_deviceStatus = DeviceStatus::NOT_INITIALIZED;
+ std::optional m_lastFailureReason;
+ uint64_t m_failureCount = 0;
+
+ int m_rawClear = 0;
+ int m_rawRed = 0;
+ int m_rawGreen = 0;
+ int m_rawBlue = 0;
+ int m_rawProximity = 0;
+};
+
+} // namespace wpi
diff --git a/drivers/src/test/java/org/wpilib/drivers/range/RevColorSensorV2Test.java b/drivers/src/test/java/org/wpilib/drivers/range/RevColorSensorV2Test.java
new file mode 100644
index 00000000000..7ab5b8b7770
--- /dev/null
+++ b/drivers/src/test/java/org/wpilib/drivers/range/RevColorSensorV2Test.java
@@ -0,0 +1,495 @@
+// 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 org.wpilib.drivers.range;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.wpilib.units.Units.Meters;
+import static org.wpilib.units.Units.Milliseconds;
+import static org.wpilib.units.Units.Seconds;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.wpilib.drivers.range.RevColorSensorV2.DeviceStatus;
+import org.wpilib.drivers.range.RevColorSensorV2.FailureReason;
+import org.wpilib.drivers.range.RevColorSensorV2.Gain;
+import org.wpilib.drivers.range.RevColorSensorV2.LedDrive;
+import org.wpilib.drivers.range.RevColorSensorV2.Register;
+import org.wpilib.hardware.bus.I2C;
+import org.wpilib.hardware.hal.HAL;
+import org.wpilib.simulation.CallbackStore;
+import org.wpilib.simulation.I2CSim;
+
+@ResourceLock("I2C")
+class RevColorSensorV2Test {
+ private static final double DELTA = 1e-9;
+
+ /**
+ * Command bit plus the auto-increment command type. The sensor advances through consecutive
+ * registers during a multi-byte read only when the command type is auto-increment.
+ */
+ private static final int COMMAND_AUTO_INCREMENT = 0x80 | (0x01 << 5);
+
+ private static final byte TMD37821_DEVICE_ID = 0x60;
+
+ private static final int STATUS_COLOR_VALID = 0x01;
+ private static final int STATUS_PROXIMITY_VALID = 0x02;
+ private static final int STATUS_ALL_VALID = STATUS_COLOR_VALID | STATUS_PROXIMITY_VALID;
+
+ /** Default integration time of 24 ms, expressed as ten 2.4 ms cycles. */
+ private static final int DEFAULT_ATIME = 246;
+
+ private static final int DEFAULT_MAXIMUM_RAW_COLOR_VALUE = 10240;
+
+ private final I2CSim m_i2cSim = new I2CSim(I2C.Port.PORT_0.value);
+ private final Map m_registerData = new HashMap<>();
+ private final List m_writes = new ArrayList<>();
+ private final List m_readRegisters = new ArrayList<>();
+ private final List m_readCounts = new ArrayList<>();
+
+ private CallbackStore m_readCallback;
+ private CallbackStore m_writeCallback;
+ private int m_selectedRegister;
+
+ @BeforeEach
+ void setUp() {
+ HAL.initialize();
+ m_i2cSim.resetData();
+ m_readCallback =
+ m_i2cSim.registerReadCallback(
+ (name, buffer, count) -> {
+ m_readRegisters.add(m_selectedRegister);
+ m_readCounts.add(count);
+ byte[] data = m_registerData.get(m_selectedRegister);
+ if (data != null) {
+ System.arraycopy(data, 0, buffer, 0, Math.min(count, data.length));
+ }
+ });
+ m_writeCallback =
+ m_i2cSim.registerWriteCallback(
+ (name, buffer, count) -> {
+ if (count == 0) {
+ return;
+ }
+ m_selectedRegister = Byte.toUnsignedInt(buffer[0]);
+ if (count > 1) {
+ m_writes.add(Arrays.copyOf(buffer, count));
+ }
+ });
+ setRegister(Register.DEVICE_ID, new byte[] {TMD37821_DEVICE_ID});
+ }
+
+ @AfterEach
+ void tearDown() {
+ m_readCallback.close();
+ m_writeCallback.close();
+ m_i2cSim.resetData();
+ }
+
+ @Test
+ void usesDefaultAddressAndCloses() {
+ var sensor = new RevColorSensorV2(I2C.Port.PORT_0);
+
+ assertEquals(I2C.Port.PORT_0, sensor.getPort());
+ assertEquals(RevColorSensorV2.DEFAULT_ADDRESS, sensor.getDeviceAddress());
+
+ sensor.close();
+ assertThrows(IllegalStateException.class, sensor::getPort);
+ assertThrows(IllegalStateException.class, sensor::update);
+ assertThrows(IllegalStateException.class, sensor::getDeviceStatus);
+ assertThrows(IllegalStateException.class, sensor::getColor);
+ assertThrows(IllegalStateException.class, sensor::getRawProximity);
+ assertThrows(IllegalStateException.class, sensor::getDistanceMeters);
+ assertThrows(IllegalStateException.class, () -> sensor.setGain(Gain.GAIN_16));
+
+ sensor.close();
+ }
+
+ @Test
+ void rejectsInvalidI2cAddresses() {
+ assertThrows(IllegalArgumentException.class, () -> new RevColorSensorV2(I2C.Port.PORT_0, -1));
+ assertThrows(IllegalArgumentException.class, () -> new RevColorSensorV2(I2C.Port.PORT_0, 0x80));
+ }
+
+ @Test
+ void configuresSensorOnConstruction() {
+ setRegister(Register.CONTROL, new byte[] {0x00});
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertEquals(TMD37821_DEVICE_ID, sensor.getDeviceId());
+ assertEquals(0, sensor.getFailureCount());
+ assertNull(sensor.getLastFailureReason());
+
+ // The integrator is turned off, the gain and timing are written, and the color and
+ // proximity channels are enabled after the power on warm-up.
+ assertEquals(6, m_writes.size());
+ assertWrite(m_writes.get(0), Register.ENABLE, 0x00);
+ assertWrite(m_writes.get(1), Register.ATIME, DEFAULT_ATIME);
+ assertWrite(m_writes.get(2), Register.CONTROL, 0x61);
+ assertWrite(m_writes.get(3), Register.PPULSE, 8);
+ assertWrite(m_writes.get(4), Register.ENABLE, 0x01);
+ assertWrite(m_writes.get(5), Register.ENABLE, 0x07);
+ }
+ }
+
+ @Test
+ void overwritesRetainedControlFields() {
+ // A retained proximity diode bit and proximity gain, as the sensor holds them across a
+ // program restart. Disabling the sensor does not reset this register, and preserving these
+ // bits would select a reserved diode and proximity gain instead of the calibrated ones.
+ setRegister(Register.CONTROL, new byte[] {0x1C});
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ // Bits 7:6 LED drive (50%), 5:4 IR diode, 3:2 proximity gain 1x, 1:0 color gain (4x).
+ assertWrite(lastWriteTo(Register.CONTROL), Register.CONTROL, 0x61);
+
+ sensor.setGain(Gain.GAIN_60);
+ sensor.setLedDrive(LedDrive.PERCENT_12_5);
+
+ // The diode and proximity gain fields stay at their calibrated values.
+ assertWrite(lastWriteTo(Register.CONTROL), Register.CONTROL, 0xE3);
+ }
+ }
+
+ @Test
+ void rejectsUnexpectedDeviceId() {
+ setRegister(Register.DEVICE_ID, new byte[] {0x44});
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ assertEquals(DeviceStatus.FAULT_UNEXPECTED_DEVICE_ID, sensor.getDeviceStatus());
+ assertEquals(FailureReason.UNEXPECTED_DEVICE_ID, sensor.getLastFailureReason());
+ assertEquals(0x44, sensor.getDeviceId());
+ assertEquals(0, m_writes.size());
+ }
+ }
+
+ @Test
+ void retriesConfigurationOnUpdate() {
+ setRegister(Register.DEVICE_ID, new byte[] {0x00});
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ assertEquals(DeviceStatus.FAULT_UNEXPECTED_DEVICE_ID, sensor.getDeviceStatus());
+
+ setRegister(Register.DEVICE_ID, new byte[] {TMD37821_DEVICE_ID});
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 1, 2, 3, 4, 5));
+ sensor.update();
+
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertEquals(1, sensor.getRawClear());
+ assertEquals(5, sensor.getRawProximity());
+ }
+ }
+
+ @Test
+ void decodesColorAndProximity() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 10240, 5120, 2560, 1024, 512));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertEquals(DEFAULT_MAXIMUM_RAW_COLOR_VALUE, sensor.getMaximumRawColorValue());
+
+ assertEquals(10240, sensor.getRawClear());
+ assertEquals(5120, sensor.getRawRed());
+ assertEquals(2560, sensor.getRawGreen());
+ assertEquals(1024, sensor.getRawBlue());
+ assertEquals(512, sensor.getRawProximity());
+
+ assertEquals(1.0, sensor.getClear(), DELTA);
+ assertEquals(0.5, sensor.getRed(), DELTA);
+ assertEquals(0.25, sensor.getGreen(), DELTA);
+ assertEquals(0.1, sensor.getBlue(), DELTA);
+
+ // Color rounds to 12 bits of precision.
+ assertEquals(0.5, sensor.getColor().red, 1e-3);
+ assertEquals(0.25, sensor.getColor().green, 1e-3);
+ assertEquals(0.1, sensor.getColor().blue, 1e-3);
+
+ assertEquals(512.0 / 1023.0, sensor.getProximity(), DELTA);
+ }
+ }
+
+ @Test
+ void addressesTheBulkReadWithTheAutoIncrementCommandType() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 1, 2, 3, 4, 5));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ m_readRegisters.clear();
+ m_readCounts.clear();
+ sensor.update();
+
+ // Without the auto-increment command type the sensor would return the STATUS register
+ // once per byte instead of advancing through the color and proximity registers.
+ assertEquals(1, m_readRegisters.size());
+ assertEquals(0xB3, m_readRegisters.get(0));
+ assertEquals(Register.STATUS.getAddress() | 0x80 | 0x20, m_readRegisters.get(0));
+ assertEquals(11, m_readCounts.get(0));
+ }
+ }
+
+ @Test
+ void clampsNormalizedChannelsWithSoftwareGain() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 0, 4096, 1024, 0, 2000));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.setSoftwareGain(4.0);
+ sensor.update();
+
+ assertEquals(4.0, sensor.getSoftwareGain(), DELTA);
+ assertEquals(1.0, sensor.getRed(), DELTA);
+ assertEquals(0.4, sensor.getGreen(), DELTA);
+ assertEquals(4096, sensor.getRawRed());
+
+ // The proximity channel saturates at its raw maximum.
+ assertEquals(1.0, sensor.getProximity(), DELTA);
+ }
+ }
+
+ @Test
+ void convertsProximityToDistance() {
+ // A raw reading of a + b * (cm + c)^-2 must convert back to cm.
+ int rawOptical = (int) Math.round(186.347 + 30403.5 * Math.pow(5.0 + 0.576649, -2.0));
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, rawOptical));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+
+ assertEquals(0.05, sensor.getDistanceMeters(), 1e-3);
+ assertEquals(0.05, sensor.getDistance().in(Meters), 1e-3);
+ }
+ }
+
+ @Test
+ void reportsOutOfRangeDistanceAsNaN() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 100));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+
+ assertTrue(Double.isNaN(sensor.getDistanceMeters()));
+ assertTrue(Double.isNaN(sensor.getDistance().magnitude()));
+ }
+ }
+
+ @Test
+ void appliesCustomDistanceCalibration() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 300));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+ double defaultDistance = sensor.getDistanceMeters();
+
+ sensor.setDistanceCalibration(186.347, 2 * 30403.5, 0.576649);
+
+ // Doubling the b parameter means the same reading is produced by a more distant target.
+ assertTrue(sensor.getDistanceMeters() > defaultDistance);
+ }
+ }
+
+ @Test
+ void preservesMeasurementsWhenDataIsNotValid() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 100, 200, 300, 400, 500));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+
+ setRegister(Register.STATUS, statusBlock(0x00, 1, 2, 3, 4, 5));
+ sensor.update();
+
+ assertEquals(DeviceStatus.FAULT_BAD_READ, sensor.getDeviceStatus());
+ assertEquals(FailureReason.PROXIMITY_DATA_NOT_VALID, sensor.getLastFailureReason());
+ assertEquals(2, sensor.getFailureCount());
+ assertEquals(100, sensor.getRawClear());
+ assertEquals(500, sensor.getRawProximity());
+
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 7, 8, 9, 10, 11));
+ sensor.update();
+
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertEquals(7, sensor.getRawClear());
+ assertEquals(11, sensor.getRawProximity());
+ }
+ }
+
+ @Test
+ void updatesProximityWhenOnlyColorIsNotValid() {
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 100, 200, 300, 400, 500));
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.update();
+
+ setRegister(Register.STATUS, statusBlock(STATUS_PROXIMITY_VALID, 1, 2, 3, 4, 900));
+ sensor.update();
+
+ assertEquals(FailureReason.COLOR_DATA_NOT_VALID, sensor.getLastFailureReason());
+ assertEquals(1, sensor.getFailureCount());
+ assertEquals(100, sensor.getRawClear());
+ assertEquals(900, sensor.getRawProximity());
+ }
+ }
+
+ @Test
+ void reportsAbortedTransactions() {
+ var i2c = new FailingI2C();
+ i2c.m_failReads = true;
+
+ try (var sensor = new RevColorSensorV2(i2c)) {
+ assertEquals(DeviceStatus.FAULT_BAD_READ, sensor.getDeviceStatus());
+ assertEquals(FailureReason.I2C_READ_ABORTED, sensor.getLastFailureReason());
+ assertEquals(1, sensor.getFailureCount());
+ assertEquals(0, m_writes.size());
+
+ i2c.m_failReads = false;
+ i2c.m_failWrites = true;
+ sensor.update();
+
+ assertEquals(FailureReason.I2C_WRITE_ABORTED, sensor.getLastFailureReason());
+ assertEquals(2, sensor.getFailureCount());
+
+ i2c.m_failWrites = false;
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 1, 2, 3, 4, 5));
+ sensor.update();
+
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertEquals(2, sensor.getRawRed());
+ }
+ }
+
+ @Test
+ void roundsIntegrationTimeUpToWholeCycles() {
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.setIntegrationTime(Milliseconds.of(100));
+ assertEquals(100.8, sensor.getIntegrationTime().in(Milliseconds), DELTA);
+ assertEquals(42 * 1024, sensor.getMaximumRawColorValue());
+ assertWrite(lastWriteTo(Register.ATIME), Register.ATIME, 256 - 42);
+
+ // The color channels saturate at 16 bits well before the longest integration time.
+ sensor.setIntegrationTime(Seconds.of(1));
+ assertEquals(614.4, sensor.getIntegrationTime().in(Milliseconds), DELTA);
+ assertEquals(65535, sensor.getMaximumRawColorValue());
+ assertWrite(lastWriteTo(Register.ATIME), Register.ATIME, 0);
+
+ sensor.setIntegrationTime(Milliseconds.of(1));
+ assertEquals(2.4, sensor.getIntegrationTime().in(Milliseconds), DELTA);
+ assertEquals(1024, sensor.getMaximumRawColorValue());
+ assertWrite(lastWriteTo(Register.ATIME), Register.ATIME, 255);
+ }
+ }
+
+ @Test
+ void rejectsInvalidConfigurationValues() {
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ assertThrows(NullPointerException.class, () -> sensor.setGain(null));
+ assertThrows(NullPointerException.class, () -> sensor.setLedDrive(null));
+ assertThrows(NullPointerException.class, () -> sensor.setIntegrationTime(null));
+ assertThrows(
+ IllegalArgumentException.class, () -> sensor.setIntegrationTime(Milliseconds.of(0)));
+ assertThrows(
+ IllegalArgumentException.class, () -> sensor.setIntegrationTime(Milliseconds.of(-1)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> sensor.setIntegrationTime(Milliseconds.of(Double.NaN)));
+ assertThrows(IllegalArgumentException.class, () -> sensor.setProximityPulseCount(0));
+ assertThrows(IllegalArgumentException.class, () -> sensor.setProximityPulseCount(256));
+ assertThrows(IllegalArgumentException.class, () -> sensor.setSoftwareGain(0.0));
+ assertThrows(IllegalArgumentException.class, () -> sensor.setSoftwareGain(Double.NaN));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> sensor.setDistanceCalibration(Double.POSITIVE_INFINITY, 1.0, 1.0));
+
+ // The sensor keeps its defaults after every rejected value.
+ assertEquals(Gain.GAIN_4, sensor.getGain());
+ assertEquals(LedDrive.PERCENT_50, sensor.getLedDrive());
+ assertEquals(24.0, sensor.getIntegrationTime().in(Milliseconds), DELTA);
+ assertEquals(8, sensor.getProximityPulseCount());
+ assertEquals(1.0, sensor.getSoftwareGain(), DELTA);
+ }
+ }
+
+ @Test
+ void storesConfigurationWrittenBeforeInitializationSucceeds() {
+ setRegister(Register.DEVICE_ID, new byte[] {0x00});
+
+ try (var sensor = new RevColorSensorV2(I2C.Port.PORT_0)) {
+ sensor.setGain(Gain.GAIN_16);
+ sensor.setProximityPulseCount(32);
+ assertEquals(0, m_writes.size());
+
+ setRegister(Register.DEVICE_ID, new byte[] {TMD37821_DEVICE_ID});
+ setRegister(Register.STATUS, statusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 0));
+ sensor.update();
+
+ assertEquals(DeviceStatus.READY, sensor.getDeviceStatus());
+ assertWrite(lastWriteTo(Register.CONTROL), Register.CONTROL, 0x62);
+ assertWrite(lastWriteTo(Register.PPULSE), Register.PPULSE, 32);
+ }
+ }
+
+ private void setRegister(Register register, byte[] data) {
+ m_registerData.put(register.getAddress() | COMMAND_AUTO_INCREMENT, data);
+ }
+
+ private byte[] lastWriteTo(Register register) {
+ for (int i = m_writes.size() - 1; i >= 0; i--) {
+ if (Byte.toUnsignedInt(m_writes.get(i)[0])
+ == (register.getAddress() | COMMAND_AUTO_INCREMENT)) {
+ return m_writes.get(i);
+ }
+ }
+ throw new AssertionError("No write to " + register);
+ }
+
+ private static byte[] statusBlock(
+ int status, int clear, int red, int green, int blue, int proximity) {
+ byte[] data = new byte[11];
+ data[0] = (byte) status;
+ putUnsignedShort(data, 1, clear);
+ putUnsignedShort(data, 3, red);
+ putUnsignedShort(data, 5, green);
+ putUnsignedShort(data, 7, blue);
+ putUnsignedShort(data, 9, proximity);
+ return data;
+ }
+
+ private static void putUnsignedShort(byte[] data, int offset, int value) {
+ data[offset] = (byte) value;
+ data[offset + 1] = (byte) (value >> 8);
+ }
+
+ private static void assertWrite(byte[] write, Register register, int expected) {
+ assertEquals(register.getAddress() | COMMAND_AUTO_INCREMENT, Byte.toUnsignedInt(write[0]));
+ assertEquals(expected, Byte.toUnsignedInt(write[1]));
+ }
+
+ private static final class FailingI2C extends I2C {
+ private boolean m_failReads;
+ private boolean m_failWrites;
+
+ FailingI2C() {
+ super(I2C.Port.PORT_0, RevColorSensorV2.DEFAULT_ADDRESS);
+ }
+
+ @Override
+ public boolean read(int registerAddress, int count, byte[] buffer) {
+ return m_failReads || super.read(registerAddress, count, buffer);
+ }
+
+ @Override
+ public synchronized boolean write(int registerAddress, int data) {
+ return m_failWrites || super.write(registerAddress, data);
+ }
+ }
+}
diff --git a/drivers/src/test/native/cpp/range/RevColorSensorV2Test.cpp b/drivers/src/test/native/cpp/range/RevColorSensorV2Test.cpp
new file mode 100644
index 00000000000..a525d367bf8
--- /dev/null
+++ b/drivers/src/test/native/cpp/range/RevColorSensorV2Test.cpp
@@ -0,0 +1,480 @@
+// 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.
+
+#include "wpi/drivers/range/RevColorSensorV2.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "wpi/hal/simulation/I2CData.h"
+
+namespace {
+
+using DeviceStatus = wpi::RevColorSensorV2::DeviceStatus;
+using FailureReason = wpi::RevColorSensorV2::FailureReason;
+using Gain = wpi::RevColorSensorV2::Gain;
+using LedDrive = wpi::RevColorSensorV2::LedDrive;
+using Register = wpi::RevColorSensorV2::Register;
+
+/// Command bit plus the auto-increment command type. The sensor advances
+/// through consecutive registers during a multi-byte read only when the command
+/// type is auto-increment.
+constexpr int COMMAND_AUTO_INCREMENT = 0x80 | (0x01 << 5);
+
+constexpr uint8_t TMD37821_DEVICE_ID = 0x60;
+
+constexpr int STATUS_COLOR_VALID = 0x01;
+constexpr int STATUS_PROXIMITY_VALID = 0x02;
+constexpr int STATUS_ALL_VALID = STATUS_COLOR_VALID | STATUS_PROXIMITY_VALID;
+
+/// Default integration time of 24 ms, expressed as ten 2.4 ms cycles.
+constexpr int DEFAULT_ATIME = 246;
+
+constexpr int DEFAULT_MAXIMUM_RAW_COLOR_VALUE = 10240;
+
+void PutUnsignedShort(std::vector& data, std::size_t offset,
+ int value) {
+ data[offset] = static_cast(value);
+ data[offset + 1] = static_cast(value >> 8);
+}
+
+std::vector StatusBlock(int status, int clear, int red, int green,
+ int blue, int proximity) {
+ std::vector data(11, 0);
+ data[0] = static_cast(status);
+ PutUnsignedShort(data, 1, clear);
+ PutUnsignedShort(data, 3, red);
+ PutUnsignedShort(data, 5, green);
+ PutUnsignedShort(data, 7, blue);
+ PutUnsignedShort(data, 9, proximity);
+ return data;
+}
+
+class ColorSensorTestFixture {
+ public:
+ ColorSensorTestFixture() {
+ HALSIM_ResetI2CData(0);
+ m_readUid = HALSIM_RegisterI2CReadCallback(0, ReadCallback, this);
+ m_writeUid = HALSIM_RegisterI2CWriteCallback(0, WriteCallback, this);
+ SetRegister(Register::DEVICE_ID, {TMD37821_DEVICE_ID});
+ }
+
+ ~ColorSensorTestFixture() {
+ HALSIM_CancelI2CReadCallback(0, m_readUid);
+ HALSIM_CancelI2CWriteCallback(0, m_writeUid);
+ HALSIM_ResetI2CData(0);
+ }
+
+ void SetRegister(Register reg, std::vector data) {
+ m_registerData[static_cast(reg) | COMMAND_AUTO_INCREMENT] =
+ std::move(data);
+ }
+
+ /// Returns the most recent two-byte write to the given register.
+ std::vector LastWriteTo(Register reg) const {
+ auto address =
+ static_cast(static_cast(reg) | COMMAND_AUTO_INCREMENT);
+ for (auto it = m_writes.rbegin(); it != m_writes.rend(); ++it) {
+ if (it->size() > 1 && (*it)[0] == address) {
+ return *it;
+ }
+ }
+ FAIL("No write to the requested register");
+ return {};
+ }
+
+ std::unordered_map> m_registerData;
+ std::vector> m_writes;
+ std::vector m_readRegisters;
+ std::vector m_readCounts;
+ int m_selectedRegister = 0;
+
+ private:
+ static void ReadCallback(const char*, void* param, unsigned char* buffer,
+ unsigned int count) {
+ auto& self = *static_cast(param);
+ self.m_readRegisters.push_back(self.m_selectedRegister);
+ self.m_readCounts.push_back(count);
+ std::fill_n(buffer, count, 0);
+ auto it = self.m_registerData.find(self.m_selectedRegister);
+ if (it != self.m_registerData.end()) {
+ std::copy_n(it->second.begin(),
+ std::min(count, it->second.size()), buffer);
+ }
+ }
+
+ static void WriteCallback(const char*, void* param,
+ const unsigned char* buffer, unsigned int count) {
+ auto& self = *static_cast(param);
+ if (count == 0) {
+ return;
+ }
+ self.m_selectedRegister = buffer[0];
+ if (count > 1) {
+ self.m_writes.emplace_back(buffer, buffer + count);
+ }
+ }
+
+ int32_t m_readUid;
+ int32_t m_writeUid;
+};
+
+std::vector RegisterWrite(Register reg, int value) {
+ return {static_cast(static_cast(reg) | COMMAND_AUTO_INCREMENT),
+ static_cast(value)};
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 configures the sensor on construction",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::CONTROL, {0x00});
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ CHECK(sensor.GetPort() == wpi::I2C::Port::PORT_0);
+ CHECK(sensor.GetDeviceAddress() == wpi::RevColorSensorV2::DEFAULT_ADDRESS);
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::READY);
+ CHECK(sensor.GetDeviceId() == TMD37821_DEVICE_ID);
+ CHECK(sensor.GetFailureCount() == 0);
+ CHECK_FALSE(sensor.GetLastFailureReason().has_value());
+
+ // The integrator is turned off, the gain and timing are written, and the
+ // color and proximity channels are enabled after the power on warm-up.
+ REQUIRE(m_writes.size() == 6);
+ CHECK(m_writes[0] == RegisterWrite(Register::ENABLE, 0x00));
+ CHECK(m_writes[1] == RegisterWrite(Register::ATIME, DEFAULT_ATIME));
+ CHECK(m_writes[2] == RegisterWrite(Register::CONTROL, 0x61));
+ CHECK(m_writes[3] == RegisterWrite(Register::PPULSE, 8));
+ CHECK(m_writes[4] == RegisterWrite(Register::ENABLE, 0x01));
+ CHECK(m_writes[5] == RegisterWrite(Register::ENABLE, 0x07));
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 overwrites retained control fields",
+ "[drivers][rev-color-sensor-v2]") {
+ // A retained proximity diode bit and proximity gain, as the sensor holds them
+ // across a program restart. Disabling the sensor does not reset this
+ // register, and preserving these bits would select a reserved diode and
+ // proximity gain instead of the calibrated ones.
+ SetRegister(Register::CONTROL, {0x1C});
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ // Bits 7:6 LED drive (50%), 5:4 IR diode, 3:2 proximity gain 1x, 1:0 color
+ // gain (4x).
+ CHECK(LastWriteTo(Register::CONTROL) ==
+ RegisterWrite(Register::CONTROL, 0x61));
+
+ sensor.SetGain(Gain::GAIN_60);
+ sensor.SetLedDrive(LedDrive::PERCENT_12_5);
+
+ // The diode and proximity gain fields stay at their calibrated values.
+ CHECK(LastWriteTo(Register::CONTROL) ==
+ RegisterWrite(Register::CONTROL, 0xE3));
+ CHECK(sensor.GetGain() == Gain::GAIN_60);
+ CHECK(sensor.GetLedDrive() == LedDrive::PERCENT_12_5);
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 rejects an unexpected device ID",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::DEVICE_ID, {0x44});
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::FAULT_UNEXPECTED_DEVICE_ID);
+ REQUIRE(sensor.GetLastFailureReason().has_value());
+ CHECK(*sensor.GetLastFailureReason() == FailureReason::UNEXPECTED_DEVICE_ID);
+ CHECK(sensor.GetDeviceId() == 0x44);
+ CHECK(m_writes.empty());
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 retries configuration on update",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::DEVICE_ID, {0x00});
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::FAULT_UNEXPECTED_DEVICE_ID);
+
+ SetRegister(Register::DEVICE_ID, {TMD37821_DEVICE_ID});
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 1, 2, 3, 4, 5));
+ sensor.Update();
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::READY);
+ CHECK(sensor.GetRawClear() == 1);
+ CHECK(sensor.GetRawProximity() == 5);
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 decodes color and proximity",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_ALL_VALID, 10240, 5120, 2560, 1024, 512));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::READY);
+ CHECK(sensor.GetMaximumRawColorValue() == DEFAULT_MAXIMUM_RAW_COLOR_VALUE);
+
+ CHECK(sensor.GetRawClear() == 10240);
+ CHECK(sensor.GetRawRed() == 5120);
+ CHECK(sensor.GetRawGreen() == 2560);
+ CHECK(sensor.GetRawBlue() == 1024);
+ CHECK(sensor.GetRawProximity() == 512);
+
+ CHECK(sensor.GetClear() == Catch::Approx(1.0));
+ CHECK(sensor.GetRed() == Catch::Approx(0.5));
+ CHECK(sensor.GetGreen() == Catch::Approx(0.25));
+ CHECK(sensor.GetBlue() == Catch::Approx(0.1));
+
+ // Color rounds to 12 bits of precision.
+ CHECK(sensor.GetColor().red == Catch::Approx(0.5).margin(1e-3));
+ CHECK(sensor.GetColor().green == Catch::Approx(0.25).margin(1e-3));
+ CHECK(sensor.GetColor().blue == Catch::Approx(0.1).margin(1e-3));
+
+ CHECK(sensor.GetProximity() == Catch::Approx(512.0 / 1023.0));
+}
+
+TEST_CASE_METHOD(
+ ColorSensorTestFixture,
+ "RevColorSensorV2 addresses the bulk read with the auto-increment type",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 1, 2, 3, 4, 5));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ m_readRegisters.clear();
+ m_readCounts.clear();
+ sensor.Update();
+
+ // Without the auto-increment command type the sensor would return the STATUS
+ // register once per byte instead of advancing through the color and
+ // proximity registers.
+ REQUIRE(m_readRegisters.size() == 1);
+ CHECK(m_readRegisters[0] == 0xB3);
+ CHECK(m_readRegisters[0] ==
+ (static_cast(Register::STATUS) | 0x80 | 0x20));
+ CHECK(m_readCounts[0] == 11);
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 clamps normalized channels",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_ALL_VALID, 0, 4096, 1024, 0, 2000));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.SetSoftwareGain(4.0);
+ sensor.Update();
+
+ CHECK(sensor.GetSoftwareGain() == Catch::Approx(4.0));
+ CHECK(sensor.GetRed() == Catch::Approx(1.0));
+ CHECK(sensor.GetGreen() == Catch::Approx(0.4));
+ CHECK(sensor.GetRawRed() == 4096);
+
+ // The proximity channel saturates at its raw maximum.
+ CHECK(sensor.GetProximity() == Catch::Approx(1.0));
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 converts proximity to distance",
+ "[drivers][rev-color-sensor-v2]") {
+ // A raw reading of a + b * (cm + c)^-2 must convert back to cm.
+ int rawOptical = static_cast(
+ std::lround(186.347 + 30403.5 * std::pow(5.0 + 0.576649, -2.0)));
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, rawOptical));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+
+ CHECK(sensor.GetDistance().value() == Catch::Approx(0.05).margin(1e-3));
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 reports an out of range distance as NaN",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 100));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+
+ CHECK(std::isnan(sensor.GetDistance().value()));
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 applies a custom distance calibration",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 300));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+ auto defaultDistance = sensor.GetDistance();
+
+ sensor.SetDistanceCalibration(186.347, 2 * 30403.5, 0.576649);
+
+ // Doubling the b parameter means the same reading is produced by a more
+ // distant target.
+ CHECK(sensor.GetDistance() > defaultDistance);
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 preserves measurements when data is invalid",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_ALL_VALID, 100, 200, 300, 400, 500));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+
+ SetRegister(Register::STATUS, StatusBlock(0x00, 1, 2, 3, 4, 5));
+ sensor.Update();
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::FAULT_BAD_READ);
+ REQUIRE(sensor.GetLastFailureReason().has_value());
+ CHECK(*sensor.GetLastFailureReason() ==
+ FailureReason::PROXIMITY_DATA_NOT_VALID);
+ CHECK(sensor.GetFailureCount() == 2);
+ CHECK(sensor.GetRawClear() == 100);
+ CHECK(sensor.GetRawProximity() == 500);
+
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 7, 8, 9, 10, 11));
+ sensor.Update();
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::READY);
+ CHECK(sensor.GetRawClear() == 7);
+ CHECK(sensor.GetRawProximity() == 11);
+}
+
+TEST_CASE_METHOD(
+ ColorSensorTestFixture,
+ "RevColorSensorV2 updates proximity when only color is invalid",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_ALL_VALID, 100, 200, 300, 400, 500));
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.Update();
+
+ SetRegister(Register::STATUS,
+ StatusBlock(STATUS_PROXIMITY_VALID, 1, 2, 3, 4, 900));
+ sensor.Update();
+
+ REQUIRE(sensor.GetLastFailureReason().has_value());
+ CHECK(*sensor.GetLastFailureReason() == FailureReason::COLOR_DATA_NOT_VALID);
+ CHECK(sensor.GetFailureCount() == 1);
+ CHECK(sensor.GetRawClear() == 100);
+ CHECK(sensor.GetRawProximity() == 900);
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 rounds integration time up to whole cycles",
+ "[drivers][rev-color-sensor-v2]") {
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ sensor.SetIntegrationTime(wpi::units::millisecond_t{100});
+ CHECK(sensor.GetIntegrationTime().value() == Catch::Approx(100.8));
+ CHECK(sensor.GetMaximumRawColorValue() == 42 * 1024);
+ CHECK(LastWriteTo(Register::ATIME) ==
+ RegisterWrite(Register::ATIME, 256 - 42));
+
+ // The color channels saturate at 16 bits well before the longest
+ // integration time.
+ sensor.SetIntegrationTime(wpi::units::second_t{1});
+ CHECK(sensor.GetIntegrationTime().value() == Catch::Approx(614.4));
+ CHECK(sensor.GetMaximumRawColorValue() == 65535);
+ CHECK(LastWriteTo(Register::ATIME) == RegisterWrite(Register::ATIME, 0));
+
+ sensor.SetIntegrationTime(wpi::units::millisecond_t{1});
+ CHECK(sensor.GetIntegrationTime().value() == Catch::Approx(2.4));
+ CHECK(sensor.GetMaximumRawColorValue() == 1024);
+ CHECK(LastWriteTo(Register::ATIME) == RegisterWrite(Register::ATIME, 255));
+}
+
+TEST_CASE_METHOD(
+ ColorSensorTestFixture,
+ "RevColorSensorV2 saturates integration times beyond the sensor range",
+ "[drivers][rev-color-sensor-v2]") {
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ // A duration whose cycle count cannot be represented as an int still has to
+ // saturate at the documented maximum. Converting such a value to int before
+ // clamping would be undefined behavior.
+ for (double milliseconds : {1e6, 1e18, std::numeric_limits::max()}) {
+ sensor.SetIntegrationTime(wpi::units::millisecond_t{milliseconds});
+
+ CHECK(sensor.GetIntegrationTime().value() == Catch::Approx(614.4));
+ CHECK(sensor.GetMaximumRawColorValue() == 65535);
+ CHECK(LastWriteTo(Register::ATIME) == RegisterWrite(Register::ATIME, 0));
+ }
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 rejects invalid configuration values",
+ "[drivers][rev-color-sensor-v2]") {
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+
+ CHECK_THROWS_AS(sensor.SetIntegrationTime(wpi::units::millisecond_t{0}),
+ std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetIntegrationTime(wpi::units::millisecond_t{-1}),
+ std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetProximityPulseCount(0), std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetProximityPulseCount(256), std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetSoftwareGain(0.0), std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetGain(static_cast(9)), std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetLedDrive(static_cast(9)),
+ std::invalid_argument);
+ CHECK_THROWS_AS(sensor.SetDistanceCalibration(
+ std::numeric_limits::infinity(), 1.0, 1.0),
+ std::invalid_argument);
+
+ // The sensor keeps its defaults after every rejected value.
+ CHECK(sensor.GetGain() == Gain::GAIN_4);
+ CHECK(sensor.GetLedDrive() == LedDrive::PERCENT_50);
+ CHECK(sensor.GetIntegrationTime().value() == Catch::Approx(24.0));
+ CHECK(sensor.GetProximityPulseCount() == 8);
+ CHECK(sensor.GetSoftwareGain() == Catch::Approx(1.0));
+}
+
+TEST_CASE_METHOD(ColorSensorTestFixture,
+ "RevColorSensorV2 rejects invalid I2C addresses",
+ "[drivers][rev-color-sensor-v2]") {
+ CHECK_THROWS_AS(wpi::RevColorSensorV2(wpi::I2C::Port::PORT_0, -1),
+ std::invalid_argument);
+ CHECK_THROWS_AS(wpi::RevColorSensorV2(wpi::I2C::Port::PORT_0, 0x80),
+ std::invalid_argument);
+}
+
+TEST_CASE_METHOD(
+ ColorSensorTestFixture,
+ "RevColorSensorV2 stores configuration written before initialization",
+ "[drivers][rev-color-sensor-v2]") {
+ SetRegister(Register::DEVICE_ID, {0x00});
+
+ wpi::RevColorSensorV2 sensor{wpi::I2C::Port::PORT_0};
+ sensor.SetGain(Gain::GAIN_16);
+ sensor.SetProximityPulseCount(32);
+ CHECK(m_writes.empty());
+
+ SetRegister(Register::DEVICE_ID, {TMD37821_DEVICE_ID});
+ SetRegister(Register::STATUS, StatusBlock(STATUS_ALL_VALID, 0, 0, 0, 0, 0));
+ sensor.Update();
+
+ CHECK(sensor.GetDeviceStatus() == DeviceStatus::READY);
+ CHECK(LastWriteTo(Register::CONTROL) ==
+ RegisterWrite(Register::CONTROL, 0x62));
+ CHECK(LastWriteTo(Register::PPULSE) == RegisterWrite(Register::PPULSE, 32));
+}
+
+} // namespace