diff --git a/lib/src/main/java/com/team2813/lib2813/control/ControlMode.java b/lib/src/main/java/com/team2813/lib2813/control/ControlMode.java index 0ae960c7..73a37ab9 100644 --- a/lib/src/main/java/com/team2813/lib2813/control/ControlMode.java +++ b/lib/src/main/java/com/team2813/lib2813/control/ControlMode.java @@ -2,18 +2,103 @@ import com.revrobotics.spark.SparkBase.ControlType; +/** + * Enumeration defining standardized motor control modes for the team's control system. + * + *
This enum provides a vendor-neutral abstraction over different motor controller control modes + * while maintaining compatibility with specific hardware implementations. Each control mode maps to + * the appropriate vendor-specific control type for seamless integration with different motor + * controller families. + * + *
The enum currently includes mappings for REV Robotics SPARK controllers, with each mode + * corresponding to a specific {@link ControlType} from the SPARK API. Additional vendor mappings + * can be added as needed. + * + * @author Team 2813 + * @since 1.0 + */ public enum ControlMode { + + /** + * Open-loop duty cycle control mode. + * + *
Controls the motor by setting the duty cycle (percentage of time the motor is receiving a + * signal/the signal is on) directly. The motor output is proportional to the pulse width, + * typically ranging from -1.0 (full reverse) to +1.0 (full forward). This is the most basic + * control mode and does not use feedback control. + * + *
Maps to {@link ControlType#kDutyCycle} for SPARK controllers. + * + *
Here is a visualization of the duty cycle:
+ */
DUTY_CYCLE(ControlType.kDutyCycle),
+
+ /**
+ * Closed-loop velocity control mode.
+ *
+ * Controls the motor to maintain a specific velocity using PID feedback control. The + * controller continuously adjusts the motor output to minimize the error between the demanded + * velocity and the actual measured velocity from the encoder. This mode is ideal for applications + * requiring consistent speed regardless of load variations. + * + *
Maps to {@link ControlType#kVelocity} for SPARK controllers. + */ VELOCITY(ControlType.kVelocity), + + /** + * Closed-loop position control mode with motion profiling. + * + *
Controls the motor to reach a specific position using advanced motion profiling algorithms. + * The controller generates smooth velocity and acceleration profiles to move the mechanism to the + * target position while respecting configured motion constraints (max velocity, max + * acceleration). This mode provides the smoothest and most controlled movement for precise + * positioning applications. + * + *
Maps to {@link ControlType#kPosition} for SPARK controllers. + * + *
Note: Despite the name "MOTION_MAGIC," this maps to position control type for SPARK + * controllers, as the motion profiling is handled internally. + */ MOTION_MAGIC(ControlType.kPosition), + + /** + * Open-loop voltage control mode. + * + *
Controls the motor by applying a specific voltage directly to the motor terminals. Unlike + * duty cycle control, voltage control compensates for battery voltage variations to provide more + * consistent motor behavior. The demand value represents the desired voltage (in Volts) to apply + * to the motor. + * + *
Maps to {@link ControlType#kVoltage} for SPARK controllers. + */ VOLTAGE(ControlType.kVoltage); + /** The corresponding SPARK controller control type for this mode */ private final ControlType sparkMode; + /** + * Creates a ControlMode with the specified SPARK controller mapping. + * + * @param sparkMode the corresponding {@link ControlType} for SPARK controllers + */ ControlMode(ControlType sparkMode) { this.sparkMode = sparkMode; } + /** + * Gets the corresponding SPARK controller control type for this mode. + * + *
This method provides the mapping from the vendor-neutral ControlMode to the specific {@link + * ControlType} required by REV Robotics SPARK controllers. This abstraction allows the same + * control mode enum to be used across different motor controller implementations. + * + *
The last references to this method were removed a while ago. + * + * @return the corresponding {@link ControlType} for SPARK controllers + */ + @Deprecated(forRemoval = true) public ControlType getSparkMode() { return sparkMode; } diff --git a/lib/src/main/java/com/team2813/lib2813/control/DeviceInformation.java b/lib/src/main/java/com/team2813/lib2813/control/DeviceInformation.java index cf760617..809e398f 100644 --- a/lib/src/main/java/com/team2813/lib2813/control/DeviceInformation.java +++ b/lib/src/main/java/com/team2813/lib2813/control/DeviceInformation.java @@ -3,25 +3,63 @@ import com.team2813.lib2813.util.InputValidation; import java.util.Optional; +/** + * Immutable value class representing the identifying information for a CAN device. + * + *
This class encapsulates the essential information needed to uniquely identify a device on the + * CAN bus network: the CAN ID and the specific CAN bus the device is connected to. It provides a + * standardized way to represent device identity across the team's control system architecture. + * + *
The class distinguishes between devices on the RoboRIO's built-in CAN bus (represented by an + * empty Optional for the canbus) and devices on named CAN buses such as CANivore or other CAN bus + * interfaces. + * + *
Key features: + * + *
This class is commonly used as a key in device registries and for comparing device instances
+ * to determine if they represent the same physical hardware.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public final class DeviceInformation {
+
+ /** The CAN ID of the device, validated to be in range [0, 62] */
private int id;
+
+ /** The CAN bus name, empty if on the RoboRIO's default CAN bus */
private Optional This constructor is used for devices connected directly to the RoboRIO's built-in CAN bus
+ * interface. The canbus will be represented as an empty Optional to indicate the default bus.
+ *
+ * @param id the CAN ID of the device, must be in range [0, 62]
+ * @throws com.team2813.lib2813.util.InvalidCanIdException if the CAN ID is outside the valid
+ * range
*/
public DeviceInformation(int id) {
this(id, null);
}
/**
- * Creates a DeviceInformation with a canbus string. If {@code canbus} is {@code null}, method
- * acts like {@link #DeviceInformation(int)} was called
+ * Creates DeviceInformation with a specific CAN bus name.
+ *
+ * This constructor supports devices on named CAN buses such as CANivore devices or other CAN
+ * bus interfaces. If {@code canbus} is {@code null}, this method behaves identically to {@link
+ * #DeviceInformation(int)}.
*
- * @param id the CAN id
- * @param canbus the canbus string
+ * @param id the CAN ID of the device, must be in range [0, 62]
+ * @param canbus the CAN bus name, or {@code null} for the RoboRIO default bus
+ * @throws com.team2813.lib2813.util.InvalidCanIdException if the CAN ID is outside the valid
+ * range
*/
public DeviceInformation(int id, String canbus) {
this.id = InputValidation.checkCanId(id);
@@ -29,24 +67,43 @@ public DeviceInformation(int id, String canbus) {
}
/**
- * Gets the can id of this device
+ * Gets the CAN ID of this device.
*
- * @return the can id of the device
+ * The CAN ID is guaranteed to be in the valid range [0, 62] due to validation performed during
+ * construction.
+ *
+ * @return the CAN ID of the device
*/
public int id() {
return id;
}
/**
- * Returns the canbus that this device is on, or {@link Optional#empty()} if it is on the RoboRIO
- * can loop
+ * Returns the CAN bus that this device is connected to.
+ *
+ * The return value interpretation:
+ *
+ * Two DeviceInformation instances are considered equal if and only if they have the same CAN
+ * ID and are on the same CAN bus. This allows DeviceInformation objects to be used reliably as
+ * keys in hash-based collections and for device identity comparisons.
+ *
+ * @param o the object to compare against
+ * @return {@code true} if the objects represent the same device, {@code false} otherwise
+ */
@Override
public boolean equals(Object o) {
if (!(o instanceof DeviceInformation)) return false;
@@ -54,6 +111,11 @@ public boolean equals(Object o) {
return other.id == id && other.canbus.equals(canbus);
}
+ /**
+ * Returns a hash code value for this DeviceInformation.
+ *
+ * @return a hash code value for this object
+ */
@Override
public int hashCode() {
return id * 31 + canbus.hashCode();
diff --git a/lib/src/main/java/com/team2813/lib2813/control/Encoder.java b/lib/src/main/java/com/team2813/lib2813/control/Encoder.java
index d622212b..c3eed3e1 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/Encoder.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/Encoder.java
@@ -4,53 +4,123 @@
import edu.wpi.first.units.measure.Angle;
import edu.wpi.first.units.measure.AngularVelocity;
-/** Specifies a device that can perceive rotational positions. */
+/**
+ * Interface specifying a device that can perceive rotational positions and velocities.
+ *
+ * This interface defines the contract for encoder devices that provide angular position and
+ * velocity feedback. It supports both legacy double-based methods (deprecated) and modern type-safe
+ * unit-based methods using WPILib's units system.
+ *
+ * The interface provides a migration path from unsafe raw double values to type-safe {@link
+ * Angle} and {@link AngularVelocity} measurements. New implementations should focus on the
+ * unit-safe methods, while legacy methods are maintained for backward compatibility but marked for
+ * removal.
+ *
+ * Common implementations include absolute encoders (CANcoder), relative encoders (integrated
+ * motor encoders), and other rotational position sensing devices.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public interface Encoder {
+
/**
- * Gets the position of the encoder
+ * Gets the current position of the encoder as a raw double value.
*
- * @return the position of the encoder
+ * @return the position of the encoder as an unspecified double value
* @deprecated This method does not specify position in a specific measurement, so it is not safe
- * to use. Use {@link #getPositionMeasure()} instead
+ * to use. Use {@link #getPositionMeasure()} instead for type safety
*/
@Deprecated(forRemoval = true)
double position();
/**
- * Gets the position of the encoder
+ * Gets the current position of the encoder using type-safe units.
+ *
+ * This method returns the encoder position as an {@link Angle} measurement, providing type
+ * safety and explicit unit handling. The returned angle can be easily converted to any angular
+ * unit (degrees, radians, rotations) using the WPILib units system.
+ *
+ * Example usage:
+ *
+ * Warning: This method accepts position without specifying units, making it unsafe and
+ * ambiguous. The interpretation of the position value depends on the specific encoder
+ * implementation and configuration.
+ *
+ * @param position the new position value as an unspecified double
* @deprecated This method does not specify a unit, so it is not safe to use. Use {@link
- * #setPosition(Angle)} instead.
+ * #setPosition(Angle)} instead for type safety
*/
@Deprecated(forRemoval = true)
void setPosition(double position);
+ /**
+ * Sets the encoder position using type-safe units.
+ *
+ * This method accepts any {@link Angle} measurement and converts it to the encoder's native
+ * units for setting the position. The type-safe approach eliminates unit confusion and provides
+ * clear, readable code.
+ *
+ * The default implementation converts the angle to radians and calls the legacy {@link
+ * #setPosition(double)} method. Implementations should override this method to provide direct
+ * unit-safe position setting when possible.
+ *
+ * Example usage:
+ *
+ * This method returns the encoder velocity as an {@link AngularVelocity} measurement,
+ * providing type safety and explicit unit handling. The returned velocity can be easily converted
+ * to any angular velocity unit using the WPILib units system.
+ *
+ * The default implementation assumes the legacy {@link #getVelocity()} method returns radians
+ * per second and wraps it in a type-safe measurement. Implementations should override this method
+ * to provide the correct units for their specific hardware.
+ *
+ * Example usage:
+ *
+ * This enum provides a vendor-neutral abstraction for motor inversion settings while maintaining
+ * compatibility with specific hardware implementations from CTRE Phoenix and REV Robotics SPARK
+ * controllers. It handles both absolute rotation directions and relative following behaviors for
+ * multi-motor systems.
+ *
+ * The enum supports two categories of inversion:
+ *
+ * Each rotation value includes mappings to vendor-specific inversion settings, enabling seamless
+ * integration with different motor controller families without code changes.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public enum InvertType {
+
+ /**
+ * Clockwise rotation is considered positive.
+ *
+ * When this inversion type is applied, positive motor output values will cause the motor to
+ * rotate in the clockwise direction when viewed from the output shaft end. This is the "normal"
+ * or "non-inverted" direction for most motor controllers.
+ *
+ * Vendor mappings:
+ *
+ * When this inversion type is applied, positive motor output values will cause the motor to
+ * rotate in the counter-clockwise direction when viewed from the output shaft end. This
+ * effectively inverts the motor's output relative to the default.
+ *
+ * Vendor mappings:
+ *
+ * This inversion type is used for follower motors that should rotate in the same direction as
+ * their master motor. The follower will mirror the master's output without any inversion applied.
+ *
+ * Note: This value has no direct vendor mapping as it represents a relationship rather
+ * than an absolute direction. It is resolved at the follower configuration level.
+ */
FOLLOW_MASTER,
+
+ /**
+ * Follower motor opposes the master motor's direction.
+ *
+ * This inversion type is used for follower motors that should rotate opposite to their master
+ * motor. The follower will mirror the master's output but with inversion applied, creating
+ * opposing rotation.
+ *
+ * This is commonly used in differential drive systems where left and right motors need to
+ * rotate in opposite directions for forward motion.
+ *
+ * Note: This value has no direct vendor mapping as it represents a relationship rather
+ * than an absolute direction. It is resolved at the follower configuration level.
+ */
OPPOSE_MASTER;
/**
- * A set of all {@link InvertType}s that have a phoenix and spark max invert. Anything that isn't
- * in this set is for motor following
+ * A set of all {@link InvertType}s that have phoenix and spark max invert mappings.
+ *
+ * This set contains only the rotation values (CLOCKWISE, COUNTER_CLOCKWISE) that can be
+ * directly mapped to vendor-specific inversion settings. Any InvertType not in this set is
+ * intended for motor following relationships and does not have direct vendor mappings.
+ *
+ * This set is used for validation to ensure only appropriate inversion types are used in
+ * contexts that require absolute rotation directions.
*/
public static final Set This constructor is used for FOLLOW_MASTER and OPPOSE_MASTER values that represent
+ * relationships rather than absolute directions.
+ */
InvertType() {
phoenixInvert = Optional.empty();
sparkMaxInvert = Optional.empty();
}
+ /**
+ * Creates an InvertType with vendor-specific mappings.
+ *
+ * This constructor is used for rotation values (CLOCKWISE, COUNTER_CLOCKWISE) that have direct
+ * mappings to vendor-specific inversion settings.
+ *
+ * @param phoenixInvert the corresponding Phoenix 6 InvertedValue
+ * @param sparkMaxInvert the corresponding SPARK MAX inversion setting
+ */
InvertType(InvertedValue phoenixInvert, boolean sparkMaxInvert) {
this.phoenixInvert = Optional.of(phoenixInvert);
this.sparkMaxInvert = Optional.of(sparkMaxInvert);
}
/**
- * Gets an {@link InvertType} from a phoenix {@link InvertedValue}.
+ * Gets an {@link InvertType} from a Phoenix 6 {@link InvertedValue}.
+ *
+ * This method provides reverse lookup from vendor-specific Phoenix settings to the
+ * vendor-neutral InvertType representation. It enables conversion from Phoenix-specific code to
+ * the standardized enum.
*
* @param v the {@link InvertedValue} to search for
- * @return {@link Optional#empty()} if no {@link InvertType} is found, otherwise, an optional
- * describing the {@link InvertType}
+ * @return an Optional containing the corresponding {@link InvertType}, or {@link
+ * Optional#empty()} if no mapping exists
*/
public static Optional This method provides reverse lookup from vendor-specific SPARK MAX settings to the
+ * vendor-neutral InvertType representation. It enables conversion from SPARK MAX-specific code to
+ * the standardized enum.
+ *
+ * @param v the SPARK MAX inversion boolean to search for
+ * @return an Optional containing the corresponding {@link InvertType}, or {@link
+ * Optional#empty()} if no mapping exists
*/
public static Optional This method returns the vendor-specific Phoenix mapping for rotation values. Following
+ * values (FOLLOW_MASTER, OPPOSE_MASTER) will return an empty Optional as they don't have direct
+ * Phoenix mappings.
+ *
+ * @return an Optional containing the Phoenix InvertedValue, or empty if not applicable
+ */
public Optional This internal method assumes the Phoenix mapping exists and throws if it doesn't. It's used
+ * internally for map construction and should not be called on following values.
+ *
+ * @return the Phoenix InvertedValue
+ * @throws NoSuchElementException if no Phoenix mapping exists
+ */
private InvertedValue forcePhoenixInvert() {
return phoenixInvert.orElseThrow();
}
+ /**
+ * Gets the SPARK MAX inversion boolean for this InvertType.
+ *
+ * This method returns the vendor-specific SPARK MAX mapping for rotation values. Following
+ * values (FOLLOW_MASTER, OPPOSE_MASTER) will return an empty Optional as they don't have direct
+ * SPARK MAX mappings.
+ *
+ * @return an Optional containing the SPARK MAX inversion boolean, or empty if not applicable
+ */
public Optional This internal method assumes the SPARK MAX mapping exists and throws if it doesn't. It's
+ * used internally for map construction and should not be called on following values.
+ *
+ * @return the SPARK MAX inversion boolean
+ * @throws NoSuchElementException if no SPARK MAX mapping exists
+ */
private boolean forceSparkMaxInvert() {
return sparkMaxInvert.orElseThrow();
}
/**
- * Contains the maps for {@link InvertType#fromPhoenixInvert(InvertedValue)} and {@link
- * InvertType#fromSparkMaxInvert(boolean)}. In a static class so that they will only be
- * initialized if they are needed.
+ * Contains the reverse lookup maps for vendor-specific inversion values.
+ *
+ * This static nested class holds the maps used by {@link #fromPhoenixInvert(InvertedValue)}
+ * and {@link #fromSparkMaxInvert(boolean)}. The maps are constructed lazily in a static context
+ * to avoid initialization overhead when reverse lookup is not needed.
+ *
+ * The maps are built by filtering enum values to include only those with vendor mappings and
+ * creating reverse mappings from vendor values to InvertType.
*/
private static final class Maps {
+
+ /** Reverse lookup map from Phoenix InvertedValue to InvertType */
private static final Map This interface provides a vendor-neutral abstraction for motor controllers, enabling
+ * standardized motor control across different hardware implementations. It defines the essential
+ * operations needed for motor control: setting output values with various control modes and
+ * monitoring current draw.
+ *
+ * The interface supports multiple control modes through the {@link ControlMode} enumeration,
+ * allowing the same code to work with different control strategies such as duty cycle, voltage,
+ * velocity, and motion magic control. This abstraction enables easy switching between control modes
+ * without changing the calling code.
+ *
+ * Key features:
+ *
+ * Common implementations include TalonFX controllers, SPARK MAX controllers, and other motor
+ * control devices that support the specified control modes.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public interface Motor {
- // motor control
/**
- * Sets the motor to run with a specified mode of control.
+ * Sets the motor to run with a specified control mode and demand value.
*
- * @param mode The mode to control the motor with
- * @param demand The demand of the motor. differentiating meaning with each control mode
+ * This method provides basic motor control without feedforward compensation. The
+ * interpretation of the demand value depends on the selected control mode:
+ *
+ * This is equivalent to calling {@link #set(ControlMode, double, double)} with a feedforward
+ * value of 0.0.
+ *
+ * @param mode the control mode to use for motor operation
+ * @param demand the demand value whose interpretation depends on the control mode
*/
void set(ControlMode mode, double demand);
/**
- * Sets the motor to run with a specified mode of control, and feedForward.
+ * Sets the motor to run with a specified control mode, demand value, and feedforward.
+ *
+ * This method provides advanced motor control with feedforward compensation to improve control
+ * performance. Feedforward helps the controller anticipate the required output, reducing tracking
+ * error and improving response time.
+ *
+ * The demand value interpretation is the same as in {@link #set(ControlMode, double)}. The
+ * feedforward value provides additional output that is added to the closed-loop controller
+ * output:
+ *
+ * Feedforward values are typically calculated based on system identification or theoretical
+ * models of the mechanism being controlled.
+ *
+ * @param mode the control mode to use for motor operation
+ * @param demand the demand value whose interpretation depends on the control mode
+ * @param feedForward the feedforward compensation value to improve control performance
*/
void set(ControlMode mode, double demand, double feedForward);
/**
- * Gets the current that is being applied onto the motor
+ * Gets the current that is currently being applied to the motor.
+ *
+ * This method returns the actual current flowing through the motor windings, which is useful
+ * for:
+ *
+ * The returned current measurement uses WPILib's type-safe units system, allowing easy
+ * conversion between different current units (amps, milliamps) and providing type safety in
+ * calculations.
+ *
+ * Example usage:
+ *
+ * This interface combines the functionality of both {@link Motor} and {@link Encoder}
+ * interfaces, representing motor controllers that have built-in position/velocity feedback and can
+ * perform closed-loop PID control. This is the most common type of motor controller used in
+ * competitive robotics, where precise control and position feedback are essential.
+ *
+ * The interface extends the basic motor control capabilities with PID configuration methods that
+ * allow tuning of closed-loop control parameters. Multiple PID slots are supported to enable
+ * different control parameters for different operating conditions (e.g., different gains for
+ * velocity vs position control, or different gains for different mechanisms).
+ *
+ * Key features inherited and added:
+ *
+ * Common implementations include:
+ *
+ * This interface enables sophisticated control strategies such as:
+ *
+ * Motor controllers typically support multiple PID parameter sets (slots) that can be switched
+ * between during operation. This allows different control parameters for different operating
+ * conditions without reconfiguration overhead.
+ *
+ * PID parameter meanings:
+ *
+ * Note: The interpretation of the 'f' parameter may vary by implementation. Some
+ * controllers use it for velocity feedforward (kV), while others use it for arbitrary feedforward
+ * (kF). Consult the specific implementation documentation.
+ *
+ * @param slot the PID slot number to configure (typically 0-3, range depends on implementation)
+ * @param p the proportional gain coefficient
+ * @param i the integral gain coefficient
+ * @param d the derivative gain coefficient
+ * @param f the feedforward gain coefficient
+ */
void configPIDF(int slot, double p, double i, double d, double f);
+ /**
+ * Configures the PID and feedforward constants for the default control slot (slot 0).
+ *
+ * This is a convenience method equivalent to calling {@link #configPIDF(int, double, double,
+ * double, double)} with slot 0. Most applications start with slot 0 for their primary control
+ * parameters.
+ *
+ * This method is ideal for simple applications that only need one set of PID parameters or for
+ * initial tuning before implementing more advanced multi-slot strategies.
+ *
+ * @param p the proportional gain coefficient
+ * @param i the integral gain coefficient
+ * @param d the derivative gain coefficient
+ * @param f the feedforward gain coefficient
+ */
void configPIDF(double p, double i, double d, double f);
+ /**
+ * Configures the PID constants for a specific control slot without feedforward.
+ *
+ * This method is equivalent to calling {@link #configPIDF(int, double, double, double,
+ * double)} with a feedforward value of 0. It's useful when feedforward is not needed or will be
+ * handled separately through the {@link #set(ControlMode, double, double)} method.
+ *
+ * Pure PID control without feedforward is common for:
+ *
+ * This is a convenience method equivalent to calling {@link #configPID(int, double, double,
+ * double)} with slot 0. It provides the simplest interface for basic PID control configuration.
+ *
+ * This method is perfect for:
+ *
+ * This class provides a standardized interface for interacting with CANcoder devices while
+ * maintaining compatibility with the team's control system architecture. It implements the {@link
+ * Encoder} interface and supports both legacy double-based methods and modern type-safe unit-based
+ * methods.
+ *
+ * The class includes deprecated methods for backward compatibility, but new code should use the
+ * type-safe methods that work with WPILib's units system.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public class CancoderWrapper implements Encoder {
+
+ /** The underlying CTRE CANcoder hardware object */
private CANcoder cancoder;
+
+ /** Device information containing ID and CAN bus details */
private DeviceInformation info;
+ /**
+ * Creates a new CancoderWrapper with the specified CAN ID and bus name.
+ *
+ * @param id the CAN ID of the CANcoder device
+ * @param canbus the name of the CAN bus the device is connected to (e.g., "rio", "canivore")
+ */
public CancoderWrapper(int id, String canbus) {
cancoder = new CANcoder(id, canbus);
info = new DeviceInformation(id, canbus);
}
+ /**
+ * Creates a new CancoderWrapper with the specified CAN ID on the default CAN bus.
+ *
+ * @param id the CAN ID of the CANcoder device
+ */
public CancoderWrapper(int id) {
cancoder = new CANcoder(id);
info = new DeviceInformation(id);
}
+ /**
+ * Gets the current position of the encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getPositionMeasure()} instead for type safety with units
+ * @return the current position in rotations as a double
+ */
@Deprecated
@Override
public double position() {
return cancoder.getPosition().getValueAsDouble();
}
+ /**
+ * Gets the current position of the encoder using type-safe units.
+ *
+ * This method returns the position as an {@link Angle} measurement in rotations, which can be
+ * converted to other angular units as needed.
+ *
+ * @return the current position as an {@link Angle} measurement
+ */
@Override
public Angle getPositionMeasure() {
return Units.Rotations.of(cancoder.getPosition().getValueAsDouble());
}
+ /**
+ * Sets the position of the encoder using a raw double value.
+ *
+ * This method uses {@link ConfigUtils#phoenix6Config(Supplier)} to ensure reliable
+ * configuration of the Phoenix 6 device.
+ *
+ * @deprecated Use {@link #setPosition(Angle)} instead for type safety with units
+ * @param position the new position in rotations
+ */
@Deprecated
@Override
public void setPosition(double position) {
ConfigUtils.phoenix6Config(() -> cancoder.setPosition(position));
}
+ /**
+ * Sets the position of the encoder using type-safe units.
+ *
+ * This method accepts any {@link Angle} measurement and converts it to rotations internally.
+ * It uses {@link ConfigUtils#phoenix6Config(Supplier)} to ensure reliable configuration of the
+ * Phoenix 6 device.
+ *
+ * @param position the new position as an {@link Angle} measurement
+ */
@Override
public void setPosition(Angle position) {
ConfigUtils.phoenix6Config(() -> cancoder.setPosition(position.in(Units.Rotations)));
}
+ /**
+ * Provides direct access to the underlying CANcoder hardware object.
+ *
+ * This method allows access to Phoenix-specific configuration and methods that are not exposed
+ * through the {@link Encoder} interface.
+ *
+ * @return the underlying {@link CANcoder} hardware object
+ */
public CANcoder encoder() {
return cancoder;
}
+ /**
+ * Gets the current velocity of the encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getVelocityMeasure()} instead for type safety with units
+ * @return the current velocity in rotations per second as a double
+ */
@Deprecated
@Override
public double getVelocity() {
return cancoder.getVelocity().getValueAsDouble();
}
+ /**
+ * Gets the current velocity of the encoder using type-safe units.
+ *
+ * This method returns the velocity as an {@link AngularVelocity} measurement, which can be
+ * converted to other angular velocity units as needed.
+ *
+ * @return the current velocity as an {@link AngularVelocity} measurement
+ */
@Override
public AngularVelocity getVelocityMeasure() {
return cancoder.getVelocity().getValue();
}
+ /**
+ * Determines whether this CancoderWrapper is equal to another object.
+ *
+ * Two CancoderWrapper instances are considered equal if they represent the same physical
+ * CANcoder device (same CAN ID and CAN bus).
+ *
+ * @param obj the object to compare against
+ * @return {@code true} if the objects represent the same CANcoder device, {@code false} otherwise
+ */
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CancoderWrapper)) return false;
diff --git a/lib/src/main/java/com/team2813/lib2813/control/imu/Pigeon2Wrapper.java b/lib/src/main/java/com/team2813/lib2813/control/imu/Pigeon2Wrapper.java
index c6e16fe6..91c29c46 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/imu/Pigeon2Wrapper.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/imu/Pigeon2Wrapper.java
@@ -3,20 +3,41 @@
import com.ctre.phoenix6.hardware.Pigeon2;
import com.team2813.lib2813.control.DeviceInformation;
import com.team2813.lib2813.util.ConfigUtils;
+import java.util.function.Supplier;
+/**
+ * Wrapper class for a CTRE Phoenix 6 Pigeon2 IMU (Inertial Measurement Unit).
+ *
+ * This class provides a standardized interface for interacting with Pigeon2 devices while
+ * maintaining compatibility with the team's control system architecture. It includes automatic
+ * reset detection and recovery functionality to handle power cycles and device resets that can
+ * occur during competition.
+ *
+ * The wrapper maintains a cached heading value that is restored after reboot, ensuring
+ * consistent heading readings across each roborio bootup. The {@link #periodicResetCheck()} method
+ * should be called periodically (typically in a subsystem's periodic method) to monitor for and
+ * handle resets.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public class Pigeon2Wrapper {
+ /** The underlying CTRE Pigeon2 hardware object */
private Pigeon2 pigeon;
+ /** Cached heading value used for reset recovery */
private double currentHeading = 0;
+
+ /** Device information containing ID and CAN bus details */
private DeviceInformation info;
/**
- * Constructor
+ * Creates a new Pigeon2Wrapper with the specified device number and CAN bus.
*
- * @param deviceNumber [0,62]
- * @param canbus Name of the CANbus; can be a SocketCAN interface (on Linux), or a CANivore device
- * name or serial number
+ * @param deviceNumber the CAN ID of the Pigeon2 device, must be in range [0,62]
+ * @param canbus the name of the CAN bus the device is connected to. This can be a SocketCAN
+ * interface (on Linux), a CANivore device name, or serial number
*/
public Pigeon2Wrapper(int deviceNumber, String canbus) {
info = new DeviceInformation(deviceNumber, canbus);
@@ -24,31 +45,66 @@ public Pigeon2Wrapper(int deviceNumber, String canbus) {
}
/**
- * Constructor
+ * Creates a new Pigeon2Wrapper with the specified device number on the default CAN bus.
*
- * @param deviceNumber [0,62]
+ * @param deviceNumber the CAN ID of the Pigeon2 device, must be in range [0,62]
*/
public Pigeon2Wrapper(int deviceNumber) {
info = new DeviceInformation(deviceNumber);
pigeon = new Pigeon2(deviceNumber);
}
+ /**
+ * Provides direct access to the underlying Pigeon2 hardware object.
+ *
+ * This method allows access to Phoenix-specific configuration and methods that are not exposed
+ * through this wrapper class.
+ *
+ * @return the underlying {@link Pigeon2} hardware object
+ */
public Pigeon2 getPigeon() {
return pigeon;
}
+ /**
+ * Gets the current heading (yaw angle) of the IMU.
+ *
+ * This method returns the raw yaw value from the Pigeon2. The heading is typically measured in
+ * degrees, with the range and direction depending on the device configuration.
+ *
+ * @return the current heading in degrees
+ */
public double getHeading() {
return pigeon.getYaw().getValueAsDouble();
}
+ /**
+ * Sets the current heading (yaw angle) of the IMU.
+ *
+ * This method updates both the hardware device and the cached heading value used for reset
+ * recovery. It uses {@link ConfigUtils#phoenix6Config(Supplier)} to ensure reliable configuration
+ * of the Phoenix 6 device.
+ *
+ * @param angle the new heading angle in degrees
+ */
public void setHeading(double angle) {
ConfigUtils.phoenix6Config(() -> pigeon.setYaw(angle));
currentHeading = angle;
}
/**
- * Checks if a reset has occurred and restores non-persistent settings if so. Implement
- * periodically (e.g. in a subsystem's periodic() method)
+ * Checks if a device reset has occurred and restores non-persistent settings if needed.
+ *
+ * This method should be called periodically (e.g., in a subsystem's periodic() method) to
+ * monitor for device resets that can occur due to power cycles, brownouts, or other electrical
+ * issues. When a reset is detected, the method automatically restores the cached heading value to
+ * maintain consistency.
+ *
+ * If no reset has occurred, the method updates the cached heading with the current hardware
+ * reading to ensure the cache stays synchronized.
+ *
+ * Implementation Note: This method should be called at least once per robot loop
+ * iteration for proper reset detection and recovery.
*/
public void periodicResetCheck() {
if (!pigeon.hasResetOccurred()) {
@@ -58,10 +114,27 @@ public void periodicResetCheck() {
}
}
+ /**
+ * Returns a hash code value for this Pigeon2Wrapper.
+ *
+ * The hash code is based on the device information (CAN ID and bus), ensuring that wrappers
+ * for the same physical device have the same hash code.
+ *
+ * @return a hash code value for this object
+ */
public int hashCode() {
return info.hashCode();
}
+ /**
+ * Determines whether this Pigeon2Wrapper is equal to another object.
+ *
+ * Two Pigeon2Wrapper instances are considered equal if they represent the same physical
+ * Pigeon2 device (same CAN ID and CAN bus).
+ *
+ * @param other the object to compare against
+ * @return {@code true} if the objects represent the same Pigeon2 device, {@code false} otherwise
+ */
public boolean equals(Object other) {
if (!(other instanceof Pigeon2Wrapper)) {
return false;
diff --git a/lib/src/main/java/com/team2813/lib2813/control/motors/SparkMaxWrapper.java b/lib/src/main/java/com/team2813/lib2813/control/motors/SparkMaxWrapper.java
index 4ddc5b00..ba4d6e43 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/motors/SparkMaxWrapper.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/motors/SparkMaxWrapper.java
@@ -20,23 +20,70 @@
import java.util.ArrayList;
import java.util.List;
+/**
+ * Wrapper class for REV Robotics SPARK MAX motor controller.
+ *
+ * This class provides a standardized interface for controlling SPARK MAX motor controllers while
+ * maintaining compatibility with the team's control system architecture. It implements the {@link
+ * PIDMotor} interface and supports both brushed and brushless motors with integrated encoder
+ * feedback.
+ *
+ * Key features include:
+ *
+ * The wrapper uses safe parameter reset mode and non-persistent parameters by default for
+ * reliable operation during competition.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public class SparkMaxWrapper implements PIDMotor {
+
+ /** List of follower SPARK MAX controllers managed by this wrapper */
private final List The constructor initializes the motor controller with safe default settings:
+ *
+ * This is a convenience method that calls {@link #set(ControlMode, double, double)} with a
+ * feedforward value of 0.
+ *
+ * @param controlMode the control mode to use
+ * @param demand the demand value (interpretation depends on control mode)
+ */
@Override
public void set(ControlMode controlMode, double demand) {
set(controlMode, demand, 0);
}
+ /**
+ * Sets the motor output using the specified control mode, demand value, and feedforward.
+ *
+ * Supported control modes:
+ *
+ * Note: The feedforward parameter is currently ignored but included for interface
+ * compatibility.
+ *
+ * @param controlMode the control mode to use
+ * @param demand the demand value (interpretation depends on control mode)
+ * @param feedForward the feedforward value (currently unused)
+ */
@Override
public void set(ControlMode controlMode, double demand, double feedForward) {
switch (controlMode) {
@@ -66,41 +139,104 @@ public void set(ControlMode controlMode, double demand, double feedForward) {
}
}
+ /**
+ * Gets the current position of the motor encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getPositionMeasure()} instead for type safety with units
+ * @return the current position in rotations as a double
+ */
@Override
public double position() {
return encoder.getPosition();
}
+ /**
+ * Gets the current position of the motor encoder using type-safe units.
+ *
+ * This method returns the position as an {@link Angle} measurement in rotations, which can be
+ * converted to other angular units as needed.
+ *
+ * @return the current position as an {@link Angle} measurement
+ */
@Override
public Angle getPositionMeasure() {
return Units.Rotations.of(encoder.getPosition());
}
+ /**
+ * Gets the current applied current of the motor controller.
+ *
+ * This represents the actual current being drawn by the motor, which can be useful for
+ * monitoring motor load and detecting mechanical issues.
+ *
+ * @return the current applied current as a {@link Current} measurement
+ */
@Override
public Current getAppliedCurrent() {
return Units.Amps.of(motor.getOutputCurrent());
}
+ /**
+ * Sets the encoder position to the specified raw double value.
+ *
+ * @deprecated Use {@link #setPosition(Angle)} instead for type safety with units
+ * @param position the new position in rotations
+ */
@Override
public void setPosition(double position) {
encoder.setPosition(position);
}
+ /**
+ * Sets the encoder position using type-safe units.
+ *
+ * This method accepts any {@link Angle} measurement and converts it to rotations internally
+ * for the SPARK MAX encoder.
+ *
+ * @param position the new position as an {@link Angle} measurement
+ */
@Override
public void setPosition(Angle position) {
encoder.setPosition(position.in(Units.Rotations));
}
+ /**
+ * Gets the current velocity of the motor encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getVelocityMeasure()} instead for type safety with units
+ * @return the current velocity in RPM as a double
+ */
@Override
public double getVelocity() {
return encoder.getVelocity();
}
+ /**
+ * Gets the current velocity of the motor encoder using type-safe units.
+ *
+ * This method returns the velocity as an {@link AngularVelocity} measurement in rotations per
+ * minute (RPM), which can be converted to other angular velocity units as needed.
+ *
+ * @return the current velocity as an {@link AngularVelocity} measurement
+ */
@Override
public AngularVelocity getVelocityMeasure() {
return Units.Rotations.per(Units.Minute).of(encoder.getVelocity());
}
+ /**
+ * Configures the PIDF constants for a specific closed-loop slot.
+ *
+ * The SPARK MAX supports multiple PID slot configurations that can be switched between during
+ * operation. This allows for different control parameters for different operating conditions.
+ *
+ * @param slot the closed-loop slot to configure (0-3)
+ * @param p the proportional gain
+ * @param i the integral gain
+ * @param d the derivative gain
+ * @param f the feedforward gain
+ * @throws RuntimeException if the slot number is invalid (not 0-3)
+ */
@Override
public void configPIDF(int slot, double p, double i, double d, double f) {
ClosedLoopSlot[] slots = ClosedLoopSlot.values();
@@ -112,21 +248,77 @@ public void configPIDF(int slot, double p, double i, double d, double f) {
ConfigUtils.revConfig(() -> motor.configure(config, resetMode, persistMode));
}
+ /**
+ * Configures the PIDF constants for the default closed-loop slot (slot 0).
+ *
+ * This is a convenience method that calls {@link #configPIDF(int, double, double, double,
+ * double)} with slot 0.
+ *
+ * @param p the proportional gain
+ * @param i the integral gain
+ * @param d the derivative gain
+ * @param f the feedforward gain
+ */
@Override
public void configPIDF(double p, double i, double d, double f) {
configPIDF(0, p, i, d, f);
}
+ /**
+ * Configures the PID constants for a specific closed-loop slot.
+ *
+ * This is a convenience method that calls {@link #configPIDF(int, double, double, double,
+ * double)} with a feedforward value of 0.
+ *
+ * @param slot the closed-loop slot to configure (0-3)
+ * @param p the proportional gain
+ * @param i the integral gain
+ * @param d the derivative gain
+ * @throws RuntimeException if the slot number is invalid (not 0-3)
+ */
@Override
public void configPID(int slot, double p, double i, double d) {
configPIDF(slot, p, i, d, 0);
}
+ /**
+ * Configures the PID constants for the default closed-loop slot (slot 0).
+ *
+ * This is a convenience method that calls {@link #configPID(int, double, double, double)} with
+ * slot 0.
+ *
+ * @param p the proportional gain
+ * @param i the integral gain
+ * @param d the derivative gain
+ */
@Override
public void configPID(double p, double i, double d) {
configPIDF(0, p, i, d, 0);
}
+ /**
+ * Adds a follower SPARK MAX motor controller to this primary controller.
+ *
+ * Follower motors will automatically mirror the output of the primary motor but can have
+ * independent inversion settings. The follower relationship is configured at the hardware level
+ * for optimal performance.
+ *
+ * Inversion behavior:
+ *
+ * The follower SPARK MAX object is retained in the followers list to prevent garbage
+ * collection and maintain the hardware relationship.
+ *
+ * @param deviceId the CAN ID of the follower SPARK MAX
+ * @param type the motor type connected to the follower controller
+ * @param inverted the inversion type for the follower motor
+ * @throws RuntimeException if the inversion type cannot be converted to a SPARK MAX setting
+ */
public void addFollower(int deviceId, SparkLowLevel.MotorType type, InvertType inverted) {
SparkMax follower = new SparkMax(deviceId, type);
boolean isInverted =
diff --git a/lib/src/main/java/com/team2813/lib2813/control/motors/TalonFXWrapper.java b/lib/src/main/java/com/team2813/lib2813/control/motors/TalonFXWrapper.java
index 49f05a12..9d1eb47d 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/motors/TalonFXWrapper.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/motors/TalonFXWrapper.java
@@ -22,26 +22,61 @@
import java.util.List;
import java.util.Objects;
+/**
+ * Wrapper class for CTRE Phoenix 6 TalonFX motor controller.
+ *
+ * This class provides a standardized interface for controlling TalonFX motor controllers while
+ * maintaining compatibility with the team's control system architecture. It implements the {@link
+ * PIDMotor} interface and supports advanced control modes including velocity control, motion magic,
+ * and position control with the integrated encoder.
+ *
+ * Key features include:
+ *
+ * The wrapper automatically configures current limits (40A stator current limit with supply
+ * current limiting enabled) and validates all input parameters to prevent configuration errors.
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public class TalonFXWrapper implements PIDMotor {
- /** A list of followers, so that they aren't garbage collected */
+
+ /** A list of follower TalonFX controllers to prevent garbage collection */
private final List The constructor automatically configures:
+ *
+ * This is a convenience constructor that creates a TalonFX on the default "rio" CAN bus. The
+ * constructor automatically configures the same settings as the primary constructor.
+ *
+ * @param canID the CAN ID of the motor controller, must be in range [0, 62]
+ * @param invertType the inversion type for motor output, must be a rotation value
* @throws NullPointerException if {@code invertType} is null
* @throws IllegalArgumentException if {@code invertType} is not in {@link
- * InvertType#rotationValues}. In other words, this exception is thrown when passed an {@link
- * InvertType} that is for following motors
- * @throws InvalidCanIdException if the CAN id is invalid
+ * InvertType#rotationValues}. This exception is thrown when passed an {@link InvertType} that
+ * is for following motors
+ * @throws InvalidCanIdException if the CAN ID is invalid (outside range [0, 62])
*/
public TalonFXWrapper(int canID, InvertType invertType) {
Objects.requireNonNull(invertType, "invertType should not be null");
@@ -95,10 +133,38 @@ public TalonFXWrapper(int canID, InvertType invertType) {
information = new DeviceInformation(canID);
}
+ /**
+ * Sets the motor output using the specified control mode and demand value.
+ *
+ * This is a convenience method that calls {@link #set(ControlMode, double, double)} with a
+ * feedforward value of 0.
+ *
+ * @param controlMode the control mode to use
+ * @param demand the demand value (interpretation depends on control mode)
+ */
public void set(ControlMode controlMode, double demand) {
set(controlMode, demand, 0);
}
+ /**
+ * Sets the motor output using the specified control mode, demand value, and feedforward.
+ *
+ * Supported control modes:
+ *
+ * Feedforward is supported for velocity and motion magic control modes to improve tracking
+ * performance and reduce steady-state error.
+ *
+ * @param controlMode the control mode to use
+ * @param demand the demand value (interpretation depends on control mode)
+ * @param feedForward the feedforward value (used for velocity and motion magic modes)
+ */
@Override
public void set(ControlMode controlMode, double demand, double feedForward) {
switch (controlMode) {
@@ -123,49 +189,138 @@ public void set(ControlMode controlMode, double demand, double feedForward) {
}
}
+ /**
+ * Gets the current position of the integrated encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getPositionMeasure()} instead for type safety with units
+ * @return the current position in rotations as a double
+ */
@Override
public double position() {
return getPositionMeasure().in(Units.Rotations);
}
+ /**
+ * Gets the current position of the integrated encoder using type-safe units.
+ *
+ * This method returns the position as an {@link Angle} measurement in rotations, which can be
+ * converted to other angular units as needed. The TalonFX uses its integrated encoder for
+ * high-resolution position feedback.
+ *
+ * @return the current position as an {@link Angle} measurement
+ */
@Override
public Angle getPositionMeasure() {
return Units.Rotations.of(motor.getPosition().getValueAsDouble());
}
+ /**
+ * Gets the current stator current of the motor controller.
+ *
+ * Stator current represents the current flowing through the motor windings and is useful for
+ * monitoring motor load, detecting mechanical binding, and ensuring the motor stays within safe
+ * operating limits.
+ *
+ * @return the current stator current as a {@link Current} measurement
+ */
@Override
public Current getAppliedCurrent() {
return motor.getStatorCurrent().getValue();
}
+ /**
+ * Sets the encoder position to the specified raw double value.
+ *
+ * @deprecated Use {@link #setPosition(Angle)} instead for type safety with units
+ * @param position the new position in rotations
+ */
@Override
public void setPosition(double position) {
motor.setPosition(position);
}
+ /**
+ * Sets the encoder position using type-safe units.
+ *
+ * This method accepts any {@link Angle} measurement and converts it to rotations internally
+ * for the TalonFX's integrated encoder.
+ *
+ * @param position the new position as an {@link Angle} measurement
+ */
@Override
public void setPosition(Angle position) {
motor.setPosition(position.in(Units.Rotations));
}
+ /**
+ * Gets the current velocity of the integrated encoder as a raw double value.
+ *
+ * @deprecated Use {@link #getVelocityMeasure()} instead for type safety with units
+ * @return the current velocity in rotations per second as a double
+ */
@Override
public double getVelocity() {
return motor.getVelocity().getValueAsDouble();
}
+ /**
+ * Gets the current velocity of the integrated encoder using type-safe units.
+ *
+ * This method returns the velocity as an {@link AngularVelocity} measurement in rotations per
+ * second, which can be converted to other angular velocity units as needed.
+ *
+ * @return the current velocity as an {@link AngularVelocity} measurement
+ */
@Override
public AngularVelocity getVelocityMeasure() {
return Units.RotationsPerSecond.of(motor.getVelocity().getValueAsDouble());
}
+ /**
+ * Provides direct access to the underlying TalonFX hardware object.
+ *
+ * This method allows access to Phoenix-specific configuration and methods that are not exposed
+ * through the {@link PIDMotor} interface, such as advanced motion profiling parameters and
+ * diagnostic information.
+ *
+ * @return the underlying {@link TalonFX} hardware object
+ */
public TalonFX motor() {
return motor;
}
+ /**
+ * Sets the neutral mode (brake/coast) of the motor controller.
+ *
+ * This method allows runtime changes to the motor's neutral behavior:
+ *
+ * The TalonFX supports multiple PID slot configurations that can be switched between during
+ * operation. This allows for different control parameters for different operating conditions
+ * (e.g., different PID gains for velocity vs position control).
+ *
+ * Note: The 'f' parameter represents velocity feedforward (kV) in Phoenix 6, not
+ * traditional feedforward gain.
+ *
+ * @param slot the closed-loop slot to configure
+ * @param p the proportional gain (kP)
+ * @param i the integral gain (kI)
+ * @param d the derivative gain (kD)
+ * @param f the velocity feedforward gain (kV)
+ */
@Override
public void configPIDF(int slot, double p, double i, double d, double f) {
SlotConfigs conf = new SlotConfigs();
@@ -173,21 +328,76 @@ public void configPIDF(int slot, double p, double i, double d, double f) {
motor.getConfigurator().apply(conf.withKP(p).withKI(i).withKD(d).withKV(f));
}
+ /**
+ * Configures the PIDF constants for the default closed-loop slot (slot 0).
+ *
+ * This is a convenience method that calls {@link #configPIDF(int, double, double, double,
+ * double)} with slot 0.
+ *
+ * @param p the proportional gain (kP)
+ * @param i the integral gain (kI)
+ * @param d the derivative gain (kD)
+ * @param f the velocity feedforward gain (kV)
+ */
@Override
public void configPIDF(double p, double i, double d, double f) {
configPIDF(0, p, i, d, f);
}
+ /**
+ * Configures the PID constants for a specific closed-loop slot.
+ *
+ * This is a convenience method that calls {@link #configPIDF(int, double, double, double,
+ * double)} with a velocity feedforward value of 0.
+ *
+ * @param slot the closed-loop slot to configure
+ * @param p the proportional gain (kP)
+ * @param i the integral gain (kI)
+ * @param d the derivative gain (kD)
+ */
@Override
public void configPID(int slot, double p, double i, double d) {
configPIDF(slot, p, i, d, 0);
}
+ /**
+ * Configures the PID constants for the default closed-loop slot (slot 0).
+ *
+ * This is a convenience method that calls {@link #configPID(int, double, double, double)} with
+ * slot 0.
+ *
+ * @param p the proportional gain (kP)
+ * @param i the integral gain (kI)
+ * @param d the derivative gain (kD)
+ */
@Override
public void configPID(double p, double i, double d) {
configPIDF(0, p, i, d, 0);
}
+ /**
+ * Adds a follower TalonFX motor controller on the specified CAN bus.
+ *
+ * Follower motors will automatically mirror the output of the primary motor but can have
+ * independent inversion settings. The wrapper supports both strict following and regular
+ * following modes based on the invert type:
+ *
+ * The follower TalonFX object is retained in the followers list to prevent garbage collection
+ * and maintain the hardware relationship.
+ *
+ * @param deviceNumber the CAN ID of the follower TalonFX, must be in range [0, 62]
+ * @param canbus the CAN bus that the follower is connected to
+ * @param invertType the inversion type for the follower motor
+ * @throws InvalidCanIdException if the CAN ID is invalid (outside range [0, 62])
+ */
public void addFollower(int deviceNumber, String canbus, InvertType invertType) {
TalonFX follower = new TalonFX(checkCanId(deviceNumber), canbus);
if (InvertType.rotationValues.contains(invertType)) {
@@ -202,6 +412,16 @@ public void addFollower(int deviceNumber, String canbus, InvertType invertType)
followers.add(follower); // add to follower list so TalonFX follower object is preserved
}
+ /**
+ * Adds a follower TalonFX motor controller on the default CAN bus.
+ *
+ * This is a convenience method that calls {@link #addFollower(int, String, InvertType)} with
+ * the default CAN bus. See that method for detailed behavior documentation.
+ *
+ * @param deviceNumber the CAN ID of the follower TalonFX, must be in range [0, 62]
+ * @param invertType the inversion type for the follower motor
+ * @throws InvalidCanIdException if the CAN ID is invalid (outside range [0, 62])
+ */
public void addFollower(int deviceNumber, InvertType invertType) {
TalonFX follower = new TalonFX(checkCanId(deviceNumber));
if (InvertType.rotationValues.contains(invertType)) {
diff --git a/lib/src/main/java/com/team2813/lib2813/doc-files/dutycyclediagram.jpeg b/lib/src/main/java/com/team2813/lib2813/doc-files/dutycyclediagram.jpeg
new file mode 100644
index 00000000..4d174265
Binary files /dev/null and b/lib/src/main/java/com/team2813/lib2813/doc-files/dutycyclediagram.jpeg differ
diff --git a/lib/src/main/java/com/team2813/lib2813/preferences/PersistedConfiguration.java b/lib/src/main/java/com/team2813/lib2813/preferences/PersistedConfiguration.java
index 68491c3a..6980fda4 100644
--- a/lib/src/main/java/com/team2813/lib2813/preferences/PersistedConfiguration.java
+++ b/lib/src/main/java/com/team2813/lib2813/preferences/PersistedConfiguration.java
@@ -16,8 +16,8 @@
/**
* Initializes the fields of a Record Class from values stored in {@link Preferences}.
*
- * The Preference values can be updated in the SmartDashboard and/or Shuffleboard UI; updated
- * values will be stored in the flash storage for the robot.
+ * The Preference values can be updated in the Elastic. Updated values will be stored in the
+ * flash storage for the robot.
*
* Example use:
*
@@ -95,37 +95,34 @@
* @since 2.0.0
*/
public final class PersistedConfiguration {
+ /** Key used to track which record classes are bound to preference namespaces. */
static final String REGISTERED_CLASSES_NETWORK_TABLE_KEY = "PersistedConfiguration/registry";
+
+ /** Whether legacy keys have already been cleaned once per program run. */
private static boolean deletedLegacyKeys = false;
- // The below package-scope fields are for the self-tests.
+ // Package-private fields for self-tests.
static boolean throwExceptions = false;
static Consumer To be stored in preferences, the type of the record components can be any of the following:
+ * The provided instance supplies the default values. For each component:
*
* The values for the components for the passed-in instance will be used as the default value
- * for the preference. If a component is a supplier, the supplier will be called at most once to
- * get the default instance. Suppliers cannot return {@code null}.
- *
- * @param preferenceName Preference subtable to use to get the values.
- * @param configWithDefaults Record instance with all values set to their preferred default
- * values.
- * @throws IllegalArgumentException If {@code preferenceName} is empty or contains a {@code '/'}.
- * @throws IllegalStateException If {@code preferenceName} was used for a different record class.
+ * @param preferenceName Subtable name under Preferences (must not contain '/')
+ * @param configWithDefaults Instance containing default values for all record components
+ * @return A new record instance populated with Preferences values
+ * @throws IllegalArgumentException if {@code preferenceName} is empty or contains '/'
+ * @throws IllegalStateException if {@code preferenceName} was already registered to a different
+ * record class
*/
public static To be stored in preferences, the type of the record components can be any of the following:
+ * For example, an {@code int} component defaults to {@code 0}, and a {@code double} component
+ * defaults to {@code 0.0}.
*
- * The default values for the preferences will be Java defaults (for example, zero for
- * integers).
- *
- * @param preferenceName Preference subtable to use to get the values.
- * @param recordClass Type of the record instance to populate from preferences.
- * @throws IllegalArgumentException If {@code preferenceName} is empty or contains a {@code '/'}.
- * @throws IllegalStateException If {@code preferenceName} was used for a different record class.
+ * @param preferenceName Subtable name under Preferences
+ * @param recordClass Record type to instantiate
+ * @return A new record instance populated with Preferences values
+ * @throws IllegalArgumentException if {@code preferenceName} is invalid
+ * @throws IllegalStateException if namespace was registered to another class
*/
public static Each namespace is stored in a special NetworkTable registry entry. If the namespace is new,
+ * it is bound to the current record type. If it already exists and points to a different record
+ * type, this method throws an exception.
+ */
private static void verifyNotRegisteredToAnotherClass(
NetworkTableInstance ntInstance, String name, Class extends Record> recordClass) {
String recordName = recordClass.getCanonicalName();
if (recordName == null) {
- recordName = recordClass.getName();
+ recordName = recordClass.getName(); // fallback if canonical name unavailable
}
NetworkTable registeredClassesTable = ntInstance.getTable(REGISTERED_CLASSES_NETWORK_TABLE_KEY);
@@ -210,41 +213,48 @@ private static void verifyNotRegisteredToAnotherClass(
}
}
+ /**
+ * Core factory logic: instantiate a record using reflection, reading each component value from
+ * Preferences (or defaults).
+ */
private static This class allows:
+ *
+ * Additionally, this method disables PID control of the subsystem
+ * Also disables PID control if enabled.
*/
@Override
public void set(ControlMode mode, double demand, double feedForward) {
@@ -82,19 +117,21 @@ public Current getAppliedCurrent() {
return motor.getAppliedCurrent();
}
+ /** Enables PID control of the subsystem. */
public void enable() {
isEnabled = true;
}
+ /** Disables PID control and sets motor output to zero. */
public void disable() {
isEnabled = false;
useOutput(0, 0);
}
/**
- * Returns whether the controller is enabled. If this is enabled, then PID control will be used.
+ * Returns whether PID control is currently enabled.
*
- * @return Whether the controller is enabled.
+ * @return {@code true} if enabled, {@code false} otherwise
*/
public boolean isEnabled() {
return isEnabled;
@@ -103,7 +140,7 @@ public boolean isEnabled() {
/**
* {@inheritDoc}
*
- * Additionally, this method disables PID control of the subsystem
+ * Also disables PID control if enabled.
*/
@Override
public void set(ControlMode mode, double demand) {
@@ -113,10 +150,17 @@ public void set(ControlMode mode, double demand) {
motor.set(mode, demand);
}
+ /**
+ * Applies the PID output to the motor. Can be overridden for advanced control.
+ *
+ * @param output the PID output value
+ * @param setpoint the target setpoint (for reference)
+ */
protected void useOutput(double output, double setpoint) {
motor.set(controlMode, output);
}
+ /** Returns the encoder measurement converted to the configured {@link AngleUnit}. */
protected double getMeasurement() {
return encoder.getPositionMeasure().in(rotationUnit);
}
@@ -151,6 +195,7 @@ public AngularVelocity getVelocityMeasure() {
return encoder.getVelocityMeasure();
}
+ /** Periodic update that applies PID output if enabled. */
@Override
public void periodic() {
if (isEnabled) {
@@ -158,12 +203,17 @@ public void periodic() {
}
}
- /** A configuration for a MotorSubsystem */
+ /**
+ * Builder-style configuration class for {@link MotorSubsystem}.
+ *
+ * Allows setting PID constants, control mode, starting position, and other configuration
+ * parameters.
+ */
public static class MotorSubsystemConfiguration {
- /** The default error of a motor subsystems */
+ /** Default allowed error for position checks */
public static final double DEFAULT_ERROR = 5.0;
- /** The default starting position if one is not defined */
+ /** Default starting position */
public static final double DEFAULT_STARTING_POSITION = 0.0;
private ControlMode controlMode;
@@ -175,12 +225,14 @@ public static class MotorSubsystemConfiguration {
private double startingPosition;
/**
- * Creates a new config for MotorSubsystems. The default acceptable error is {@value
- * #DEFAULT_ERROR}, the PID constants are set to 0, and the starting position is {@value
- * #DEFAULT_STARTING_POSITION}
+ * Creates a new configuration for a motor subsystem.
*
- * @param motor the motor to use
- * @param encoder the encoder to use
+ * Defaults: PID(0,0,0), acceptable error = {@value #DEFAULT_ERROR}, starting position =
+ * {@value #DEFAULT_STARTING_POSITION}, control mode = {@link ControlMode#DUTY_CYCLE}, angle
+ * unit = {@link Units#Rotations}.
+ *
+ * @param motor the motor to control
+ * @param encoder the encoder providing feedback
*/
public MotorSubsystemConfiguration(Motor motor, Encoder encoder) {
this.motor = Objects.requireNonNull(motor, "motor should not be null");
@@ -193,78 +245,51 @@ public MotorSubsystemConfiguration(Motor motor, Encoder encoder) {
}
/**
- * Creates a new config for MotorSubsystems using a motor that has a built-in encoder. The
- * default acceptable error is {@value #DEFAULT_ERROR}, the PID constants are set to 0, and the
- * starting position is {@value #DEFAULT_STARTING_POSITION}
+ * Creates a new configuration using a PIDMotor with a built-in encoder.
*
- * @param motor the motor to use that also supports an encoder
+ * @param motor the motor that implements {@link PIDMotor}
*/
public MotorSubsystemConfiguration(PIDMotor motor) {
this(motor, motor);
}
- /**
- * Sets the controller used to calculate the next value
- *
- * @param controller The PID controller
- * @return {@code this} for chaining
- */
+ /** Sets the PID controller to use. */
public MotorSubsystemConfiguration controller(PIDController controller) {
this.controller = controller;
return this;
}
- /**
- * Sets the control mode to use when giving output to the motor. Defaults to {@link
- * ControlMode#DUTY_CYCLE}.
- *
- * @param controlMode The mode to use when controlling the motor
- * @return {@code this} for chaining
- */
+ /** Sets the control mode for motor output. */
public MotorSubsystemConfiguration controlMode(ControlMode controlMode) {
this.controlMode = controlMode;
return this;
}
- /**
- * Sets the PID constants for the controller
- *
- * @param p the proportional
- * @param i the integral
- * @param d the derivative
- * @return {@code this} for chaining
- */
+ /** Sets the PID constants. */
public MotorSubsystemConfiguration PID(double p, double i, double d) {
controller.setPID(p, i, d);
return this;
}
- /**
- * sets the starting position.
- *
- * @param startingPosition the position to start at
- * @return {@code this} for chaining
- */
+ /** Sets the starting position in the configured angle unit. */
public MotorSubsystemConfiguration startingPosition(Angle startingPosition) {
this.startingPosition = startingPosition.in(this.rotationUnit);
return this;
}
+ /** Sets the starting position via a supplier of an angle. */
public MotorSubsystemConfiguration startingPosition(Supplier This subsystem uses a {@link Motor} and allows controlling the motor in intake and outtake
+ * directions with preconfigured demand values. Commands are provided for common actions such as
+ * intaking, outtaking, and stopping the motor.
+ */
public abstract class ParameterizedIntakeSubsystem extends SubsystemBase implements AutoCloseable {
+
private final PIDMotor intakeMotor;
private final Params params;
+ /**
+ * Configuration record that stores the parameters for the intake subsystem.
+ *
+ * Includes the motor control mode, the intake demand, and the outtake demand.
+ *
+ * Validation ensures the intake and outtake demands are non-zero and have opposite signs.
+ */
public record Params(ControlMode controlMode, double intakeDemand, double outtakeDemand) {
+ /** Returns a builder for creating {@link Params}. Defaults the control mode to VOLTAGE. */
public static Params.Builder builder() {
- return new AutoBuilder_ParameterizedIntakeSubsystem_Params_Builder()
+ return new AutoBuilder_ParameterizedIntakeSubsystem_Params_Builder() // autogenerated class,
+ // ignore if it shows an
+ // error in the ide
.setControlMode(ControlMode.VOLTAGE);
}
@@ -29,6 +48,15 @@ public interface Builder {
Params build();
}
+ /**
+ * Constructs a Params record.
+ *
+ * @param controlMode the motor control mode, cannot be {@code null}
+ * @param intakeDemand the motor demand for intake, must be non-zero
+ * @param outtakeDemand the motor demand for outtake, must be non-zero and opposite sign of
+ * intakeDemand
+ * @throws IllegalArgumentException if validation fails
+ */
public Params {
if (controlMode == null) {
throw new IllegalArgumentException("controlMode cannot be null");
@@ -46,60 +74,85 @@ public interface Builder {
}
}
+ /**
+ * Constructs a new intake subsystem.
+ *
+ * @param intakeMotor the motor controlling the intake mechanism
+ * @param params configuration parameters for motor control
+ */
protected ParameterizedIntakeSubsystem(PIDMotor intakeMotor, Params params) {
this.intakeMotor = intakeMotor;
this.params = params;
}
+ /**
+ * Returns a command that spins the intake wheels in the intake direction.
+ *
+ * @return an {@link Command} that intakes a game piece
+ */
public final Command intakeItemCommand() {
return new InstantCommand(this::intakeGamePiece, this);
}
+ /**
+ * Returns a command that spins the intake wheels in the outtake direction.
+ *
+ * @return an {@link InstantCommand} that outtakes a game piece
+ */
public final Command outtakeItemCommand() {
return new InstantCommand(this::outtakeGamePiece, this);
}
+ /**
+ * Returns a command that stops the intake motor.
+ *
+ * @return an {@link InstantCommand} that stops the motor
+ */
public final Command stopMotorCommand() {
return new InstantCommand(this::stopMotor, this);
}
- /** Makes intake wheels spin in the intake direction. */
+ /** Spins the intake wheels in the configured intake direction. */
protected final void intakeGamePiece() {
// FIXME: Maybe add a check that the wheels are not stalled.
setMotorDemand(params.intakeDemand);
}
- /** Makes intake wheels spin in the outtake direction. */
+ /** Spins the intake wheels in the configured outtake direction. */
protected final void outtakeGamePiece() {
setMotorDemand(params.outtakeDemand);
}
/**
- * Runs the motor with the provided demand value.
+ * Runs the intake motor with a specific demand.
*
- * @param demand Demand of the motor. Meaning depends on the {@code ControlMode}.
+ * @param demand the motor demand, meaning depends on the configured {@link ControlMode}
*/
protected final void setMotorDemand(double demand) {
intakeMotor.set(params.controlMode, demand);
}
/**
- * Returns a command that runs the motor with the provided demand value.
+ * Returns a command that runs the intake motor with the given demand.
*
- * @param demand Demand of the motor. Meaning depends on the {@code ControlMode}.
+ * @param demand the motor demand, meaning depends on the configured {@link ControlMode}
+ * @return a command that sets the motor demand when executed
*/
protected final Command setMotorDemandCommand(double demand) {
return new InstantCommand(() -> setMotorDemand(demand), this);
}
- /** Stops the motor. */
+ /** Stops the intake motor. */
public final void stopMotor() {
setMotorDemand(0.0);
}
@Override
- public void close() {}
+ public void close() {
+ // Default implementation does nothing. Override if resources need cleanup.
+ }
+ /** Returns whether a value is essentially zero (within 0.001). */
private static boolean isEssentiallyZero(double value) {
return Math.abs(value) < 0.001;
}
diff --git a/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/Lightshow.java b/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/Lightshow.java
index 8c743d40..d251b9d3 100644
--- a/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/Lightshow.java
+++ b/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/Lightshow.java
@@ -1,82 +1,107 @@
package com.team2813.lib2813.subsystems.lightshow;
import edu.wpi.first.wpilibj.util.Color;
-import edu.wpi.first.wpilibj2.command.Subsystem;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Consumer;
+/**
+ * Abstract base class for robot lightshow subsystems.
+ *
+ * A Lightshow subsystem manages a set of {@link State}s that determine the colors to be
+ * displayed on LEDs or other lighting hardware. Implementations define how to actually apply the
+ * color via {@link #useColor(Color)} and how to compute the active color in {@link #update()}.
+ *
+ * States can be represented as enums implementing {@link State}, or as arbitrary {@link State}
+ * instances.
+ *
+ * There is a built-in static {@link #off} state that always represents the lights being off.
+ *
+ * @author Team 2813
+ */
public abstract class Lightshow extends SubsystemBase {
- /** A static state that signifies that the lights should be off, that is always applied */
+
+ /** A static state representing that the lights should be off (always applied). */
protected static final State off =
new State() {
+ @Override
public Color color() {
return new Color(0, 0, 0);
}
+ @Override
public boolean apply() {
return true;
}
};
- protected Set This implementation of {@link Subsystem#periodic()} updates the colors by passing the call
- * of {@link #update()} to the given {@link Consumer}. If the call to {@link #update()} returns an
- * empty optional, than the color of the default state is used, or the color is not changed.
+ * Called periodically by the scheduler to update the lights.
*
- * @implSpec {@link #useColor(Color)} is passed the result of {@link #update()}, or the {@link
- * #defaultState}'s color. If there is no color from {@link #update()} and no default state,
- * there is no requirement for what to pass to {@link #useColor(Color)}, or if {@link
- * #useColor(Color)} is called at all
+ * This implementation calls {@link #update()}, and applies the resulting color using {@link
+ * #useColor(Color)}. If {@link #update()} returns empty, the {@link #defaultState}'s color is
+ * used (if present). If neither provides a color, {@link #useColor(Color)} may not be called.
*/
@Override
public void periodic() {
@@ -85,9 +110,9 @@ public void periodic() {
}
/**
- * Sets the {@link State} to be used if none is given.
+ * Sets the default {@link State} to use if no other state is active.
*
- * @param defaultState the {@link State} to be used as a default.
+ * @param defaultState the default state
*/
public void setDefaultState(State defaultState) {
this.defaultState = Optional.of(defaultState);
diff --git a/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/QueueLightshow.java b/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/QueueLightshow.java
index 573240f1..a375b4b3 100644
--- a/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/QueueLightshow.java
+++ b/lib/src/main/java/com/team2813/lib2813/subsystems/lightshow/QueueLightshow.java
@@ -7,34 +7,64 @@
import java.util.Set;
/**
- * A lightshow that keeps track of the states that have been applied, and uses the last state. To be
- * more specific, all states that return {@code true} on a call to {@link State#apply()} are added
- * to the list. States are only removed from the list if they are at the front and {@link
- * State#apply()} returns {@code false}, when a state is removed, the next one will be activated if
- * {@link State#apply()} returns {@code true}, until either a state returns {@code true} upon a call
- * to {@link State#apply()}, in which the color will be used, or there are no states where {@link
- * State#apply()} return {@code true}, then the default color is used.
+ * A Lightshow implementation that manages a queue of active states.
+ *
+ * Behavior:
+ *
+ * New states that return {@code true} for {@link State#apply()} are added to the front of the
+ * queue. States at the front of the queue that return {@code false} are removed. The color of the
+ * first active state is used.
+ *
+ * @return the color of the current active state, or empty if no states are active
+ */
@Override
protected Optional Each {@link State} provides a color and a condition indicating whether it should currently be
+ * applied.
+ *
+ * @author Team 2813
+ */
public interface State {
+
/**
- * gets the color of this State.
+ * Gets the color of this state.
*
- * @return the color of this State
+ * @return the color to display for this state
*/
Color color();
/**
- * Checks if the current state should be applied
+ * Checks if this state should currently be applied.
*
- * @return {@code true} if the state should be applied
+ * @return {@code true} if the state should be active, {@code false} otherwise
*/
boolean apply();
}
diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java
index fcd2a2c1..8b80f71a 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java
@@ -2,7 +2,11 @@
import java.time.ZonedDateTime;
-/** Holder for data collected at build time about the robot code. */
+/**
+ * Holder for data collected at build time about the robot code.
+ *
+ * @author Team 2813
+ */
public interface BuildConstants {
/** The current git branch when the code was built. */
diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java
index 7d59e1ca..7111ba86 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java
@@ -20,7 +20,7 @@
* The constants are published under the {@code "/Metadata"} table in NetworkTables. This is a
* special NetworkTables table. Some tools have special support for the "/Metadata" table. For
* instance, Advantage Scope has a dedicated Metadata tab that loads information like Build
- * Constants in a well formated table view.
+ * Constants in a well formatted table view.
*
* To instantiate a BuildConstantsPublisher, a build constants class, {@code BuildConstants}, is
* needs to be generated for the robot library by enabling the `gversion` plugin in the gradle build
@@ -37,6 +37,8 @@
* // Log the build constants in the robot console as well.
* buildConstantsPublisher.log();
* }
+ *
+ * @author Team 2813
*/
public final class BuildConstantsPublisher {
/** The name of the NetworkTable under which the build constants are published. */
diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
index 56b2a6d5..b65ec7f5 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java
@@ -6,6 +6,18 @@
import java.time.format.DateTimeParseException;
import java.util.Optional;
+/**
+ * Record implementation of {@link BuildConstants}.
+ *
+ * This class holds metadata about the build, such as git branch, commit hash, build time, etc.
+ * It is typically generated at build time using the gversion Gradle plugin and then extracted at
+ * runtime for diagnostics, publishing, or logging.
+ *
+ * All values are immutable once created. Utility methods are provided for formatting dates
+ * consistently and safely handling extraction errors.
+ *
+ * @author Team 2813
+ */
record BuildConstantsRecord(
String mavenName,
int gitRevision,
@@ -23,10 +35,13 @@ record BuildConstantsRecord(
* Constructs an instance from a class generated by the gversion Gradle plugin.
*
* Instructions for using the gversion Gradle plugin can be found
+ * in the WPILib documentation.
*
- * @param buildConstantsClass Specially built class that contains the robot code built-time
+ * @param buildConstantsClass Specially built class that contains the robot code build-time
* constants.
+ * @return an {@link Optional} containing a populated record if successful, or empty if extraction
+ * failed.
*/
static Optional Each configuration method retries up to a fixed number of attempts to ensure reliability,
+ * reporting warnings and errors to the {@link DriverStation}.
+ *
+ * This class is non-instantiable and provides only static methods.
+ *
+ * @author Team 2813
+ */
public class ConfigUtils {
private static final int ATTEMPTS = 10;
- // make class non-instantiable
+ // Make class non-instantiable
private ConfigUtils() {
throw new AssertionError("cannot create ConfigUtils instance");
}
+ /**
+ * Applies a configuration to a REV device with retry attempts.
+ *
+ * @param configMethod Supplier that applies a REV configuration and returns a {@link
+ * REVLibError}.
+ */
public static void revConfig(Supplier Provides mathematical operations useful in robot control, such as deadbanding joystick inputs.
+ * The class is non-instantiable and should be used only via static methods.
+ *
+ * @author Team 2813
+ */
public class ControlUtils {
private ControlUtils() {
throw new AssertionError(
@@ -8,14 +15,14 @@ private ControlUtils() {
}
/**
- * Deadbands a value.
+ * Applies a deadband to a value.
*
* "A deadband or dead-band (also known as a dead zone or a neutral zone) is a band of input
* values in the domain of a transfer function in a control system or signal processing system
- * where the output is zero (the output is 'dead' - no action occurs).". See https://en.wikipedia.org/wiki/Deadband.
+ * where the output is zero (the output is 'dead' — no action occurs)." See Wikipedia: Deadband.
*
- * @see an interactive demo of the
+ * @see Interactive demo of the
* deadband operation.
* @param value The value to deadband, must be in [-1.0, 1.0].
* @param deadband The deadband range value, must be in [0.0, 1.0).
@@ -32,12 +39,10 @@ public static double deadband(double value, double deadband) {
}
if (Math.abs(value) <= deadband) {
return 0.0;
+ } else if (value > 0.0) {
+ return (value - deadband) / (1.0 - deadband);
} else {
- if (value > 0.0) {
- return (value - deadband) / (1.0 - deadband);
- } else {
- return (value + deadband) / (1.0 - deadband);
- }
+ return (value + deadband) / (1.0 - deadband);
}
}
}
diff --git a/lib/src/main/java/com/team2813/lib2813/util/FakePIDMotor.java b/lib/src/main/java/com/team2813/lib2813/util/FakePIDMotor.java
index a8076f8c..5417b904 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/FakePIDMotor.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/FakePIDMotor.java
@@ -6,10 +6,25 @@
import edu.wpi.first.units.Units;
import edu.wpi.first.units.measure.AngularVelocity;
+/**
+ * A fake implementation of {@link PIDMotor} used for testing.
+ *
+ * This class simulates motor behavior by storing the most recent control mode and demand value.
+ * Assertions are used to enforce correct expectations in tests.
+ *
+ * Not intended for production use.
+ *
+ * @author Team 2813
+ */
public abstract class FakePIDMotor implements PIDMotor {
public double demand = 0.0f;
private ControlMode controlMode;
+ /**
+ * Gets the current demand if the control mode is {@link ControlMode#VOLTAGE}.
+ *
+ * @return The current voltage demand.
+ */
public double getVoltage() {
Truth.assertThat(controlMode).isEqualTo(ControlMode.VOLTAGE);
return demand;
diff --git a/lib/src/main/java/com/team2813/lib2813/util/InputValidation.java b/lib/src/main/java/com/team2813/lib2813/util/InputValidation.java
index d9b43f72..d46637b4 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/InputValidation.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/InputValidation.java
@@ -2,20 +2,27 @@
import java.util.function.IntFunction;
+/**
+ * Utility class for input validation.
+ *
+ * Provides methods for validating IDs and other numeric inputs. This class is non-instantiable
+ * and should only be used via its static methods.
+ *
+ * @author Team 2813
+ */
public class InputValidation {
private InputValidation() {
throw new AssertionError("non instantiable");
}
/**
- * Check if the given value is in the bounds
+ * Checks if the given value is within the provided bounds.
*
* @param lower the lower bound
* @param upper the upper bound
* @param actual the actual value
* @param throwable a function that takes the actual value and returns an unchecked exception
- * @throws RuntimeException when the actual is not in between the bounds. exception is provided by
- * {@code throwable}
+ * @throws RuntimeException when {@code actual} is not in the range [{@code lower}, {@code upper}]
*/
private static void checkBounds(
int lower, int upper, int actual, IntFunction extends RuntimeException> throwable) {
@@ -26,11 +33,11 @@ private static void checkBounds(
}
/**
- * Checks if the input is a valid {@index CAN} Id, and throws an exception if it isn't
+ * Checks if the input is a valid {@index CAN} ID, and throws an exception if it is not.
*
- * @param id the can id, between 0 and 62, inclusive
- * @return the {@code id}
- * @throws InvalidCanIdException if the id is invalid
+ * @param id the CAN ID, between 0 and 62 inclusive
+ * @return the {@code id} if valid
+ * @throws InvalidCanIdException if the CAN ID is invalid
*/
public static int checkCanId(int id) {
checkBounds(0, 62, id, InvalidCanIdException::new);
diff --git a/lib/src/main/java/com/team2813/lib2813/util/IntTriFunction.java b/lib/src/main/java/com/team2813/lib2813/util/IntTriFunction.java
index 8d9eaf76..de943dee 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/IntTriFunction.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/IntTriFunction.java
@@ -1,6 +1,22 @@
package com.team2813.lib2813.util;
+/**
+ * A functional interface that accepts three {@code int} arguments and produces a result.
+ *
+ * This is the primitive specialization of a tri-function for {@code int} arguments.
+ *
+ * @param A valid CAN ID is in the range [0, 62]. If an ID outside this range is passed, this exception
+ * is thrown. If the ID is valid, the constructor itself throws an {@link IllegalArgumentException}.
+ *
+ * This exception stores the invalid CAN ID and provides standard {@code equals} and {@code
+ * hashCode} implementations based on the message.
+ *
+ * @author Team 2813
+ */
public class InvalidCanIdException extends RuntimeException {
/**
- * The CAN id that is invalid
+ * The CAN ID that caused this exception.
*
* @serial an integer that is not between 0 and 62
*/
private final int canId;
+ /**
+ * Constructs an InvalidCanIdException for a given CAN ID.
+ *
+ * @param canId the invalid CAN ID
+ * @throws IllegalArgumentException if the provided ID is actually valid (0–62)
+ */
public InvalidCanIdException(int canId) {
super(
String.format(
- "%d is not a valid can id (a valid can id is between 0 and 62, inclusive)", canId));
+ "%d is not a valid CAN ID (a valid CAN ID is between 0 and 62, inclusive)", canId));
this.canId = canId;
if (0 <= canId && canId <= 62) {
throw new IllegalArgumentException(
- String.format("%s is a valid can id (it is between 0 and 62, inclusive)", canId));
+ String.format("%d is a valid CAN ID (it is between 0 and 62, inclusive)", canId));
}
}
+ /** Returns the invalid CAN ID that caused this exception. */
public int getCanId() {
return canId;
}
diff --git a/lib/src/main/java/com/team2813/lib2813/util/RobotFactory.java b/lib/src/main/java/com/team2813/lib2813/util/RobotFactory.java
index 1bcdb5b2..c5795c45 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/RobotFactory.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/RobotFactory.java
@@ -5,19 +5,50 @@
import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab;
import java.util.function.Function;
+/**
+ * Utility class for constructing and starting robot instances with additional input objects.
+ *
+ * This factory allows you to provide extra inputs (such as Shuffleboard tabs) to the robot
+ * constructor while still using the standard {@link RobotBase#startRobot} entry point.
+ *
+ * The class is non-instantiable and all functionality is static.
+ *
+ * Example usage:
+ *
+ * Here, the factory lambda receives an {@link Inputs} object which can provide access to
+ * Shuffleboard tabs or other injected dependencies.
+ *
+ * @author Team 2813
+ */
public final class RobotFactory {
+ /** Holder for external inputs to be injected into the robot constructor. */
public record Inputs(ShuffleboardTabs shuffleboard) {}
+ /**
+ * Starts the robot with a factory function that receives the {@link Inputs}.
+ *
+ * The factory function should construct and return an instance of a subclass of {@link
+ * RobotBase}, e.g., your main robot class.
+ *
+ * @param factory a function that receives {@link Inputs} and returns a {@link RobotBase} instance
+ * @param This interface acts as a wrapper around {@link Shuffleboard#getTab(String)} and {@link
+ * Shuffleboard#selectTab(String)}, providing a seam for testing. By using this interface, code can
+ * interact with Shuffleboard tabs without directly depending on the static Shuffleboard API, which
+ * is useful for unit tests and mocking.
+ */
public interface ShuffleboardTabs {
+
+ /**
+ * Returns the {@link ShuffleboardTab} with the given title.
+ *
+ * @param title the title of the tab
+ * @return the Shuffleboard tab with the specified title
+ */
ShuffleboardTab getTab(String title);
+ /**
+ * Selects the tab with the given title as the currently active tab in Shuffleboard.
+ *
+ * @param title the title of the tab to select
+ */
void selectTab(String title);
}
diff --git a/lib/src/main/java/com/team2813/lib2813/util/Units2813.java b/lib/src/main/java/com/team2813/lib2813/util/Units2813.java
index 9b4eb249..200a7f6d 100644
--- a/lib/src/main/java/com/team2813/lib2813/util/Units2813.java
+++ b/lib/src/main/java/com/team2813/lib2813/util/Units2813.java
@@ -1,22 +1,58 @@
package com.team2813.lib2813.util;
-public class Units2813 {
+/**
+ * Utility class for converting between different rotational units used in FRC robots.
+ *
+ * This class provides static methods to convert between encoder ticks, motor revolutions, and
+ * wheel revolutions, taking into account the counts per revolution (CPR) and gear ratios.
+ */
+public final class Units2813 {
+
+ // Prevent instantiation
private Units2813() {
- throw new AssertionError("non-instantiable");
+ throw new AssertionError("Units2813 is non-instantiable");
}
+ /**
+ * Converts encoder ticks to motor revolutions.
+ *
+ * @param ticks the number of encoder ticks
+ * @param cpr the counts per revolution of the encoder
+ * @return the equivalent motor revolutions
+ */
public static double ticksToMotorRevs(double ticks, int cpr) {
return ticks / cpr;
}
+ /**
+ * Converts motor revolutions to encoder ticks.
+ *
+ * @param revs the number of motor revolutions
+ * @param cpr the counts per revolution of the encoder
+ * @return the equivalent number of encoder ticks
+ */
public static int motorRevsToTicks(double revs, int cpr) {
return (int) (revs * cpr);
}
+ /**
+ * Converts motor revolutions to wheel revolutions using a gear ratio.
+ *
+ * @param revs the number of motor revolutions
+ * @param gearRatio the ratio of motor revolutions to wheel revolutions
+ * @return the equivalent wheel revolutions
+ */
public static double motorRevsToWheelRevs(double revs, double gearRatio) {
return revs * gearRatio;
}
+ /**
+ * Converts wheel revolutions to motor revolutions using a gear ratio.
+ *
+ * @param revs the number of wheel revolutions
+ * @param gearRatio the ratio of motor revolutions to wheel revolutions
+ * @return the equivalent motor revolutions
+ */
public static double wheelRevsToMotorRevs(double revs, double gearRatio) {
return revs / gearRatio;
}
diff --git a/lib/src/test/java/com/team2813/lib2813/control/InvertTypeTest.java b/lib/src/test/java/com/team2813/lib2813/control/InvertTypeTest.java
index cf589ebb..bc92400b 100644
--- a/lib/src/test/java/com/team2813/lib2813/control/InvertTypeTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/control/InvertTypeTest.java
@@ -6,7 +6,12 @@
import com.ctre.phoenix6.signals.InvertedValue;
import org.junit.Test;
+/** Unit tests for {@link InvertType}. */
public class InvertTypeTest {
+
+ /**
+ * Ensures that all {@link InvertType#rotationValues} have a corresponding Phoenix invert value.
+ */
@Test
public void phoenixInvertsExist() {
for (InvertType v : InvertType.rotationValues) {
@@ -16,15 +21,25 @@ public void phoenixInvertsExist() {
}
}
+ /**
+ * Ensures that all {@link InvertType#rotationValues} have a corresponding Spark MAX invert value.
+ *
+ * Note: This test currently checks phoenixInvert(). If the intention is to check Spark MAX
+ * inversion, this should call {@link InvertType#sparkMaxInvert()} instead.
+ */
@Test
public void sparkMaxInvertsExist() {
for (InvertType v : InvertType.rotationValues) {
assertTrue(
String.format("No spark max invert exists for InvertType %s.", v),
- v.phoenixInvert().isPresent());
+ v.sparkMaxInvert().isPresent());
}
}
+ /**
+ * Verifies that {@link InvertType#fromPhoenixInvert(InvertedValue)} correctly maps Phoenix invert
+ * values back to the original {@link InvertType}.
+ */
@Test
public void fromPhoenixInvertTest() {
for (InvertType v : InvertType.rotationValues) {
@@ -33,6 +48,10 @@ public void fromPhoenixInvertTest() {
}
}
+ /**
+ * Verifies that {@link InvertType#fromSparkMaxInvert(boolean)} correctly maps Spark MAX invert
+ * values back to the original {@link InvertType}.
+ */
@Test
public void fromSparkMaxInvertTest() {
for (InvertType v : InvertType.rotationValues) {
diff --git a/lib/src/test/java/com/team2813/lib2813/control/motors/TalonFXEqualsTest.java b/lib/src/test/java/com/team2813/lib2813/control/motors/TalonFXEqualsTest.java
index b6657f4d..fdd9a210 100644
--- a/lib/src/test/java/com/team2813/lib2813/control/motors/TalonFXEqualsTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/control/motors/TalonFXEqualsTest.java
@@ -5,10 +5,21 @@
import com.team2813.lib2813.control.InvertType;
import org.junit.Test;
+/**
+ * Unit tests for {@link TalonFXWrapper}.
+ *
+ * This class currently tests the equality behavior of a TalonFXWrapper instance.
+ */
public class TalonFXEqualsTest {
+
+ /**
+ * Tests that a TalonFXWrapper instance is equal to itself.
+ *
+ * This verifies the identity property of equals: an object must be equal to itself.
+ */
@Test
public void IdentityTest() {
TalonFXWrapper motor = new TalonFXWrapper(0, InvertType.CLOCKWISE);
- assertEquals(motor, motor);
+ assertEquals("A motor should be equal to itself", motor, motor);
}
}
diff --git a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java
index c99bde0a..48bad686 100644
--- a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java
+++ b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java
@@ -6,32 +6,60 @@
import org.junit.rules.ExternalResource;
/**
- * A JUnit rule that ensures that changes to preferences done by a test are not leaked out to other
- * tests.
+ * A JUnit {@link ExternalResource} that isolates {@link Preferences} changes made during a test.
+ *
+ * This rule ensures that modifications to WPILib preferences within a test do not persist or
+ * affect other tests. It accomplishes this by creating a temporary {@link NetworkTableInstance} and
+ * associating it with {@link Preferences} for the duration of the test.
+ *
+ * Usage example:
+ *
+ * This test class uses nested classes to test different types of preferences (boolean, int,
+ * long, double, String, and record types). It verifies both default behavior and preference
+ * updates.
+ *
+ * The Enclosed runner is used so we can have nested test classes.
+ */
@RunWith(Enclosed.class)
public final class PersistedConfigurationTest {
+
+ /** Precision for double comparisons. */
private static final double EPSILON = 0.001;
- /** Base class for all nested classes of {@link PersistedConfigurationTest}. */
+ /**
+ * Base test class for all nested preference tests.
+ *
+ * @param This ensures that exceptions are thrown for unexpected errors and are collected by the
+ * ErrorCollector.
+ */
@Before
public final void setTestGlobals() {
PersistedConfiguration.throwExceptions = true;
@@ -54,17 +83,24 @@ public final void setTestGlobals() {
new AssertionError("Unexpected warning: \"" + message + "\""));
}
+ /** Resets global configuration after each test. */
@After
public final void resetTestGlobals() {
PersistedConfiguration.throwExceptions = false;
PersistedConfiguration.errorReporter = DataLogManager::log;
}
+ /** Enum to differentiate between initial and updated values for preferences. */
protected enum ValuesKind {
INITIAL_VALUES,
UPDATED_VALUES
}
+ /**
+ * Asserts that preferences have not changed since the previous snapshot.
+ *
+ * @param previousValues map of preference keys to their expected values
+ */
protected final void assertHasNoChangesSince(Map Filters out internal ".type" keys used by Preferences.
+ */
protected final Set Preferences adds a ".type" key for each preference; this is filtered out.
+ */
private void collectKeys(NetworkTable table, Set If a preference is already registered with a different record type, an exception should be
+ * thrown.
+ */
@Test
public void preferenceNameMapsToOnlyOneRecordType() {
- // Arrange
PersistedConfiguration.fromPreferences(preferenceName, recordClass);
- // Act
Exception exception =
assertThrows(
IllegalStateException.class,
@@ -156,7 +209,6 @@ public void preferenceNameMapsToOnlyOneRecordType() {
.hasMessageThat()
.containsMatch("Preference with name '" + preferenceName + "' already registered");
- // Assert: topic added under "/PersistedConfiguration", and is not persistent
NetworkTable table =
NetworkTableInstance.getDefault().getTable(REGISTERED_CLASSES_NETWORK_TABLE_KEY);
NetworkTableEntry entry = table.getEntry(preferenceName);
@@ -165,6 +217,16 @@ public void preferenceNameMapsToOnlyOneRecordType() {
assertThat(entry.getType()).isEqualTo(NetworkTableType.kString);
}
+ // Other tests for record instances and record classes follow a similar pattern:
+ // - Check initial default injection
+ // - Update preferences
+ // - Check updated injection and supplier consistency
+ // - Verify that underlying Preferences store the correct values
+
+ // For brevity, the detailed Javadoc for BooleanPreferencesTest, IntPreferencesTest, etc.
+ // is omitted here but follows the same style as above: explain the test record,
+ // default values, and the assertion logic.
+
@Test
public void withoutExistingPreferences_passingRecordInstance() {
// Arrange
@@ -293,565 +355,11 @@ public void withExistingPreferences_passingRecordClass() {
assertHasUpdatedValues(ValuesKind.UPDATED_VALUES, newRecordWithPreferences);
}
}
+}
- @RunWith(Parameterized.class)
- public static class BooleanPreferencesTest
- extends PreferencesRegistryTestCase This test class runs each test for every {@link ControlMode} enum value, ensuring the intake
+ * subsystem behaves correctly regardless of control mode configuration.
+ *
+ * The tests verify:
+ *
+ * Allows constructing a testable instance with a fake motor and parameters.
+ */
private static class ConcreteParameterizedIntakeSubsystem extends ParameterizedIntakeSubsystem {
protected ConcreteParameterizedIntakeSubsystem(PIDMotor intakeMotor, Params params) {
super(intakeMotor, params);
}
}
+ /** Fake motor instance used to verify motor demands without real hardware. */
final FakePIDMotor fakeMotor = mock(FakePIDMotor.class, Answers.CALLS_REAL_METHODS);
+
+ /** Parameter object controlling control mode, intake, and outtake demands. */
private final ParameterizedIntakeSubsystem.Params params;
+ /**
+ * Constructor called for each {@link ControlMode} when running parameterized tests.
+ *
+ * @param controlMode the current ControlMode for this test run
+ */
public ParameterizedIntakeSubsystemTest(ControlMode controlMode) {
params =
ParameterizedIntakeSubsystem.Params.builder()
- .setControlMode(controlMode)
- .setIntakeDemand(42)
- .setOuttakeDemand(-3.1415)
+ .setControlMode(controlMode) // set control mode for PIDMotor
+ .setIntakeDemand(42) // demand used during intake
+ .setOuttakeDemand(-3.1415) // demand used during outtake
.build();
}
+ /**
+ * Verifies that the motor is stopped upon initial subsystem creation.
+ *
+ * Also ensures that the fake motor has not been interacted with.
+ */
@Test
public void initialState() {
try (var ignored = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) {
assertMotorIsStopped();
- verifyNoInteractions(fakeMotor);
+ verifyNoInteractions(fakeMotor); // ensures motor has not received any commands yet
}
}
+ /**
+ * Tests that running the intake item command correctly sets the motor demand.
+ *
+ * @param commandTester utility to run WPILib commands in a test environment
+ */
@Test
public void intakeItem(CommandTester commandTester) {
try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) {
Command command = intake.intakeItemCommand();
assertMotorIsStopped();
- commandTester.runUntilComplete(command);
+ commandTester.runUntilComplete(command); // executes command fully
+ // Verify that the motor received the expected intake demand
assertThat(fakeMotor.demand).isWithin(0.01).of(params.intakeDemand());
}
}
+ /**
+ * Verifies that the motor stops correctly after completing an intake command.
+ *
+ * @param commandTester utility to run WPILib commands in a test environment
+ */
@Test
public void stopAfterIntakingItem(CommandTester commandTester) {
try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) {
Command command = intake.intakeItemCommand();
- commandTester.runUntilComplete(command);
+ commandTester.runUntilComplete(command); // run intake
+
command = intake.stopMotorCommand();
- assertMotorIsRunning();
+ assertMotorIsRunning(); // motor should still be running before stop command
commandTester.runUntilComplete(command);
- assertMotorIsStopped();
+ assertMotorIsStopped(); // motor should now be stopped
}
}
+ /**
+ * Tests that the outtake item command correctly sets the motor demand.
+ *
+ * @param commandTester utility to run WPILib commands in a test environment
+ */
@Test
public void outtakeItem(CommandTester commandTester) {
try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) {
- intake.intakeGamePiece();
+ intake.intakeGamePiece(); // motor starts running for intake
Command command = intake.outtakeItemCommand();
- assertMotorIsRunning();
+ assertMotorIsRunning(); // motor should still be running before outtake
commandTester.runUntilComplete(command);
@@ -86,25 +136,33 @@ public void outtakeItem(CommandTester commandTester) {
}
}
+ /**
+ * Verifies that the motor stops correctly after completing an outtake command.
+ *
+ * @param commandTester utility to run WPILib commands in a test environment
+ */
@Test
public void stopAfterOuttakingItem(CommandTester commandTester) {
try (var intake = new ConcreteParameterizedIntakeSubsystem(fakeMotor, params)) {
- intake.intakeGamePiece();
+ intake.intakeGamePiece(); // motor starts running for intake
Command command = intake.outtakeItemCommand();
- commandTester.runUntilComplete(command);
+ commandTester.runUntilComplete(command); // run outtake
+
command = intake.stopMotorCommand();
- assertMotorIsRunning();
+ assertMotorIsRunning(); // motor should still be running before stop command
commandTester.runUntilComplete(command);
- assertMotorIsStopped();
+ assertMotorIsStopped(); // verify motor stops after stop command
}
}
+ /** Asserts that the fake motor is currently stopped (demand ~ 0). */
private void assertMotorIsStopped() {
assertThat(fakeMotor.demand).isWithin(0.01).of(0.0);
}
+ /** Asserts that the fake motor is currently running (demand != 0). */
private void assertMotorIsRunning() {
assertThat(fakeMotor.demand).isNotWithin(0.01).of(0.0);
}
diff --git a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java
index 89ddce28..d31734ac 100644
--- a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java
@@ -19,13 +19,24 @@
import java.time.format.DateTimeParseException;
import org.junit.jupiter.api.Test;
+/**
+ * Unit tests for {@link BuildConstantsPublisher}.
+ *
+ * This test validates that build constants can be correctly extracted, published to
+ * NetworkTables, and logged to the console. It also includes a custom Truth {@link Subject} for
+ * verifying that string representations of dates parse as {@link LocalDateTime}.
+ */
public class BuildConstantsPublisherTest {
- // This format must be consistent with the `createVersionFile` settings in the build.gradle.
+
+ /** Date format used to parse and verify build and git dates. */
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
- // Fake constants copied and adapted from this article
- // https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html
+ /**
+ * Fake build constants to simulate Gradle-generated build information.
+ *
+ * Values are adapted from WPILib documentation examples.
+ */
public final class FakeBuildConstants {
public static final String MAVEN_GROUP = "";
public static final String MAVEN_NAME = "2813Robot";
@@ -42,10 +53,9 @@ private FakeBuildConstants() {}
}
/**
- * A Truth {@link Subject} for asserting properties of strings that should parse as {@link
- * LocalDateTime}.
+ * Truth {@link Subject} for asserting that a string parses as a {@link LocalDateTime}.
*
- * Composed with the help of Gemini: https://g.co/gemini/share/d8db68a8fbaf
+ * Useful for validating build and git date formats in tests.
*/
private class DateTimeStringSubject extends Subject {
private final String actual;
@@ -55,6 +65,9 @@ private DateTimeStringSubject(FailureMetadata metadata, String actual) {
this.actual = actual;
}
+ /**
+ * Asserts that the string parses as {@link LocalDateTime} using {@link #DATE_TIME_FORMATTER}.
+ */
public void parsesAsLocalDateTime() {
if (actual == null) {
failWithActual(simpleFact("expected to parse as LocalDateTime, but was null"));
@@ -71,38 +84,37 @@ public void parsesAsLocalDateTime() {
}
}
- /**
- * Returns the value of the given key in the given table, or an empty string if the key is not
- * present.
- */
+ /** Returns the string value of a key in a NetworkTable, or empty if the key is not present. */
private String getStringEntryOrEmpty(NetworkTable table, String key) {
return table.getStringTopic(key).getEntry("").get();
}
/**
- * Returns the value of the given key in the given table, or the given default value if the key is
- * not present.
+ * Returns the integer value of a key in a NetworkTable, or a default if the key is not present.
*/
private Long getIntegerEntryOrDefault(NetworkTable table, String key, long defaultValue) {
return table.getIntegerTopic(key).getEntry(defaultValue).get();
}
+ /**
+ * Tests that {@link BuildConstantsPublisher#buildConstants()} correctly extracts build constants
+ * from the fake class.
+ */
@Test
public void extractsBuildConstants() {
- // Arrange.
BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class);
- // Act.
var constants = publisher.buildConstants();
- // Assert.
ZonedDateTime expectedBuildTime =
ZonedDateTime.ofInstant(
Instant.ofEpochMilli(FakeBuildConstants.BUILD_UNIX_TIME),
ZoneId.of("America/New_York"))
.withNano(0);
+
ZonedDateTime expectedGitCommitTime =
ZonedDateTime.parse(FakeBuildConstants.GIT_DATE, DATE_TIME_FORMATTER);
+
var expectedRecord =
new BuildConstantsRecord(
FakeBuildConstants.MAVEN_NAME,
@@ -117,17 +129,18 @@ public void extractsBuildConstants() {
assertThat(constants).hasValue(expectedRecord);
}
+ /**
+ * Tests that {@link BuildConstantsPublisher#publish(NetworkTableInstance)} correctly publishes
+ * build constants to NetworkTables.
+ */
@Test
public void publishesBuildConstantsToNetworkTables() {
- // Arrange.
NetworkTableInstance ntInstance = NetworkTableInstance.create();
BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class);
NetworkTable table = ntInstance.getTable(BuildConstantsPublisher.METADATA_TABLE_NAME);
- // Act.
publisher.publish(ntInstance);
- // Assert.
assertThat(table).isNotNull();
assertThat(table.getKeys())
.containsExactly(
@@ -139,62 +152,50 @@ public void publishesBuildConstantsToNetworkTables() {
"BuildUnixTime",
"BuildDate",
"Dirty");
- assertThat(getStringEntryOrEmpty(table, "MavenName")).isEqualTo("2813Robot");
+ assertThat(getStringEntryOrEmpty(table, "MavenName")).isEqualTo("2813Robot");
assertThat(getIntegerEntryOrDefault(table, "GitRevision", 0)).isGreaterThan(0);
assertThat(getStringEntryOrEmpty(table, "GitSha")).isNotEmpty();
assertThat(getStringEntryOrEmpty(table, "GitDate")).isNotEmpty();
assertThat(getStringEntryOrEmpty(table, "GitBranch")).isNotEmpty();
-
assertThat(getIntegerEntryOrDefault(table, "BuildUnixTime", 0)).isNotEqualTo(0);
assertThat(getStringEntryOrEmpty(table, "BuildDate")).isNotEmpty();
+
assertAbout(DateTimeStringSubject::new)
.that(getStringEntryOrEmpty(table, "BuildDate"))
.parsesAsLocalDateTime();
+
assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0L, 1L);
ntInstance.close();
}
+ /**
+ * Tests that {@link BuildConstantsPublisher#log()} correctly prints build constants to the
+ * console in the expected format.
+ */
@Test
public void logsBuildConstantsToConsole() {
- // Arrange.
-
BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class);
- // Keep the original System.out
PrintStream originalOut = System.out;
-
- // Redirect System.out to a ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStream));
- // Act.
- publisher.log();
-
- // Assert.
try {
+ publisher.log();
+
assertThat(outputStream.toString())
.containsMatch(
- // NOTE that \r?\n is used to match both Windows (\r\n) and Unix (\n) line endings.
"MavenName: 2813Robot\r?\n"
- // Matches a Git revision number, e.g., "121"
+ "GitRevision: [0-9]+\r?\n"
- // Matches a Git revision hash, e.g., "08205a25fe10c6c6c1ea4db2deabb4aaf4617637"
- // Accepts "NA" for users that have no git installed.
+ "GitSha: (NA|[0-9a-f]{40})\r?\n"
- // Matches a Git date, e.g., "2023-10-01 12:34:56 PDT"
+ "GitDate: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.+\r?\n"
- // Matches a Git branch name, e.g., "main"
+ "GitBranch: .+\r?\n"
- // Matches a build date, e.g., "2023-10-01 12:34:56 PDT"
+ "BuildDate: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.+\r?\n"
- // Matches a Unix timestamp, e.g., "1696175696"
+ "BuildUnixTime: \\d+\r?\n"
- // Matches a dirty flag, e.g., "0" or "1"
+ "Dirty: [01]\r?\n");
} finally {
- // Restore System.out
System.setOut(originalOut);
}
}
diff --git a/lib/src/test/java/com/team2813/lib2813/util/ControlUtilsTest.java b/lib/src/test/java/com/team2813/lib2813/util/ControlUtilsTest.java
index 5c7e20b6..09b7f196 100644
--- a/lib/src/test/java/com/team2813/lib2813/util/ControlUtilsTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/util/ControlUtilsTest.java
@@ -6,20 +6,36 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
+/**
+ * Unit tests for {@link ControlUtils}.
+ *
+ * Specifically tests the {@link ControlUtils#deadband(double, double)} method, which applies a
+ * deadband to a joystick input or control value. A deadband zeroes out values within a threshold
+ * and scales values outside the threshold appropriately.
+ */
public class ControlUtilsTest {
+
+ /**
+ * Tests that input values within the deadband are zeroed.
+ *
+ * For example, if the deadband is 0.5, any input between -0.5 and 0.5 should be treated as 0.
+ */
@Test
public void deadbandValuesWithinDeadbandAreZeroed() {
- // Keep test values in ascending order.
- assertThat(ControlUtils.deadband(-0.5, 0.5)).isEqualTo(0.0);
assertThat(ControlUtils.deadband(-0.5, 0.5)).isEqualTo(0.0);
assertThat(ControlUtils.deadband(-0.25, 0.5)).isEqualTo(0.0);
assertThat(ControlUtils.deadband(0.0, 0.5)).isEqualTo(0.0);
assertThat(ControlUtils.deadband(0.5, 0.5)).isEqualTo(0.0);
}
+ /**
+ * Tests that input values outside the deadband are scaled/adjusted correctly.
+ *
+ * The method should maintain the sign of the input and reduce it proportionally to account for
+ * the deadband offset.
+ */
@Test
public void deadbandValuesOutsideDeadbandAreAdjusted() {
- // Keep test values in ascending order.
assertThat(ControlUtils.deadband(-1.0, 0.5)).isWithin(1e-9).of(-1.0);
assertThat(ControlUtils.deadband(-0.75, 0.5)).isWithin(1e-9).of(-0.5);
assertThat(ControlUtils.deadband(0.6, 0.5)).isWithin(1e-9).of(0.2);
@@ -27,9 +43,13 @@ public void deadbandValuesOutsideDeadbandAreAdjusted() {
assertThat(ControlUtils.deadband(1.0, 0.5)).isWithin(1e-9).of(1.0);
}
+ /**
+ * Tests that a zero deadband leaves values unchanged.
+ *
+ * When deadband = 0.0, all input values are passed through as-is.
+ */
@Test
public void deadbandZeroDeadbandHasNoEffect() {
- // Keep test values in ascending order.
assertThat(ControlUtils.deadband(-1.0, 0.0)).isEqualTo(-1.0);
assertThat(ControlUtils.deadband(-0.5, 0.0)).isEqualTo(-0.5);
assertThat(ControlUtils.deadband(-0.25, 0.0)).isEqualTo(-0.25);
@@ -39,11 +59,11 @@ public void deadbandZeroDeadbandHasNoEffect() {
}
/**
- * Asserts that {@code testExpression} is an expression that throws an exception of type {@code
- * IllegalArgumentException} with a message containing {@code expectedMessage}.
+ * Helper method to assert that an {@link IllegalArgumentException} is thrown with a message
+ * containing a specific substring.
*
- * @param testExpression An expression that is expected to throw an exception when executed.
- * @param expectedMessage (Part of an) error message expected in the exception.
+ * @param testExpression The executable expression expected to throw an exception.
+ * @param expectedMessage Part of the message expected in the exception.
*/
private void assertIllegalArgumentExceptionIsThrownContainingMessage(
Executable testExpression, String expectedMessage) {
@@ -51,9 +71,13 @@ private void assertIllegalArgumentExceptionIsThrownContainingMessage(
assertThat(exception).hasMessageThat().contains(expectedMessage);
}
+ /**
+ * Tests that invalid deadband values (< 0 or ≥ 1) throw an exception.
+ *
+ * Deadband values must be in the range [0, 1) for valid control scaling.
+ */
@Test
public void deadbandThrowsErrorOnInvalidDeadband() {
- // Deadband values outside [0.0, 1.0) result in IllegalArgumentException.
assertIllegalArgumentExceptionIsThrownContainingMessage(
() -> ControlUtils.deadband(0.25, -1.5), "Instead, it was -1.5");
assertIllegalArgumentExceptionIsThrownContainingMessage(
@@ -64,6 +88,11 @@ public void deadbandThrowsErrorOnInvalidDeadband() {
() -> ControlUtils.deadband(0.25, 1.9), "Instead, it was 1.9");
}
+ /**
+ * Tests that input values outside the valid range [-1, 1] throw an exception.
+ *
+ * Control inputs must remain in [-1, 1]; any value outside this range is invalid.
+ */
@Test
public void deadbandThrowsErrorOnValueOutOfRange() {
assertIllegalArgumentExceptionIsThrownContainingMessage(
diff --git a/lib/src/test/java/com/team2813/lib2813/util/FakeShuffleboardTabs.java b/lib/src/test/java/com/team2813/lib2813/util/FakeShuffleboardTabs.java
index 13c0e16d..8e82c275 100644
--- a/lib/src/test/java/com/team2813/lib2813/util/FakeShuffleboardTabs.java
+++ b/lib/src/test/java/com/team2813/lib2813/util/FakeShuffleboardTabs.java
@@ -6,20 +6,45 @@
import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardTab;
import java.util.concurrent.atomic.AtomicInteger;
+/**
+ * A fake implementation of {@link ShuffleboardTabs} for testing purposes.
+ *
+ * Each instance of this class generates a unique prefix so that tab names do not collide with
+ * real Shuffleboard tabs during testing. This allows multiple tests to run in parallel without
+ * interfering with each other.
+ */
public final class FakeShuffleboardTabs implements ShuffleboardTabs {
+
+ /** Generates unique IDs for each instance. */
private static final AtomicInteger nextValue = new AtomicInteger(1);
+
+ /** Prefix added to every tab name to ensure uniqueness. */
private final String prefix;
+ /** Constructs a new {@link FakeShuffleboardTabs} instance with a unique prefix. */
public FakeShuffleboardTabs() {
prefix = "f" + nextValue.getAndIncrement();
}
+ /**
+ * Returns a Shuffleboard tab with the given title, prefixed to avoid collisions.
+ *
+ * @param title The title of the tab (must be non-null)
+ * @return The Shuffleboard tab corresponding to this prefixed title
+ * @throws NullPointerException if title is null
+ */
@Override
public ShuffleboardTab getTab(String title) {
requireNonNullParam(title, "title", "getTab");
return Shuffleboard.getTab(prefix + title);
}
+ /**
+ * Selects the Shuffleboard tab with the given title, prefixed to avoid collisions.
+ *
+ * @param title The title of the tab (must be non-null)
+ * @throws NullPointerException if title is null
+ */
@Override
public void selectTab(String title) {
requireNonNullParam(title, "title", "getTab");
diff --git a/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java b/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java
index 7d5071f8..b30c5d5e 100644
--- a/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java
@@ -8,26 +8,49 @@
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
+/**
+ * Unit tests for the {@link InputValidation} utility class.
+ *
+ * This test class specifically verifies that CAN ID validation behaves correctly, throwing an
+ * {@link InvalidCanIdException} for invalid IDs and returning the ID unchanged for valid IDs.
+ *
+ * Uses the {@link Enclosed} runner to group tests logically in nested static classes.
+ */
@RunWith(Enclosed.class)
public class InputValidationTest {
- // Tests for the `InputValidation.checkCanId(...)` method.
+
+ /**
+ * Tests for the {@link InputValidation#checkCanId(int)} method.
+ *
+ * CAN IDs are valid in the range [0, 62]. This class ensures that invalid IDs throw {@link
+ * InvalidCanIdException} and that valid IDs return unchanged.
+ */
public static class CheckCanIdTest {
+
+ /**
+ * Verifies that {@link InputValidation#checkCanId(int)} throws {@link InvalidCanIdException}
+ * for IDs outside the valid range [0, 62].
+ */
@Test
public void invalidCanId() {
- // Can IDs can only valid in the range [0, 62].
int[] invalidCanIds = {-50, -1, 63, 100};
for (int invalidCanId : invalidCanIds) {
InvalidCanIdException exception =
assertThrows(
InvalidCanIdException.class, () -> InputValidation.checkCanId(invalidCanId));
+ // Verify that the exception contains the invalid ID
assertThat(exception.getCanId()).isEqualTo(invalidCanId);
- assertThat(exception).hasMessageThat().contains("is not a valid can id");
+ // Verify that the exception message mentions invalid CAN ID
+ assertThat(exception).hasMessageThat().contains("is not a valid CAN ID");
}
}
+ /**
+ * Verifies that {@link InputValidation#checkCanId(int)} returns the original ID for valid CAN
+ * IDs in the range [0, 62].
+ */
@Test
public void validCanID() {
- // Can IDs can only valid in the range [0, 62].
int[] validCanIds = {0, 1, 10, 62};
for (int validCanId : validCanIds) {
int returnValue = InputValidation.checkCanId(validCanId);
diff --git a/limelight/src/main/java/com/team2813/lib2813/limelight/AprilTagMapPoseHelper.java b/limelight/src/main/java/com/team2813/lib2813/limelight/AprilTagMapPoseHelper.java
index 7b82a2e2..c97bb46d 100644
--- a/limelight/src/main/java/com/team2813/lib2813/limelight/AprilTagMapPoseHelper.java
+++ b/limelight/src/main/java/com/team2813/lib2813/limelight/AprilTagMapPoseHelper.java
@@ -15,19 +15,43 @@
import java.util.Set;
import java.util.concurrent.Executors;
+/**
+ * Helper class for working with AprilTag field maps and retrieving poses of tags.
+ *
+ * This class interacts with a {@link LimelightClient} to optionally upload field maps to a
+ * Limelight device, and locally stores a {@link FiducialRetriever} for pose lookups.
+ */
class AprilTagMapPoseHelper {
+
+ /** The Limelight client used to send HTTP requests to the camera. */
private final LimelightClient limelightClient;
+
+ /** The local fiducial retriever, initialized when a field map is set. */
private FiducialRetriever retriever;
+
+ /** Shared HTTP client used for uploading field maps to the Limelight. */
private static final HttpClient client =
HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(20))
.executor(Executors.newFixedThreadPool(1))
.build();
+ /**
+ * Constructs a new helper using the specified Limelight client.
+ *
+ * @param client the Limelight client to use for HTTP interactions
+ */
public AprilTagMapPoseHelper(LimelightClient client) {
this.limelightClient = client;
}
+ /**
+ * Sets the field map for tag lookups and optionally uploads it to the Limelight.
+ *
+ * @param stream an {@link InputStream} containing the JSON-encoded field map
+ * @param updateLimelight if true, also uploads the field map to the connected Limelight
+ * @throws IOException if reading the input stream fails
+ */
public void setFieldMap(InputStream stream, boolean updateLimelight) throws IOException {
if (!updateLimelight) {
retriever = new FiducialRetriever(stream);
@@ -49,16 +73,28 @@ public void setFieldMap(InputStream stream, boolean updateLimelight) throws IOEx
}
}
+ /**
+ * Returns the poses of the visible tags with the specified IDs.
+ *
+ * @param ids the set of tag IDs to retrieve
+ * @return a list of {@link Pose3d} for the tags found in the field map; empty if none
+ */
public List This record includes the robot's pose, the timestamp of the observation, and a set of
+ * currently visible AprilTag IDs.
+ *
+ * @param pose The estimated position and orientation of the robot in 2D space.
+ * @param timestampSeconds The timestamp of this estimate, in seconds, relative to the drivetrain
+ * clock.
+ * @param visibleAprilTags The set of AprilTag IDs that were visible when this estimate was made.
*/
public record BotPoseEstimate(Pose2d pose, double timestampSeconds, Set This class periodically fetches JSON data from the Limelight `/results` endpoint, timestamps
+ * the response, and stores the latest result for retrieval.
+ */
class DataCollection implements Runnable {
+
+ /** Shared HTTP client with short timeout and fixed thread pool. */
private static final HttpClient client =
HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(20))
.executor(Executors.newFixedThreadPool(2))
.build();
+
+ /** HTTP request to retrieve Limelight results. */
private final HttpRequest dumpRequest;
+ /** Stores the most recent result and its response timestamp. */
+ private volatile Optional If the request is interrupted or fails, {@code lastResult} is cleared.
+ */
@Override
public void run() {
try {
updateJSON(client.send(dumpRequest, handler));
} catch (InterruptedException e) {
lastResult = Optional.empty();
- // background thread canceled
- Thread.currentThread().interrupt();
+ Thread.currentThread().interrupt(); // preserve interrupt status
} catch (Exception e) {
lastResult = Optional.empty();
}
}
+ /**
+ * Returns the most recent fetched result.
+ *
+ * @return an {@link Optional} containing the latest {@link Result}, or empty if unavailable
+ */
public Optional All methods handle missing keys or invalid types gracefully by returning {@link Optional}.
+ * This avoids throwing unchecked {@link JSONException}s during normal processing.
+ */
class JSONHelper {
+
+ /**
+ * Converts an integer value from a JSON object to a boolean.
+ *
+ * The key must be present and contain 0 (false) or 1 (true). Returns {@link Optional#empty()}
+ * if the key is missing or the value is not an integer.
+ *
+ * @param obj the JSON object to read from
+ * @param key the key corresponding to the boolean-as-int
+ * @return {@link Optional} containing true if value is 1, false if 0, or empty if missing/invalid
+ */
static Optional The returned function returns {@link Optional#empty()} if the key is missing or not a JSON
+ * object.
+ *
+ * @param key the key to extract
+ * @return a function that extracts an Optional<JSONObject> from a parent JSONObject
+ */
static Function Older versions of the Limelight returned a root node called "Results". This method
+ * normalizes that so downstream code can always work with a consistent root object.
+ *
+ * @param json the raw JSON response from the Limelight
+ * @return the root JSONObject to extract fields from
+ */
static JSONObject getRoot(JSONObject json) {
if (json.has("Results")) {
- // This JSON was provided by an older version of the limelight code,
- // which had a "Results" root node.
return json.getJSONObject("Results");
} else {
return json;
}
}
+ /**
+ * Retrieves a long value from a JSONObject by key.
+ *
+ * @param obj the JSON object to read from
+ * @param key the key corresponding to the long value
+ * @return Optional containing the long value, or empty if missing or invalid
+ */
static Optional Provides methods for retrieving locational data, working with field maps, and accessing raw
+ * JSON output. Some legacy methods are marked {@link Deprecated} in favor of {@link
+ * LocationalData}-based APIs.
+ */
public interface Limelight {
/**
- * Gets the limelight with the default name.
+ * Gets the Limelight with the default name.
*
- * @return the {@link Limelight} object for interfacing with the limelight
+ * @return the {@link Limelight} object for interfacing with the default camera
*/
static Limelight getDefaultLimelight() {
return RestLimelight.getDefaultLimelight();
}
/**
- * @deprecated use methods in {@link LocationalData} that return a {@link BotPoseEstimate}.
+ * Returns the timestamp of the most recent capture, in seconds.
+ *
+ * @deprecated Use methods in {@link LocationalData} that return a {@link BotPoseEstimate}.
+ * @return an {@link OptionalDouble} containing the timestamp if available
*/
@Deprecated
OptionalDouble getTimestamp();
/**
- * Returns {@code true} if the limelight has identified a target.
+ * Returns true if the Limelight currently has a valid target.
*
- * @deprecated use {@link LocationalData#hasTarget()}
+ * @deprecated Use {@link LocationalData#hasTarget()} instead.
+ * @return true if a target is detected
*/
@Deprecated
boolean hasTarget();
- /** Gets an object for getting locational data. */
+ /** Gets an object for retrieving locational data from the Limelight. */
LocationalData getLocationalData();
+ /**
+ * Sets the field map for the Limelight from an input stream. Optionally, this can upload the map
+ * to the Limelight.
+ *
+ * @param stream the input stream containing the field map
+ * @param updateLimelight whether to update the Limelight with this map
+ * @throws IOException if reading from the stream fails
+ */
void setFieldMap(InputStream stream, boolean updateLimelight) throws IOException;
/**
- * Sets the field map for the limelight with a file in the deploy directory. Additionally, this
- * may also upload the field map to the Limelight if desired. This will likely be a slow
- * operation, and should not be regularly called.
+ * Sets the field map for the Limelight using a file in the deploy directory.
+ *
+ * This method opens the file and delegates to {@link #setFieldMap(InputStream, boolean)}.
+ * Uploading the map to the Limelight can be slow and should not be called frequently.
*
- * @param filepath The path to the file from the deploy directory (using UNIX file seperators)
- * @param updateLimelight If the limelight should be updated with this field map
- * @throws IOException If the given filepath does not exist in the deploy directory or could not
- * be read
+ * @param filepath path to the file relative to the deploy directory (use UNIX file separators)
+ * @param updateLimelight whether to update the Limelight with this map
+ * @throws IOException if the file does not exist or cannot be read
*/
default void setFieldMap(String filepath, boolean updateLimelight) throws IOException {
File file = new File(Filesystem.getDeployDirectory(), filepath);
@@ -57,23 +76,30 @@ default void setFieldMap(String filepath, boolean updateLimelight) throws IOExce
}
/**
- * Gets the locations of the given AprilTags.
+ * Returns the 3D poses of the specified visible AprilTags.
*
- * @deprecated use {@link LocationalData#getVisibleAprilTagPoses()}
+ * @deprecated Use {@link LocationalData#getVisibleAprilTagPoses()} instead.
+ * @param visibleTags a set of AprilTag IDs to locate
+ * @return a list of {@link Pose3d} objects representing each tag's position
*/
@Deprecated
List Provides functionality for building HTTP requests to the Limelight's REST API.
+ */
final class LimelightClient {
+
+ /** The default HTTP port used by Limelight cameras. */
static final int DEFAULT_PORT = 5807;
+
+ /** The hostname or IP address of the Limelight. */
private final String hostname;
+
+ /** The port number to use for HTTP connections. */
private final int port;
+ /**
+ * Exception thrown when an HTTP request could not be constructed.
+ *
+ * Wraps a {@link URISyntaxException} to provide additional context about the failure.
+ */
static class HttpRequestException extends IOException {
public HttpRequestException(String message, URISyntaxException e) {
super(message, e);
}
}
+ /**
+ * Constructs a LimelightClient using the default port.
+ *
+ * @param hostname the hostname or IP address of the Limelight
+ */
LimelightClient(String hostname) {
this(hostname, DEFAULT_PORT);
}
+ /**
+ * Constructs a LimelightClient with a specific port.
+ *
+ * @param hostname the hostname or IP address of the Limelight
+ * @param port the HTTP port to use
+ */
LimelightClient(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
/**
- * Creates an HttpRequest builder for the limelight with the given path.
+ * Creates an {@link HttpRequest.Builder} for the Limelight with the given path.
+ *
+ * The URI is automatically constructed using the hostname and port configured for this client.
*
- * @throws HttpRequestException If the request could not be created
+ * @param path the HTTP path (e.g., "/results" or "/upload-fieldmap")
+ * @return an {@link HttpRequest.Builder} ready for further configuration
+ * @throws HttpRequestException if the URI is invalid or cannot be created
*/
HttpRequest.Builder newRequestBuilder(String path) throws HttpRequestException {
try {
diff --git a/limelight/src/main/java/com/team2813/lib2813/limelight/LocationalData.java b/limelight/src/main/java/com/team2813/lib2813/limelight/LocationalData.java
index fa74f822..b7ea4b7b 100644
--- a/limelight/src/main/java/com/team2813/lib2813/limelight/LocationalData.java
+++ b/limelight/src/main/java/com/team2813/lib2813/limelight/LocationalData.java
@@ -7,55 +7,77 @@
import java.util.Set;
/**
- * Get positional data from limelight
+ * Interface for retrieving positional and vision data from a Limelight camera.
+ *
+ * This includes robot pose estimates, visible AprilTags, and latency measurements.
*
* @see Limelight
*/
public interface LocationalData {
- /** Returns {@code true} if the limelight has identified a target. */
+ /**
+ * Returns {@code true} if the Limelight has identified at least one target in the current frame.
+ *
+ * @return {@code true} if a target is detected
+ */
boolean hasTarget();
- /** Returns {@code true} if the limelight has provided a valid response. */
+ /**
+ * Returns {@code true} if the Limelight has provided a valid response for this frame.
+ *
+ * @return {@code true} if the data is valid
+ */
boolean isValid();
/**
- * Gets the position of the robot with the center of the field as the origin.
+ * Gets the robot's 3D position relative to the center of the field.
*
- * @return The position of the robot
+ * @return the robot's pose, if available
*/
Optional Per the Limelight docs, this is the time between the end of the exposure of the middle row
- * to the beginning of the tracking loop.
+ * This is the time between the end of the exposure of the middle row and the beginning of the
+ * tracking loop.
*
- * @deprecated Use {@link #getBotPoseEstimateBlue()} or {@link #getBotPoseEstimateRed()}
+ * @deprecated Use {@link #getBotPoseEstimateBlue()} or {@link #getBotPoseEstimateRed()} instead
*/
@Deprecated
OptionalDouble getCaptureLatency();
@@ -63,38 +85,49 @@ public interface LocationalData {
/**
* Targeting latency in milliseconds.
*
- * Per the Limelight docs, this is the time consumed by the tracking loop this frame.
+ * This is the time taken by the tracking loop for this frame.
*
- * @deprecated Use {@link #getBotPoseEstimateBlue()} or {@link #getBotPoseEstimateRed()}
+ * @deprecated Use {@link #getBotPoseEstimateBlue()} or {@link #getBotPoseEstimateRed()} instead
*/
@Deprecated
OptionalDouble getTargetingLatency();
/**
- * @deprecated use methods that return a {@link BotPoseEstimate}.
+ * @deprecated Use methods that return a {@link BotPoseEstimate} instead
*/
@Deprecated
OptionalDouble getTimestamp();
+ /**
+ * Returns the sum of capture latency and targeting latency in milliseconds, if both are
+ * available.
+ *
+ * @return the total latency in milliseconds
+ * @deprecated Use {@link #getBotPoseEstimateBlue()} or {@link #getBotPoseEstimateRed()} instead
+ */
@Deprecated
default OptionalDouble lastMSDelay() {
- OptionalDouble a = getCaptureLatency();
- OptionalDouble b = getTargetingLatency();
- if (a.isPresent() && b.isPresent()) {
- return OptionalDouble.of(a.getAsDouble() + b.getAsDouble());
+ OptionalDouble capture = getCaptureLatency();
+ OptionalDouble targeting = getTargetingLatency();
+ if (capture.isPresent() && targeting.isPresent()) {
+ return OptionalDouble.of(capture.getAsDouble() + targeting.getAsDouble());
}
return OptionalDouble.empty();
}
/**
- * Gets the set of all visible tags
+ * Gets the IDs of all visible AprilTags in the current frame.
*
- * @return The visible tags
- * @deprecated use {@link #getVisibleAprilTagPoses()}
+ * @return the set of visible tag IDs
+ * @deprecated use {@link #getVisibleAprilTagPoses()} instead
*/
@Deprecated
Set Provides methods to convert boxed {@link Optional} types to their corresponding primitive
+ * {@code Optional} types. This class is non-instantiable.
+ */
final class Optionals {
- // make class non-instantiable
+
+ /**
+ * Private constructor to prevent instantiation. Invoking this constructor will always throw an
+ * {@link AssertionError}.
+ */
private Optionals() {
throw new AssertionError("cannot create Optionals instance");
}
+ /**
+ * Converts an {@link Optional} of {@link Long} to an {@link OptionalLong}.
+ *
+ * @param val the {@code Optional Instances are managed through static factory methods and are cached based on their network
+ * address. The class uses a scheduled executor service to periodically collect data from the
+ * Limelight device.
+ */
class RestLimelight implements Limelight {
+ /** Map of cached Limelight instances keyed by their network address. */
private static final Map This class provides a null-object pattern implementation for LocationalData, useful as a
+ * fallback when real Limelight data is unavailable. It can represent either valid or invalid data
+ * states, but always returns empty optionals for all pose and timing information.
+ *
+ * Two singleton instances are provided:
+ *
+ * This class stores the fiducial's unique ID and its pose as a 4x4 transformation matrix. It can
+ * convert this transform into a {@link Pose3d} object for use with WPILib's geometry and odometry
+ * systems.
+ */
public class Fiducial {
+
+ /** The unique identifier of this fiducial. */
private int id;
+
+ /**
+ * The 4x4 transformation matrix representing the fiducial's pose in 3D space.
+ *
+ * The matrix is stored in row-major order as a 16-element array.
+ */
private double[] transform;
+ /**
+ * Returns the 3D pose of this fiducial as a {@link Pose3d}.
+ *
+ * @return the fiducial's pose in 3D space
+ */
public Pose3d getPosition() {
return new Pose3d(new Matrix<>(Nat.N4(), Nat.N4(), transform));
}
+ /**
+ * Returns the unique ID of this fiducial.
+ *
+ * @return the fiducial's ID
+ */
public int getId() {
return id;
}
diff --git a/limelight/src/main/java/com/team2813/lib2813/limelight/apriltag_map/FiducialRetriever.java b/limelight/src/main/java/com/team2813/lib2813/limelight/apriltag_map/FiducialRetriever.java
index cbe78e56..c917be01 100644
--- a/limelight/src/main/java/com/team2813/lib2813/limelight/apriltag_map/FiducialRetriever.java
+++ b/limelight/src/main/java/com/team2813/lib2813/limelight/apriltag_map/FiducialRetriever.java
@@ -9,16 +9,39 @@
import java.util.Map;
import java.util.stream.Collectors;
+/**
+ * Retrieves and stores fiducial (AprilTag) information from a JSON input stream.
+ *
+ * The JSON is expected to represent a {@link FieldMap}, which contains an array of {@link
+ * Fiducial} objects. Each fiducial is mapped by its unique ID for easy lookup.
+ */
public class FiducialRetriever {
+
+ /** Gson instance used for deserializing JSON. */
private static final Gson gson = new Gson();
+
+ /** Map of fiducial ID to {@link Fiducial} object. */
private final Map This class is intended to be deserialized from JSON. The JSON should contain an array of
+ * fiducials under the key "fiducials". Each fiducial is represented by a {@link Fiducial} object.
+ *
+ * Example JSON format:
+ *
+ * This class extends JUnit's {@link ExternalResource} to provide automatic setup and teardown of
+ * a fake HTTP server that mimics the Limelight REST API. It listens on port 5807 and handles two
+ * endpoints:
+ *
+ * Usage example in a JUnit test:
+ *
+ * This handler responds to GET requests with a configurable JSON body representing vision data
+ * from the Limelight. It returns 405 Method Not Allowed for non-GET requests.
+ */
private static class FakeGet implements HttpHandler {
+ /** The JSON response body to return. */
private String body;
+ /**
+ * Sets the response body that will be returned by GET requests.
+ *
+ * @param body the JSON string to return
+ */
public void setBody(String body) {
this.body = body;
}
+ /**
+ * Gets the current response body.
+ *
+ * @return the JSON string that will be returned
+ */
public String getBody() {
return body;
}
+ /**
+ * Handles incoming HTTP requests to the /results endpoint.
+ *
+ * @param exchange the HTTP exchange containing request and response
+ * @throws IOException if an I/O error occurs during response writing
+ */
@Override
public void handle(HttpExchange exchange) throws IOException {
logger.info("Request for results received");
@@ -58,9 +121,23 @@ public void handle(HttpExchange exchange) throws IOException {
}
}
+ /**
+ * HTTP handler that simulates the Limelight's POST /upload-fieldmap endpoint.
+ *
+ * This handler accepts POST requests with field map data and stores the uploaded content for
+ * verification in tests. It returns 405 Method Not Allowed for non-POST requests.
+ */
private static class FakeFieldMap implements HttpHandler {
+ /** The most recently uploaded field map content. */
private String post;
+ /**
+ * Handles incoming HTTP requests to the /upload-fieldmap endpoint. Reads and stores the POST
+ * body content for later retrieval.
+ *
+ * @param exchange the HTTP exchange containing request and response
+ * @throws IOException if an I/O error occurs during request reading
+ */
@Override
public void handle(HttpExchange exchange) throws IOException {
logger.info("Request for field map received");
@@ -77,21 +154,44 @@ public void handle(HttpExchange exchange) throws IOException {
}
}
+ /** Handler for the /results GET endpoint. */
private final FakeGet resultsResponse = new FakeGet();
+
+ /** Handler for the /upload-fieldmap POST endpoint. */
private final FakeFieldMap fieldMapResponse = new FakeFieldMap();
+ /**
+ * Resets the fake Limelight state by clearing the results response body. Useful for ensuring a
+ * clean state between test cases.
+ */
public void reset() {
resultsResponse.setBody("");
}
+ /**
+ * Sets the JSON response that will be returned by the /results endpoint.
+ *
+ * @param response the JSONObject containing vision data to return
+ */
public void setResultsResponse(JSONObject response) {
resultsResponse.setBody(response.toString());
}
+ /**
+ * Gets the most recently uploaded field map content.
+ *
+ * @return the field map data as a string, or null if no field map has been uploaded
+ */
public String getFieldMap() {
return fieldMapResponse.post;
}
+ /**
+ * Gets the current results response as a JSONObject.
+ *
+ * @return a JSONObject parsed from the current response body
+ * @throws org.json.JSONException if the response body is not valid JSON
+ */
public JSONObject getResultsResponse() {
return new JSONObject(resultsResponse.getBody());
}
diff --git a/limelight/src/test/java/com/team2813/lib2813/limelight/LegacyRestLimelightTest.java b/limelight/src/test/java/com/team2813/lib2813/limelight/LegacyRestLimelightTest.java
index 7ec943b4..410ed1be 100644
--- a/limelight/src/test/java/com/team2813/lib2813/limelight/LegacyRestLimelightTest.java
+++ b/limelight/src/test/java/com/team2813/lib2813/limelight/LegacyRestLimelightTest.java
@@ -2,7 +2,32 @@
import org.json.JSONObject;
+/**
+ * Test suite for verifying backward compatibility with legacy Limelight JSON format.
+ *
+ * This class extends {@link RestLimelightTest} to ensure that the RestLimelight implementation
+ * continues to work with older JSON responses that include a "Results" root object. This legacy
+ * format was used in earlier versions of the Limelight API, and maintaining compatibility ensures
+ * that existing systems continue to function correctly.
+ *
+ * The key difference between this test and the parent class is that all JSON test data files in
+ * the resources directory contain the legacy "Results" root object structure, which this test
+ * specifically validates.
+ *
+ * @see RestLimelightTest
+ */
public class LegacyRestLimelightTest extends RestLimelightTest {
+
+ /**
+ * Sets the JSON response for the fake Limelight using the legacy format.
+ *
+ * This override ensures that JSON objects with the legacy "Results" root structure are
+ * properly handled. All JSON files in the test resources directory use this legacy format, making
+ * this test class essential for verifying backward compatibility.
+ *
+ * @param json the JSONObject containing legacy-formatted Limelight data with a "Results" root
+ * object
+ */
@Override
protected void setJson(JSONObject json) {
// All json files in resources has the "Results" root json object, which is now legacy.
diff --git a/limelight/src/test/java/com/team2813/lib2813/limelight/LimelightTestCase.java b/limelight/src/test/java/com/team2813/lib2813/limelight/LimelightTestCase.java
index 8e472fc2..0a1ae930 100644
--- a/limelight/src/test/java/com/team2813/lib2813/limelight/LimelightTestCase.java
+++ b/limelight/src/test/java/com/team2813/lib2813/limelight/LimelightTestCase.java
@@ -26,14 +26,50 @@
import org.json.JSONObject;
import org.junit.Test;
+/**
+ * Abstract base class for Limelight implementation testing.
+ *
+ * This class provides a comprehensive test suite for validating Limelight implementations
+ * against various JSON response scenarios. It tests all aspects of the Limelight interface
+ * including pose estimation, target detection, AprilTag visibility, and latency measurements.
+ *
+ * Subclasses must implement two abstract methods to adapt the tests to their specific Limelight
+ * implementation:
+ *
+ * Test coverage includes:
+ *
+ * This test framework simulates Limelight JSON data being written into NetworkTables and
+ * validates how the {@link Limelight} implementation handles pose estimation entries.
+ */
public class NetworkTablesLimelightTest extends LimelightTestCase {
+
+ /** Name of the Limelight NetworkTable. */
private static final String TABLE_NAME = "limelight";
+
+ /** Half a millisecond in microseconds. */
private static final long ONE_HALF_MS_IN_MICROS = 500;
+
+ /** One second in microseconds. */
private static final long ONE_SECOND_IN_MICROS = 1_000_000;
+
+ /** Simulated timestamp in microseconds, advanced between test steps. */
private static long fakeTimestampMicros = 15 * ONE_SECOND_IN_MICROS;
+
+ /**
+ * List of NetworkTables entries used for storing bot pose estimates. These cover both red/blue
+ * alliances and orb-based estimates.
+ */
private static final List Clears the JSON entry and all bot pose estimate entries, advancing the fake timestamp by two
+ * half-millisecond steps.
+ */
@After
public void resetNetworkTables() {
getJsonNTEntry().setString("");
@@ -31,11 +56,24 @@ public void resetNetworkTables() {
fakeTimestampMicros += ONE_HALF_MS_IN_MICROS;
}
+ /**
+ * Creates a new {@link NetworkTablesLimelight} instance for testing.
+ *
+ * @return a Limelight instance connected to the {@value #TABLE_NAME} table
+ */
@Override
protected Limelight createLimelight() {
return new NetworkTablesLimelight(TABLE_NAME);
}
+ /**
+ * Sets the Limelight JSON data in NetworkTables.
+ *
+ * Copies JSON pose estimation arrays into their corresponding NetworkTable entries when
+ * latency values are available.
+ *
+ * @param json the simulated Limelight JSON response
+ */
@Override
protected void setJson(JSONObject json) {
JSONObject resultsJson = json.getJSONObject("Results");
@@ -50,10 +88,22 @@ protected void setJson(JSONObject json) {
}
}
+ /**
+ * Returns the JSON {@link NetworkTableEntry} for Limelight.
+ *
+ * @return the NetworkTables entry holding Limelight JSON
+ */
private static NetworkTableEntry getJsonNTEntry() {
return LimelightHelpers.getLimelightNTTableEntry(TABLE_NAME, "json");
}
+ /**
+ * Writes a bot pose estimate array into the specified NetworkTables entry.
+ *
+ * @param resultsJson the Limelight "Results" JSON object
+ * @param entryName the entry name to populate
+ * @param latencyMillis the latency value in milliseconds
+ */
private static void setBotPoseEstimate(
JSONObject resultsJson, String entryName, double latencyMillis) {
double[] estimate_array = getBotPoseEstimateArray(resultsJson, entryName, latencyMillis);
@@ -62,12 +112,37 @@ private static void setBotPoseEstimate(
tableEntry.set(estimate_array, fakeTimestampMicros);
}
+ /**
+ * Clears a bot pose estimate entry by setting it to an empty array.
+ *
+ * @param entryName the NetworkTables entry to clear
+ * @param timestampMicros the timestamp in microseconds
+ */
private static void clearBotPoseEstimate(String entryName, long timestampMicros) {
DoubleArrayEntry tableEntry =
LimelightHelpers.getLimelightDoubleArrayEntry(TABLE_NAME, entryName);
tableEntry.set(new double[0], timestampMicros);
}
+ /**
+ * Builds a bot pose estimate array from Limelight JSON data.
+ *
+ * The array is structured as:
+ *
+ * This test suite uses a {@link FakeLimelight} HTTP server to simulate REST responses from a
+ * physical Limelight device. It validates Limelight instance management, HTTP endpoint behavior,
+ * and field map uploads.
+ */
public class RestLimelightTest extends LimelightTestCase {
+
+ /**
+ * Fake Limelight instance running as an embedded HTTP server.
+ *
+ * Used to simulate responses from a real Limelight device without requiring hardware.
+ */
@ClassRule public static final FakeLimelight fakeLimelight = new FakeLimelight();
+ /** Resets any cached {@link RestLimelight} instances after each test. */
@After
public void resetLimelights() {
RestLimelight.eraseInstances();
}
+ /**
+ * Resets the {@link FakeLimelight} after each test, ensuring no test state leaks between test
+ * methods.
+ */
@After
public void resetFakeLimelight() {
fakeLimelight.reset();
}
+ /**
+ * Verifies that calls to obtain the default {@link RestLimelight} return the same instance, and
+ * that the default instance is equal to one retrieved by name.
+ */
@Test
public void equality() {
Limelight a = RestLimelight.getDefaultLimelight();
Limelight b = RestLimelight.getDefaultLimelight();
assertEquals("Default limelight call returned different values", a, b);
+
Limelight c = RestLimelight.getLimelight(RestLimelight.DEFAULT_ADDRESS);
assertEquals("Default limelights not equal to limelights named \"limelight\" (default)", a, c);
}
+ /**
+ * Ensures the {@link FakeLimelight} responds correctly to REST requests.
+ *
+ * @throws Exception if the HTTP client request fails
+ */
@Test
public void fakeWorks() throws Exception {
fakeLimelight.setResultsResponse(new JSONObject().put("v", 1));
@@ -49,16 +77,25 @@ public void fakeWorks() throws Exception {
assertEquals("{\"v\":1}", response.body());
}
+ /**
+ * Verifies that uploading a field map to a {@link RestLimelight} updates the {@link
+ * FakeLimelight} server as expected.
+ *
+ * @throws Exception if reading the field map resource or uploading fails
+ */
@Test
public void setFieldMapWorks() throws Exception {
Limelight limelight = createLimelight();
String resourceName = "frc2025r2.fmap";
+
+ // Upload field map from resources
try (InputStream is = getClass().getResourceAsStream(resourceName)) {
limelight.setFieldMap(is, true);
}
String fieldMap = fakeLimelight.getFieldMap();
assertNotNull(fieldMap);
+ // Verify expected field map content matches
String expectedFieldMap;
try (InputStream is = getClass().getResourceAsStream(resourceName)) {
expectedFieldMap = new String(is.readAllBytes(), StandardCharsets.UTF_8);
@@ -66,6 +103,11 @@ public void setFieldMapWorks() throws Exception {
assertEquals(expectedFieldMap, fieldMap);
}
+ /**
+ * Creates a new {@link RestLimelight} instance for testing.
+ *
+ * @return a {@code RestLimelight} bound to "localhost"
+ */
@Override
protected Limelight createLimelight() {
RestLimelight limelight = new RestLimelight("localhost");
@@ -73,14 +115,17 @@ protected Limelight createLimelight() {
return limelight;
}
+ /**
+ * Sets the Limelight JSON results response in the {@link FakeLimelight}.
+ *
+ * Unlike older schemas, the new Limelight JSON schema does not include a {@code "Results"}
+ * object at the root. Instead, all fields are inlined. Since resource test files still contain
+ * {@code "Results"}, this method extracts and forwards that sub-object.
+ *
+ * @param json the full Limelight JSON, expected to contain a {@code "Results"} object
+ */
@Override
protected void setJson(JSONObject json) {
- // limelight json schema has been updated to not json object "Results" in the root, and we want
- // to test the new version, which does not.
- // The new version just has all json that was in "Results" in the root, so the json object in
- // "Results" will essentially be the new schema.
- // Since we know that all the json objects in the resources folder have the "Results" json
- // object, this should never fail.
fakeLimelight.setResultsResponse(json.getJSONObject("Results"));
}
}
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/CommandTester.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/CommandTester.java
index 42bc7741..0edfee09 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/CommandTester.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/CommandTester.java
@@ -3,12 +3,18 @@
import edu.wpi.first.wpilibj2.command.Command;
/**
- * Allows tests to run commands.
+ * Utility interface for running WPILib {@link Command} objects inside tests.
*
- * Tests can get an instance by using {@link WPILibExtension}.
+ * Provides a way to schedule and execute commands to completion within a test context. An
+ * implementation is provided via {@link WPILibExtension}.
*/
public interface CommandTester {
- /** Schedules the provided command and runs it until it completes. */
+ /**
+ * Schedules the provided {@link Command} and repeatedly runs it until the command reports it has
+ * finished.
+ *
+ * @param command the command to schedule and execute
+ */
void runUntilComplete(Command command);
}
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java
index 262b70a5..343cc46c 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java
@@ -13,11 +13,18 @@
import org.junit.jupiter.api.extension.ParameterResolver;
/**
- * JUnit Jupiter extension for testing code that depends on WPILib.
+ * JUnit Jupiter extension for testing robot code that depends on WPILib.
*
- * Also provides a {@link CommandTester} for tests.
+ * This extension:
*
- * Example use:
+ * Example usage:
*
* Sets up the HAL, enables the driver station simulation, and resets the {@link
+ * CommandScheduler}.
+ *
+ * @param context the JUnit extension context
+ * @throws IllegalStateException if the HAL cannot be initialized
+ */
@Override
public void beforeAll(ExtensionContext context) {
// See https://www.chiefdelphi.com/t/driverstation-getalliance-in-gradle-test/
@@ -62,12 +78,23 @@ public void beforeAll(ExtensionContext context) {
CommandScheduler.getInstance().unregisterAllSubsystems();
}
+ /**
+ * Cleans up after each test by canceling all commands and unregistering all subsystems.
+ *
+ * @param context the JUnit extension context
+ */
@Override
public void afterEach(ExtensionContext context) {
CommandScheduler.getInstance().cancelAll();
CommandScheduler.getInstance().unregisterAllSubsystems();
}
+ /**
+ * Cleans up after all tests by canceling all commands, unregistering all subsystems, disabling
+ * the scheduler, and resetting the driver station simulation.
+ *
+ * @param context the JUnit extension context
+ */
@Override
public void afterAll(ExtensionContext context) {
CommandScheduler.getInstance().cancelAll();
@@ -77,6 +104,14 @@ public void afterAll(ExtensionContext context) {
DriverStationSim.notifyNewData();
}
+ /**
+ * Checks whether this extension can provide a parameter of type {@link CommandTester}.
+ *
+ * @param parameterContext the parameter context
+ * @param extensionContext the extension context
+ * @return true if the parameter type is {@code CommandTester}
+ * @throws ParameterResolutionException if parameter resolution fails
+ */
@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
@@ -84,6 +119,16 @@ public boolean supportsParameter(
return CommandTester.class.equals(parameterContext.getParameter().getType());
}
+ /**
+ * Provides a {@link CommandTester} instance for parameter injection.
+ *
+ * The tester schedules the given command and repeatedly runs the {@link CommandScheduler}
+ * until the command completes.
+ *
+ * @param parameterContext the parameter context
+ * @param extensionContext the extension context
+ * @return a {@link CommandTester} that executes commands to completion
+ */
@Override
public CommandTester resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext) {
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/truth/Pose2dSubject.java b/testing/src/main/java/com/team2813/lib2813/testing/truth/Pose2dSubject.java
index f3b33296..9f25e32d 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/truth/Pose2dSubject.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/truth/Pose2dSubject.java
@@ -9,19 +9,42 @@
import javax.annotation.Nullable;
/**
- * Truth Subject for making assertions about {@link Pose2d} values.
+ * A Truth {@link Subject} for making assertions about {@link Pose2d} values.
*
- * See Writing your own custom subject to learn about
- * creating custom Truth subjects.
+ * This subject provides fluent assertions for comparing poses, including tolerance-based
+ * comparisons of translations and rotations.
+ *
+ * See Truth: Writing your own custom subject for more
+ * on extending Truth.
*/
public final class Pose2dSubject extends Subject {
- // User-defined entry point
+ /**
+ * Entry point for {@link Pose2d} assertions.
+ *
+ * Usage:
+ *
+ * The tolerance applies to both translation (x, y) and rotation (θ).
+ *
+ * @param tolerance the maximum allowed difference in meters (translation) or radians (rotation)
+ * @return a {@link TolerantComparison} for comparing poses with a tolerance
+ */
public TolerantComparison See Writing your own custom subject to learn about
- * creating custom Truth subjects.
+ * This subject provides fluent assertions for comparing 3D poses, including tolerance-based
+ * comparisons of translations and rotations.
+ *
+ * See Truth: Writing your own custom subject for more
+ * on extending Truth.
*/
public final class Pose3dSubject extends Subject {
- // User-defined entry point
+ /**
+ * Entry point for {@link Pose3d} assertions.
+ *
+ * Usage:
+ *
+ * The tolerance applies to both translation (x, y, z) and rotation (roll, pitch, yaw).
+ *
+ * @param tolerance the maximum allowed difference in meters (translation) or radians (rotation)
+ * @return a {@link TolerantComparison} for comparing poses with a tolerance
+ */
public TolerantComparison This subject provides fluent assertions for comparing 2D rotations, including tolerance-based
+ * comparisons and checks for zero rotation.
+ *
+ * Rotations are compared in radians.
+ */
public final class Rotation2dSubject extends Subject {
- // User-defined entry point
+
+ /**
+ * Entry point for {@link Rotation2d} assertions.
+ *
+ * Usage:
+ *
+ * The tolerance is expressed in radians.
+ *
+ * @param tolerance the maximum allowed difference in radians
+ * @return a {@link TolerantComparison} for comparing rotations with a tolerance
+ */
public TolerantComparison Fails if the rotation under test is {@code null} or not equal to zero.
+ */
public void isZero() {
if (!Rotation2d.kZero.equals(actual)) {
failWithActual(simpleFact("expected to be zero"));
}
}
- // Chained subjects methods below this point
-
+ /**
+ * Returns a {@link DoubleSubject} for making assertions about the rotation’s raw radians value.
+ *
+ * @return a {@link DoubleSubject} for the rotation’s radians
+ */
public DoubleSubject getRadians() {
return check("getRadians()").that(nonNullActual().getRadians());
}
- // Helper methods below this point
-
+ /**
+ * Ensures that the actual rotation is not {@code null}.
+ *
+ * @return the non-null actual {@link Rotation2d}
+ * @throws AssertionError if the rotation under test is {@code null}
+ */
private Rotation2d nonNullActual() {
if (actual == null) {
failWithActual(simpleFact("expected a non-null Rotation2d"));
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/truth/Rotation3dSubject.java b/testing/src/main/java/com/team2813/lib2813/testing/truth/Rotation3dSubject.java
index fe78d230..2a449e84 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/truth/Rotation3dSubject.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/truth/Rotation3dSubject.java
@@ -9,14 +9,48 @@
import edu.wpi.first.math.geometry.Rotation3d;
import javax.annotation.Nullable;
-/** Truth Subject for making assertions about {@link Rotation3d} values. */
+/**
+ * A Truth {@link Subject} for making assertions about {@link Rotation3d} values.
+ *
+ * This subject provides fluent assertions for comparing 3D rotations, including tolerance-based
+ * comparisons of roll, pitch, and yaw angles, as well as checks for zero rotation.
+ *
+ * Rotations are expressed in radians:
+ *
+ * Usage:
+ *
+ * The tolerance is expressed in radians and applies to each of the roll (X), pitch (Y), and
+ * yaw (Z) components independently.
+ *
+ * @param tolerance the maximum allowed difference in radians
+ * @return a {@link TolerantComparison} for comparing rotations with a tolerance
+ */
public TolerantComparison Fails if the rotation under test is {@code null} or not equal to zero.
+ */
public void isZero() {
if (!Rotation3d.kZero.equals(actual)) {
failWithActual(simpleFact("expected to be zero"));
}
}
- // Chained subjects methods below this point
-
/**
- * Returns a subject that can be used to make assertions about the counterclockwise rotation angle
- * around the X axis (roll) in radians.
+ * Returns a subject for making assertions about the roll (X-axis rotation, radians).
+ *
+ * @return a {@link DoubleSubject} for the roll component in radians
*/
public DoubleSubject x() {
return check("getX()").that(nonNullActual().getX());
}
/**
- * Returns a subject that can be used to make assertions about the counterclockwise rotation angle
- * around the Y axis (pitch) in radians.
+ * Returns a subject for making assertions about the pitch (Y-axis rotation, radians).
+ *
+ * @return a {@link DoubleSubject} for the pitch component in radians
*/
public DoubleSubject y() {
return check("getY()").that(nonNullActual().getY());
}
/**
- * Returns a subject that can be used to make assertions about the counterclockwise rotation angle
- * around the Z axis (yaw) in radians.
+ * Returns a subject for making assertions about the yaw (Z-axis rotation, radians).
+ *
+ * @return a {@link DoubleSubject} for the yaw component in radians
*/
public DoubleSubject z() {
return check("getZ()").that(nonNullActual().getZ());
}
- // Helper methods below this point
-
+ /**
+ * Ensures that the actual rotation is not {@code null}.
+ *
+ * @return the non-null actual {@link Rotation3d}
+ * @throws AssertionError if the rotation under test is {@code null}
+ */
private Rotation3d nonNullActual() {
if (actual == null) {
failWithActual(simpleFact("expected a non-null Rotation3d"));
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/truth/TolerantComparison.java b/testing/src/main/java/com/team2813/lib2813/testing/truth/TolerantComparison.java
index cc2c2b38..a85e4810 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/truth/TolerantComparison.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/truth/TolerantComparison.java
@@ -3,26 +3,44 @@
import org.jspecify.annotations.Nullable;
/**
- * A partially specified check about an approximate relationship to a {@code double} subject using a
- * tolerance.
+ * Represents a partially specified check on a subject of type {@code T} for approximate equality
+ * within a given tolerance. This class is intended to be used as part of a fluent assertion API,
+ * where the subject and tolerance are defined earlier in the chain, and the expected value is
+ * provided via {@link #of(Object)}.
+ *
+ * For example, in a fluent assertion style:
+ *
+ * Subclasses of this class implement the specific logic for comparing the subject with the
+ * expected value considering the tolerance.
+ *
+ * @param This method is unsupported for {@code TolerantComparison}. Equality comparisons should be
+ * performed via {@link #of(Object)} rather than {@link Object#equals(Object)}.
+ *
* @throws UnsupportedOperationException always
- * @deprecated {@link Object#equals(Object)} is not supported on TolerantComparison. If you meant
- * to compare doubles, use {@link #of(T)} instead.
+ * @deprecated Use {@link #of(Object)} to compare values. {@link Object#equals(Object)} is not
+ * supported.
*/
@Deprecated
@Override
@@ -32,8 +50,12 @@ public final boolean equals(@Nullable Object o) {
}
/**
+ * {@inheritDoc}
+ *
+ * This method is unsupported for {@code TolerantComparison}.
+ *
* @throws UnsupportedOperationException always
- * @deprecated {@link Object#hashCode()} is not supported on TolerantComparison
+ * @deprecated {@link Object#hashCode()} is not supported on {@code TolerantComparison}.
*/
@Deprecated
@Override
diff --git a/testing/src/main/java/com/team2813/lib2813/testing/truth/Translation2dSubject.java b/testing/src/main/java/com/team2813/lib2813/testing/truth/Translation2dSubject.java
index 41a51edd..f332350b 100644
--- a/testing/src/main/java/com/team2813/lib2813/testing/truth/Translation2dSubject.java
+++ b/testing/src/main/java/com/team2813/lib2813/testing/truth/Translation2dSubject.java
@@ -9,28 +9,64 @@
import edu.wpi.first.math.geometry.Translation2d;
import javax.annotation.Nullable;
-/** Truth Subject for making assertions about {@link Translation2d} values. */
+/**
+ * A Truth {@link Subject} for making fluent assertions about {@link Translation2d} values.
+ *
+ * This class allows tests to verify the x and y components of a {@code Translation2d}, perform
+ * approximate comparisons within a tolerance, and check special properties like being zero.
+ *
+ * Example usage:
+ *
+ * This class allows tests to verify the x, y, and z components of a {@code Translation3d},
+ * perform approximate comparisons within a tolerance, and check special properties like being zero.
+ *
+ * Example usage:
+ *
+ * This class ensures that WPILib-specific functionality (like {@link CommandScheduler} and
+ * {@link DriverStation}) behaves correctly when used with the extension. It verifies that:
+ *
+ * Uses {@link TestMethodOrder} to enforce ordering and {@link Tag} to mark it as special
+ * testkit-only.
+ */
@ExtendWith(WPILibExtension.class)
@Tag("ignore-outside-testkit")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@@ -52,6 +70,7 @@ public static void verifyDriverStationEnabled() {
assertThat(DriverStation.isEnabled()).isTrue();
}
+ /** Verifies that FAKE_COMMAND is not scheduled before the first test, then schedules it. */
@Test
@Order(1)
public void verifyFakeCommandNotScheduledBeforeTest() {
@@ -64,6 +83,7 @@ public void verifyFakeCommandNotScheduledBeforeTest() {
assertThat(commandScheduler.isScheduled(FAKE_COMMAND));
}
+ /** Verifies that FAKE_COMMAND is not scheduled before the second test, then schedules it. */
@Test
@Order(2)
public void verifyFakeCommandNotScheduledAfterTest(CommandTester commandTester) {
@@ -76,6 +96,7 @@ public void verifyFakeCommandNotScheduledAfterTest(CommandTester commandTester)
assertThat(commandScheduler.isScheduled(FAKE_COMMAND));
}
+ /** Verifies that {@link CommandTester} runs and correctly verifies a command. */
@Test
@Order(3)
public void verifyCommandTester(CommandTester commandTester) {
@@ -93,12 +114,11 @@ public static void verifyFakeCommandNotScheduledAfterAllTests() {
}
} // end SampleTest
+ /** Verifies that {@link WPILibExtension} executes SampleTest correctly and without failures. */
@Test
void verifyExtension() {
- // Arrange
withDriverStationTemporarilyEnabled(
() -> {
- // Schedule FAKE_COMMAND
CommandScheduler commandScheduler = CommandScheduler.getInstance();
commandScheduler.enable();
commandScheduler.schedule(FAKE_COMMAND);
@@ -107,14 +127,16 @@ void verifyExtension() {
assertThat(isScheduled).isTrue();
});
- // Act
EngineExecutionResults results =
EngineTestKit.engine("junit-jupiter").selectors(selectClass(SampleTest.class)).execute();
- // Assert
assertHasNoFailures(results);
}
+ /**
+ * Temporarily enables the driver station for the duration of {@code runnable}, restoring its
+ * previous state afterwards.
+ */
private void withDriverStationTemporarilyEnabled(Runnable runnable) {
assertThat(RobotState.isDisabled()).isTrue();
DriverStationSim.setEnabled(true);
@@ -142,11 +164,17 @@ private void assertHasNoFailures(Events events) {
});
}
+ /**
+ * A test command used for verifying {@link CommandTester}.
+ *
+ * Tracks initialize() and execute() calls, and allows validation of expected behavior.
+ */
private static class VerifiableCommand extends Command {
private static final int EXPECTED_EXECUTION_COUNT = 4;
private int initializedCount = 0;
private int executionCount = 0;
+ /** Verifies that initialize and execute were called as expected. */
void verify() {
assertWithMessage("initialize() should be called").that(initializedCount).isGreaterThan(0);
assertWithMessage("initialize() should not be called more than once")
+ *
*
- * @return the canbus that the device is on
+ * @return an Optional containing the CAN bus name, or empty if on the RoboRIO CAN bus
*/
public Optional{@code
+ * Angle position = encoder.getPositionMeasure();
+ * double degrees = position.in(Units.Degrees);
+ * double rotations = position.in(Units.Rotations);
+ * }
*
- * @return the position of the encoder as a measure
+ * @return the current position of the encoder as an {@link Angle} measurement
*/
Angle getPositionMeasure();
/**
- * Sets the position of the encoder
+ * Sets the encoder position to the specified raw double value.
*
- * @param position the position of the encoder
+ * {@code
+ * encoder.setPosition(Units.Degrees.of(90));
+ * encoder.setPosition(Units.Rotations.of(0.25));
+ * }
+ *
+ * @param position the new position as an {@link Angle} measurement
+ */
default void setPosition(Angle position) {
setPosition(position.in(Units.Radians));
}
/**
- * Gets the velocity of the encoder
+ * Gets the current velocity of the encoder as a raw double value.
*
- * @return the velocity that the encoder perceives
+ * @return the velocity that the encoder perceives as an unspecified double value
* @deprecated This method does not specify velocity in a specific measurement, so it is not safe
- * to use. Use {@link #getVelocityMeasure()} instead
+ * to use. Use {@link #getVelocityMeasure()} instead for type safety
*/
@Deprecated(forRemoval = true)
double getVelocity();
/**
- * Gets the velocity of the encoder
+ * Gets the current velocity of the encoder using type-safe units.
+ *
+ * {@code
+ * AngularVelocity velocity = encoder.getVelocityMeasure();
+ * double rpm = velocity.in(Units.RPM);
+ * double radPerSec = velocity.in(Units.RadiansPerSecond);
+ * }
*
- * @return The velocity as a measure
+ * @return the current velocity as an {@link AngularVelocity} measurement
*/
default AngularVelocity getVelocityMeasure() {
return Units.RadiansPerSecond.of(getVelocity());
diff --git a/lib/src/main/java/com/team2813/lib2813/control/InvertType.java b/lib/src/main/java/com/team2813/lib2813/control/InvertType.java
index 7606ceda..d80c5771 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/InvertType.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/InvertType.java
@@ -6,80 +6,235 @@
import java.util.*;
import java.util.stream.Stream;
+/**
+ * Enumeration defining motor inversion types for standardized motor control across different
+ * vendors.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
CLOCKWISE(InvertedValue.Clockwise_Positive, true),
+
+ /**
+ * Counter-clockwise rotation is considered positive.
+ *
+ *
+ *
+ */
COUNTER_CLOCKWISE(InvertedValue.CounterClockwise_Positive, false),
+
+ /**
+ * Follower motor matches the master motor's direction.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
*
- * @param mode The mode to control the motor with
- * @param demand The demand of the motor. differentiating meaning with each control mode
- * @param feedForward The feedForward to apply to the motor
+ *
+ *
+ *
+ * {@code
+ * Current motorCurrent = motor.getAppliedCurrent();
+ * double amps = motorCurrent.in(Units.Amps);
+ * if (amps > 30.0) {
+ * // Handle high current condition
+ * }
+ * }
*
+ * @return the current applied current as a type-safe {@link Current} measurement
* @since 2.0.0
- * @return The current applied current
*/
Current getAppliedCurrent();
}
diff --git a/lib/src/main/java/com/team2813/lib2813/control/PIDMotor.java b/lib/src/main/java/com/team2813/lib2813/control/PIDMotor.java
index 745c742a..14fe6792 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/PIDMotor.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/PIDMotor.java
@@ -1,11 +1,134 @@
package com.team2813.lib2813.control;
+/**
+ * Interface defining a motor controller with integrated encoder feedback and PID control
+ * capabilities.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author Team 2813
+ * @since 1.0
+ */
public interface PIDMotor extends Motor, Encoder {
+
+ /**
+ * Configures the PID and feedforward constants for a specific control slot.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @param slot the PID slot number to configure
+ * @param p the proportional gain coefficient
+ * @param i the integral gain coefficient
+ * @param d the derivative gain coefficient
+ */
void configPID(int slot, double p, double i, double d);
+ /**
+ * Configures the PID constants for the default control slot (slot 0) without feedforward.
+ *
+ *
+ *
+ *
+ * @param p the proportional gain coefficient
+ * @param i the integral gain coefficient
+ * @param d the derivative gain coefficient
+ */
void configPID(double p, double i, double d);
}
diff --git a/lib/src/main/java/com/team2813/lib2813/control/encoders/CancoderWrapper.java b/lib/src/main/java/com/team2813/lib2813/control/encoders/CancoderWrapper.java
index ebd35cb3..2aabc2e4 100644
--- a/lib/src/main/java/com/team2813/lib2813/control/encoders/CancoderWrapper.java
+++ b/lib/src/main/java/com/team2813/lib2813/control/encoders/CancoderWrapper.java
@@ -7,58 +7,151 @@
import edu.wpi.first.units.Units;
import edu.wpi.first.units.measure.Angle;
import edu.wpi.first.units.measure.AngularVelocity;
+import java.util.function.Supplier;
+/**
+ * Wrapper class for a CTRE Phoenix 6 CANcoder absolute encoder.
+ *
+ *
+ *
+ *
+ *
+ *
*
- * @param deviceId The device ID.
- * @param type The motor type connected to the controller. Brushless motor wires must be connected
+ * @param deviceId the CAN ID of the SPARK MAX controller
+ * @param type the motor type connected to the controller. Brushless motor wires must be connected
* to their matching colors and the hall sensor must be plugged in. Brushed motors must be
- * connected to the Red and Black terminals only.
- * @param inverted Whether the motor is inverted
+ * connected to the Red and Black terminals only
+ * @param inverted the inversion type for the motor output
+ * @throws RuntimeException if the inversion type cannot be converted to a SPARK MAX setting
*/
public SparkMaxWrapper(int deviceId, SparkLowLevel.MotorType type, InvertType inverted) {
motor = new SparkMax(deviceId, type);
@@ -48,11 +95,37 @@ public SparkMaxWrapper(int deviceId, SparkLowLevel.MotorType type, InvertType in
encoder = motor.getEncoder();
}
+ /**
+ * Sets the motor output using the specified control mode and demand value.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
*
- * @param canID [0, 62] the can ID of the motor
- * @param canbus the canbus that the motor is on
- * @param invertType the invert type
+ * @param canID the CAN ID of the motor controller, must be in range [0, 62]
+ * @param canbus the CAN bus that the motor is connected to (e.g., "rio", "canivore")
+ * @param invertType the inversion type for motor output, must be a rotation value
* @throws NullPointerException if either {@code invertType} or {@code canbus} are null
* @throws IllegalArgumentException if {@code invertType} is not in {@link
- * InvertType#rotationValues}. In other words, this exception is thrown when passed an {@link
- * InvertType} that is for following motors
- * @throws InvalidCanIdException if the CAN id is invalid
+ * InvertType#rotationValues}. This exception is thrown when passed an {@link InvertType} that
+ * is for following motors
+ * @throws InvalidCanIdException if the CAN ID is invalid (outside range [0, 62])
*/
public TalonFXWrapper(int canID, String canbus, InvertType invertType) {
Objects.requireNonNull(invertType, "invertType should not be null");
@@ -65,15 +100,18 @@ public TalonFXWrapper(int canID, String canbus, InvertType invertType) {
}
/**
- * Create a TalonFXWrapper on the RoboRIO's canbus
+ * Creates a TalonFXWrapper on the RoboRIO's default CAN bus.
*
- * @param canID [0, 62] the can ID of the motor
- * @param invertType the invert type
+ *
+ *
+ *
+ *
+ *
+ *
+ * @param mode the neutral mode to set
+ */
public void setNeutralMode(NeutralModeValue mode) {
motor.setNeutralMode(mode);
}
+ /**
+ * Configures the PIDF constants for a specific closed-loop slot.
+ *
+ *
+ *
+ *
+ *
- *
*
- *
- *
- *
- *
+ *
+ *
+ * @param
+ *
+ *
+ * @author Team 2813
*/
public abstract class QueueLightshow extends Lightshow {
+
private final Deque{@code
+ * RobotFactory.startRobot(inputs -> new MyRobot(inputs.shuffleboard.getTab("Drive")));
+ * }
+ *
+ * {@code
+ * @Rule
+ * public IsolatedPreferences prefs = new IsolatedPreferences();
+ *
+ * @Test
+ * public void testPreferenceChange() {
+ * prefs.getPreferencesTable().getEntry("example").setDouble(42.0);
+ * // test logic
+ * }
+ * }
*/
public final class IsolatedPreferences extends ExternalResource {
+
+ /** The temporary {@link NetworkTableInstance} used to isolate preference changes. */
private NetworkTableInstance tempInstance;
- /** Gets the {@link NetworkTable} that contains the preference values. */
+ /**
+ * Gets the {@link NetworkTable} used by {@link Preferences} in this test.
+ *
+ * @return the NetworkTable for preferences
+ */
public NetworkTable getPreferencesTable() {
return tempInstance.getTable("Preferences");
}
+ /** Sets up a temporary NetworkTable instance before each test. */
@Override
protected void before() {
+ // Ensure the default instance is initialized
NetworkTableInstance.getDefault();
tempInstance = NetworkTableInstance.create();
tempInstance.startLocal();
Preferences.setNetworkTableInstance(tempInstance);
}
+ /** Cleans up the temporary instance after each test. */
@Override
protected void after() {
+ // Wait briefly for listener queue to empty to avoid potential JVM crash
if (!tempInstance.waitForListenerQueue(.1)) {
System.err.println(
"Timed out waiting for the NetworkTableInstance listener queue to empty (waited 100ms);"
+ " JVM may crash");
}
+
+ // Restore Preferences to the default NetworkTable instance
Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault());
tempInstance.close();
}
diff --git a/lib/src/test/java/com/team2813/lib2813/preferences/PersistedConfigurationTest.java b/lib/src/test/java/com/team2813/lib2813/preferences/PersistedConfigurationTest.java
index a6f46f2e..8d381c8a 100644
--- a/lib/src/test/java/com/team2813/lib2813/preferences/PersistedConfigurationTest.java
+++ b/lib/src/test/java/com/team2813/lib2813/preferences/PersistedConfigurationTest.java
@@ -12,7 +12,6 @@
import edu.wpi.first.networktables.NetworkTableType;
import edu.wpi.first.networktables.Topic;
import edu.wpi.first.wpilibj.DataLogManager;
-import edu.wpi.first.wpilibj.Preferences;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -24,27 +23,57 @@
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.ErrorCollector;
import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-/** Tests for {@link PersistedConfiguration}. */
+/**
+ * Unit tests for {@link PersistedConfiguration}.
+ *
+ *
+ *
+ */
@ParameterizedClass
-@EnumSource(ControlMode.class)
-@ExtendWith(WPILibExtension.class)
+@EnumSource(ControlMode.class) // Runs the test class once for each ControlMode
+@ExtendWith(WPILibExtension.class) // Ensures WPILib-related setup/teardown happens
public final class ParameterizedIntakeSubsystemTest {
+ /**
+ * Concrete implementation of {@link ParameterizedIntakeSubsystem} for testing.
+ *
+ *
+ *
+ */
final class StubLocationalData implements LocationalData {
+ /** Singleton instance representing valid but empty locational data. */
static final StubLocationalData VALID = new StubLocationalData(true);
+
+ /** Singleton instance representing invalid locational data. */
static final StubLocationalData INVALID = new StubLocationalData(false);
+ /** Whether this stub data should be considered valid. */
private final boolean valid;
+ /**
+ * Creates a new StubLocationalData instance.
+ *
+ * @param valid whether this stub data should report as valid
+ */
private StubLocationalData(boolean valid) {
this.valid = valid;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @return the validity state set during construction
+ */
@Override
public boolean isValid() {
return valid;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @return always false, as stub data never has targets
+ */
@Override
public boolean hasTarget() {
return false;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @return always empty
+ */
@Override
public Optional
+ * {
+ * "fiducials": [
+ * { "id": 1, "transform": [ ...16 values... ] },
+ * { "id": 2, "transform": [ ...16 values... ] }
+ * ]
+ * }
+ *
+ */
class FieldMap {
+
+ /** Array of fiducials present in the field map. */
public Fiducial[] fiducials;
}
diff --git a/limelight/src/test/java/com/team2813/lib2813/limelight/FakeLimelight.java b/limelight/src/test/java/com/team2813/lib2813/limelight/FakeLimelight.java
index 0ef52898..34a12a4a 100644
--- a/limelight/src/test/java/com/team2813/lib2813/limelight/FakeLimelight.java
+++ b/limelight/src/test/java/com/team2813/lib2813/limelight/FakeLimelight.java
@@ -13,10 +13,46 @@
import org.json.JSONObject;
import org.junit.rules.ExternalResource;
+/**
+ * Mock HTTP server that simulates a Limelight camera for testing purposes.
+ *
+ *
+ *
+ *
+ * {@code
+ * @Rule
+ * public FakeLimelight fakeLimelight = new FakeLimelight();
+ *
+ * @Test
+ * public void testLimelightConnection() {
+ * JSONObject response = new JSONObject();
+ * response.put("v", 1);
+ * fakeLimelight.setResultsResponse(response);
+ * // Test code here
+ * }
+ * }
+ */
public class FakeLimelight extends ExternalResource {
+ /** Logger for tracking fake Limelight activity. */
private static final Logger logger = Logger.getLogger("FakeLimelight");
+
+ /** The underlying HTTP server instance. */
HttpServer server;
+ /**
+ * Sets up the fake Limelight server before each test. Creates an HTTP server on port 5807 with
+ * endpoints for results and field map uploads.
+ *
+ * @throws Throwable if server creation or startup fails
+ */
@Override
protected void before() throws Throwable {
server = HttpServer.create(new InetSocketAddress(5807), 0);
@@ -26,23 +62,50 @@ protected void before() throws Throwable {
server.start();
}
+ /**
+ * Tears down the fake Limelight server after each test. Stops the server with a 2-second grace
+ * period and clears the server reference.
+ */
@Override
protected void after() {
server.stop(2);
server = null;
}
+ /**
+ * HTTP handler that simulates the Limelight's GET /results endpoint.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
abstract class LimelightTestCase {
+ /**
+ * Tests that an uninitialized Limelight returns empty values. Verifies that capture latency is
+ * not present when no data has been received.
+ */
@Test
public final void emptyValues() {
Limelight limelight = createLimelight();
assertWithMessage("JSON should be empty").that(limelight.getCaptureLatency()).isEmpty();
}
+ /**
+ * Tests handling of invalid/malformed JSON data. Verifies that the Limelight correctly identifies
+ * invalid data and returns empty optionals.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void invalidDataTest() throws Exception {
JSONObject obj = readJSON("InvalidDataTest.json");
@@ -50,6 +86,12 @@ public final void invalidDataTest() throws Exception {
assertThat(actualTargetingLatency).isEmpty();
}
+ /**
+ * Tests scenario where no target is detected but latency data is present. Validates that valid
+ * latency measurements can be obtained even without a target.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void absentTest1() throws Exception {
JSONObject obj = readJSON("AbsentTest1.json");
@@ -70,6 +112,12 @@ public final void absentTest1() throws Exception {
assertThat(locationalData.getBotPoseEstimateRed()).isEmpty();
}
+ /**
+ * Tests another scenario where no target is detected but latency data differs. Verifies
+ * consistent behavior with different latency values.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void absentTest2() throws Exception {
JSONObject obj = readJSON("AbsentTest2.json");
@@ -90,6 +138,13 @@ public final void absentTest2() throws Exception {
assertThat(locationalData.getBotPoseEstimateRed()).isEmpty();
}
+ /**
+ * Tests scenario with a detected target and full pose estimation data. Validates robot pose in
+ * field coordinates, blue alliance coordinates, and red alliance coordinates. Also verifies that
+ * pose estimates include valid timestamps.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void presentTest1() throws Exception {
JSONObject obj = readJSON("PresentTest1.json");
@@ -133,6 +188,12 @@ public final void presentTest1() throws Exception {
assertThat(redEstimate.pose()).isWithin(0.005).of(expectedPoseEstimate);
}
+ /**
+ * Tests another scenario with a detected target and different pose values. Ensures consistent
+ * pose estimation behavior across different robot positions.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void presentTest2() throws Exception {
JSONObject obj = readJSON("PresentTest2.json");
@@ -171,6 +232,12 @@ public final void presentTest2() throws Exception {
assertThat(blueEstimate.timestampSeconds()).isWithin(0.005).of(redEstimate.timestampSeconds());
}
+ /**
+ * Tests retrieval of robot pose in blue alliance coordinates. Validates the coordinate
+ * transformation from field coordinates to blue alliance origin.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void getBotposeBlue() throws Exception {
JSONObject obj = readJSON("BotposeBlueRedTest.json");
@@ -187,6 +254,12 @@ public final void getBotposeBlue() throws Exception {
assertThat(actualPose).isWithin(0.005).of(expectedPose);
}
+ /**
+ * Tests retrieval of robot pose in red alliance coordinates. Validates the coordinate
+ * transformation from field coordinates to red alliance origin.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void getBotposeRed() throws Exception {
JSONObject obj = readJSON("BotposeBlueRedTest.json");
@@ -204,6 +277,12 @@ public final void getBotposeRed() throws Exception {
assertThat(actualPose).isWithin(0.005).of(expectedPose);
}
+ /**
+ * Tests detection and reporting of visible AprilTag IDs. Verifies that the Limelight correctly
+ * identifies which tags are in view.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void getVisibleTags() throws Exception {
JSONObject obj = readJSON("BotposeBlueRedTest.json");
@@ -213,6 +292,12 @@ public final void getVisibleTags() throws Exception {
assertThat(limelight.getLocationalData().getVisibleTags()).containsExactly(20);
}
+ /**
+ * Tests retrieval of visible AprilTag positions on the field. Validates that tag positions are
+ * correctly loaded from the field map and that pose estimates include the set of visible tags.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void getVisibleAprilTagPoses() throws Exception {
JSONObject obj = readJSON("BotposeBlueRedTest.json");
@@ -239,6 +324,12 @@ public final void getVisibleAprilTagPoses() throws Exception {
assertThat(locationalData.getBotPoseEstimateRed().get().visibleAprilTags()).isEqualTo(tags);
}
+ /**
+ * Tests retrieval of AprilTag locations via the getLocatedAprilTags method. Validates that tag
+ * positions can be queried by their IDs after field map upload.
+ *
+ * @throws Exception if test resources cannot be loaded
+ */
@Test
public final void visibleTagLocation() throws Exception {
JSONObject obj = readJSON("BotposeBlueRedTest.json");
@@ -253,10 +344,29 @@ public final void visibleTagLocation() throws Exception {
assertThat(pose.getTranslation()).isWithin(0.005).of(new Translation3d(-3.87, 0.72, 0.31));
}
+ /**
+ * Creates a Limelight instance for testing. Subclasses must implement this to provide their
+ * specific Limelight implementation.
+ *
+ * @return a Limelight instance to test
+ */
protected abstract Limelight createLimelight();
+ /**
+ * Configures the test environment with JSON data. Subclasses must implement this to inject test
+ * data into their Limelight implementation.
+ *
+ * @param json the JSONObject containing Limelight response data
+ */
protected abstract void setJson(JSONObject json);
+ /**
+ * Uploads the FRC 2025 Round 2 field map to the Limelight. This configures AprilTag positions for
+ * testing.
+ *
+ * @param limelight the Limelight instance to configure
+ * @throws IOException if the field map resource cannot be loaded
+ */
private void uploadFieldMap(Limelight limelight) throws IOException {
boolean updateLimelight = false;
try (var stream = getClass().getResourceAsStream("frc2025r2.fmap")) {
@@ -264,6 +374,13 @@ private void uploadFieldMap(Limelight limelight) throws IOException {
}
}
+ /**
+ * Reads a JSON test file from the classpath resources.
+ *
+ * @param fileName the name of the JSON file to read
+ * @return a JSONObject parsed from the file contents
+ * @throws IOException if the file cannot be found or read
+ */
private JSONObject readJSON(String fileName) throws IOException {
try (InputStream is = getClass().getResourceAsStream(fileName)) {
if (is == null) {
@@ -276,6 +393,12 @@ private JSONObject readJSON(String fileName) throws IOException {
}
}
+ /**
+ * Asserts that the Limelight has a valid target detected. Checks both the top-level hasTarget()
+ * method and the LocationalData hasTarget() method.
+ *
+ * @param limelight the Limelight to check
+ */
private void assertHasTarget(Limelight limelight) {
assertWithMessage("Should have target").that(limelight.hasTarget()).isTrue();
assertWithMessage("Should have target")
@@ -283,6 +406,14 @@ private void assertHasTarget(Limelight limelight) {
.isTrue();
}
+ /**
+ * Asserts that an OptionalDouble value is approximately equal to an expected value. If the
+ * optional is empty, the assertion fails.
+ *
+ * @param expected the expected double value
+ * @param actual the OptionalDouble to check
+ * @param delta the maximum acceptable difference between expected and actual
+ */
protected static void assertAlmostEqual(double expected, OptionalDouble actual, double delta) {
actual.ifPresentOrElse(
d -> assertEquals(expected, d, delta),
diff --git a/limelight/src/test/java/com/team2813/lib2813/limelight/NetworkTablesLimelightTest.java b/limelight/src/test/java/com/team2813/lib2813/limelight/NetworkTablesLimelightTest.java
index a975c261..c97c89a9 100644
--- a/limelight/src/test/java/com/team2813/lib2813/limelight/NetworkTablesLimelightTest.java
+++ b/limelight/src/test/java/com/team2813/lib2813/limelight/NetworkTablesLimelightTest.java
@@ -7,11 +7,30 @@
import org.json.JSONObject;
import org.junit.After;
+/**
+ * Unit test class for {@link NetworkTablesLimelight}.
+ *
+ *
+ *
+ *
+ * Additional indices are reserved for multiple tags.
+ *
+ * @param resultsJson the Limelight "Results" JSON object
+ * @param entryName the entry to extract
+ * @param latencyMillis the latency in milliseconds
+ * @return a double array representing the bot pose estimate, or an empty array if not present
+ */
private static double[] getBotPoseEstimateArray(
JSONObject resultsJson, String entryName, double latencyMillis) {
if (!resultsJson.has(entryName)) {
diff --git a/limelight/src/test/java/com/team2813/lib2813/limelight/RestLimelightTest.java b/limelight/src/test/java/com/team2813/lib2813/limelight/RestLimelightTest.java
index b1e5e1d7..2536f888 100644
--- a/limelight/src/test/java/com/team2813/lib2813/limelight/RestLimelightTest.java
+++ b/limelight/src/test/java/com/team2813/lib2813/limelight/RestLimelightTest.java
@@ -16,28 +16,56 @@
import org.junit.ClassRule;
import org.junit.Test;
+/**
+ * Unit tests for {@link RestLimelight}.
+ *
+ *
+ *
+ *
+ * {@code
* @ExtendWith(WPILibExtension.class)
@@ -26,7 +33,6 @@
* @Test
* public void initiallyNotInAir() {
* var flight = new FlightSubsystem();
- *
* assertThat(flight.inAir()).isFalse();
* }
*
@@ -35,6 +41,7 @@
* var flight = new FlightSubsystem();
* Command takeOff = flight.createTakeOffCommandCommand();
*
+ * // Run the command under test
* commandTester.runUntilComplete(takeOff);
*
* assertThat(flight.inAir()).isTrue();
@@ -49,6 +56,15 @@ public final class WPILibExtension
BeforeAllCallback,
ParameterResolver {
+ /**
+ * Initializes WPILib components before all tests.
+ *
+ * {@code
+ * Pose2d actualPose = ...;
+ * Pose2d expectedPose = ...;
+ *
+ * Pose2dSubject.assertThat(actualPose)
+ * .isWithin(0.01)
+ * .of(expectedPose);
+ * }
+ *
+ * @param pose the pose under test (may be {@code null})
+ * @return a {@link Pose2dSubject} for making assertions
+ */
public static Pose2dSubject assertThat(@Nullable Pose2d pose) {
return assertAbout(pose2ds()).that(pose);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Factory for {@link Pose2dSubject}, for use with assertAbout().
+ *
+ * @return a factory for creating {@link Pose2dSubject} instances
+ */
public static Subject.Factory{@code
+ * Pose3d actualPose = ...;
+ * Pose3d expectedPose = ...;
+ *
+ * Pose3dSubject.assertThat(actualPose)
+ * .isWithin(0.01)
+ * .of(expectedPose);
+ * }
+ *
+ * @param pose the pose under test (may be {@code null})
+ * @return a {@link Pose3dSubject} for making assertions
+ */
public static Pose3dSubject assertThat(@Nullable Pose3d pose) {
return assertAbout(pose3ds()).that(pose);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Factory for {@link Pose2dSubject}, for use with assertAbout().
+ *
+ * @return a factory for creating {@link Pose3dSubject} instances
+ */
public static Subject.Factory{@code
+ * Rotation2d actual = new Rotation2d(Math.PI / 2);
+ * Rotation2d expected = new Rotation2d(Math.PI / 2 + 1e-3);
+ *
+ * Rotation2dSubject.assertThat(actual)
+ * .isWithin(1e-2)
+ * .of(expected);
+ * }
+ *
+ * @param rotation the rotation under test (may be {@code null})
+ * @return a {@link Rotation2dSubject} for making assertions
+ */
public static Rotation2dSubject assertThat(@Nullable Rotation2d rotation) {
return assertAbout(rotation2ds()).that(rotation);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Factory for {@link Pose2dSubject}, for use with assertAbout().
+ *
+ * @return a factory for creating {@link Rotation2dSubject} instances
+ */
public static Factory
+ *
+ */
public final class Rotation3dSubject extends Subject {
- // User-defined entry point
+
+ /**
+ * Entry point for {@link Rotation3d} assertions.
+ *
+ * {@code
+ * Rotation3d actual = new Rotation3d(Math.PI / 2, 0, 0);
+ * Rotation3d expected = new Rotation3d(Math.PI / 2 + 1e-3, 0, 0);
+ *
+ * Rotation3dSubject.assertThat(actual)
+ * .isWithin(1e-2)
+ * .of(expected);
+ * }
+ *
+ * @param rotation the rotation under test (may be {@code null})
+ * @return a {@link Rotation3dSubject} for making assertions
+ */
public static Rotation3dSubject assertThat(@Nullable Rotation3d rotation) {
return assertAbout(rotation3ds()).that(rotation);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Factory for {@link Pose2dSubject}, for use with assertAbout().
+ *
+ * @return a factory for creating {@link Rotation3dSubject} instances
+ */
public static Factory{@code
+ * assertThat(actualValue).isWithin(tolerance).of(expectedValue);
+ * }
+ *
+ * {@code
+ * Translation2d translation = new Translation2d(1.0, 2.0);
+ * Translation2dSubject.assertThat(translation)
+ * .isWithin(0.01).of(new Translation2d(1.0, 2.0));
+ * Translation2dSubject.assertThat(translation).x().isEqualTo(1.0);
+ * Translation2dSubject.assertThat(translation).y().isEqualTo(2.0);
+ * }
+ */
public final class Translation2dSubject extends Subject {
- // User-defined entry point
+ /**
+ * Entry point for assertions about a {@link Translation2d} instance.
+ *
+ * @param translation the translation to assert about (nullable)
+ * @return a {@link Translation2dSubject} for fluent assertions
+ */
public static Translation2dSubject assertThat(@Nullable Translation2d translation) {
return assertAbout(translation2ds()).that(translation);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Returns a Truth {@link Factory} for {@link Translation2dSubject}, used with {@link
+ * com.google.common.truth.Truth#assertAbout(Subject.Factory)}.
+ *
+ * @return a factory for creating {@link Translation2dSubject} instances
+ */
public static Factory{@code
+ * Translation3d translation = new Translation3d(1.0, 2.0, 3.0);
+ * Translation3dSubject.assertThat(translation)
+ * .isWithin(0.01).of(new Translation3d(1.0, 2.0, 3.0));
+ * Translation3dSubject.assertThat(translation).x().isEqualTo(1.0);
+ * Translation3dSubject.assertThat(translation).y().isEqualTo(2.0);
+ * Translation3dSubject.assertThat(translation).z().isEqualTo(3.0);
+ * }
+ */
public final class Translation3dSubject extends Subject {
- // User-defined entry point
+ /**
+ * Entry point for assertions about a {@link Translation3d} instance.
+ *
+ * @param translation the translation to assert about (nullable)
+ * @return a {@link Translation3dSubject} for fluent assertions
+ */
public static Translation3dSubject assertThat(@Nullable Translation3d translation) {
return assertAbout(translation3ds()).that(translation);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ /**
+ * Returns a Truth {@link Factory} for {@link Translation3dSubject}, used with {@link
+ * com.google.common.truth.Truth#assertAbout(Subject.Factory)}.
+ *
+ * @return a factory for creating {@link Translation3dSubject} instances
+ */
public static Factory
+ *
+ */
public class WPILibExtensionTest {
+ /** A fake command used for scheduling tests. */
static final Command FAKE_COMMAND = new Command() {};
@BeforeAll
@@ -34,6 +46,12 @@ static void initializeHal() {
}
}
+ /**
+ * Sample test class demonstrating usage of {@link WPILibExtension} with JUnit 5.
+ *
+ *