Squashed 'third_party/allwpilib/' changes from f1a82828fe..ce550705d7

ce550705d7 [ntcore] Fix client "received unknown id -1" (#6186)
3989617bde [ntcore] NetworkTable::GetStruct: Add I template param (#6183)
f1836e1321 [fieldImages] Fix 2024 field json (#6179)
d05f179a9a [build] Fix running apriltagsvision Java example (#6173)
b1b03bed85 [wpilib] Update MotorControllerGroup deprecation message (#6171)
fa63fbf446 LICENSE.md: Bump year to 2024 (#6169)
4809f3d0fc [apriltag] Add 2024 AprilTag locations (#6168)
dd90965362 [wpiutil] Fix RawFrame.setInfo() NPE (#6167)
8659372d08 [fieldImages] Add 2024 field image (#6166)
a2e4d0b15d [docs] Fix docs for SysID routine (#6164)
0a46a3a618 [wpilib] Make ADXL345 default I2C address public (#6163)
7c26bc70ab [sysid] Load DataLog files directly for analysis (#6103)
f94e3d81b9 [docs] Fix SysId routine JavaDoc warnings (#6159)
6bed82a18e [wpilibc] Clean up C++ SysId routine (#6160)
4595f84719 [wpilib] Report LiveWindow-enabled-in-test (#6158)
707cb06105 [wpilib] Add SysIdRoutine logging utility and command factory (#6033)
3e40b9e5da [wpilib] Correct SmartDashboard usage reporting (#6157)
106518c3f8 [docs] Fix wpilibj JavaDoc warnings (#6154)
19cb2a8eb4 [wpilibj] Make class variables private to match C++ (#6153)
13f4460e00 [docs] Add missing docs to enum fields (NFC) (#6150)
4210f5635d [docs] Fix warnings about undocumented default constructors (#6151)
0f060afb55 [ntcore] Disable WebSocket fragmentation (#6149)
f29a7d2e50 [docs] Add missing JavaDocs (#6146)
6e58db398d [commands] Make Java fields private (#6148)
4ac0720385 [build] Clean up CMake files (#6141)
44db3e0ac0 [sysid] Make constexpr variables outside class scope inline (#6145)
73c7d87db7 [glass] NTStringChooser: Properly set retained (#6144)
25636b712f [build] Remove unnecessary native dependencies in wpilibjExamples (#6143)
01fb98baaa [docs] Add Missing JNI docs from C++ (NFC) (#6139)
5c424248c4 [wpilibj] Remove unused AnalogTriggerException (#6142)
c486972c55 [wpimath] Make ExponentialProfile.State mutable (#6138)
783acb9b72 [wpilibj] Store long preferences as integers (#6136)
99ab836894 [wpiutil] Add missing JavaDocs (NFC) (#6132)
ad0859a8c9 [docs] Add missing JavaDocs (#6125)
5579219716 [docs] Exclude quickbuf files and proto/struct packages from doclint (#6128)
98f06911c7 [sysid] Use eigenvector component instead of eigenvalue for fit quality check (#6131)
e1d49b975c [wpimath] Add LinearFilter reset() overload to initialize input and output buffers (#6133)
8a0bf2b7a4 [hal] Add CANAPITypes to java (#6121)
91d8837c11 [wpilib] Make protected fields in accelerometers/gyros private (#6134)
e7c9f27683 [wpilib] Add functional interface equivalents to MotorController (#6053)
8aca706217 [glass] Add type information to SmartDashboard menu (#6117)
7d3e4ddba9 [docs] Add warning about using user button to docs (NFC) (#6129)
ec3cb3dcba [build] Disable clang-tidy warning about test case names (#6127)
495585b25d [examples] Update april tag family to 36h11 (#6126)
f9aabc5ab2 [wpilib] Throw early when EventLoop is modified while running (#6115)
c16946c0ec [hal] Add CANJNI docs (NFC) (#6120)
b7f4eb2811 [doc] Update maven artifacts for units and apriltags (NFC) (#6123)
f419a62b38 [doc] Update maintainers.md (NFC) (#6124)
938bf45fd9 [wpiutil] Remove type param from ProtobufSerializable and StructSerializable (#6122)
c34debe012 [docs] Link to external OpenCV docs (#6119)
07183765de [hal] Fix formatting of HAL_ENUM enums (NFC) (#6114)
af46034b7f [wpilib] Document only first party controllers are guaranteed to have correct mapping (#6112)
636ef58d94 [hal] Properly error check readCANStreamSession (#6108)
cc631d2a69 [build] Fix generated source set location in the HAL (#6113)
09f76b32c2 [wpimath] Compile with UTF-8 encoding (#6111)
47c5fd8620 [sysid] Check data quality before OLS (#6110)
24a76be694 [hal] Add method to detect if the CAN Stream has overflowed (#6105)
9333951736 [hal] Allocate CANStreamMessage in JNI if null (#6107)
6a2d3c30a6 [wpiutil] Struct: Add info template parameter pack (#6086)
e07de37e64 [commands] Mark ParallelDeadlineGroup.setDeadline() final (#6102)
141241d2d6 [wpilib] Fix usage reporting for static classes (#6090)
f2c2bab7dc [sysid] Fix adjusted R² calculation (#6101)
5659038443 [wpiutil,cscore,apriltag] Fix RawFrame (#6098)
8aeee03626 [commands] Improve error message when composing commands twice in same composition (#6091)
55508706ff [wpiutil,cscore] Move VideoMode.PixelFormat to wpiutil (#6097)
ab78b930e9 [wpilib] ADIS16470: Add access to all 3 axes (#6074)
795d4be9fd [wpilib] Fix precision issue in Color round-and-clamp (#6100)
7aa9ad44b8 [commands] Deprecate C++ TransferOwnership() (#6095)
92c81d0791 [ci] Update pregenerate workflow to actions/checkout@v4 (#6094)
1ce617be07 [ci] Update artifact actions to v4 (#6092)
2441b57156 [wpilib] Add PWMSparkFlex MotorController (#6089)
21d1972d7a [wpiutil] DataLog: Ensure file is written on shutdown (#6087)
c29e8c66cf [wpiutil] DataLog: Fix UB in AppendImpl (#6088)
ab309e34ef [glass] Fix order of loading window settings (#6056)
22a322c9f3 [wpimath] Report error on negative PID gains (#6055)
1dba26c937 [wpilib] Add method to get breaker fault at a specific channel in PowerDistribution[Sticky]Faults (#5521)
ef1cb3f41e [commands] Fix compose-while-scheduled issue and test all compositions (#5581)
aeb1a4aa33 [wpiutil] Add serializable marker interfaces (#6060)
c1178d5add [wpilib] Add StadiaController and command wrapper (#6083)
4e4a468d4d [wpimath] Make feedforward classes throw exceptions for negative Kv or Ka (#6084)
d1793f077d [build] cmake: Add NO_WERROR option to disable -Werror (#6071)
43fb6e9f87 [glass] Add Profiled PID controller support & IZone Support (#5959)
bcef6c5398 [apriltag] Fix Java generation functions (#6063)
4059e0cd9f [hal,wpilib] Add function to control "Radio" LED (#6073)
0b2cfb3abc [dlt] Change datalogtool default folder to logs folder (#6079)
df5e439b0c [wpilib] PS4Controller: enable usage reporting (#6081)
0ff7478968 [cscore] Fix RawFrame class not being loaded in JNI (#6077)
6f23d32fe1 [wpilib] AddressableLED: Update warning about single driver (NFC) (#6069)
35a1c52788 [build] Upgrade quickbuf to 1.3.3 (#6072)
e4e2bafdb1 [sysid] Document timestamp units (#6065)
3d201c71f7 [ntcore] Fix overlapping subscriber handling (#6067)
f02984159f [glass] Check for null entries when updating struct/proto (#6059)
a004c9e05f [commands] SubsystemBase: allow setting name in constructor (#6052)
0b4c6a1546 [wpimath] Add more docs to SimulatedAnnealing (NFC) (#6054)
ab15dae887 [wpilib] ArcadeDrive: Fix max output handling (#6051)
9599c1f56f [hal] Add usage reporting ids from 2024v2 image (#6041)
f87c64af8a [wpimath] MecanumDriveWheelSpeeds: Fix desaturate() (#6040)
8798700cec [wpilibcExamples] Add inline specifier to constexpr constants (#6049)
85c9ae6eff [wpilib] Fix PS5 Controller mappings (#6050)
7c8b7a97ad [wpiutil] Zero out roborio system timestamp (#6042)
d9b504bc84 [wpilib] DataLogManager: Change sim location to logs subdir (#6039)
906b810136 [build] cmake: Fix ntcore generated header install (#6038)
56e5b404d1 Update to final 2024 V2 image (#6034)
8723ee5c39 [ntcore] Add cached topic property (#5494)
192a28af47 Fix JDK 21 warnings (#6028)
d40bdd70ba [build] Upgrade to spotbugs Gradle plugin 6.0.2 (#6027)
7bfadf32e5 [wpilibj] Joystick: make remainder of get axis methods final (#6024)
a770110438 [commands] CommandCompositionError: Include stacktrace of original composition (#5984)
54a55b8b53 [wpiutil,hal] Update image; init Rio Now() HMB with a FPGA session (#6016)
7d4e515a6b [wpimath] Simplify calculation of C for DARE precondition (#6022)
5200316c14 [ntcore] Update transmit period on topic add/remove (#6021)
ddf79a25d4 [wpiunits] Overload Measure.per(Time) to return Measure<Velocity> (#6018)
a71adef316 [wpiutil] Clean up circular_buffer iterator syntax (#6020)
39a0bf4b98 [examples] Call resetOdometry() when controller command is executed (#5905)
f5fc101fda [build] cmake: Export jars and clean up jar installs (#6014)
38bf024c96 [build] Update to Gradle 8.5 (#6007)
9d11544c18 [wpimath] Rotate traveling salesman solution so input and solution have same initial pose (#6015)
28deba20f5 [wpimath] Commit generated quickbuf Java files (#5994)
c2971c0bb3 [build] cmake: Export apriltag and wpimath (#6012)
41cfc961e4 gitattributes: Add linguist-generated locations (#6004)
14c3ade155 [wpimath] Struct cleanup (#6011)
90757b9e90 [wpilib] Make Color::HexString() constexpr (#5985)
2676b77873 Fix compilation issues that occur when building with bazel (#6008)
d32c10487c [examples] Update C++ examples to use CommandPtr (#5988)
9bc5fcf886 [build] cmake: Default WITH_JAVA_SOURCE to WITH_JAVA (#6005)
d431abba3b [upstream_utils] Fix GCEM namespace usage and add hypot(x, y, z) (#6002)
2bb1409b82 Clean up Java style (#5990)
66172ab288 Remove submodule (#6003)
e8f8c0ceb0 [upstream_utils] Update to latest Eigen HEAD (#5996)
890992a849 [hal] Commit generated usage reporting files (#5993)
a583ca01e1 [wpiutil] Change Struct to allow non-constexpr implementation (#5992)
ca272de400 [build] Fix Gradle compile_commands.json and clang-tidy warnings (#5977)
76ae090570 [wpiutil] type_traits: Add is_constexpr() (#5997)
5172ab8fd0 [commands] C++ CommandPtr: Prevent null initialization (#5991)
96914143ba [build] Bump native-utils to fix compile_commands.json (#5989)
464e6121ef [ci] Report failed status to Azure on failed tests (#2654)
5dad46cd45 [wpimath] Commit generated files (#5986)
54ab65a63a [ntcore] Commit generated files (#5962)
7ed900ae3a [wpilib] Add hex string constructor to Color and Color8Bit (#5063)
74b85b76a9 [wpimath] Make gcem call std functions if not constant-evaluated (#5983)
30816111db [wpimath] Fix TimeInterpolatableBuffer crash (#5972)
5cc923de33 [wpilib] DataLogManager: Use logs subdirectory on USB drives (#5975)
1144115da0 [commands] Add GetName to Subsystem, use in Scheduler tracer epochs (#5836)
ac7d726ac3 [wpimath] Add simulated annealing (#5961)
e09be72ee0 [wpimath] Remove unused SimpleMatrixUtils class (#5979)
0f9ebe92d9 [wpimath] Add generic circular buffer class to Java (#5969)
9fa28eb07a [ci] Bump actions/checkout to v4 (#5736)
ca684ac207 [hal] Add capability to read power distribution data as a stream (#4983)
51eecef2bd [wpimath] Optimize 2nd derivative of quintic splines (#3292)
4fcf0b25a1 [build] Apply a formatter for CMake files (#5973)
9b8011aa67 [build] Pin wpiformat version (#5982)
e00a0e84c1 [build] cmake: fix protobuf dependency finding for certain distributions (#5981)
23dd591394 [upstream_utils] Remove libuv patch that adjusts whitespace (#5976)
b0719942f0 [wpiutil] Timestamp: Report errors on Rio HMB init failure (#5974)
7bc89c4322 [wpilib] Update getAlliance() docs (NFC) (#5971)
841ea682d1 [upstream_utils] Upgrade to LLVM 17.0.5 (#5970)
a74db52dae [cameraserver] Add getVideo() pixelFormat overload (#5966)
a7eb422662 [build] Update native utils for new compile commands files (#5968)
544b231d4d [sysid] Add missing cassert include (#5967)
31cd015970 [wpimath] Add SysId doc links to LinearSystemId in C++ (NFC) (#5960)
9280054eab Revert "[build] Export wpimath protobuf symbols (#5952)"
2aba97c610 Export pb files from wpimath
c80b2d2017 [build] Export wpimath protobuf symbols (#5952)
3c0652c18a [cscore] Replace CS_PixelFormat with WPI_PixelFormat (#5954)
95716eb0cb [wpiunits] Documentation improvements (#5932)
423fd75fa8 [wpilib] Default LiveWindowEnabledInTest to false (#5950)
dfdea9c992 [wpimath] Make KalmanFilter variant for asymmetric updates (#5951)
ca81ced409 [wpiutil] Move RawFrame to wpiutil; add generation of RawFrame for AprilTags (#5923)
437cc91af5 [cscore] CvSink: Allow specifying output PixelFormat (#5943)
25b7dca46b [build] Remove CMake flat install option (#5944)
bb05e20247 [wpimath] Add protobuf/struct for trivial types (#5935)
35744a036e [wpimath] Move struct/proto classes to separate files (#5918)
80d7ad58ea [build] Declare platform launcher dependency explicitly (#5909)
f8d983b154 [ntcore] Protobuf/Struct: Use atomic_bool instead of atomic_flag (#5946)
4a44210ee3 [ntcore] NetworkTableInstance: Suppress unused lambda capture warning (#5947)
bdc8620d55 [upstream_utils] Fix fmt compilation errors on Windows (#5948)
0ca1e9b5f9 [wpimath] Add basic wpiunits support (#5821)
cc30824409 [ntcore] Increase client meta-topic decoding limit (#5934)
b1fad062f7 [wpilib] Use RKDP in DifferentialDrivetrainSim (#5931)
ead9ae5a69 [build] Add generateProto dependency to test and dev (#5933)
cfbff32185 [wpiutil] timestamp: Fix startup race on Rio (#5930)
7d90d0bcc3 [wpimath] Clean up StateSpaceUtil (#5891)
7755e45aac [build] Add generated protobuf headers to C++ test include path (#5926)
3985c031da [ntcore] ProtobufSubscriber: Fix typos (#5928)
7a87fe4b60 [ntcore] ProtobufSubscriber: Make mutex and msg mutable (#5927)
09f3ed6a5f [commands] Add static Trigger factories for robot mode changes (#5902)
79dd795bc0 [wpimath] Clean up VecBuilder and MatBuilder (#5906)
e117274a67 [wpilib] Change default Rio log dir from /home/lvuser to /home/lvuser/logs (#5899)
a8b80ca256 [upstream_utils] Update to libuv 1.47.0 (#5889)
b3a9c3e96b [build] Bump macOS deployment target to 12 (#5890)
0f8129677b [build] Distribute wpimath protobuf headers (#5925)
d105f9e3e9 [wpiutil] ProtobufBuffer: Fix buffer reallocation (#5924)
c5f2f6a0fb [fieldImages] Fix typo in field images artifact name (#5922)
c1a57e422a [commands] Clean up make_vector.h (#5917)
78ebc6e9ec [wpimath] change G to gearing in LinearSystemId factories (#5834)
9ada181866 [hal] DriverStation.h: Add stddef.h include (#5897)
95fa5ec72f [wpilibc,ntcoreffi] DataLogManager: join on Stop() call (#5910)
b6f2d3cc14 [build] Remove usage of Version.parse (#5911)
cc2cbeb04c [examples] Replace gyro rotation with poseEstimator rotation (#5900)
fa6b171e1c [wpiutil] Suppress protobuf warning false positives on GCC 13 (#5907)
d504639bbe [apriltag] Improve AprilTag docs (#5895)
3a1194be40 Replace static_cast<void>() with [[maybe_unused]] attribute (#5892)
70392cbbcb [build] cmake: Add protobuf dependency to wpiutil-config (#5886)
17c1bd5a83 [ntcore] Use json_fwd (#5881)
e69a9efeba [wpilibcExamples] Match array parameter bounds (#5880)
14dcd0d26f Use char instead of uint8_t for json::parse (#5877)
ec1d261984 [hal] Fix garbage data for match info before DS connection (#5879)
63dbf5c614 [wpiutil] MemoryBuffer: Fix normal read and file type check (#5875)
b2e7be9250 [ntcore] Only datalog meta-topics if specifically requested (#5873)
201a42a3cd [wpimath] Reorder TrapezoidProfile.calculate() arguments (#5874)
04a781b4d7 [apriltag] Add GetTags to C++ version of AprilTagFieldLayout (#5872)
87a8a1ced4 [docs] Exclude eigen and protobuf from doxygen (#5871)

git-subtree-dir: third_party/allwpilib
git-subtree-split: ce550705d7cdab117c0153a202973fc026a81274
Signed-off-by: Maxwell Henderson <mxwhenderson@gmail.com>
Change-Id: Ic8645d0551d62b411b0a816c493f0f33291896a1
diff --git a/wpilibj/src/generate/WPILibVersion.java.in b/wpilibj/src/generate/WPILibVersion.java.in
index cc5e6b0..f38d1b0 100644
--- a/wpilibj/src/generate/WPILibVersion.java.in
+++ b/wpilibj/src/generate/WPILibVersion.java.in
@@ -1,10 +1,13 @@
 package edu.wpi.first.wpilibj.util;
 
-/*
+/**
  * Autogenerated file! Do not manually edit this file. This version is regenerated
  * any time the publish task is run, or when this file is deleted.
  */
-
 public final class WPILibVersion {
+  /** The version number. */
   public static final String Version = "${wpilib_version}";
+
+  /** Utility class. */
+  private WPILibVersion() {}
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16448_IMU.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16448_IMU.java
index 2eddf64..0b55105 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16448_IMU.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16448_IMU.java
@@ -94,30 +94,47 @@
   private static final int PROD_ID = 0x56; // Product identifier
   private static final int SERIAL_NUM = 0x58; // Lot-specific serial number
 
+  /** ADIS16448 calibration times. */
   public enum CalibrationTime {
+    /** 32 ms calibration time */
     _32ms(0),
+    /** 64 ms calibration time */
     _64ms(1),
+    /** 128 ms calibration time */
     _128ms(2),
+    /** 256 ms calibration time */
     _256ms(3),
+    /** 512 ms calibration time */
     _512ms(4),
+    /** 1 s calibration time */
     _1s(5),
+    /** 2 s calibration time */
     _2s(6),
+    /** 4 s calibration time */
     _4s(7),
+    /** 8 s calibration time */
     _8s(8),
+    /** 16 s calibration time */
     _16s(9),
+    /** 32 s calibration time */
     _32s(10),
+    /** 64 s calibration time */
     _64s(11);
 
-    private int value;
+    private final int value;
 
-    private CalibrationTime(int value) {
+    CalibrationTime(int value) {
       this.value = value;
     }
   }
 
+  /** IMU axes. */
   public enum IMUAxis {
+    /** The IMU's X axis. */
     kX,
+    /** The IMU's Y axis. */
     kY,
+    /** The IMU's Z axis. */
     kZ
   }
 
@@ -130,9 +147,9 @@
   private IMUAxis m_yaw_axis;
 
   /* Offset data storage */
-  private double m_offset_data_gyro_rate_x[];
-  private double m_offset_data_gyro_rate_y[];
-  private double m_offset_data_gyro_rate_z[];
+  private double[] m_offset_data_gyro_rate_x;
+  private double[] m_offset_data_gyro_rate_y;
+  private double[] m_offset_data_gyro_rate_z;
 
   /* Instant raw output variables */
   private double m_gyro_rate_x = 0.0;
@@ -199,7 +216,7 @@
   private SimDouble m_simAccelZ;
 
   /* CRC-16 Look-Up Table */
-  int adiscrc[] =
+  int[] adiscrc =
       new int[] {
         0x0000, 0x17CE, 0x0FDF, 0x1811, 0x1FBE, 0x0870, 0x1061, 0x07AF,
         0x1F3F, 0x08F1, 0x10E0, 0x072E, 0x0081, 0x174F, 0x0F5E, 0x1890,
@@ -248,15 +265,19 @@
     }
   }
 
+  /** Creates a new ADIS16448_IMU object. */
   public ADIS16448_IMU() {
     this(IMUAxis.kZ, SPI.Port.kMXP, CalibrationTime._512ms);
   }
 
   /**
+   * Creates a new ADIS16448_IMU object.
+   *
    * @param yaw_axis The axis that measures the yaw
    * @param port The SPI Port the gyro is plugged into
    * @param cal_time Calibration time
    */
+  @SuppressWarnings("this-escape")
   public ADIS16448_IMU(final IMUAxis yaw_axis, SPI.Port port, CalibrationTime cal_time) {
     m_yaw_axis = yaw_axis;
     m_spi_port = port;
@@ -331,6 +352,11 @@
     m_connected = true;
   }
 
+  /**
+   * Checks the connection status of the IMU.
+   *
+   * @return True if the IMU is connected, false otherwise.
+   */
   public boolean isConnected() {
     if (m_simConnected != null) {
       return m_simConnected.get();
@@ -338,7 +364,6 @@
     return m_connected;
   }
 
-  /** */
   private static int toUShort(byte[] buf) {
     return (((buf[0] & 0xFF) << 8) + ((buf[1] & 0xFF) << 0));
   }
@@ -347,7 +372,7 @@
     return (buf[0] & 0xFF);
   }
 
-  public static int toUShort(int... buf) {
+  private static int toUShort(int... buf) {
     return (((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF));
   }
 
@@ -358,7 +383,7 @@
 
   /** */
   private static int toShort(int... buf) {
-    return (short) (((buf[0] & 0xFF) << 8) + ((buf[1] & 0xFF) << 0));
+    return (short) (((buf[0] & 0xFF) << 8) + ((buf[1] & 0xFF)));
   }
 
   /** */
@@ -480,8 +505,19 @@
     return true;
   }
 
-  public int configDecRate(int m_decRate) {
-    int writeValue = m_decRate;
+  /**
+   * Configures the decimation rate of the IMU.
+   *
+   * @param decimationRate The new decimation value.
+   * @return 0 if success, 1 if no change, 2 if error.
+   */
+  public int configDecRate(int decimationRate) {
+    // Switches the active SPI port to standard SPI mode, writes a new value to
+    // the DECIMATE register in the IMU, and re-enables auto SPI.
+    //
+    // This function enters standard SPI mode, writes a new DECIMATE setting to
+    // the IMU, adjusts the sample scale factor, and re-enters auto SPI mode.
+    int writeValue;
     int readbackValue;
     if (!switchToStandardSPI()) {
       DriverStation.reportError("Failed to configure/reconfigure standard SPI.", false);
@@ -489,19 +525,19 @@
     }
 
     /* Check max */
-    if (m_decRate > 9) {
+    if (decimationRate > 9) {
       DriverStation.reportError(
           "Attempted to write an invalid decimation value. Capping at 9", false);
-      m_decRate = 9;
+      decimationRate = 9;
     }
-    if (m_decRate < 0) {
+    if (decimationRate < 0) {
       DriverStation.reportError(
           "Attempted to write an invalid decimation value. Capping at 0", false);
-      m_decRate = 0;
+      decimationRate = 0;
     }
 
     /* Shift decimation setting to correct position and select internal sync */
-    writeValue = (m_decRate << 8) | 0x1;
+    writeValue = (decimationRate << 8) | 0x1;
 
     /* Apply to IMU */
     writeRegister(SMPL_PRD, writeValue);
@@ -527,7 +563,7 @@
    * @param new_cal_time New calibration time
    * @return 1 if the new calibration time is the same as the current one else 0
    */
-  public int configCalTime(CalibrationTime new_cal_time) {
+  public final int configCalTime(CalibrationTime new_cal_time) {
     if (m_calibration_time == new_cal_time) {
       return 1;
     } else {
@@ -623,6 +659,12 @@
     m_spi.write(buf, 2);
   }
 
+  /**
+   * Reset the gyro.
+   *
+   * <p>Resets the gyro accumulations to a heading of zero. This can be used if there is significant
+   * drift in the gyro and it needs to be recalibrated after running.
+   */
   public void reset() {
     synchronized (this) {
       m_integ_gyro_angle_x = 0.0;
@@ -956,7 +998,9 @@
   }
 
   /**
-   * @return Yaw axis angle in degrees (CCW positive)
+   * Returns the yaw axis angle in degrees (CCW positive).
+   *
+   * @return Yaw axis angle in degrees (CCW positive).
    */
   public synchronized double getAngle() {
     switch (m_yaw_axis) {
@@ -972,7 +1016,9 @@
   }
 
   /**
-   * @return Yaw axis angular rate in degrees per second (CCW positive)
+   * Returns the yaw axis angular rate in degrees per second (CCW positive).
+   *
+   * @return Yaw axis angular rate in degrees per second (CCW positive).
    */
   public synchronized double getRate() {
     switch (m_yaw_axis) {
@@ -988,14 +1034,18 @@
   }
 
   /**
-   * @return Yaw Axis
+   * Returns which axis, kX, kY, or kZ, is set to the yaw axis.
+   *
+   * @return IMUAxis Yaw Axis
    */
   public IMUAxis getYawAxis() {
     return m_yaw_axis;
   }
 
   /**
-   * @return accumulated gyro angle in the X axis in degrees
+   * Returns the accumulated gyro angle in the X axis in degrees.
+   *
+   * @return The accumulated gyro angle in the X axis in degrees.
    */
   public synchronized double getGyroAngleX() {
     if (m_simGyroAngleX != null) {
@@ -1005,7 +1055,9 @@
   }
 
   /**
-   * @return accumulated gyro angle in the Y axis in degrees
+   * Returns the accumulated gyro angle in the Y axis in degrees.
+   *
+   * @return The accumulated gyro angle in the Y axis in degrees.
    */
   public synchronized double getGyroAngleY() {
     if (m_simGyroAngleY != null) {
@@ -1015,7 +1067,9 @@
   }
 
   /**
-   * @return accumulated gyro angle in the Z axis in degrees
+   * Returns the accumulated gyro angle in the Z axis in degrees.
+   *
+   * @return The accumulated gyro angle in the Z axis in degrees.
    */
   public synchronized double getGyroAngleZ() {
     if (m_simGyroAngleZ != null) {
@@ -1025,7 +1079,9 @@
   }
 
   /**
-   * @return gyro angular rate in the X axis in degrees per second
+   * Returns the gyro angular rate in the X axis in degrees per second.
+   *
+   * @return The gyro angular rate in the X axis in degrees per second.
    */
   public synchronized double getGyroRateX() {
     if (m_simGyroRateX != null) {
@@ -1035,7 +1091,9 @@
   }
 
   /**
-   * @return gyro angular rate in the Y axis in degrees per second
+   * Returns the gyro angular rate in the Y axis in degrees per second.
+   *
+   * @return The gyro angular rate in the Y axis in degrees per second.
    */
   public synchronized double getGyroRateY() {
     if (m_simGyroRateY != null) {
@@ -1045,7 +1103,9 @@
   }
 
   /**
-   * @return gyro angular rate in the Z axis in degrees per second
+   * Returns the gyro angular rate in the Z axis in degrees per second.
+   *
+   * @return The gyro angular rate in the Z axis in degrees per second.
    */
   public synchronized double getGyroRateZ() {
     if (m_simGyroRateZ != null) {
@@ -1055,7 +1115,9 @@
   }
 
   /**
-   * @return urrent acceleration in the X axis in meters per second squared
+   * Returns the acceleration in the X axis in meters per second squared.
+   *
+   * @return The acceleration in the X axis in meters per second squared.
    */
   public synchronized double getAccelX() {
     if (m_simAccelX != null) {
@@ -1065,7 +1127,9 @@
   }
 
   /**
-   * @return current acceleration in the Y axis in meters per second squared
+   * Returns the acceleration in the Y axis in meters per second squared.
+   *
+   * @return The acceleration in the Y axis in meters per second squared.
    */
   public synchronized double getAccelY() {
     if (m_simAccelY != null) {
@@ -1075,7 +1139,9 @@
   }
 
   /**
-   * @return current acceleration in the Z axis in meters per second squared
+   * Returns the acceleration in the Z axis in meters per second squared.
+   *
+   * @return The acceleration in the Z axis in meters per second squared.
    */
   public synchronized double getAccelZ() {
     if (m_simAccelZ != null) {
@@ -1085,7 +1151,9 @@
   }
 
   /**
-   * @return Magnetic field strength in the X axis in Tesla
+   * Returns the magnetic field strength in the X axis in Tesla.
+   *
+   * @return The magnetic field strength in the X axis in Tesla.
    */
   public synchronized double getMagneticFieldX() {
     // mG to T
@@ -1093,7 +1161,9 @@
   }
 
   /**
-   * @return Magnetic field strength in the Y axis in Tesla
+   * Returns the magnetic field strength in the Y axis in Tesla.
+   *
+   * @return The magnetic field strength in the Y axis in Tesla.
    */
   public synchronized double getMagneticFieldY() {
     // mG to T
@@ -1101,7 +1171,9 @@
   }
 
   /**
-   * @return Magnetic field strength in the Z axis in Tesla
+   * Returns the magnetic field strength in the Z axis in Tesla.
+   *
+   * @return The magnetic field strength in the Z axis in Tesla.
    */
   public synchronized double getMagneticFieldZ() {
     // mG to T
@@ -1109,35 +1181,47 @@
   }
 
   /**
-   * @return X-axis complementary angle in degrees
+   * Returns the complementary angle around the X axis computed from accelerometer and gyro rate
+   * measurements.
+   *
+   * @return The X-axis complementary angle in degrees.
    */
   public synchronized double getXComplementaryAngle() {
     return m_compAngleX;
   }
 
   /**
-   * @return Y-axis complementary angle in degrees
+   * Returns the complementary angle around the Y axis computed from accelerometer and gyro rate
+   * measurements.
+   *
+   * @return The Y-axis complementary angle in degrees.
    */
   public synchronized double getYComplementaryAngle() {
     return m_compAngleY;
   }
 
   /**
-   * @return X-axis filtered acceleration angle in degrees
+   * Returns the X-axis filtered acceleration angle in degrees.
+   *
+   * @return The X-axis filtered acceleration angle in degrees.
    */
   public synchronized double getXFilteredAccelAngle() {
     return m_accelAngleX;
   }
 
   /**
-   * @return Y-axis filtered acceleration angle in degrees
+   * Returns the Y-axis filtered acceleration angle in degrees.
+   *
+   * @return The Y-axis filtered acceleration angle in degrees.
    */
   public synchronized double getYFilteredAccelAngle() {
     return m_accelAngleY;
   }
 
   /**
-   * @return Barometric Pressure in PSI
+   * Returns the barometric pressure in PSI.
+   *
+   * @return The barometric pressure in PSI.
    */
   public synchronized double getBarometricPressure() {
     // mbar to PSI conversion
@@ -1145,7 +1229,9 @@
   }
 
   /**
-   * @return Temperature in degrees Celsius
+   * Returns the temperature in degrees Celsius.
+   *
+   * @return The temperature in degrees Celsius.
    */
   public synchronized double getTemperature() {
     return m_temp;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16470_IMU.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16470_IMU.java
index 2fad3f3..b7236a5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16470_IMU.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADIS16470_IMU.java
@@ -122,45 +122,21 @@
   private static final int FLSHCNT_LOW = 0x7C; // Flash update count, lower word
   private static final int FLSHCNT_HIGH = 0x7E; // Flash update count, upper word
 
-  private static final byte[] m_autospi_x_packet = {
+  // Weight between the previous and current gyro angles represents 1 second for the timestamp,
+  // this is the point at which we ignore the previous angle because it is too old to be of use.
+  // The IMU timestamp conversion is 1 = 49.02us, the value 1_000_000 is the number of microseconds
+  // we average over.
+  private static final double AVERAGE_RATE_SCALING_FACTOR = 49.02 / 1_000_000;
+
+  private static final byte[] m_autospi_allAngle_packet = {
     X_DELTANG_OUT,
     FLASH_CNT,
     X_DELTANG_LOW,
     FLASH_CNT,
-    X_GYRO_OUT,
-    FLASH_CNT,
-    Y_GYRO_OUT,
-    FLASH_CNT,
-    Z_GYRO_OUT,
-    FLASH_CNT,
-    X_ACCL_OUT,
-    FLASH_CNT,
-    Y_ACCL_OUT,
-    FLASH_CNT,
-    Z_ACCL_OUT,
-    FLASH_CNT
-  };
-
-  private static final byte[] m_autospi_y_packet = {
     Y_DELTANG_OUT,
     FLASH_CNT,
     Y_DELTANG_LOW,
     FLASH_CNT,
-    X_GYRO_OUT,
-    FLASH_CNT,
-    Y_GYRO_OUT,
-    FLASH_CNT,
-    Z_GYRO_OUT,
-    FLASH_CNT,
-    X_ACCL_OUT,
-    FLASH_CNT,
-    Y_ACCL_OUT,
-    FLASH_CNT,
-    Z_ACCL_OUT,
-    FLASH_CNT
-  };
-
-  private static final byte[] m_autospi_z_packet = {
     Z_DELTANG_OUT,
     FLASH_CNT,
     Z_DELTANG_LOW,
@@ -179,31 +155,59 @@
     FLASH_CNT
   };
 
+  /** ADIS16470 calibration times. */
   public enum CalibrationTime {
+    /** 32 ms calibration time */
     _32ms(0),
+    /** 64 ms calibration time */
     _64ms(1),
+    /** 128 ms calibration time */
     _128ms(2),
+    /** 256 ms calibration time */
     _256ms(3),
+    /** 512 ms calibration time */
     _512ms(4),
+    /** 1 s calibration time */
     _1s(5),
+    /** 2 s calibration time */
     _2s(6),
+    /** 4 s calibration time */
     _4s(7),
+    /** 8 s calibration time */
     _8s(8),
+    /** 16 s calibration time */
     _16s(9),
+    /** 32 s calibration time */
     _32s(10),
+    /** 64 s calibration time */
     _64s(11);
 
-    private int value;
+    private final int value;
 
-    private CalibrationTime(int value) {
+    CalibrationTime(int value) {
       this.value = value;
     }
   }
 
+  /**
+   * IMU axes.
+   *
+   * <p>kX, kY, and kZ refer to the IMU's X, Y, and Z axes respectively. kYaw, kPitch, and kRoll are
+   * configured by the user to refer to an X, Y, or Z axis.
+   */
   public enum IMUAxis {
+    /** The IMU's X axis */
     kX,
+    /** The IMU's Y axis */
     kY,
-    kZ
+    /** The IMU's Z axis */
+    kZ,
+    /** The user-configured yaw axis */
+    kYaw,
+    /** The user-configured pitch axis */
+    kPitch,
+    /** The user-configured roll axis */
+    kRoll,
   }
 
   // Static Constants
@@ -212,19 +216,26 @@
   private static final double deg_to_rad = 0.0174532;
   private static final double grav = 9.81;
 
-  // User-specified yaw axis
+  // User-specified axes
   private IMUAxis m_yaw_axis;
+  private IMUAxis m_pitch_axis;
+  private IMUAxis m_roll_axis;
 
   // Instant raw outputs
   private double m_gyro_rate_x = 0.0;
   private double m_gyro_rate_y = 0.0;
   private double m_gyro_rate_z = 0.0;
+  private double m_average_gyro_rate_x = 0.0;
+  private double m_average_gyro_rate_y = 0.0;
+  private double m_average_gyro_rate_z = 0.0;
   private double m_accel_x = 0.0;
   private double m_accel_y = 0.0;
   private double m_accel_z = 0.0;
 
   // Integrated gyro angle
-  private double m_integ_angle = 0.0;
+  private double m_integ_angle_x = 0.0;
+  private double m_integ_angle_y = 0.0;
+  private double m_integ_angle_z = 0.0;
 
   // Complementary filter variables
   private double m_dt = 0.0;
@@ -278,17 +289,74 @@
     }
   }
 
+  /**
+   * Creates a new ADIS16740 IMU object.
+   *
+   * <p>The default setup is the onboard SPI port with a calibration time of 4 seconds. Yaw, pitch,
+   * and roll are kZ, kX, and kY respectively.
+   */
   public ADIS16470_IMU() {
-    this(IMUAxis.kZ, SPI.Port.kOnboardCS0, CalibrationTime._4s);
+    this(IMUAxis.kZ, IMUAxis.kX, IMUAxis.kY, SPI.Port.kOnboardCS0, CalibrationTime._4s);
   }
 
   /**
+   * Creates a new ADIS16740 IMU object.
+   *
+   * <p>The default setup is the onboard SPI port with a calibration time of 4 seconds.
+   *
+   * <p><b><i>Input axes limited to kX, kY and kZ. Specifying kYaw, kPitch,or kRoll will result in
+   * an error.</i></b>
+   *
    * @param yaw_axis The axis that measures the yaw
+   * @param pitch_axis The axis that measures the pitch
+   * @param roll_axis The axis that measures the roll
+   */
+  public ADIS16470_IMU(IMUAxis yaw_axis, IMUAxis pitch_axis, IMUAxis roll_axis) {
+    this(yaw_axis, pitch_axis, roll_axis, SPI.Port.kOnboardCS0, CalibrationTime._4s);
+  }
+
+  /**
+   * Creates a new ADIS16740 IMU object.
+   *
+   * <p><b><i>Input axes limited to kX, kY and kZ. Specifying kYaw, kPitch,or kRoll will result in
+   * an error.</i></b>
+   *
+   * @param yaw_axis The axis that measures the yaw
+   * @param pitch_axis The axis that measures the pitch
+   * @param roll_axis The axis that measures the roll
    * @param port The SPI Port the gyro is plugged into
    * @param cal_time Calibration time
    */
-  public ADIS16470_IMU(IMUAxis yaw_axis, SPI.Port port, CalibrationTime cal_time) {
+  @SuppressWarnings("this-escape")
+  public ADIS16470_IMU(
+      IMUAxis yaw_axis,
+      IMUAxis pitch_axis,
+      IMUAxis roll_axis,
+      SPI.Port port,
+      CalibrationTime cal_time) {
+    if (yaw_axis == IMUAxis.kYaw
+        || yaw_axis == IMUAxis.kPitch
+        || yaw_axis == IMUAxis.kRoll
+        || pitch_axis == IMUAxis.kYaw
+        || pitch_axis == IMUAxis.kPitch
+        || pitch_axis == IMUAxis.kRoll
+        || roll_axis == IMUAxis.kYaw
+        || roll_axis == IMUAxis.kPitch
+        || roll_axis == IMUAxis.kRoll) {
+      DriverStation.reportError(
+          "ADIS16470 constructor only allows IMUAxis.kX, IMUAxis.kY or IMUAxis.kZ as arguments.",
+          false);
+      DriverStation.reportError(
+          "Constructing ADIS with default axes. (IMUAxis.kZ is defined as Yaw)", false);
+      yaw_axis = IMUAxis.kZ;
+      pitch_axis = IMUAxis.kY;
+      roll_axis = IMUAxis.kX;
+    }
+
     m_yaw_axis = yaw_axis;
+    m_pitch_axis = pitch_axis;
+    m_roll_axis = roll_axis;
+
     m_calibration_time = cal_time.value;
     m_spi_port = port;
 
@@ -368,6 +436,11 @@
     m_connected = true;
   }
 
+  /**
+   * Checks the connection status of the IMU.
+   *
+   * @return True if the IMU is connected, false otherwise.
+   */
   public boolean isConnected() {
     if (m_simConnected != null) {
       return m_simConnected.get();
@@ -396,7 +469,7 @@
    * @return
    */
   private static int toShort(int... buf) {
-    return (short) (((buf[0] & 0xFF) << 8) + ((buf[1] & 0xFF) << 0));
+    return (short) (((buf[0] & 0xFF) << 8) + ((buf[1] & 0xFF)));
   }
 
   /**
@@ -410,7 +483,7 @@
   /**
    * Switch to standard SPI mode.
    *
-   * @return
+   * @return True if successful, false otherwise.
    */
   private boolean switchToStandardSPI() {
     // Check to see whether the acquire thread is active. If so, wait for it to stop
@@ -476,7 +549,9 @@
   }
 
   /**
-   * @return
+   * Switch to auto SPI mode.
+   *
+   * @return True if successful, false otherwise.
    */
   boolean switchToAutoSPI() {
     // No SPI port has been set up. Go set one up first.
@@ -498,17 +573,7 @@
       m_auto_configured = true;
     }
     // Do we need to change auto SPI settings?
-    switch (m_yaw_axis) {
-      case kX:
-        m_spi.setAutoTransmitData(m_autospi_x_packet, 2);
-        break;
-      case kY:
-        m_spi.setAutoTransmitData(m_autospi_y_packet, 2);
-        break;
-      default:
-        m_spi.setAutoTransmitData(m_autospi_z_packet, 2);
-        break;
-    }
+    m_spi.setAutoTransmitData(m_autospi_allAngle_packet, 2);
     // Configure auto stall time
     m_spi.configureAutoStall(5, 1000, 1);
     // Kick off auto SPI (Note: Device configuration impossible after auto SPI is
@@ -560,18 +625,28 @@
     return 0;
   }
 
-  public int configDecRate(int reg) {
-    int m_reg = reg;
+  /**
+   * Configures the decimation rate of the IMU.
+   *
+   * @param decimationRate The new decimation value.
+   * @return 0 if success, 1 if no change, 2 if error.
+   */
+  public int configDecRate(int decimationRate) {
+    // Switches the active SPI port to standard SPI mode, writes a new value to
+    // the DECIMATE register in the IMU, and re-enables auto SPI.
+    //
+    // This function enters standard SPI mode, writes a new DECIMATE setting to
+    // the IMU, adjusts the sample scale factor, and re-enters auto SPI mode.
     if (!switchToStandardSPI()) {
       DriverStation.reportError("Failed to configure/reconfigure standard SPI.", false);
       return 2;
     }
-    if (m_reg > 1999) {
-      DriverStation.reportError("Attempted to write an invalid deimation value.", false);
-      m_reg = 1999;
+    if (decimationRate > 1999) {
+      DriverStation.reportError("Attempted to write an invalid decimation value.", false);
+      decimationRate = 1999;
     }
-    m_scaled_sample_rate = (((m_reg + 1.0) / 2000.0) * 1000000.0);
-    writeRegister(DEC_RATE, m_reg);
+    m_scaled_sample_rate = (((decimationRate + 1.0) / 2000.0) * 1000000.0);
+    writeRegister(DEC_RATE, decimationRate);
     System.out.println("Decimation register: " + readRegister(DEC_RATE));
     if (!switchToAutoSPI()) {
       DriverStation.reportError("Failed to configure/reconfigure auto SPI.", false);
@@ -593,29 +668,6 @@
     if (!switchToAutoSPI()) {
       DriverStation.reportError("Failed to configure/reconfigure auto SPI.", false);
     }
-    ;
-  }
-
-  /**
-   * Sets the yaw axis
-   *
-   * @param yaw_axis The new yaw axis to use
-   * @return 1 if the new yaw axis is the same as the current one, 2 if the switch to Standard SPI
-   *     failed, else 0.
-   */
-  public int setYawAxis(IMUAxis yaw_axis) {
-    if (m_yaw_axis == yaw_axis) {
-      return 1;
-    }
-    if (!switchToStandardSPI()) {
-      DriverStation.reportError("Failed to configure/reconfigure standard SPI.", false);
-      return 2;
-    }
-    m_yaw_axis = yaw_axis;
-    if (!switchToAutoSPI()) {
-      DriverStation.reportError("Failed to configure/reconfigure auto SPI.", false);
-    }
-    return 0;
   }
 
   /**
@@ -650,12 +702,6 @@
     m_spi.write(buf, 2);
   }
 
-  public void reset() {
-    synchronized (this) {
-      m_integ_angle = 0.0;
-    }
-  }
-
   /** Delete (free) the spi port used for the IMU. */
   @Override
   public void close() {
@@ -691,7 +737,7 @@
   /** */
   private void acquire() {
     // Set data packet length
-    final int dataset_len = 19; // 18 data points + timestamp
+    final int dataset_len = 27; // 26 data points + timestamp
     final int BUFFER_SIZE = 4000;
 
     // Set up buffers and variables
@@ -700,7 +746,9 @@
     int data_remainder = 0;
     int data_to_read = 0;
     double previous_timestamp = 0.0;
-    double delta_angle = 0.0;
+    double delta_angle_x = 0.0;
+    double delta_angle_y = 0.0;
+    double delta_angle_z = 0.0;
     double gyro_rate_x = 0.0;
     double gyro_rate_y = 0.0;
     double gyro_rate_z = 0.0;
@@ -730,8 +778,8 @@
 
         data_count =
             m_spi.readAutoReceivedData(
-                buffer, 0, 0); // Read number of bytes currently stored in the
-        // buffer
+                buffer, 0, 0); // Read number of bytes currently stored in the buffer
+
         data_remainder =
             data_count % dataset_len; // Check if frame is incomplete. Add 1 because of timestamp
         data_to_read = data_count - data_remainder; // Remove incomplete data from read count
@@ -772,18 +820,27 @@
            */
 
           /*
-           * Get delta angle value for selected yaw axis and scale by the elapsed time
+           * Get delta angle value for all 3 axes and scale by the elapsed time
            * (based on timestamp)
            */
-          delta_angle =
+          delta_angle_x =
               (toInt(buffer[i + 3], buffer[i + 4], buffer[i + 5], buffer[i + 6]) * delta_angle_sf)
                   / (m_scaled_sample_rate / (buffer[i] - previous_timestamp));
-          gyro_rate_x = (toShort(buffer[i + 7], buffer[i + 8]) / 10.0);
-          gyro_rate_y = (toShort(buffer[i + 9], buffer[i + 10]) / 10.0);
-          gyro_rate_z = (toShort(buffer[i + 11], buffer[i + 12]) / 10.0);
-          accel_x = (toShort(buffer[i + 13], buffer[i + 14]) / 800.0);
-          accel_y = (toShort(buffer[i + 15], buffer[i + 16]) / 800.0);
-          accel_z = (toShort(buffer[i + 17], buffer[i + 18]) / 800.0);
+          delta_angle_y =
+              (toInt(buffer[i + 7], buffer[i + 8], buffer[i + 9], buffer[i + 10]) * delta_angle_sf)
+                  / (m_scaled_sample_rate / (buffer[i] - previous_timestamp));
+          delta_angle_z =
+              (toInt(buffer[i + 11], buffer[i + 12], buffer[i + 13], buffer[i + 14])
+                      * delta_angle_sf)
+                  / (m_scaled_sample_rate / (buffer[i] - previous_timestamp));
+
+          gyro_rate_x = (toShort(buffer[i + 15], buffer[i + 16]) / 10.0);
+          gyro_rate_y = (toShort(buffer[i + 17], buffer[i + 18]) / 10.0);
+          gyro_rate_z = (toShort(buffer[i + 19], buffer[i + 20]) / 10.0);
+
+          accel_x = (toShort(buffer[i + 21], buffer[i + 22]) / 800.0);
+          accel_y = (toShort(buffer[i + 23], buffer[i + 24]) / 800.0);
+          accel_z = (toShort(buffer[i + 25], buffer[i + 26]) / 800.0);
 
           // Convert scaled sensor data to SI units (for tilt calculations)
           // TODO: Should the unit outputs be selectable?
@@ -809,6 +866,10 @@
                     accel_y_si, Math.sqrt((accel_x_si * accel_x_si) + (accel_z_si * accel_z_si)));
             compAngleX = accelAngleX;
             compAngleY = accelAngleY;
+
+            m_average_gyro_rate_x = 0.0;
+            m_average_gyro_rate_y = 0.0;
+            m_average_gyro_rate_z = 0.0;
           } else {
             // Run inclinometer calculations
             accelAngleX =
@@ -830,9 +891,13 @@
                * Don't accumulate first run. previous_timestamp will be "very" old and the
                * integration will end up way off
                */
-              m_integ_angle = 0.0;
+              m_integ_angle_x = 0.0;
+              m_integ_angle_y = 0.0;
+              m_integ_angle_z = 0.0;
             } else {
-              m_integ_angle += delta_angle;
+              m_integ_angle_x += delta_angle_x;
+              m_integ_angle_y += delta_angle_y;
+              m_integ_angle_z += delta_angle_z;
             }
             m_gyro_rate_x = gyro_rate_x;
             m_gyro_rate_y = gyro_rate_y;
@@ -844,16 +909,28 @@
             m_compAngleY = compAngleY * rad_to_deg;
             m_accelAngleX = accelAngleX * rad_to_deg;
             m_accelAngleY = accelAngleY * rad_to_deg;
+            m_average_gyro_rate_x += gyro_rate_x;
+            m_average_gyro_rate_y += gyro_rate_y;
+            m_average_gyro_rate_z += gyro_rate_z;
           }
           m_first_run = false;
         }
+
+        // The inverse of data to read divided by dataset length, his is the number of iterations
+        // of the for loop inverted (so multiplication can be used instead of division)
+        double invTotalIterations = (double) dataset_len / data_to_read;
+        m_average_gyro_rate_x *= invTotalIterations;
+        m_average_gyro_rate_y *= invTotalIterations;
+        m_average_gyro_rate_z *= invTotalIterations;
       } else {
         m_thread_idle = true;
         data_count = 0;
         data_remainder = 0;
         data_to_read = 0;
         previous_timestamp = 0.0;
-        delta_angle = 0.0;
+        delta_angle_x = 0.0;
+        delta_angle_y = 0.0;
+        delta_angle_z = 0.0;
         gyro_rate_x = 0.0;
         gyro_rate_y = 0.0;
         gyro_rate_z = 0.0;
@@ -933,111 +1010,267 @@
   }
 
   /**
-   * @return Yaw axis angle in degrees (CCW positive)
+   * Reset the gyro.
+   *
+   * <p>Resets the gyro accumulations to a heading of zero. This can be used if there is significant
+   * drift in the gyro and it needs to be recalibrated after running.
    */
-  public synchronized double getAngle() {
-    switch (m_yaw_axis) {
+  public void reset() {
+    synchronized (this) {
+      m_integ_angle_x = 0.0;
+      m_integ_angle_y = 0.0;
+      m_integ_angle_z = 0.0;
+    }
+  }
+
+  /**
+   * Allow the designated gyro angle to be set to a given value. This may happen with unread values
+   * in the buffer, it is suggested that the IMU is not moving when this method is run.
+   *
+   * @param axis IMUAxis that will be changed
+   * @param angle A double in degrees (CCW positive)
+   */
+  public void setGyroAngle(IMUAxis axis, double angle) {
+    switch (axis) {
+      case kYaw:
+        axis = m_yaw_axis;
+        break;
+      case kPitch:
+        axis = m_pitch_axis;
+        break;
+      case kRoll:
+        axis = m_roll_axis;
+        break;
+      default:
+    }
+
+    switch (axis) {
+      case kX:
+        this.setGyroAngleX(angle);
+        break;
+      case kY:
+        this.setGyroAngleY(angle);
+        break;
+      case kZ:
+        this.setGyroAngleZ(angle);
+        break;
+      default:
+    }
+  }
+
+  /**
+   * Allow the gyro angle X to be set to a given value. This may happen with unread values in the
+   * buffer, it is suggested that the IMU is not moving when this method is run.
+   *
+   * @param angle A double in degrees (CCW positive)
+   */
+  public void setGyroAngleX(double angle) {
+    synchronized (this) {
+      m_integ_angle_x = angle;
+    }
+  }
+
+  /**
+   * Allow the gyro angle Y to be set to a given value. This may happen with unread values in the
+   * buffer, it is suggested that the IMU is not moving when this method is run.
+   *
+   * @param angle A double in degrees (CCW positive)
+   */
+  public void setGyroAngleY(double angle) {
+    synchronized (this) {
+      m_integ_angle_y = angle;
+    }
+  }
+
+  /**
+   * Allow the gyro angle Z to be set to a given value. This may happen with unread values in the
+   * buffer, it is suggested that the IMU is not moving when this method is run.
+   *
+   * @param angle A double in degrees (CCW positive)
+   */
+  public void setGyroAngleZ(double angle) {
+    synchronized (this) {
+      m_integ_angle_z = angle;
+    }
+  }
+
+  /**
+   * Returns the axis angle in degrees (CCW positive).
+   *
+   * @param axis The IMUAxis whose angle to return.
+   * @return The axis angle in degrees (CCW positive).
+   */
+  public synchronized double getAngle(IMUAxis axis) {
+    switch (axis) {
+      case kYaw:
+        axis = m_yaw_axis;
+        break;
+      case kPitch:
+        axis = m_pitch_axis;
+        break;
+      case kRoll:
+        axis = m_roll_axis;
+        break;
+      default:
+    }
+
+    switch (axis) {
       case kX:
         if (m_simGyroAngleX != null) {
           return m_simGyroAngleX.get();
         }
-        break;
+        return m_integ_angle_x;
       case kY:
         if (m_simGyroAngleY != null) {
           return m_simGyroAngleY.get();
         }
-        break;
+        return m_integ_angle_y;
       case kZ:
         if (m_simGyroAngleZ != null) {
           return m_simGyroAngleZ.get();
         }
-        break;
+        return m_integ_angle_z;
+      default:
     }
-    return m_integ_angle;
+
+    return 0.0;
   }
 
   /**
-   * @return Yaw axis angular rate in degrees per second (CCW positive)
+   * Returns the axis angular rate in degrees per second (CCW positive).
+   *
+   * @param axis The IMUAxis whose rate to return.
+   * @return Axis angular rate in degrees per second (CCW positive).
    */
-  public synchronized double getRate() {
-    if (m_yaw_axis == IMUAxis.kX) {
-      if (m_simGyroRateX != null) {
-        return m_simGyroRateX.get();
-      }
-      return m_gyro_rate_x;
-    } else if (m_yaw_axis == IMUAxis.kY) {
-      if (m_simGyroRateY != null) {
-        return m_simGyroRateY.get();
-      }
-      return m_gyro_rate_y;
-    } else if (m_yaw_axis == IMUAxis.kZ) {
-      if (m_simGyroRateZ != null) {
-        return m_simGyroRateZ.get();
-      }
-      return m_gyro_rate_z;
-    } else {
-      return 0.0;
+  public synchronized double getRate(IMUAxis axis) {
+    switch (axis) {
+      case kYaw:
+        axis = m_yaw_axis;
+        break;
+      case kPitch:
+        axis = m_pitch_axis;
+        break;
+      case kRoll:
+        axis = m_roll_axis;
+        break;
+      default:
     }
+
+    switch (axis) {
+      case kX:
+        if (m_simGyroRateX != null) {
+          return m_simGyroRateX.get();
+        }
+        return m_gyro_rate_x;
+      case kY:
+        if (m_simGyroRateY != null) {
+          return m_simGyroRateY.get();
+        }
+        return m_gyro_rate_y;
+      case kZ:
+        if (m_simGyroRateZ != null) {
+          return m_simGyroRateZ.get();
+        }
+        return m_gyro_rate_z;
+      default:
+    }
+    return 0.0;
   }
 
   /**
-   * @return Yaw Axis
+   * Returns which axis, kX, kY, or kZ, is set to the yaw axis.
+   *
+   * @return IMUAxis Yaw Axis
    */
   public IMUAxis getYawAxis() {
     return m_yaw_axis;
   }
 
   /**
-   * @return current acceleration in the X axis
+   * Returns which axis, kX, kY, or kZ, is set to the pitch axis.
+   *
+   * @return IMUAxis Pitch Axis
+   */
+  public IMUAxis getPitchAxis() {
+    return m_pitch_axis;
+  }
+
+  /**
+   * Returns which axis, kX, kY, or kZ, is set to the roll axis.
+   *
+   * @return IMUAxis Roll Axis
+   */
+  public IMUAxis getRollAxis() {
+    return m_roll_axis;
+  }
+
+  /**
+   * Returns the acceleration in the X axis in meters per second squared.
+   *
+   * @return The acceleration in the X axis in meters per second squared.
    */
   public synchronized double getAccelX() {
     return m_accel_x * 9.81;
   }
 
   /**
-   * @return current acceleration in the Y axis
+   * Returns the acceleration in the Y axis in meters per second squared.
+   *
+   * @return The acceleration in the Y axis in meters per second squared.
    */
   public synchronized double getAccelY() {
     return m_accel_y * 9.81;
   }
 
   /**
-   * @return current acceleration in the Z axis
+   * Returns the acceleration in the Z axis in meters per second squared.
+   *
+   * @return The acceleration in the Z axis in meters per second squared.
    */
   public synchronized double getAccelZ() {
     return m_accel_z * 9.81;
   }
 
   /**
-   * @return X-axis complementary angle
+   * Returns the complementary angle around the X axis computed from accelerometer and gyro rate
+   * measurements.
+   *
+   * @return The X-axis complementary angle in degrees.
    */
   public synchronized double getXComplementaryAngle() {
     return m_compAngleX;
   }
 
   /**
-   * @return Y-axis complementary angle
+   * Returns the complementary angle around the Y axis computed from accelerometer and gyro rate
+   * measurements.
+   *
+   * @return The Y-axis complementary angle in degrees.
    */
   public synchronized double getYComplementaryAngle() {
     return m_compAngleY;
   }
 
   /**
-   * @return X-axis filtered acceleration angle
+   * Returns the X-axis filtered acceleration angle in degrees.
+   *
+   * @return The X-axis filtered acceleration angle in degrees.
    */
   public synchronized double getXFilteredAccelAngle() {
     return m_accelAngleX;
   }
 
   /**
-   * @return Y-axis filtered acceleration angle
+   * Returns the Y-axis filtered acceleration angle in degrees.
+   *
+   * @return The Y-axis filtered acceleration angle in degrees.
    */
   public synchronized double getYFilteredAccelAngle() {
     return m_accelAngleY;
   }
 
   /**
-   * Get the SPI port number.
+   * Gets the SPI port number.
    *
    * @return The SPI port number.
    */
@@ -1048,6 +1281,6 @@
   @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("Gyro");
-    builder.addDoubleProperty("Value", this::getAngle, null);
+    builder.addDoubleProperty("Value", () -> getAngle(m_yaw_axis), null);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java
index 7ceff95..4f6803d 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java
@@ -27,7 +27,9 @@
  */
 @SuppressWarnings({"TypeName", "PMD.UnusedPrivateField"})
 public class ADXL345_I2C implements NTSendable, AutoCloseable {
-  private static final byte kAddress = 0x1D;
+  /** Default I2C device address. */
+  public static final byte kAddress = 0x1D;
+
   private static final byte kPowerCtlRegister = 0x2D;
   private static final byte kDataFormatRegister = 0x31;
   private static final byte kDataRegister = 0x32;
@@ -43,16 +45,25 @@
   private static final byte kDataFormat_FullRes = 0x08;
   private static final byte kDataFormat_Justify = 0x04;
 
+  /** Accelerometer range. */
   public enum Range {
+    /** 2 Gs max. */
     k2G,
+    /** 4 Gs max. */
     k4G,
+    /** 8 Gs max. */
     k8G,
+    /** 16 Gs max. */
     k16G
   }
 
+  /** Accelerometer axes. */
   public enum Axes {
+    /** X axis. */
     kX((byte) 0x00),
+    /** Y axis. */
     kY((byte) 0x02),
+    /** Z axis. */
     kZ((byte) 0x04);
 
     /** The integer value representing this enumeration. */
@@ -63,20 +74,29 @@
     }
   }
 
+  /** Container type for accelerations from all axes. */
   @SuppressWarnings("MemberName")
   public static class AllAxes {
+    /** Acceleration along the X axis in g-forces. */
     public double XAxis;
+
+    /** Acceleration along the Y axis in g-forces. */
     public double YAxis;
+
+    /** Acceleration along the Z axis in g-forces. */
     public double ZAxis;
+
+    /** Default constructor. */
+    public AllAxes() {}
   }
 
-  protected I2C m_i2c;
+  private I2C m_i2c;
 
-  protected SimDevice m_simDevice;
-  protected SimEnum m_simRange;
-  protected SimDouble m_simX;
-  protected SimDouble m_simY;
-  protected SimDouble m_simZ;
+  private SimDevice m_simDevice;
+  private SimEnum m_simRange;
+  private SimDouble m_simX;
+  private SimDouble m_simY;
+  private SimDouble m_simZ;
 
   /**
    * Constructs the ADXL345 Accelerometer with I2C address 0x1D.
@@ -95,6 +115,7 @@
    * @param range The range (+ or -) that the accelerometer will measure.
    * @param deviceAddress I2C address of the accelerometer (0x1D or 0x53)
    */
+  @SuppressWarnings("this-escape")
   public ADXL345_I2C(I2C.Port port, Range range, int deviceAddress) {
     m_i2c = new I2C(port, deviceAddress);
 
@@ -122,10 +143,20 @@
     SendableRegistry.addLW(this, "ADXL345_I2C", port.value);
   }
 
+  /**
+   * Returns the I2C port.
+   *
+   * @return The I2C port.
+   */
   public int getPort() {
     return m_i2c.getPort();
   }
 
+  /**
+   * Returns the I2C device address.
+   *
+   * @return The I2C device address.
+   */
   public int getDeviceAddress() {
     return m_i2c.getDeviceAddress();
   }
@@ -149,7 +180,7 @@
    * @param range The maximum acceleration, positive or negative, that the accelerometer will
    *     measure.
    */
-  public void setRange(Range range) {
+  public final void setRange(Range range) {
     final byte value;
     switch (range) {
       case k2G:
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java
index 7291443..4c8435a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java
@@ -40,16 +40,25 @@
   private static final int kDataFormat_FullRes = 0x08;
   private static final int kDataFormat_Justify = 0x04;
 
+  /** Accelerometer range. */
   public enum Range {
+    /** 2 Gs max. */
     k2G,
+    /** 4 Gs max. */
     k4G,
+    /** 8 Gs max. */
     k8G,
+    /** 16 Gs max. */
     k16G
   }
 
+  /** Accelerometer axes. */
   public enum Axes {
+    /** X axis. */
     kX((byte) 0x00),
+    /** Y axis. */
     kY((byte) 0x02),
+    /** Z axis. */
     kZ((byte) 0x04);
 
     /** The integer value representing this enumeration. */
@@ -60,20 +69,29 @@
     }
   }
 
+  /** Container type for accelerations from all axes. */
   @SuppressWarnings("MemberName")
   public static class AllAxes {
+    /** Acceleration along the X axis in g-forces. */
     public double XAxis;
+
+    /** Acceleration along the Y axis in g-forces. */
     public double YAxis;
+
+    /** Acceleration along the Z axis in g-forces. */
     public double ZAxis;
+
+    /** Default constructor. */
+    public AllAxes() {}
   }
 
-  protected SPI m_spi;
+  private SPI m_spi;
 
-  protected SimDevice m_simDevice;
-  protected SimEnum m_simRange;
-  protected SimDouble m_simX;
-  protected SimDouble m_simY;
-  protected SimDouble m_simZ;
+  private SimDevice m_simDevice;
+  private SimEnum m_simRange;
+  private SimDouble m_simX;
+  private SimDouble m_simY;
+  private SimDouble m_simZ;
 
   /**
    * Constructor.
@@ -81,6 +99,7 @@
    * @param port The SPI port that the accelerometer is connected to
    * @param range The range (+ or -) that the accelerometer will measure.
    */
+  @SuppressWarnings("this-escape")
   public ADXL345_SPI(SPI.Port port, Range range) {
     m_spi = new SPI(port);
     // simulation
@@ -101,6 +120,11 @@
     SendableRegistry.addLW(this, "ADXL345_SPI", port.value);
   }
 
+  /**
+   * Returns the SPI port.
+   *
+   * @return The SPI port.
+   */
   public int getPort() {
     return m_spi.getPort();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java
index b094be3..c3de6dd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java
@@ -43,17 +43,26 @@
 
   private static final byte kPowerCtl_Measure = 0x02;
 
+  /** Accelerometer range. */
   public enum Range {
+    /** 2 Gs max. */
     k2G,
+    /** 4 Gs max. */
     k4G,
+    /** 8 Gs max. */
     k8G
   }
 
+  /** Accelerometer axes. */
   public enum Axes {
+    /** X axis. */
     kX((byte) 0x00),
+    /** Y axis. */
     kY((byte) 0x02),
+    /** Z axis. */
     kZ((byte) 0x04);
 
+    /** Axis value. */
     public final byte value;
 
     Axes(byte value) {
@@ -61,11 +70,20 @@
     }
   }
 
+  /** Container type for accelerations from all axes. */
   @SuppressWarnings("MemberName")
   public static class AllAxes {
+    /** Acceleration along the X axis in g-forces. */
     public double XAxis;
+
+    /** Acceleration along the Y axis in g-forces. */
     public double YAxis;
+
+    /** Acceleration along the Z axis in g-forces. */
     public double ZAxis;
+
+    /** Default constructor. */
+    public AllAxes() {}
   }
 
   private SPI m_spi;
@@ -93,6 +111,7 @@
    * @param port The SPI port that the accelerometer is connected to
    * @param range The range (+ or -) that the accelerometer will measure.
    */
+  @SuppressWarnings("this-escape")
   public ADXL362(SPI.Port port, Range range) {
     m_spi = new SPI(port);
 
@@ -141,6 +160,11 @@
     SendableRegistry.addLW(this, "ADXL362", port.value);
   }
 
+  /**
+   * Returns the SPI port.
+   *
+   * @return The SPI port.
+   */
   public int getPort() {
     return m_spi.getPort();
   }
@@ -164,7 +188,7 @@
    * @param range The maximum acceleration, positive or negative, that the accelerometer will
    *     measure.
    */
-  public void setRange(Range range) {
+  public final void setRange(Range range) {
     if (m_spi == null) {
       return;
     }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXRS450_Gyro.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXRS450_Gyro.java
index cbe34c8..799d432 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXRS450_Gyro.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXRS450_Gyro.java
@@ -59,6 +59,7 @@
    *
    * @param port The SPI port that the gyro is connected to
    */
+  @SuppressWarnings("this-escape")
   public ADXRS450_Gyro(SPI.Port port) {
     m_spi = new SPI(port);
 
@@ -113,7 +114,7 @@
    * are in progress, this is typically done when the robot is first turned on while it's sitting at
    * rest before the competition starts.
    */
-  public void calibrate() {
+  public final void calibrate() {
     if (m_spi == null) {
       return;
     }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java
index 61a2fa6..bb097ab 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java
@@ -14,7 +14,8 @@
  *
  * <p>By default, the timing supports WS2812B LEDs, but is configurable using setBitTiming()
  *
- * <p>Only 1 LED driver is currently supported by the roboRIO.
+ * <p>Only 1 LED driver is currently supported by the roboRIO. However, multiple LED strips can be
+ * connected in series and controlled from the single driver.
  */
 public class AddressableLED implements AutoCloseable {
   private final int m_pwmHandle;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java
index 3ea9ef7..0bdb2ff 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java
@@ -36,6 +36,7 @@
    *
    * @param channel The channel number for the analog input the accelerometer is connected to
    */
+  @SuppressWarnings("this-escape")
   public AnalogAccelerometer(final int channel) {
     this(new AnalogInput(channel), true);
     SendableRegistry.addChild(this, m_analogChannel);
@@ -49,10 +50,12 @@
    * @param channel The existing AnalogInput object for the analog input the accelerometer is
    *     connected to
    */
+  @SuppressWarnings("this-escape")
   public AnalogAccelerometer(final AnalogInput channel) {
     this(channel, false);
   }
 
+  @SuppressWarnings("this-escape")
   private AnalogAccelerometer(final AnalogInput channel, final boolean allocatedChannel) {
     requireNonNullParam(channel, "channel", "AnalogAccelerometer");
     m_allocatedChannel = allocatedChannel;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java
index 8e45869..727ad2c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java
@@ -22,9 +22,9 @@
   private double m_distancePerRotation = 1.0;
   private double m_lastPosition;
 
-  protected SimDevice m_simDevice;
-  protected SimDouble m_simPosition;
-  protected SimDouble m_simAbsolutePosition;
+  private SimDevice m_simDevice;
+  private SimDouble m_simPosition;
+  private SimDouble m_simAbsolutePosition;
 
   /**
    * Construct a new AnalogEncoder attached to a specific AnalogIn channel.
@@ -40,6 +40,7 @@
    *
    * @param analogInput the analog input to attach to
    */
+  @SuppressWarnings("this-escape")
   public AnalogEncoder(AnalogInput analogInput) {
     m_analogInput = analogInput;
     init();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java
index f5be4ad..29a17bb 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java
@@ -25,13 +25,13 @@
  */
 public class AnalogGyro implements Sendable, AutoCloseable {
   private static final double kDefaultVoltsPerDegreePerSecond = 0.007;
-  protected AnalogInput m_analog;
+  private AnalogInput m_analog;
   private boolean m_channelAllocated;
 
   private int m_gyroHandle;
 
   /** Initialize the gyro. Calibration is handled by calibrate(). */
-  public void initGyro() {
+  private void initGyro() {
     if (m_gyroHandle == 0) {
       m_gyroHandle = AnalogGyroJNI.initializeAnalogGyro(m_analog.m_port);
     }
@@ -78,6 +78,7 @@
    * @param channel The analog channel the gyro is connected to. Gyros can only be used on on-board
    *     channels 0-1.
    */
+  @SuppressWarnings("this-escape")
   public AnalogGyro(int channel) {
     this(new AnalogInput(channel));
     m_channelAllocated = true;
@@ -91,6 +92,7 @@
    * @param channel The AnalogInput object that the gyro is connected to. Gyros can only be used on
    *     on-board channels 0-1.
    */
+  @SuppressWarnings("this-escape")
   public AnalogGyro(AnalogInput channel) {
     requireNonNullParam(channel, "channel", "AnalogGyro");
 
@@ -108,6 +110,7 @@
    * @param center Preset uncalibrated value to use as the accumulator center value.
    * @param offset Preset uncalibrated value to use as the gyro offset.
    */
+  @SuppressWarnings("this-escape")
   public AnalogGyro(int channel, int center, double offset) {
     this(new AnalogInput(channel), center, offset);
     m_channelAllocated = true;
@@ -123,6 +126,7 @@
    * @param center Preset uncalibrated value to use as the accumulator center value.
    * @param offset Preset uncalibrated value to use as the gyro offset.
    */
+  @SuppressWarnings("this-escape")
   public AnalogGyro(AnalogInput channel, int center, double offset) {
     requireNonNullParam(channel, "channel", "AnalogGyro");
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java
index e90bfd2..3c6458b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java
@@ -38,6 +38,7 @@
    *
    * @param channel The channel number to represent. 0-3 are on-board 4-7 are on the MXP port.
    */
+  @SuppressWarnings("this-escape")
   public AnalogInput(final int channel) {
     AnalogJNI.checkAnalogInputChannel(channel);
     m_channel = channel;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java
index 299d89c..354cedc 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java
@@ -21,6 +21,7 @@
    *
    * @param channel The channel number to represent.
    */
+  @SuppressWarnings("this-escape")
   public AnalogOutput(final int channel) {
     SensorUtil.checkAnalogOutputChannel(channel);
     m_channel = channel;
@@ -49,10 +50,20 @@
     return m_channel;
   }
 
+  /**
+   * Set the value of the analog output.
+   *
+   * @param voltage The output value in Volts, from 0.0 to +5.0.
+   */
   public void setVoltage(double voltage) {
     AnalogJNI.setAnalogOutput(m_port, voltage);
   }
 
+  /**
+   * Get the voltage of the analog output.
+   *
+   * @return The value in Volts, from 0.0 to +5.0.
+   */
   public double getVoltage() {
     return AnalogJNI.getAnalogOutput(m_port);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java
index 7d58585..1547349 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java
@@ -33,6 +33,7 @@
    * @param fullRange The scaling to multiply the fraction by to get a meaningful unit.
    * @param offset The offset to add to the scaled value for controlling the zero value
    */
+  @SuppressWarnings("this-escape")
   public AnalogPotentiometer(final int channel, double fullRange, double offset) {
     this(new AnalogInput(channel), fullRange, offset);
     m_initAnalogInput = true;
@@ -53,6 +54,7 @@
    *     input.
    * @param offset The angular value (in desired units) representing the angular output at 0V.
    */
+  @SuppressWarnings("this-escape")
   public AnalogPotentiometer(final AnalogInput input, double fullRange, double offset) {
     SendableRegistry.addLW(this, "AnalogPotentiometer", input.getChannel());
     m_analogInput = input;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java
index 8baa508..ac25fbb 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java
@@ -15,30 +15,22 @@
 
 /** Class for creating and configuring Analog Triggers. */
 public class AnalogTrigger implements Sendable, AutoCloseable {
-  /** Exceptions dealing with improper operation of the Analog trigger. */
-  public static class AnalogTriggerException extends RuntimeException {
-    /**
-     * Create a new exception with the given message.
-     *
-     * @param message the message to pass with the exception
-     */
-    public AnalogTriggerException(String message) {
-      super(message);
-    }
-  }
-
   /** Where the analog trigger is attached. */
   protected int m_port;
 
-  protected AnalogInput m_analogInput;
-  protected DutyCycle m_dutyCycle;
-  protected boolean m_ownsAnalog;
+  private AnalogInput m_analogInput;
+
+  @SuppressWarnings({"PMD.SingularField", "PMD.UnusedPrivateField"})
+  private DutyCycle m_dutyCycle;
+
+  private boolean m_ownsAnalog;
 
   /**
    * Constructor for an analog trigger given a channel number.
    *
    * @param channel the port to use for the analog trigger
    */
+  @SuppressWarnings("this-escape")
   public AnalogTrigger(final int channel) {
     this(new AnalogInput(channel));
     m_ownsAnalog = true;
@@ -51,6 +43,7 @@
    *
    * @param channel the AnalogInput to use for the analog trigger
    */
+  @SuppressWarnings("this-escape")
   public AnalogTrigger(AnalogInput channel) {
     m_analogInput = channel;
 
@@ -67,6 +60,7 @@
    *
    * @param input the DutyCycle to use for the analog trigger
    */
+  @SuppressWarnings("this-escape")
   public AnalogTrigger(DutyCycle input) {
     m_dutyCycle = input;
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java
index c80179b..4d459fc 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java
@@ -106,9 +106,13 @@
 
   /** Defines the state in which the AnalogTrigger triggers. */
   public enum AnalogTriggerType {
+    /** In window. */
     kInWindow(AnalogJNI.AnalogTriggerType.kInWindow),
+    /** State. */
     kState(AnalogJNI.AnalogTriggerType.kState),
+    /** Rising pulse. */
     kRisingPulse(AnalogJNI.AnalogTriggerType.kRisingPulse),
+    /** Falling pulse. */
     kFallingPulse(AnalogJNI.AnalogTriggerType.kFallingPulse);
 
     private final int value;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java
index 1a6b103..522ab88 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java
@@ -17,9 +17,13 @@
  * <p>This class allows access to the roboRIO's internal accelerometer.
  */
 public class BuiltInAccelerometer implements Sendable, AutoCloseable {
+  /** Accelerometer range. */
   public enum Range {
+    /** 2 Gs max. */
     k2G,
+    /** 4 Gs max. */
     k4G,
+    /** 8 Gs max. */
     k8G
   }
 
@@ -28,6 +32,7 @@
    *
    * @param range The range the accelerometer will measure
    */
+  @SuppressWarnings("this-escape")
   public BuiltInAccelerometer(Range range) {
     setRange(range);
     HAL.report(tResourceType.kResourceType_Accelerometer, 0, 0, "Built-in accelerometer");
@@ -50,7 +55,7 @@
    * @param range The maximum acceleration, positive or negative, that the accelerometer will
    *     measure.
    */
-  public void setRange(Range range) {
+  public final void setRange(Range range) {
     AccelerometerJNI.setAccelerometerActive(false);
 
     switch (range) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java
index 8808a4b..df0be48 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java
@@ -5,6 +5,7 @@
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.CANAPIJNI;
+import edu.wpi.first.hal.CANAPITypes;
 import edu.wpi.first.hal.CANData;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
@@ -21,8 +22,11 @@
  * calls.
  */
 public class CAN implements Closeable {
-  public static final int kTeamManufacturer = 8;
-  public static final int kTeamDeviceType = 10;
+  /** Team manufacturer. */
+  public static final int kTeamManufacturer = CANAPITypes.CANManufacturer.kTeamUse.id;
+
+  /** Team device type. */
+  public static final int kTeamDeviceType = CANAPITypes.CANDeviceType.kMiscellaneous.id;
 
   private final int m_handle;
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java
index f2cfeb3..e7a14bd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java
@@ -31,6 +31,7 @@
    * @param module The module ID to use.
    * @param moduleType The module type to use.
    */
+  @SuppressWarnings("this-escape")
   public Compressor(int module, PneumaticsModuleType moduleType) {
     m_module = PneumaticsBase.getForType(module, moduleType);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CompressorConfigType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CompressorConfigType.java
index 48bb81f..7735cdd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CompressorConfigType.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CompressorConfigType.java
@@ -6,12 +6,18 @@
 
 import edu.wpi.first.hal.REVPHJNI;
 
+/** Compressor config type. */
 public enum CompressorConfigType {
+  /** Disabled. */
   Disabled(REVPHJNI.COMPRESSOR_CONFIG_TYPE_DISABLED),
+  /** Digital. */
   Digital(REVPHJNI.COMPRESSOR_CONFIG_TYPE_DIGITAL),
+  /** Analog. */
   Analog(REVPHJNI.COMPRESSOR_CONFIG_TYPE_ANALOG),
+  /** Hybrid. */
   Hybrid(REVPHJNI.COMPRESSOR_CONFIG_TYPE_HYBRID);
 
+  /** CompressorConfigType value. */
   public final int value;
 
   CompressorConfigType(int value) {
@@ -37,6 +43,11 @@
     }
   }
 
+  /**
+   * Returns the CompressorConfigType's value.
+   *
+   * @return The CompressorConfigType's value.
+   */
   public int getValue() {
     return value;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java
index d8c5015..eca3dd5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java
@@ -38,6 +38,7 @@
     /** mode: external direction. */
     kExternalDirection(3);
 
+    /** Mode value. */
     public final int value;
 
     Mode(int value) {
@@ -45,19 +46,30 @@
     }
   }
 
-  protected DigitalSource m_upSource; // /< What makes the counter count up.
-  protected DigitalSource m_downSource; // /< What makes the counter count down.
+  /** What makes the counter count up. */
+  protected DigitalSource m_upSource;
+
+  /** What makes the counter count down. */
+  protected DigitalSource m_downSource;
+
   private boolean m_allocatedUpSource;
   private boolean m_allocatedDownSource;
-  int m_counter; // /< The FPGA counter object.
-  private int m_index; // /< The index of this counter.
-  private double m_distancePerPulse = 1; // distance of travel for each tick
+
+  /** The FPGA counter object. */
+  int m_counter;
+
+  /** The index of this counter. */
+  private int m_index;
+
+  /** Distance of travel for each tick. */
+  private double m_distancePerPulse = 1;
 
   /**
    * Create an instance of a counter with the given mode.
    *
    * @param mode The counter mode.
    */
+  @SuppressWarnings("this-escape")
   public Counter(final Mode mode) {
     ByteBuffer index = ByteBuffer.allocateDirect(4);
     // set the byte order
@@ -95,6 +107,7 @@
    *
    * @param source the digital source to count
    */
+  @SuppressWarnings("this-escape")
   public Counter(DigitalSource source) {
     this();
 
@@ -109,6 +122,7 @@
    *
    * @param channel the DIO channel to use as the up source. 0-9 are on-board, 10-25 are on the MXP
    */
+  @SuppressWarnings("this-escape")
   public Counter(int channel) {
     this();
     setUpSource(channel);
@@ -125,6 +139,7 @@
    * @param downSource second source for direction
    * @param inverted true to invert the count
    */
+  @SuppressWarnings("this-escape")
   public Counter(
       EncodingType encodingType,
       DigitalSource upSource,
@@ -162,6 +177,7 @@
    *
    * @param trigger the analog trigger to count
    */
+  @SuppressWarnings("this-escape")
   public Counter(AnalogTrigger trigger) {
     this();
 
@@ -200,7 +216,7 @@
    *
    * @param channel the DIO channel to count 0-9 are on-board, 10-25 are on the MXP
    */
-  public void setUpSource(int channel) {
+  public final void setUpSource(int channel) {
     setUpSource(new DigitalInput(channel));
     m_allocatedUpSource = true;
     SendableRegistry.addChild(this, m_upSource);
@@ -401,7 +417,7 @@
    * @param maxPeriod The maximum period where the counted device is considered moving in seconds.
    */
   @Override
-  public void setMaxPeriod(double maxPeriod) {
+  public final void setMaxPeriod(double maxPeriod) {
     CounterJNI.setCounterMaxPeriod(m_counter, maxPeriod);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java
index 60d5690..6e03b72 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java
@@ -22,6 +22,7 @@
     /** Count rising and falling on both channels. */
     k4X(2);
 
+    /** EncodingType value. */
     public final int value;
 
     EncodingType(int value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java
index a919da5..bd26a7b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java
@@ -7,9 +7,11 @@
 import edu.wpi.first.hal.DMAJNI;
 import edu.wpi.first.wpilibj.motorcontrol.PWMMotorController;
 
+/** Class for configuring Direct Memory Access (DMA) of FPGA inputs. */
 public class DMA implements AutoCloseable {
   final int m_dmaHandle;
 
+  /** Default constructor. */
   public DMA() {
     m_dmaHandle = DMAJNI.initialize();
   }
@@ -19,50 +21,128 @@
     DMAJNI.free(m_dmaHandle);
   }
 
+  /**
+   * Sets whether DMA is paused.
+   *
+   * @param pause True pauses DMA.
+   */
   public void setPause(boolean pause) {
     DMAJNI.setPause(m_dmaHandle, pause);
   }
 
+  /**
+   * Sets DMA to trigger at an interval.
+   *
+   * @param periodSeconds Period at which to trigger DMA in seconds.
+   */
   public void setTimedTrigger(double periodSeconds) {
     DMAJNI.setTimedTrigger(m_dmaHandle, periodSeconds);
   }
 
+  /**
+   * Sets number of DMA cycles to trigger.
+   *
+   * @param cycles Number of cycles.
+   */
   public void setTimedTriggerCycles(int cycles) {
     DMAJNI.setTimedTriggerCycles(m_dmaHandle, cycles);
   }
 
+  /**
+   * Adds position data for an encoder to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param encoder Encoder to add to DMA.
+   */
   public void addEncoder(Encoder encoder) {
     DMAJNI.addEncoder(m_dmaHandle, encoder.m_encoder);
   }
 
+  /**
+   * Adds timer data for an encoder to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param encoder Encoder to add to DMA.
+   */
   public void addEncoderPeriod(Encoder encoder) {
     DMAJNI.addEncoderPeriod(m_dmaHandle, encoder.m_encoder);
   }
 
+  /**
+   * Adds position data for an counter to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param counter Counter to add to DMA.
+   */
   public void addCounter(Counter counter) {
     DMAJNI.addCounter(m_dmaHandle, counter.m_counter);
   }
 
+  /**
+   * Adds timer data for an counter to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param counter Counter to add to DMA.
+   */
   public void addCounterPeriod(Counter counter) {
     DMAJNI.addCounterPeriod(m_dmaHandle, counter.m_counter);
   }
 
+  /**
+   * Adds a digital source to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param digitalSource DigitalSource to add to DMA.
+   */
   public void addDigitalSource(DigitalSource digitalSource) {
     DMAJNI.addDigitalSource(m_dmaHandle, digitalSource.getPortHandleForRouting());
   }
 
+  /**
+   * Adds a duty cycle input to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param dutyCycle DutyCycle to add to DMA.
+   */
   public void addDutyCycle(DutyCycle dutyCycle) {
     DMAJNI.addDutyCycle(m_dmaHandle, dutyCycle.m_handle);
   }
 
+  /**
+   * Adds an analog input to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param analogInput AnalogInput to add to DMA.
+   */
   public void addAnalogInput(AnalogInput analogInput) {
     DMAJNI.addAnalogInput(m_dmaHandle, analogInput.m_port);
   }
 
+  /**
+   * Adds averaged data of an analog input to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param analogInput AnalogInput to add to DMA.
+   */
   public void addAveragedAnalogInput(AnalogInput analogInput) {
     DMAJNI.addAveragedAnalogInput(m_dmaHandle, analogInput.m_port);
   }
 
+  /**
+   * Adds accumulator data of an analog input to be collected by DMA.
+   *
+   * <p>This can only be called if DMA is not started.
+   *
+   * @param analogInput AnalogInput to add to DMA.
+   */
   public void addAnalogAccumulator(AnalogInput analogInput) {
     DMAJNI.addAnalogAccumulator(m_dmaHandle, analogInput.m_port);
   }
@@ -84,26 +164,58 @@
         falling);
   }
 
-  public int setPwmEdgeTrigger(PWMMotorController pwm, boolean rising, boolean falling) {
-    return DMAJNI.setExternalTrigger(m_dmaHandle, pwm.getPwmHandle(), 0, rising, falling);
-  }
-
+  /**
+   * Sets a DMA PWM edge trigger.
+   *
+   * @param pwm the PWM to trigger from.
+   * @param rising trigger on rising edge.
+   * @param falling trigger on falling edge.
+   * @return the index of the trigger
+   */
   public int setPwmEdgeTrigger(PWM pwm, boolean rising, boolean falling) {
     return DMAJNI.setExternalTrigger(m_dmaHandle, pwm.getHandle(), 0, rising, falling);
   }
 
+  /**
+   * Sets a DMA PWMMotorController edge trigger.
+   *
+   * @param pwm the PWMMotorController to trigger from.
+   * @param rising trigger on rising edge.
+   * @param falling trigger on falling edge.
+   * @return the index of the trigger
+   */
+  public int setPwmEdgeTrigger(PWMMotorController pwm, boolean rising, boolean falling) {
+    return DMAJNI.setExternalTrigger(m_dmaHandle, pwm.getPwmHandle(), 0, rising, falling);
+  }
+
+  /**
+   * Clear all sensors from the DMA collection list.
+   *
+   * <p>This can only be called if DMA is not started.
+   */
   public void clearSensors() {
     DMAJNI.clearSensors(m_dmaHandle);
   }
 
+  /**
+   * Clear all external triggers from the DMA trigger list.
+   *
+   * <p>This can only be called if DMA is not started.
+   */
   public void clearExternalTriggers() {
     DMAJNI.clearExternalTriggers(m_dmaHandle);
   }
 
+  /**
+   * Starts DMA Collection.
+   *
+   * @param queueDepth The number of objects to be able to queue.
+   */
   public void start(int queueDepth) {
     DMAJNI.startDMA(m_dmaHandle, queueDepth);
   }
 
+  /** Stops DMA Collection. */
   public void stop() {
     DMAJNI.stopDMA(m_dmaHandle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMASample.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMASample.java
index 1055dbc..1435cd3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMASample.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMASample.java
@@ -7,10 +7,15 @@
 import edu.wpi.first.hal.AnalogJNI;
 import edu.wpi.first.hal.DMAJNISample;
 
+/** DMA sample. */
 public class DMASample {
+  /** DMA read status. */
   public enum DMAReadStatus {
+    /** OK status. */
     kOk(1),
+    /** Timeout status. */
     kTimeout(2),
+    /** Error status. */
     kError(3);
 
     private final int value;
@@ -19,6 +24,11 @@
       this.value = value;
     }
 
+    /**
+     * Returns the DMAReadStatus value.
+     *
+     * @return The DMAReadStatus value.
+     */
     public int getValue() {
       return value;
     }
@@ -41,39 +51,80 @@
 
   private final DMAJNISample m_dmaSample = new DMAJNISample();
 
+  /** Default constructor. */
+  public DMASample() {}
+
+  /**
+   * Retrieves a new DMA sample.
+   *
+   * @param dma DMA object.
+   * @param timeoutSeconds Timeout in seconds for retrieval.
+   * @return DMA read status.
+   */
   public DMAReadStatus update(DMA dma, double timeoutSeconds) {
     return DMAReadStatus.getValue(m_dmaSample.update(dma.m_dmaHandle, timeoutSeconds));
   }
 
-  public int getCaptureSize() {
-    return m_dmaSample.getCaptureSize();
-  }
-
-  public int getTriggerChannels() {
-    return m_dmaSample.getTriggerChannels();
-  }
-
-  public int getRemaining() {
-    return m_dmaSample.getRemaining();
-  }
-
+  /**
+   * Returns the DMA sample time in microseconds.
+   *
+   * @return The DMA sample time in microseconds.
+   */
   public long getTime() {
     return m_dmaSample.getTime();
   }
 
+  /**
+   * Returns the DMA sample timestamp in seconds.
+   *
+   * @return The DMA sample timestamp in seconds.
+   */
   public double getTimeStamp() {
     return getTime() * 1.0e-6;
   }
 
+  /**
+   * Returns the DMA sample capture size.
+   *
+   * @return The DMA sample capture size.
+   */
+  public int getCaptureSize() {
+    return m_dmaSample.getCaptureSize();
+  }
+
+  /**
+   * Returns the number of DMA trigger channels.
+   *
+   * @return The number of DMA trigger channels.
+   */
+  public int getTriggerChannels() {
+    return m_dmaSample.getTriggerChannels();
+  }
+
+  /**
+   * Returns the number of remaining samples.
+   *
+   * @return The number of remaining samples.
+   */
+  public int getRemaining() {
+    return m_dmaSample.getRemaining();
+  }
+
+  /**
+   * Returns raw encoder value from DMA.
+   *
+   * @param encoder Encoder used for DMA.
+   * @return Raw encoder value from DMA.
+   */
   public int getEncoderRaw(Encoder encoder) {
     return m_dmaSample.getEncoder(encoder.m_encoder);
   }
 
   /**
-   * Gets the scaled encoder distance for this sample.
+   * Returns encoder distance from DMA.
    *
-   * @param encoder the encoder to use to read
-   * @return the distance
+   * @param encoder Encoder used for DMA.
+   * @return Encoder distance from DMA.
    */
   public double getEncoderDistance(Encoder encoder) {
     double val = getEncoderRaw(encoder);
@@ -82,43 +133,103 @@
     return val;
   }
 
+  /**
+   * Returns raw encoder period from DMA.
+   *
+   * @param encoder Encoder used for DMA.
+   * @return Raw encoder period from DMA.
+   */
   public int getEncoderPeriodRaw(Encoder encoder) {
     return m_dmaSample.getEncoderPeriod(encoder.m_encoder);
   }
 
+  /**
+   * Returns counter value from DMA.
+   *
+   * @param counter Counter used for DMA.
+   * @return Counter value from DMA.
+   */
   public int getCounter(Counter counter) {
     return m_dmaSample.getCounter(counter.m_counter);
   }
 
+  /**
+   * Returns counter period from DMA.
+   *
+   * @param counter Counter used for DMA.
+   * @return Counter period from DMA.
+   */
   public int getCounterPeriod(Counter counter) {
     return m_dmaSample.getCounterPeriod(counter.m_counter);
   }
 
+  /**
+   * Returns digital source value from DMA.
+   *
+   * @param digitalSource DigitalSource used for DMA.
+   * @return DigitalSource value from DMA.
+   */
   public boolean getDigitalSource(DigitalSource digitalSource) {
     return m_dmaSample.getDigitalSource(digitalSource.getPortHandleForRouting());
   }
 
+  /**
+   * Returns raw analog input value from DMA.
+   *
+   * @param analogInput AnalogInput used for DMA.
+   * @return Raw analog input value from DMA.
+   */
   public int getAnalogInputRaw(AnalogInput analogInput) {
     return m_dmaSample.getAnalogInput(analogInput.m_port);
   }
 
+  /**
+   * Returns analog input voltage from DMA.
+   *
+   * @param analogInput AnalogInput used for DMA.
+   * @return Analog input voltage from DMA.
+   */
   public double getAnalogInputVoltage(AnalogInput analogInput) {
     return AnalogJNI.getAnalogValueToVolts(analogInput.m_port, getAnalogInputRaw(analogInput));
   }
 
+  /**
+   * Returns averaged raw analog input value from DMA.
+   *
+   * @param analogInput AnalogInput used for DMA.
+   * @return Averaged raw analog input value from DMA.
+   */
   public int getAveragedAnalogInputRaw(AnalogInput analogInput) {
     return m_dmaSample.getAnalogInputAveraged(analogInput.m_port);
   }
 
+  /**
+   * Returns averaged analog input voltage from DMA.
+   *
+   * @param analogInput AnalogInput used for DMA.
+   * @return Averaged analog input voltage from DMA.
+   */
   public double getAveragedAnalogInputVoltage(AnalogInput analogInput) {
     return AnalogJNI.getAnalogValueToVolts(
         analogInput.m_port, getAveragedAnalogInputRaw(analogInput));
   }
 
+  /**
+   * Returns raw duty cycle output from DMA.
+   *
+   * @param dutyCycle DutyCycle used for DMA.
+   * @return Raw duty cycle output from DMA.
+   */
   public int getDutyCycleOutputRaw(DutyCycle dutyCycle) {
     return m_dmaSample.getDutyCycleOutput(dutyCycle.m_handle);
   }
 
+  /**
+   * Returns duty cycle output (0-1) from DMA.
+   *
+   * @param dutyCycle DutyCycle used for DMA.
+   * @return Duty cycle output (0-1) from DMA.
+   */
   public double getDutyCycleOutput(DutyCycle dutyCycle) {
     return m_dmaSample.getDutyCycleOutput(dutyCycle.m_handle)
         / (double) dutyCycle.getOutputScaleFactor();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java
index 8837d41..a656d9c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java
@@ -20,7 +20,7 @@
   }
 
   /** Update internal Driver Station control word. */
-  public void refresh() {
+  public final void refresh() {
     DriverStation.refreshControlWordFromCache(m_controlWord);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DataLogManager.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DataLogManager.java
index 8098ae9..d4ec796 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DataLogManager.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DataLogManager.java
@@ -25,8 +25,8 @@
 /**
  * Centralized data log that provides automatic data log file management. It automatically cleans up
  * old files when disk space is low and renames the file based either on current date/time or (if
- * available) competition match number. The deta file will be saved to a USB flash drive if one is
- * attached, or to /home/lvuser otherwise.
+ * available) competition match number. The data file will be saved to a USB flash drive in a folder
+ * named "logs" if one is attached, or to /home/lvuser/logs otherwise.
  *
  * <p>Log files are initially named "FRC_TBD_{random}.wpilog" until the DS connects. After the DS
  * connects, the log file is renamed to "FRC_yyyyMMdd_HHmmss.wpilog" (where the date/time is UTC).
@@ -214,7 +214,10 @@
         // prefer a mounted USB drive if one is accessible
         Path usbDir = Paths.get("/u").toRealPath();
         if (Files.isWritable(usbDir)) {
-          return usbDir.toString();
+          if (!new File("/u/logs").mkdir()) {
+            // ignored
+          }
+          return "/u/logs";
         }
       } catch (IOException ex) {
         // ignored
@@ -225,8 +228,16 @@
                 + " Plug in a FAT32 formatted flash drive!",
             false);
       }
+      if (!new File("/home/lvuser/logs").mkdir()) {
+        // ignored
+      }
+      return "/home/lvuser/logs";
     }
-    return Filesystem.getOperatingDirectory().getAbsolutePath();
+    String logDir = Filesystem.getOperatingDirectory().getAbsolutePath() + "/logs";
+    if (!new File(logDir).mkdir()) {
+      // ignored
+    }
+    return logDir;
   }
 
   private static String makeLogFilename(String filenameOverride) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java
index 3422d34..2fcc9dd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java
@@ -20,6 +20,7 @@
  */
 public class DigitalGlitchFilter implements Sendable, AutoCloseable {
   /** Configures the Digital Glitch Filter to its default settings. */
+  @SuppressWarnings("this-escape")
   public DigitalGlitchFilter() {
     m_mutex.lock();
     try {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java
index 5b4cf5e..ab3d5e5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java
@@ -27,6 +27,7 @@
    *
    * @param channel the DIO channel for the digital input 0-9 are on-board, 10-25 are on the MXP
    */
+  @SuppressWarnings("this-escape")
   public DigitalInput(int channel) {
     SensorUtil.checkDigitalChannel(channel);
     m_channel = channel;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java
index 78e8bed..a7446f6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java
@@ -29,6 +29,7 @@
    * @param channel the DIO channel to use for the digital output. 0-9 are on-board, 10-25 are on
    *     the MXP
    */
+  @SuppressWarnings("this-escape")
   public DigitalOutput(int channel) {
     SensorUtil.checkDigitalChannel(channel);
     m_channel = channel;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java
index cb8b024..494efe9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java
@@ -11,8 +11,21 @@
  * source. The source can either be a digital input or analog trigger but not both.
  */
 public abstract class DigitalSource implements AutoCloseable {
+  /** Default constructor. */
+  public DigitalSource() {}
+
+  /**
+   * Returns true if this DigitalSource is an AnalogTrigger.
+   *
+   * @return True if this DigitalSource is an AnalogTrigger.
+   */
   public abstract boolean isAnalogTrigger();
 
+  /**
+   * The DigitalSource channel.
+   *
+   * @return The DigitalSource channel.
+   */
   public abstract int getChannel();
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java
index d749ccc..de0697c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java
@@ -21,8 +21,11 @@
 public class DoubleSolenoid implements Sendable, AutoCloseable {
   /** Possible values for a DoubleSolenoid. */
   public enum Value {
+    /** Off position. */
     kOff,
+    /** Forward position. */
     kForward,
+    /** Reverse position. */
     kReverse
   }
 
@@ -53,7 +56,7 @@
    * @param forwardChannel The forward channel on the module to control.
    * @param reverseChannel The reverse channel on the module to control.
    */
-  @SuppressWarnings("PMD.UseTryWithResources")
+  @SuppressWarnings({"PMD.UseTryWithResources", "this-escape"})
   public DoubleSolenoid(
       final int module,
       final PneumaticsModuleType moduleType,
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java
index ce3a3e0..4ee656b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java
@@ -29,7 +29,7 @@
 
 /** Provide access to the network communication data to / from the Driver Station. */
 public final class DriverStation {
-  /** Number of Joystick Ports. */
+  /** Number of Joystick ports. */
   public static final int kJoystickPorts = 6;
 
   private static class HALJoystickButtons {
@@ -69,14 +69,21 @@
 
   /** The robot alliance that the robot is a part of. */
   public enum Alliance {
+    /** Red alliance. */
     Red,
+    /** Blue alliance. */
     Blue
   }
 
+  /** The type of robot match that the robot is part of. */
   public enum MatchType {
+    /** None. */
     None,
+    /** Practice. */
     Practice,
+    /** Qualification. */
     Qualification,
+    /** Elimination. */
     Elimination
   }
 
@@ -239,6 +246,7 @@
         for (int i = 0; i < count; i++) {
           if (axes.m_axes[i] != m_prevAxes.m_axes[i]) {
             needToLog = true;
+            break;
           }
         }
       }
@@ -255,6 +263,7 @@
         for (int i = 0; i < count; i++) {
           if (povs.m_povs[i] != m_prevPOVs.m_povs[i]) {
             needToLog = true;
+            break;
           }
         }
       }
@@ -1113,7 +1122,7 @@
    *
    * <p>If the FMS is not connected, it is set from the team alliance setting on the driver station.
    *
-   * @return the current alliance
+   * @return The alliance (red or blue) or an empty optional if the alliance is invalid
    */
   public static Optional<Alliance> getAlliance() {
     AllianceStationID allianceStationID = DriverStationJNI.getAllianceStation();
@@ -1312,10 +1321,20 @@
     }
   }
 
+  /**
+   * Registers the given handle for DS data refresh notifications.
+   *
+   * @param handle The event handle.
+   */
   public static void provideRefreshedDataEventHandle(int handle) {
     m_refreshEvents.add(handle);
   }
 
+  /**
+   * Unregisters the given handle from DS data refresh notifications.
+   *
+   * @param handle The event handle.
+   */
   public static void removeRefreshedDataEventHandle(int handle) {
     m_refreshEvents.remove(handle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java
index d500ba7..8c26d55 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java
@@ -33,6 +33,7 @@
    *
    * @param digitalSource The DigitalSource to use.
    */
+  @SuppressWarnings("this-escape")
   public DutyCycle(DigitalSource digitalSource) {
     m_handle =
         DutyCycleJNI.initialize(
@@ -102,6 +103,11 @@
     return DutyCycleJNI.getFPGAIndex(m_handle);
   }
 
+  /**
+   * Get the channel of the source.
+   *
+   * @return the source channel
+   */
   public int getSourceChannel() {
     return m_source.getChannel();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java
index afdcb3f..a0858ce 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java
@@ -30,17 +30,18 @@
   private double m_sensorMin;
   private double m_sensorMax = 1.0;
 
-  protected SimDevice m_simDevice;
-  protected SimDouble m_simPosition;
-  protected SimDouble m_simAbsolutePosition;
-  protected SimDouble m_simDistancePerRotation;
-  protected SimBoolean m_simIsConnected;
+  private SimDevice m_simDevice;
+  private SimDouble m_simPosition;
+  private SimDouble m_simAbsolutePosition;
+  private SimDouble m_simDistancePerRotation;
+  private SimBoolean m_simIsConnected;
 
   /**
    * Construct a new DutyCycleEncoder on a specific channel.
    *
    * @param channel the channel to attach to
    */
+  @SuppressWarnings("this-escape")
   public DutyCycleEncoder(int channel) {
     m_digitalInput = new DigitalInput(channel);
     m_ownsDutyCycle = true;
@@ -53,6 +54,7 @@
    *
    * @param dutyCycle the duty cycle to attach to
    */
+  @SuppressWarnings("this-escape")
   public DutyCycleEncoder(DutyCycle dutyCycle) {
     m_dutyCycle = dutyCycle;
     init();
@@ -63,6 +65,7 @@
    *
    * @param source the digital source to attach to
    */
+  @SuppressWarnings("this-escape")
   public DutyCycleEncoder(DigitalSource source) {
     m_ownsDutyCycle = true;
     m_dutyCycle = new DutyCycle(source);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java
index 2cbb5c7..0aa667e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java
@@ -29,12 +29,18 @@
  * before use.
  */
 public class Encoder implements CounterBase, Sendable, AutoCloseable {
+  /** Encoder indexing types. */
   public enum IndexingType {
+    /** Reset while the signal is high. */
     kResetWhileHigh(0),
+    /** Reset while the signal is low. */
     kResetWhileLow(1),
+    /** Reset on falling edge of the signal. */
     kResetOnFallingEdge(2),
+    /** Reset on rising edge of the signal. */
     kResetOnRisingEdge(3);
 
+    /** IndexingType value. */
     public final int value;
 
     IndexingType(int value) {
@@ -122,6 +128,7 @@
    *     selected, then a counter object will be used and the returned value will either exactly
    *     match the spec'd count or be double (2x) the spec'd count.
    */
+  @SuppressWarnings("this-escape")
   public Encoder(
       final int channelA,
       final int channelB,
@@ -152,6 +159,7 @@
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
    *     if necessary so forward represents positive values.
    */
+  @SuppressWarnings("this-escape")
   public Encoder(
       final int channelA, final int channelB, final int indexChannel, boolean reverseDirection) {
     this(channelA, channelB, reverseDirection);
@@ -222,6 +230,7 @@
    *     selected then a counter object will be used and the returned value will either exactly
    *     match the spec'd count or be double (2x) the spec'd count.
    */
+  @SuppressWarnings("this-escape")
   public Encoder(
       DigitalSource sourceA,
       DigitalSource sourceB,
@@ -492,7 +501,7 @@
    *
    * @param channel A DIO channel to set as the encoder index
    */
-  public void setIndexSource(int channel) {
+  public final void setIndexSource(int channel) {
     setIndexSource(channel, IndexingType.kResetOnRisingEdge);
   }
 
@@ -502,7 +511,7 @@
    *
    * @param source A digital source to set as the encoder index
    */
-  public void setIndexSource(DigitalSource source) {
+  public final void setIndexSource(DigitalSource source) {
     setIndexSource(source, IndexingType.kResetOnRisingEdge);
   }
 
@@ -513,7 +522,7 @@
    * @param channel A DIO channel to set as the encoder index
    * @param type The state that will cause the encoder to reset
    */
-  public void setIndexSource(int channel, IndexingType type) {
+  public final void setIndexSource(int channel, IndexingType type) {
     if (m_allocatedI) {
       throw new AllocationException("Digital Input for Indexing already allocated");
     }
@@ -530,7 +539,7 @@
    * @param source A digital source to set as the encoder index
    * @param type The state that will cause the encoder to reset
    */
-  public void setIndexSource(DigitalSource source, IndexingType type) {
+  public final void setIndexSource(DigitalSource source, IndexingType type) {
     EncoderJNI.setEncoderIndexSource(
         m_encoder,
         source.getPortHandleForRouting(),
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java
index 58e55f0..e112e95 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java
@@ -19,32 +19,54 @@
  * the mapping of ports to hardware buttons depends on the code in the Driver Station.
  */
 public class GenericHID {
-  /** Represents a rumble output on the JoyStick. */
+  /** Represents a rumble output on the Joystick. */
   public enum RumbleType {
+    /** Left rumble motor. */
     kLeftRumble,
+    /** Right rumble motor. */
     kRightRumble,
+    /** Both left and right rumble motors. */
     kBothRumble
   }
 
+  /** USB HID interface type. */
   public enum HIDType {
+    /** Unknown. */
     kUnknown(-1),
+    /** XInputUnknown. */
     kXInputUnknown(0),
+    /** XInputGamepad. */
     kXInputGamepad(1),
+    /** XInputWheel. */
     kXInputWheel(2),
+    /** XInputArcadeStick. */
     kXInputArcadeStick(3),
+    /** XInputFlightStick. */
     kXInputFlightStick(4),
+    /** XInputDancePad. */
     kXInputDancePad(5),
+    /** XInputGuitar. */
     kXInputGuitar(6),
+    /** XInputGuitar2. */
     kXInputGuitar2(7),
+    /** XInputDrumKit. */
     kXInputDrumKit(8),
+    /** XInputGuitar3. */
     kXInputGuitar3(11),
+    /** XInputArcadePad. */
     kXInputArcadePad(19),
+    /** HIDJoystick. */
     kHIDJoystick(20),
+    /** HIDGamepad. */
     kHIDGamepad(21),
+    /** HIDDriving. */
     kHIDDriving(22),
+    /** HIDFlight. */
     kHIDFlight(23),
+    /** HID1stPerson. */
     kHID1stPerson(24);
 
+    /** HIDType value. */
     public final int value;
 
     @SuppressWarnings("PMD.UseConcurrentHashMap")
@@ -60,6 +82,12 @@
       }
     }
 
+    /**
+     * Creates an HIDType with the given value.
+     *
+     * @param value HIDType's value.
+     * @return HIDType with the given value.
+     */
     public static HIDType of(int value) {
       return map.get(value);
     }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java
index 1c37ecf..1ed131b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java
@@ -23,10 +23,14 @@
  * WPILib Known Issues</a> page for details.
  */
 public class I2C implements AutoCloseable {
+  /** I2C connection ports. */
   public enum Port {
+    /** Onboard I2C port. */
     kOnboard(0),
+    /** MXP (roboRIO MXP) I2C port. */
     kMXP(1);
 
+    /** Port value. */
     public final int value;
 
     Port(int value) {
@@ -58,10 +62,20 @@
     HAL.report(tResourceType.kResourceType_I2C, deviceAddress);
   }
 
+  /**
+   * Returns I2C port.
+   *
+   * @return I2C port.
+   */
   public int getPort() {
     return m_port;
   }
 
+  /**
+   * Returns I2C device address.
+   *
+   * @return I2C device address.
+   */
   public int getDeviceAddress() {
     return m_deviceAddress;
   }
@@ -369,7 +383,7 @@
 
     byte[] deviceData = new byte[4];
     for (int i = 0; i < count; i += 4) {
-      int toRead = count - i < 4 ? count - i : 4;
+      int toRead = Math.min(count - i, 4);
       // Read the chunk of data. Return false if the sensor does not
       // respond.
       dataToSend[0] = (byte) (registerAddress + i);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java
index 4afece4..1243393 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java
@@ -5,6 +5,8 @@
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.DriverStationJNI;
+import edu.wpi.first.hal.FRCNetComm.tInstances;
+import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.networktables.NetworkTableInstance;
 import edu.wpi.first.wpilibj.livewindow.LiveWindow;
@@ -70,7 +72,7 @@
   private final double m_period;
   private final Watchdog m_watchdog;
   private boolean m_ntFlushEnabled = true;
-  private boolean m_lwEnabledInTest = true;
+  private boolean m_lwEnabledInTest;
   private boolean m_calledDsConnected;
 
   /**
@@ -257,16 +259,22 @@
     m_ntFlushEnabled = enabled;
   }
 
+  private boolean m_reportedLw;
+
   /**
    * Sets whether LiveWindow operation is enabled during test mode. Calling
    *
-   * @param testLW True to enable, false to disable. Defaults to true.
+   * @param testLW True to enable, false to disable. Defaults to false.
    * @throws ConcurrentModificationException if this is called during test mode.
    */
   public void enableLiveWindowInTest(boolean testLW) {
     if (isTestEnabled()) {
       throw new ConcurrentModificationException("Can't configure test mode while in test mode!");
     }
+    if (!m_reportedLw && testLW) {
+      HAL.report(tResourceType.kResourceType_SmartDashboard, tInstances.kSmartDashboard_LiveWindow);
+      m_reportedLw = true;
+    }
     m_lwEnabledInTest = testLW;
   }
 
@@ -288,6 +296,7 @@
     return m_period;
   }
 
+  /** Loop function. */
   protected void loopFunc() {
     DriverStation.refreshData();
     m_watchdog.reset();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java
index d73b7bb..5536009 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java
@@ -17,20 +17,35 @@
  * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
  */
 public class Joystick extends GenericHID {
+  /** Default X axis channel. */
   public static final byte kDefaultXChannel = 0;
+
+  /** Default Y axis channel. */
   public static final byte kDefaultYChannel = 1;
+
+  /** Default Z axis channel. */
   public static final byte kDefaultZChannel = 2;
+
+  /** Default twist axis channel. */
   public static final byte kDefaultTwistChannel = 2;
+
+  /** Default throttle axis channel. */
   public static final byte kDefaultThrottleChannel = 3;
 
   /** Represents an analog axis on a joystick. */
   public enum AxisType {
+    /** X axis. */
     kX(0),
+    /** Y axis. */
     kY(1),
+    /** Z axis. */
     kZ(2),
+    /** Twist axis. */
     kTwist(3),
+    /** Throttle axis. */
     kThrottle(4);
 
+    /** AxisType value. */
     public final int value;
 
     AxisType(int value) {
@@ -40,9 +55,12 @@
 
   /** Represents a digital button on a joystick. */
   public enum ButtonType {
+    /** kTrigger. */
     kTrigger(1),
+    /** kTop. */
     kTop(2);
 
+    /** ButtonType value. */
     public final int value;
 
     ButtonType(int value) {
@@ -184,7 +202,7 @@
    *
    * @return the z position
    */
-  public double getZ() {
+  public final double getZ() {
     return getRawAxis(m_axes[AxisType.kZ.value]);
   }
 
@@ -194,7 +212,7 @@
    *
    * @return The Twist value of the joystick.
    */
-  public double getTwist() {
+  public final double getTwist() {
     return getRawAxis(m_axes[AxisType.kTwist.value]);
   }
 
@@ -204,7 +222,7 @@
    *
    * @return The Throttle value of the joystick.
    */
-  public double getThrottle() {
+  public final double getThrottle() {
     return getRawAxis(m_axes[AxisType.kThrottle.value]);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java
index c1bf04b..dc969a9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java
@@ -62,11 +62,12 @@
   }
 
   /** MotorSafety constructor. */
+  @SuppressWarnings("this-escape")
   public MotorSafety() {
     synchronized (m_listMutex) {
       m_instanceList.add(this);
       if (m_safetyThread == null) {
-        m_safetyThread = new Thread(() -> threadMain(), "MotorSafety Thread");
+        m_safetyThread = new Thread(MotorSafety::threadMain, "MotorSafety Thread");
         m_safetyThread.setDaemon(true);
         m_safetyThread.start();
       }
@@ -185,7 +186,13 @@
     }
   }
 
+  /** Called to stop the motor when the timeout expires. */
   public abstract void stopMotor();
 
+  /**
+   * Returns a description to print when an error occurs.
+   *
+   * @return Description to print when an error occurs.
+   */
   public abstract String getDescription();
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java
index 7ed6162..8f381a0 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java
@@ -113,7 +113,7 @@
                     updateAlarm();
                   } else {
                     // Need to update the alarm to cause it to wait again
-                    updateAlarm((long) -1);
+                    updateAlarm(-1);
                   }
                 } finally {
                   m_processLock.unlock();
@@ -134,7 +134,7 @@
             error = cause;
           }
           DriverStation.reportError(
-              "Unhandled exception in Notifier thread: " + error.toString(), error.getStackTrace());
+              "Unhandled exception in Notifier thread: " + error, error.getStackTrace());
           DriverStation.reportError(
               "The Runnable for this Notifier (or methods called by it) should have handled "
                   + "the exception above.\n"
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java
index 2359b7b..e000c6f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java
@@ -4,6 +4,8 @@
 
 package edu.wpi.first.wpilibj;
 
+import edu.wpi.first.hal.FRCNetComm.tResourceType;
+import edu.wpi.first.hal.HAL;
 import edu.wpi.first.wpilibj.event.BooleanEvent;
 import edu.wpi.first.wpilibj.event.EventLoop;
 
@@ -13,6 +15,10 @@
  * <p>This class handles PS4 input that comes from the Driver Station. Each time a value is
  * requested the most recent value is returned. There is a single class instance for each controller
  * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
+ *
+ * <p>Only first party controllers from Sony are guaranteed to have the correct mapping, and only
+ * through the official NI DS. Sim is not guaranteed to have the same mapping, as well as any 3rd
+ * party controllers.
  */
 public class PS4Controller extends GenericHID {
   /**
@@ -23,27 +29,41 @@
   public PS4Controller(int port) {
     super(port);
 
-    // re-enable when PS4Controller is added to Usage Reporting
-    // HAL.report(tResourceType.kResourceType_PS4Controller, port + 1); /
+    HAL.report(tResourceType.kResourceType_PS4Controller, port + 1);
   }
 
   /** Represents a digital button on a PS4Controller. */
   public enum Button {
+    /** Square button. */
     kSquare(1),
+    /** X button. */
     kCross(2),
+    /** Circle button. */
     kCircle(3),
+    /** Triangle button. */
     kTriangle(4),
+    /** Left Trigger 1 button. */
     kL1(5),
+    /** Right Trigger 1 button. */
     kR1(6),
+    /** Left Trigger 2 button. */
     kL2(7),
+    /** Right Trigger 2 button. */
     kR2(8),
+    /** Share button. */
     kShare(9),
+    /** Option button. */
     kOptions(10),
+    /** Left stick button. */
     kL3(11),
+    /** Right stick button. */
     kR3(12),
+    /** PlayStation button. */
     kPS(13),
+    /** Touchpad click button. */
     kTouchpad(14);
 
+    /** Button value. */
     public final int value;
 
     Button(int index) {
@@ -70,13 +90,20 @@
 
   /** Represents an axis on a PS4Controller. */
   public enum Axis {
+    /** Left X axis. */
     kLeftX(0),
+    /** Left Y axis. */
     kLeftY(1),
+    /** Right X axis. */
     kRightX(2),
+    /** Right Y axis. */
     kRightY(5),
+    /** Left Trigger 2. */
     kL2(3),
+    /** Right Trigger 2. */
     kR2(4);
 
+    /** Axis value. */
     public final int value;
 
     Axis(int index) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS5Controller.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS5Controller.java
index 8af98a5..18cb738 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS5Controller.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS5Controller.java
@@ -13,6 +13,10 @@
  * <p>This class handles PS5 input that comes from the Driver Station. Each time a value is
  * requested the most recent value is returned. There is a single class instance for each controller
  * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
+ *
+ * <p>Only first party controllers from Sony are guaranteed to have the correct mapping, and only
+ * through the official NI DS. Sim is not guaranteed to have the same mapping, as well as any 3rd
+ * party controllers.
  */
 public class PS5Controller extends GenericHID {
   /**
@@ -27,21 +31,36 @@
 
   /** Represents a digital button on a PS5Controller. */
   public enum Button {
-    kCross(1),
-    kCircle(2),
-    kSquare(3),
+    /** Square button. */
+    kSquare(1),
+    /** X button. */
+    kCross(2),
+    /** Circle button. */
+    kCircle(3),
+    /** Triangle button. */
     kTriangle(4),
+    /** Left trigger 1 button. */
     kL1(5),
+    /** Right trigger 1 button. */
     kR1(6),
+    /** Left trigger 2 button. */
     kL2(7),
+    /** Right trigger 2 button. */
     kR2(8),
+    /** Create button. */
     kCreate(9),
+    /** Options button. */
     kOptions(10),
-    kPS(11),
-    kL3(12),
-    kR3(13),
+    /** Left stick button. */
+    kL3(11),
+    /** Right stick button. */
+    kR3(12),
+    /** PlayStation button. */
+    kPS(13),
+    /** Touchpad click button. */
     kTouchpad(14);
 
+    /** Button value. */
     public final int value;
 
     Button(int index) {
@@ -68,13 +87,20 @@
 
   /** Represents an axis on a PS5Controller. */
   public enum Axis {
+    /** Left X axis. */
     kLeftX(0),
+    /** Left Y axis. */
     kLeftY(1),
-    kL2(2),
-    kRightX(3),
-    kRightY(4),
-    kR2(5);
+    /** Right X axis. */
+    kRightX(2),
+    /** Right Y axis. */
+    kRightY(5),
+    /** Left Trigger 2. */
+    kL2(3),
+    /** Right Trigger 2. */
+    kR2(4);
 
+    /** Axis value. */
     public final int value;
 
     Axis(int index) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java
index b0b3810..af600ec 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java
@@ -54,6 +54,7 @@
    * @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
    * @param registerSendable If true, adds this instance to SendableRegistry and LiveWindow
    */
+  @SuppressWarnings("this-escape")
   public PWM(final int channel, final boolean registerSendable) {
     SensorUtil.checkPWMChannel(channel);
     m_channel = channel;
@@ -200,7 +201,7 @@
   }
 
   /** Temporarily disables the PWM output. The next set call will re-enable the output. */
-  public void setDisabled() {
+  public final void setDisabled() {
     PWMJNI.setPWMDisabled(m_handle);
   }
 
@@ -228,6 +229,7 @@
     }
   }
 
+  /** Latches PWM to zero. */
   public void setZeroLatch() {
     PWMJNI.latchPWMZero(m_handle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java
index b7df47f..32d2ec5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java
@@ -59,8 +59,7 @@
             output.write(("currentVersion=" + fwVersion).getBytes(StandardCharsets.UTF_8));
           }
         } catch (IOException ex) {
-          DriverStation.reportError(
-              "Could not write " + fileName + ": " + ex.toString(), ex.getStackTrace());
+          DriverStation.reportError("Could not write " + fileName + ": " + ex, ex.getStackTrace());
         }
       }
 
@@ -115,14 +114,12 @@
 
   /** Converts volts to PSI per the REV Analog Pressure Sensor datasheet. */
   private static double voltsToPsi(double sensorVoltage, double supplyVoltage) {
-    double pressure = 250 * (sensorVoltage / supplyVoltage) - 25;
-    return pressure;
+    return 250 * (sensorVoltage / supplyVoltage) - 25;
   }
 
   /** Converts PSI to volts per the REV Analog Pressure Sensor datasheet. */
   private static double psiToVolts(double pressure, double supplyVoltage) {
-    double voltage = supplyVoltage * (0.004 * pressure + 0.1);
-    return voltage;
+    return supplyVoltage * (0.004 * pressure + 0.1);
   }
 
   private final DataStore m_dataStore;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java
index d9b493b..90f49f5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java
@@ -4,6 +4,7 @@
 
 package edu.wpi.first.wpilibj;
 
+/** Interface for pneumatics devices. */
 public interface PneumaticsBase extends AutoCloseable {
   /**
    * For internal use to get a module for a specific type.
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java
index aac16a3..4f1d70c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java
@@ -186,10 +186,23 @@
     return CTREPCMJNI.getSolenoidDisabledList(m_handle);
   }
 
+  /**
+   * Returns whether the solenoid is currently reporting a voltage fault.
+   *
+   * @return True if solenoid is reporting a fault, otherwise false.
+   * @see #getSolenoidVoltageStickyFault()
+   */
   public boolean getSolenoidVoltageFault() {
     return CTREPCMJNI.getSolenoidVoltageFault(m_handle);
   }
 
+  /**
+   * Returns whether the solenoid has reported a voltage fault since sticky faults were last
+   * cleared. This fault is persistent and can be cleared by ClearAllStickyFaults()
+   *
+   * @return True if solenoid is reporting a fault, otherwise false.
+   * @see #getSolenoidVoltageFault()
+   */
   public boolean getSolenoidVoltageStickyFault() {
     return CTREPCMJNI.getSolenoidVoltageStickyFault(m_handle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java
index a7951e8..e1525c3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java
@@ -4,7 +4,10 @@
 
 package edu.wpi.first.wpilibj;
 
+/** Pneumatics module type. */
 public enum PneumaticsModuleType {
+  /** CTRE PCM. */
   CTREPCM,
-  REVPH;
+  /** REV PH. */
+  REVPH
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistribution.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistribution.java
index a0c2104..57709b7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistribution.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistribution.java
@@ -22,12 +22,17 @@
   private final int m_handle;
   private final int m_module;
 
+  /** Default module number. */
   public static final int kDefaultModule = PowerDistributionJNI.DEFAULT_MODULE;
 
+  /** Power distribution module type. */
   public enum ModuleType {
+    /** CTRE (Cross The Road Electronics) CTRE Power Distribution Panel (PDP). */
     kCTRE(PowerDistributionJNI.CTRE_TYPE),
+    /** REV Power Distribution Hub (PDH). */
     kRev(PowerDistributionJNI.REV_TYPE);
 
+    /** ModuleType value. */
     public final int value;
 
     ModuleType(int value) {
@@ -41,6 +46,7 @@
    * @param module The CAN ID of the PDP/PDH.
    * @param moduleType Module type (CTRE or REV).
    */
+  @SuppressWarnings("this-escape")
   public PowerDistribution(int module, ModuleType moduleType) {
     m_handle = PowerDistributionJNI.initialize(module, moduleType.value);
     m_module = PowerDistributionJNI.getModuleNumber(m_handle);
@@ -54,6 +60,7 @@
    *
    * <p>Detects the connected PDP/PDH using the default CAN ID (0 for CTRE and 1 for REV).
    */
+  @SuppressWarnings("this-escape")
   public PowerDistribution() {
     m_handle = PowerDistributionJNI.initialize(kDefaultModule, PowerDistributionJNI.AUTOMATIC_TYPE);
     m_module = PowerDistributionJNI.getModuleNumber(m_handle);
@@ -101,9 +108,7 @@
    * @return The current of the channel in Amperes
    */
   public double getCurrent(int channel) {
-    double current = PowerDistributionJNI.getChannelCurrent(m_handle, channel);
-
-    return current;
+    return PowerDistributionJNI.getChannelCurrent(m_handle, channel);
   }
 
   /**
@@ -184,14 +189,29 @@
     PowerDistributionJNI.setSwitchableChannel(m_handle, enabled);
   }
 
+  /**
+   * Returns the power distribution version number.
+   *
+   * @return The power distribution version number.
+   */
   public PowerDistributionVersion getVersion() {
     return PowerDistributionJNI.getVersion(m_handle);
   }
 
+  /**
+   * Returns the power distribution faults.
+   *
+   * @return The power distribution faults.
+   */
   public PowerDistributionFaults getFaults() {
     return PowerDistributionJNI.getFaults(m_handle);
   }
 
+  /**
+   * Returns the power distribution sticky faults.
+   *
+   * @return The power distribution sticky faults.
+   */
   public PowerDistributionStickyFaults getStickyFaults() {
     return PowerDistributionJNI.getStickyFaults(m_handle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java
index 087c156..69d0fb3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java
@@ -230,7 +230,7 @@
    */
   public static void setLong(String key, long value) {
     NetworkTableEntry entry = m_table.getEntry(key);
-    entry.setDouble(value);
+    entry.setInteger(value);
     entry.setPersistent();
   }
 
@@ -242,7 +242,7 @@
    */
   public static void initLong(String key, long value) {
     NetworkTableEntry entry = m_table.getEntry(key);
-    entry.setDefaultDouble(value);
+    entry.setDefaultInteger(value);
     entry.setPersistent();
   }
 
@@ -345,6 +345,6 @@
    * @return either the value in the table, or the backup
    */
   public static long getLong(String key, long backup) {
-    return (long) m_table.getEntry(key).getDouble(backup);
+    return m_table.getEntry(key).getInteger(backup);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java
index a2a58d3..dce3096 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java
@@ -44,9 +44,13 @@
 
   /** The state to drive a Relay to. */
   public enum Value {
+    /** Off. */
     kOff("Off"),
+    /** On. */
     kOn("On"),
+    /** Forward. */
     kForward("Forward"),
+    /** Reverse. */
     kReverse("Reverse");
 
     private final String m_prettyValue;
@@ -55,10 +59,21 @@
       m_prettyValue = prettyValue;
     }
 
+    /**
+     * Returns the pretty string representation of the value.
+     *
+     * @return The pretty string representation of the value.
+     */
     public String getPrettyValue() {
       return m_prettyValue;
     }
 
+    /**
+     * Returns the value for a given pretty string.
+     *
+     * @param value The pretty string.
+     * @return The value or an empty optional if there is no corresponding value.
+     */
     public static Optional<Value> getValueOf(String value) {
       return Arrays.stream(Value.values()).filter(v -> v.m_prettyValue.equals(value)).findFirst();
     }
@@ -66,11 +81,11 @@
 
   /** The Direction(s) that a relay is configured to operate in. */
   public enum Direction {
-    /** direction: both directions are valid. */
+    /** Both directions are valid. */
     kBoth,
-    /** direction: Only forward is valid. */
+    /** Only forward is valid. */
     kForward,
-    /** direction: only reverse is valid. */
+    /** Only reverse is valid. */
     kReverse
   }
 
@@ -110,6 +125,7 @@
    * @param channel The channel number for this relay (0 - 3).
    * @param direction The direction that the Relay object will control.
    */
+  @SuppressWarnings("this-escape")
   public Relay(final int channel, Direction direction) {
     m_channel = channel;
     m_direction = requireNonNullParam(direction, "direction", "Relay");
@@ -279,6 +295,7 @@
    *
    * @param direction The direction for the relay to operate in
    */
+  @SuppressWarnings("this-escape")
   public void setDirection(Direction direction) {
     requireNonNullParam(direction, "direction", "setDirection");
     if (m_direction == direction) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java
index 2f59a3d..855b118 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java
@@ -178,6 +178,11 @@
     Shuffleboard.disableActuatorWidgets();
   }
 
+  /**
+   * Returns the main thread ID.
+   *
+   * @return The main thread ID.
+   */
   public static long getMainThreadId() {
     return m_threadId;
   }
@@ -321,8 +326,7 @@
         robotName = elements[0].getClassName();
       }
       DriverStation.reportError(
-          "Unhandled exception instantiating robot " + robotName + " " + throwable.toString(),
-          elements);
+          "Unhandled exception instantiating robot " + robotName + " " + throwable, elements);
       DriverStation.reportError(
           "The robot program quit unexpectedly."
               + " This is usually due to a code error.\n"
@@ -353,8 +357,7 @@
           output.write(WPILibVersion.Version.getBytes(StandardCharsets.UTF_8));
         }
       } catch (IOException ex) {
-        DriverStation.reportError(
-            "Could not write FRC_Lib_Version.ini: " + ex.toString(), ex.getStackTrace());
+        DriverStation.reportError("Could not write FRC_Lib_Version.ini: " + ex, ex.getStackTrace());
       }
     }
 
@@ -366,8 +369,7 @@
       if (cause != null) {
         throwable = cause;
       }
-      DriverStation.reportError(
-          "Unhandled exception: " + throwable.toString(), throwable.getStackTrace());
+      DriverStation.reportError("Unhandled exception: " + throwable, throwable.getStackTrace());
       errorOnExit = true;
     } finally {
       m_runMutex.lock();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java
index 29bcad8..d8faafa 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java
@@ -6,6 +6,7 @@
 
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.HALUtil;
+import edu.wpi.first.hal.LEDJNI;
 import edu.wpi.first.hal.PowerJNI;
 import edu.wpi.first.hal.can.CANJNI;
 import edu.wpi.first.hal.can.CANStatus;
@@ -33,7 +34,7 @@
    * @return FPGA Revision number.
    */
   public static long getFPGARevision() {
-    return (long) HALUtil.getFPGARevision();
+    return HALUtil.getFPGARevision();
   }
 
   /**
@@ -78,6 +79,10 @@
   /**
    * Get the state of the "USER" button on the roboRIO.
    *
+   * <p>Warning: the User Button is used to stop user programs from automatically loading if it is
+   * held for more then 5 seconds. Because of this, it's not recommended to be used by teams for any
+   * other purpose.
+   *
    * @return true if the button is currently pressed down
    */
   public static boolean getUserButton() {
@@ -315,6 +320,66 @@
     return PowerJNI.getCPUTemp();
   }
 
+  /** State for the radio led. */
+  public enum RadioLEDState {
+    /** Off. */
+    kOff(LEDJNI.RADIO_LED_STATE_OFF),
+    /** Green. */
+    kGreen(LEDJNI.RADIO_LED_STATE_GREEN),
+    /** Red. */
+    kRed(LEDJNI.RADIO_LED_STATE_RED),
+    /** Orange. */
+    kOrange(LEDJNI.RADIO_LED_STATE_ORANGE);
+
+    /** The native value for this state. */
+    public final int value;
+
+    RadioLEDState(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Gets a state from an int value.
+     *
+     * @param value int value
+     * @return state
+     */
+    public static RadioLEDState fromValue(int value) {
+      switch (value) {
+        case LEDJNI.RADIO_LED_STATE_OFF:
+          return RadioLEDState.kOff;
+        case LEDJNI.RADIO_LED_STATE_GREEN:
+          return RadioLEDState.kGreen;
+        case LEDJNI.RADIO_LED_STATE_RED:
+          return RadioLEDState.kRed;
+        case LEDJNI.RADIO_LED_STATE_ORANGE:
+          return RadioLEDState.kOrange;
+        default:
+          return RadioLEDState.kOff;
+      }
+    }
+  }
+
+  /**
+   * Set the state of the "Radio" LED. On the RoboRIO, this writes to sysfs, so this function should
+   * not be called multiple times per loop cycle to avoid overruns.
+   *
+   * @param state The state to set the LED to.
+   */
+  public static void setRadioLEDState(RadioLEDState state) {
+    LEDJNI.setRadioLEDState(state.value);
+  }
+
+  /**
+   * Get the state of the "Radio" LED. On the RoboRIO, this reads from sysfs, so this function
+   * should not be called multiple times per loop cycle to avoid overruns.
+   *
+   * @return The state of the LED.
+   */
+  public static RadioLEDState getRadioLEDState() {
+    return RadioLEDState.fromValue(LEDJNI.getRadioLEDState());
+  }
+
   /**
    * Get the current status of the CAN bus.
    *
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java
index 44198df..405dfc4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java
@@ -4,6 +4,7 @@
 
 package edu.wpi.first.wpilibj;
 
+/** Robot state utility functions. */
 public final class RobotState {
   /**
    * Returns true if the robot is disabled.
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java
index e7af118..cad1c51 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java
@@ -6,11 +6,16 @@
 
 import edu.wpi.first.hal.HALUtil;
 
+/** Runtime type. */
 public enum RuntimeType {
+  /** roboRIO 1.0. */
   kRoboRIO(HALUtil.RUNTIME_ROBORIO),
+  /** roboRIO 2.0. */
   kRoboRIO2(HALUtil.RUNTIME_ROBORIO2),
+  /** Simulation runtime. */
   kSimulation(HALUtil.RUNTIME_SIMULATION);
 
+  /** RuntimeType value. */
   public final int value;
 
   RuntimeType(int value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java
index 632770c..1a3bb34 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java
@@ -14,13 +14,20 @@
 
 /** Represents an SPI bus port. */
 public class SPI implements AutoCloseable {
+  /** SPI port. */
   public enum Port {
+    /** Onboard SPI bus port CS0. */
     kOnboardCS0(SPIJNI.ONBOARD_CS0_PORT),
+    /** Onboard SPI bus port CS1. */
     kOnboardCS1(SPIJNI.ONBOARD_CS1_PORT),
+    /** Onboard SPI bus port CS2. */
     kOnboardCS2(SPIJNI.ONBOARD_CS2_PORT),
+    /** Onboard SPI bus port CS3. */
     kOnboardCS3(SPIJNI.ONBOARD_CS3_PORT),
+    /** MXP (roboRIO MXP) SPI bus port. */
     kMXP(SPIJNI.MXP_PORT);
 
+    /** SPI port value. */
     public final int value;
 
     Port(int value) {
@@ -28,6 +35,7 @@
     }
   }
 
+  /** SPI mode. */
   public enum Mode {
     /** Clock idle low, data sampled on rising edge. */
     kMode0(SPIJNI.SPI_MODE0),
@@ -38,6 +46,7 @@
     /** Clock idle high, data sampled on rising edge. */
     kMode3(SPIJNI.SPI_MODE3);
 
+    /** SPI mode value. */
     public final int value;
 
     Mode(int value) {
@@ -64,6 +73,11 @@
     HAL.report(tResourceType.kResourceType_SPI, port.value + 1);
   }
 
+  /**
+   * Returns the SPI port value.
+   *
+   * @return SPI port value.
+   */
   public int getPort() {
     return m_port;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java
index bcedd81..258fbc7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java
@@ -47,8 +47,10 @@
   /** Number of PCM Modules. */
   public static final int kCTREPCMModules = PortsJNI.getNumCTREPCMModules();
 
+  /** Number of power distribution channels per PH. */
   public static final int kREVPHChannels = PortsJNI.getNumREVPHChannels();
 
+  /** Number of PH modules. */
   public static final int kREVPHModules = PortsJNI.getNumREVPHModules();
 
   /**
@@ -59,12 +61,12 @@
    */
   public static void checkDigitalChannel(final int channel) {
     if (!DIOJNI.checkDIOChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested DIO channel is out of range. Minimum: 0, Maximum: ")
-          .append(kDigitalChannels)
-          .append(", Requested: ")
-          .append(channel);
-      throw new IllegalArgumentException(buf.toString());
+      String buf =
+          "Requested DIO channel is out of range. Minimum: 0, Maximum: "
+              + kDigitalChannels
+              + ", Requested: "
+              + channel;
+      throw new IllegalArgumentException(buf);
     }
   }
 
@@ -76,12 +78,12 @@
    */
   public static void checkRelayChannel(final int channel) {
     if (!RelayJNI.checkRelayChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested relay channel is out of range. Minimum: 0, Maximum: ")
-          .append(kRelayChannels)
-          .append(", Requested: ")
-          .append(channel);
-      throw new IllegalArgumentException(buf.toString());
+      String buf =
+          "Requested relay channel is out of range. Minimum: 0, Maximum: "
+              + kRelayChannels
+              + ", Requested: "
+              + channel;
+      throw new IllegalArgumentException(buf);
     }
   }
 
@@ -93,12 +95,12 @@
    */
   public static void checkPWMChannel(final int channel) {
     if (!PWMJNI.checkPWMChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested PWM channel is out of range. Minimum: 0, Maximum: ")
-          .append(kPwmChannels)
-          .append(", Requested: ")
-          .append(channel);
-      throw new IllegalArgumentException(buf.toString());
+      String buf =
+          "Requested PWM channel is out of range. Minimum: 0, Maximum: "
+              + kPwmChannels
+              + ", Requested: "
+              + channel;
+      throw new IllegalArgumentException(buf);
     }
   }
 
@@ -110,12 +112,12 @@
    */
   public static void checkAnalogInputChannel(final int channel) {
     if (!AnalogJNI.checkAnalogInputChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested analog input channel is out of range. Minimum: 0, Maximum: ")
-          .append(kAnalogInputChannels)
-          .append(", Requested: ")
-          .append(channel);
-      throw new IllegalArgumentException(buf.toString());
+      String buf =
+          "Requested analog input channel is out of range. Minimum: 0, Maximum: "
+              + kAnalogInputChannels
+              + ", Requested: "
+              + channel;
+      throw new IllegalArgumentException(buf);
     }
   }
 
@@ -127,12 +129,12 @@
    */
   public static void checkAnalogOutputChannel(final int channel) {
     if (!AnalogJNI.checkAnalogOutputChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested analog output channel is out of range. Minimum: 0, Maximum: ")
-          .append(kAnalogOutputChannels)
-          .append(", Requested: ")
-          .append(channel);
-      throw new IllegalArgumentException(buf.toString());
+      String buf =
+          "Requested analog output channel is out of range. Minimum: 0, Maximum: "
+              + kAnalogOutputChannels
+              + ", Requested: "
+              + channel;
+      throw new IllegalArgumentException(buf);
     }
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java
index 1a05c8a..d978d78 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java
@@ -13,13 +13,20 @@
 public class SerialPort implements AutoCloseable {
   private int m_portHandle;
 
+  /** Serial port. */
   public enum Port {
+    /** Onboard serial port on the roboRIO. */
     kOnboard(0),
+    /** MXP (roboRIO MXP) serial port. */
     kMXP(1),
+    /** USB serial port (same as KUSB1). */
     kUSB(2),
+    /** USB serial port 1. */
     kUSB1(2),
+    /** USB serial port 2. */
     kUSB2(3);
 
+    /** Port value. */
     public final int value;
 
     Port(int value) {
@@ -29,12 +36,18 @@
 
   /** Represents the parity to use for serial communications. */
   public enum Parity {
+    /** No parity. */
     kNone(0),
+    /** Odd parity. */
     kOdd(1),
+    /** Even parity. */
     kEven(2),
+    /** Parity bit always on. */
     kMark(3),
+    /** Parity bit always off. */
     kSpace(4);
 
+    /** Parity value. */
     public final int value;
 
     Parity(int value) {
@@ -44,10 +57,14 @@
 
   /** Represents the number of stop bits to use for Serial Communication. */
   public enum StopBits {
+    /** One stop bit. */
     kOne(10),
+    /** One and a half stop bits. */
     kOnePointFive(15),
+    /** Two stop bits. */
     kTwo(20);
 
+    /** StopBits value. */
     public final int value;
 
     StopBits(int value) {
@@ -57,11 +74,16 @@
 
   /** Represents what type of flow control to use for serial communication. */
   public enum FlowControl {
+    /** No flow control. */
     kNone(0),
+    /** XON/XOFF flow control. */
     kXonXoff(1),
+    /** RTS/CTS flow control. */
     kRtsCts(2),
+    /** DTS/DSR flow control. */
     kDtsDsr(4);
 
+    /** FlowControl value. */
     public final int value;
 
     FlowControl(int value) {
@@ -71,9 +93,12 @@
 
   /** Represents which type of buffer mode to use when writing to a serial port. */
   public enum WriteBufferMode {
+    /** Flush the buffer on each access. */
     kFlushOnAccess(1),
+    /** Flush the buffer when it is full. */
     kFlushWhenFull(2);
 
+    /** WriteBufferMode value. */
     public final int value;
 
     WriteBufferMode(int value) {
@@ -186,7 +211,7 @@
   }
 
   /** Disable termination behavior. */
-  public void disableTermination() {
+  public final void disableTermination() {
     SerialPortJNI.serialDisableTermination(m_portHandle);
   }
 
@@ -216,7 +241,7 @@
    */
   public String readString(int count) {
     byte[] out = read(count);
-    return new String(out, 0, out.length, StandardCharsets.US_ASCII);
+    return new String(out, StandardCharsets.US_ASCII);
   }
 
   /**
@@ -268,7 +293,7 @@
    *
    * @param timeout The number of seconds to wait for I/O.
    */
-  public void setTimeout(double timeout) {
+  public final void setTimeout(double timeout) {
     SerialPortJNI.serialSetTimeout(m_portHandle, timeout);
   }
 
@@ -283,7 +308,7 @@
    *
    * @param size The read buffer size.
    */
-  public void setReadBufferSize(int size) {
+  public final void setReadBufferSize(int size) {
     SerialPortJNI.serialSetReadBufferSize(m_portHandle, size);
   }
 
@@ -309,7 +334,7 @@
    *
    * @param mode The write buffer mode.
    */
-  public void setWriteBufferMode(WriteBufferMode mode) {
+  public final void setWriteBufferMode(WriteBufferMode mode) {
     SerialPortJNI.serialSetWriteMode(m_portHandle, (byte) mode.value);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java
index 1f1c747..ebd2a8b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java
@@ -19,18 +19,19 @@
   private static final double kMaxServoAngle = 180.0;
   private static final double kMinServoAngle = 0.0;
 
-  protected static final int kDefaultMaxServoPWM = 2400;
-  protected static final int kDefaultMinServoPWM = 600;
+  private static final int kDefaultMaxServoPWM = 2400;
+  private static final int kDefaultMinServoPWM = 600;
 
   /**
-   * Constructor.<br>
+   * Constructor.
    *
-   * <p>By default {@value #kDefaultMaxServoPWM} ms is used as the maxPWM value<br>
-   * By default {@value #kDefaultMinServoPWM} ms is used as the minPWM value<br>
+   * <p>By default, {@value #kDefaultMaxServoPWM} ms is used as the max PWM value and {@value
+   * #kDefaultMinServoPWM} ms is used as the minPWM value.
    *
    * @param channel The PWM channel to which the servo is attached. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public Servo(final int channel) {
     super(channel);
     setBoundsMicroseconds(kDefaultMaxServoPWM, 0, 0, 0, kDefaultMinServoPWM);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java
index be87444..9c516da 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java
@@ -39,6 +39,7 @@
    * @param moduleType The module type to use.
    * @param channel The channel the solenoid is on.
    */
+  @SuppressWarnings("this-escape")
   public Solenoid(final int module, final PneumaticsModuleType moduleType, final int channel) {
     m_module = PneumaticsBase.getForType(module, moduleType);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/StadiaController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/StadiaController.java
new file mode 100644
index 0000000..d092d8b
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/StadiaController.java
@@ -0,0 +1,737 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+// import edu.wpi.first.hal.FRCNetComm.tResourceType;
+// import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.event.BooleanEvent;
+import edu.wpi.first.wpilibj.event.EventLoop;
+
+/**
+ * Handle input from Stadia controllers connected to the Driver Station.
+ *
+ * <p>This class handles Stadia input that comes from the Driver Station. Each time a value is
+ * requested the most recent value is returned. There is a single class instance for each controller
+ * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
+ */
+public class StadiaController extends GenericHID {
+  /** Represents a digital button on a StadiaController. */
+  public enum Button {
+    /** A button. */
+    kA(1),
+    /** B button. */
+    kB(2),
+    /** X Button. */
+    kX(3),
+    /** Y Button. */
+    kY(4),
+    /** Left bumper button. */
+    kLeftBumper(5),
+    /** Right bumper button. */
+    kRightBumper(6),
+    /** Left stick button. */
+    kLeftStick(7),
+    /** Right stick button. */
+    kRightStick(8),
+    /** Ellipses button. */
+    kEllipses(9),
+    /** Hamburger button. */
+    kHamburger(10),
+    /** Stadia button. */
+    kStadia(11),
+    /** Right trigger button. */
+    kRightTrigger(12),
+    /** Left trigger button. */
+    kLeftTrigger(13),
+    /** Google button. */
+    kGoogle(14),
+    /** Frame button. */
+    kFrame(15);
+
+    /** Button value. */
+    public final int value;
+
+    Button(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Get the human-friendly name of the button, matching the relevant methods. This is done by
+     * stripping the leading `k`, and if not a Bumper button append `Button`.
+     *
+     * <p>Primarily used for automated unit tests.
+     *
+     * @return the human-friendly name of the button.
+     */
+    @Override
+    public String toString() {
+      var name = this.name().substring(1); // Remove leading `k`
+      if (name.endsWith("Bumper")) {
+        return name;
+      }
+      return name + "Button";
+    }
+  }
+
+  /** Represents an axis on a StadiaController. */
+  public enum Axis {
+    /** Left X axis. */
+    kLeftX(0),
+    /** Right X axis. */
+    kRightX(3),
+    /** Left Y axis. */
+    kLeftY(1),
+    /** Right Y axis. */
+    kRightY(4);
+
+    /** Axis value. */
+    public final int value;
+
+    Axis(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Get the human-friendly name of the axis, matching the relevant methods. This is done by
+     * stripping the leading `k`, and if a trigger axis append `Axis`.
+     *
+     * <p>Primarily used for automated unit tests.
+     *
+     * @return the human-friendly name of the axis.
+     */
+    @Override
+    public String toString() {
+      var name = this.name().substring(1); // Remove leading `k`
+      if (name.endsWith("Trigger")) {
+        return name + "Axis";
+      }
+      return name;
+    }
+  }
+
+  /**
+   * Construct an instance of a controller.
+   *
+   * @param port The port index on the Driver Station that the controller is plugged into.
+   */
+  public StadiaController(final int port) {
+    super(port);
+    // re-enable when StadiaController is added to Usage Reporting
+    // HAL.report(tResourceType.kResourceType_Joystick, port + 1);
+  }
+
+  /**
+   * Get the X axis value of left side of the controller.
+   *
+   * @return The axis value.
+   */
+  public double getLeftX() {
+    return getRawAxis(Axis.kLeftX.value);
+  }
+
+  /**
+   * Get the X axis value of right side of the controller.
+   *
+   * @return The axis value.
+   */
+  public double getRightX() {
+    return getRawAxis(Axis.kRightX.value);
+  }
+
+  /**
+   * Get the Y axis value of left side of the controller.
+   *
+   * @return The axis value.
+   */
+  public double getLeftY() {
+    return getRawAxis(Axis.kLeftY.value);
+  }
+
+  /**
+   * Get the Y axis value of right side of the controller.
+   *
+   * @return The axis value.
+   */
+  public double getRightY() {
+    return getRawAxis(Axis.kRightY.value);
+  }
+
+  /**
+   * Read the value of the left bumper (LB) button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getLeftBumper() {
+    return getRawButton(Button.kLeftBumper.value);
+  }
+
+  /**
+   * Read the value of the right bumper (RB) button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getRightBumper() {
+    return getRawButton(Button.kRightBumper.value);
+  }
+
+  /**
+   * Whether the left bumper (LB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getLeftBumperPressed() {
+    return getRawButtonPressed(Button.kLeftBumper.value);
+  }
+
+  /**
+   * Whether the right bumper (RB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getRightBumperPressed() {
+    return getRawButtonPressed(Button.kRightBumper.value);
+  }
+
+  /**
+   * Whether the left bumper (LB) was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getLeftBumperReleased() {
+    return getRawButtonReleased(Button.kLeftBumper.value);
+  }
+
+  /**
+   * Whether the right bumper (RB) was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getRightBumperReleased() {
+    return getRawButtonReleased(Button.kRightBumper.value);
+  }
+
+  /**
+   * Constructs an event instance around the right bumper's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the right bumper's digital signal attached to the given
+   *     loop.
+   */
+  public BooleanEvent leftBumper(EventLoop loop) {
+    return new BooleanEvent(loop, this::getLeftBumper);
+  }
+
+  /**
+   * Constructs an event instance around the left bumper's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the left bumper's digital signal attached to the given
+   *     loop.
+   */
+  public BooleanEvent rightBumper(EventLoop loop) {
+    return new BooleanEvent(loop, this::getRightBumper);
+  }
+
+  /**
+   * Read the value of the left stick button (LSB) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getLeftStickButton() {
+    return getRawButton(Button.kLeftStick.value);
+  }
+
+  /**
+   * Read the value of the right stick button (RSB) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getRightStickButton() {
+    return getRawButton(Button.kRightStick.value);
+  }
+
+  /**
+   * Whether the left stick button (LSB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getLeftStickButtonPressed() {
+    return getRawButtonPressed(Button.kLeftStick.value);
+  }
+
+  /**
+   * Whether the right stick button (RSB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getRightStickButtonPressed() {
+    return getRawButtonPressed(Button.kRightStick.value);
+  }
+
+  /**
+   * Whether the left stick button (LSB) was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getLeftStickButtonReleased() {
+    return getRawButtonReleased(Button.kLeftStick.value);
+  }
+
+  /**
+   * Whether the right stick (RSB) button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getRightStickButtonReleased() {
+    return getRawButtonReleased(Button.kRightStick.value);
+  }
+
+  /**
+   * Constructs an event instance around the left stick button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the left stick button's digital signal attached to the
+   *     given loop.
+   */
+  public BooleanEvent leftStick(EventLoop loop) {
+    return new BooleanEvent(loop, this::getLeftStickButton);
+  }
+
+  /**
+   * Constructs an event instance around the right stick button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the right stick button's digital signal attached to the
+   *     given loop.
+   */
+  public BooleanEvent rightStick(EventLoop loop) {
+    return new BooleanEvent(loop, this::getRightStickButton);
+  }
+
+  /**
+   * Read the value of the left trigger button (LTB) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getLeftTriggerButton() {
+    return getRawButton(Button.kLeftTrigger.value);
+  }
+
+  /**
+   * Read the value of the right trigger button (RTB) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getRightTriggerButton() {
+    return getRawButton(Button.kRightTrigger.value);
+  }
+
+  /**
+   * Whether the left trigger button (LTB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getLeftTriggerButtonPressed() {
+    return getRawButtonPressed(Button.kLeftTrigger.value);
+  }
+
+  /**
+   * Whether the right trigger button (RTB) was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getRightTriggerButtonPressed() {
+    return getRawButtonPressed(Button.kRightTrigger.value);
+  }
+
+  /**
+   * Whether the left trigger button (LTB) was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getLeftTriggerButtonReleased() {
+    return getRawButtonReleased(Button.kLeftTrigger.value);
+  }
+
+  /**
+   * Whether the right trigger (RTB) button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getRightTriggerButtonReleased() {
+    return getRawButtonReleased(Button.kRightTrigger.value);
+  }
+
+  /**
+   * Constructs an event instance around the left trigger button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the left trigger button's digital signal attached to the
+   *     given loop.
+   */
+  public BooleanEvent leftTrigger(EventLoop loop) {
+    return new BooleanEvent(loop, this::getLeftTriggerButton);
+  }
+
+  /**
+   * Constructs an event instance around the right trigger button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the right trigger button's digital signal attached to
+   *     the given loop.
+   */
+  public BooleanEvent rightTrigger(EventLoop loop) {
+    return new BooleanEvent(loop, this::getRightTriggerButton);
+  }
+
+  /**
+   * Read the value of the A button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getAButton() {
+    return getRawButton(Button.kA.value);
+  }
+
+  /**
+   * Whether the A button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getAButtonPressed() {
+    return getRawButtonPressed(Button.kA.value);
+  }
+
+  /**
+   * Whether the A button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getAButtonReleased() {
+    return getRawButtonReleased(Button.kA.value);
+  }
+
+  /**
+   * Constructs an event instance around the A button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the A button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent a(EventLoop loop) {
+    return new BooleanEvent(loop, this::getAButton);
+  }
+
+  /**
+   * Read the value of the B button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getBButton() {
+    return getRawButton(Button.kB.value);
+  }
+
+  /**
+   * Whether the B button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getBButtonPressed() {
+    return getRawButtonPressed(Button.kB.value);
+  }
+
+  /**
+   * Whether the B button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getBButtonReleased() {
+    return getRawButtonReleased(Button.kB.value);
+  }
+
+  /**
+   * Constructs an event instance around the B button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the B button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent b(EventLoop loop) {
+    return new BooleanEvent(loop, this::getBButton);
+  }
+
+  /**
+   * Read the value of the X button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getXButton() {
+    return getRawButton(Button.kX.value);
+  }
+
+  /**
+   * Whether the X button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getXButtonPressed() {
+    return getRawButtonPressed(Button.kX.value);
+  }
+
+  /**
+   * Whether the X button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getXButtonReleased() {
+    return getRawButtonReleased(Button.kX.value);
+  }
+
+  /**
+   * Constructs an event instance around the X button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the X button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent x(EventLoop loop) {
+    return new BooleanEvent(loop, this::getXButton);
+  }
+
+  /**
+   * Read the value of the Y button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getYButton() {
+    return getRawButton(Button.kY.value);
+  }
+
+  /**
+   * Whether the Y button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getYButtonPressed() {
+    return getRawButtonPressed(Button.kY.value);
+  }
+
+  /**
+   * Whether the Y button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getYButtonReleased() {
+    return getRawButtonReleased(Button.kY.value);
+  }
+
+  /**
+   * Constructs an event instance around the Y button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the Y button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent y(EventLoop loop) {
+    return new BooleanEvent(loop, this::getYButton);
+  }
+
+  /**
+   * Read the value of the ellipses button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getEllipsesButton() {
+    return getRawButton(Button.kEllipses.value);
+  }
+
+  /**
+   * Whether the ellipses button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getEllipsesButtonPressed() {
+    return getRawButtonPressed(Button.kEllipses.value);
+  }
+
+  /**
+   * Whether the ellipses button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getEllipsesButtonReleased() {
+    return getRawButtonReleased(Button.kEllipses.value);
+  }
+
+  /**
+   * Constructs an event instance around the ellipses button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the ellipses button's digital signal attached to the
+   *     given loop.
+   */
+  public BooleanEvent ellipses(EventLoop loop) {
+    return new BooleanEvent(loop, this::getEllipsesButton);
+  }
+
+  /**
+   * Read the value of the hamburger button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getHamburgerButton() {
+    return getRawButton(Button.kHamburger.value);
+  }
+
+  /**
+   * Whether the hamburger button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getHamburgerButtonPressed() {
+    return getRawButtonPressed(Button.kHamburger.value);
+  }
+
+  /**
+   * Whether the hamburger button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getHamburgerButtonReleased() {
+    return getRawButtonReleased(Button.kHamburger.value);
+  }
+
+  /**
+   * Constructs an event instance around the hamburger button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the hamburger button's digital signal attached to the
+   *     given loop.
+   */
+  public BooleanEvent hamburger(EventLoop loop) {
+    return new BooleanEvent(loop, this::getHamburgerButton);
+  }
+
+  /**
+   * Read the value of the stadia button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getStadiaButton() {
+    return getRawButton(Button.kStadia.value);
+  }
+
+  /**
+   * Whether the stadia button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getStadiaButtonPressed() {
+    return getRawButtonPressed(Button.kStadia.value);
+  }
+
+  /**
+   * Whether the stadia button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getStadiaButtonReleased() {
+    return getRawButtonReleased(Button.kStadia.value);
+  }
+
+  /**
+   * Constructs an event instance around the stadia button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the stadia button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent stadia(EventLoop loop) {
+    return new BooleanEvent(loop, this::getStadiaButton);
+  }
+
+  /**
+   * Read the value of the google button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getGoogleButton() {
+    return getRawButton(Button.kGoogle.value);
+  }
+
+  /**
+   * Whether the google button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getGoogleButtonPressed() {
+    return getRawButtonPressed(Button.kGoogle.value);
+  }
+
+  /**
+   * Whether the google button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getGoogleButtonReleased() {
+    return getRawButtonReleased(Button.kGoogle.value);
+  }
+
+  /**
+   * Constructs an event instance around the google button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the google button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent google(EventLoop loop) {
+    return new BooleanEvent(loop, this::getGoogleButton);
+  }
+
+  /**
+   * Read the value of the frame button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getFrameButton() {
+    return getRawButton(Button.kFrame.value);
+  }
+
+  /**
+   * Whether the frame button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getFrameButtonPressed() {
+    return getRawButtonPressed(Button.kFrame.value);
+  }
+
+  /**
+   * Whether the frame button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getFrameButtonReleased() {
+    return getRawButtonReleased(Button.kFrame.value);
+  }
+
+  /**
+   * Constructs an event instance around the frame button's digital signal.
+   *
+   * @param loop the event loop instance to attach the event to.
+   * @return an event instance representing the frame button's digital signal attached to the given
+   *     loop.
+   */
+  @SuppressWarnings("MethodName")
+  public BooleanEvent frame(EventLoop loop) {
+    return new BooleanEvent(loop, this::getFrameButton);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java
index 705d6b1..8f78865 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java
@@ -23,11 +23,16 @@
 
   /** Event trigger combinations for a synchronous interrupt. */
   public enum WaitResult {
+    /** Timeout event. */
     kTimeout(0x0),
+    /** Rising edge event. */
     kRisingEdge(0x1),
+    /** Falling edge event. */
     kFallingEdge(0x100),
+    /** Both rising and falling edge events. */
     kBoth(0x101);
 
+    /** WaitResult value. */
     public final int value;
 
     WaitResult(int value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java
index 8b0d167..5ff0540 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java
@@ -6,6 +6,7 @@
 
 import edu.wpi.first.hal.ThreadsJNI;
 
+/** Thread utility functions. */
 public final class Threads {
   /**
    * Get the thread priority for the current thread.
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java
index 5a53b14..12346c4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java
@@ -65,6 +65,7 @@
     }
   }
 
+  /** Default loop period. */
   public static final double kDefaultPeriod = 0.02;
 
   // The C pointer to the notifier object. We don't use it directly, it is
@@ -159,7 +160,7 @@
    * @param callback The callback to run.
    * @param periodSeconds The period at which to run the callback in seconds.
    */
-  public void addPeriodic(Runnable callback, double periodSeconds) {
+  public final void addPeriodic(Runnable callback, double periodSeconds) {
     m_callbacks.add(new Callback(callback, m_startTime, periodSeconds, 0.0));
   }
 
@@ -174,7 +175,7 @@
    * @param offsetSeconds The offset from the common starting time in seconds. This is useful for
    *     scheduling a callback in a different timeslot relative to TimedRobot.
    */
-  public void addPeriodic(Runnable callback, double periodSeconds, double offsetSeconds) {
+  public final void addPeriodic(Runnable callback, double periodSeconds, double offsetSeconds) {
     m_callbacks.add(new Callback(callback, m_startTime, periodSeconds, offsetSeconds));
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java
index 4f948f5..63e5ed4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java
@@ -83,7 +83,7 @@
    *
    * <p>Make the timer startTime the current time so new requests will be relative now.
    */
-  public void reset() {
+  public final void reset() {
     m_accumulatedTime = 0;
     m_startTime = getMsClock();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java
index 2415079..15ca7ff 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java
@@ -36,7 +36,7 @@
   }
 
   /** Restarts the epoch timer. */
-  public void resetTimer() {
+  public final void resetTimer() {
     m_startTime = RobotController.getFPGATime();
   }
 
@@ -75,9 +75,7 @@
       StringBuilder sb = new StringBuilder();
       m_lastEpochsPrintTime = now;
       m_epochs.forEach(
-          (key, value) -> {
-            sb.append(String.format("\t%s: %.6fs\n", key, value / 1.0e6));
-          });
+          (key, value) -> sb.append(String.format("\t%s: %.6fs\n", key, value / 1.0e6)));
       if (sb.length() > 0) {
         output.accept(sb.toString());
       }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java
index fd2b05b..a7b5e71 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java
@@ -108,6 +108,11 @@
     SendableRegistry.addLW(this, "Ultrasonic", m_echoChannel.getChannel());
   }
 
+  /**
+   * Returns the echo channel.
+   *
+   * @return The echo channel.
+   */
   public int getEchoChannel() {
     return m_echoChannel.getChannel();
   }
@@ -121,6 +126,7 @@
    * @param echoChannel The digital input channel that receives the echo. The length of time that
    *     the echo is high represents the round trip time of the ping, and the distance.
    */
+  @SuppressWarnings("this-escape")
   public Ultrasonic(final int pingChannel, final int echoChannel) {
     m_pingChannel = new DigitalOutput(pingChannel);
     m_echoChannel = new DigitalInput(echoChannel);
@@ -138,6 +144,7 @@
    *     10uS pulse to start.
    * @param echoChannel The digital input object that times the return pulse to determine the range.
    */
+  @SuppressWarnings("this-escape")
   public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel) {
     requireNonNullParam(pingChannel, "pingChannel", "Ultrasonic");
     requireNonNullParam(echoChannel, "echoChannel", "Ultrasonic");
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java
index f7bf85b..18d4b64 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java
@@ -209,7 +209,7 @@
   }
 
   private static void updateAlarm() {
-    if (m_watchdogs.size() == 0) {
+    if (m_watchdogs.isEmpty()) {
       NotifierJNI.cancelNotifierAlarm(m_notifier);
     } else {
       NotifierJNI.updateNotifierAlarm(
@@ -233,7 +233,7 @@
 
       m_queueMutex.lock();
       try {
-        if (m_watchdogs.size() == 0) {
+        if (m_watchdogs.isEmpty()) {
           continue;
         }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java
index c346ae3..5bd7a01 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java
@@ -15,21 +15,36 @@
  * <p>This class handles Xbox input that comes from the Driver Station. Each time a value is
  * requested the most recent value is returned. There is a single class instance for each controller
  * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
+ *
+ * <p>Only first party controllers from Microsoft are guaranteed to have the correct mapping, and
+ * only through the official NI DS. Sim is not guaranteed to have the same mapping, as well as any
+ * 3rd party controllers.
  */
 public class XboxController extends GenericHID {
   /** Represents a digital button on an XboxController. */
   public enum Button {
+    /** Left bumper. */
     kLeftBumper(5),
+    /** Right bumper. */
     kRightBumper(6),
+    /** Left stick. */
     kLeftStick(9),
+    /** Right stick. */
     kRightStick(10),
+    /** A. */
     kA(1),
+    /** B. */
     kB(2),
+    /** X. */
     kX(3),
+    /** Y. */
     kY(4),
+    /** Back. */
     kBack(7),
+    /** Start. */
     kStart(8);
 
+    /** Button value. */
     public final int value;
 
     Button(int value) {
@@ -56,13 +71,20 @@
 
   /** Represents an axis on an XboxController. */
   public enum Axis {
+    /** Left X. */
     kLeftX(0),
+    /** Right X. */
     kRightX(4),
+    /** Left Y. */
     kLeftY(1),
+    /** Right Y. */
     kRightY(5),
+    /** Left trigger. */
     kLeftTrigger(2),
+    /** Right trigger. */
     kRightTrigger(3);
 
+    /** Axis value. */
     public final int value;
 
     Axis(int value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/EdgeConfiguration.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/EdgeConfiguration.java
index 9d58fbf..c969249 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/EdgeConfiguration.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/EdgeConfiguration.java
@@ -4,15 +4,22 @@
 
 package edu.wpi.first.wpilibj.counter;
 
+/** Edge configuration. */
 public enum EdgeConfiguration {
+  /** No edge configuration (neither rising nor falling). */
   kNone(false, false),
+  /** Rising edge configuration. */
   kRisingEdge(true, false),
+  /** Falling edge configuration. */
   kFallingEdge(false, true),
+  /** Both rising and falling edge configuration. */
   kBoth(true, true);
 
+  /** True if triggering on rising edge. */
   @SuppressWarnings("MemberName")
   public final boolean rising;
 
+  /** True if triggering on falling edge. */
   @SuppressWarnings("MemberName")
   public final boolean falling;
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/ExternalDirectionCounter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/ExternalDirectionCounter.java
index 6828b9a..97fbf5b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/ExternalDirectionCounter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/ExternalDirectionCounter.java
@@ -34,6 +34,7 @@
    * @param countSource The source for counting.
    * @param directionSource The source for selecting count direction.
    */
+  @SuppressWarnings("this-escape")
   public ExternalDirectionCounter(DigitalSource countSource, DigitalSource directionSource) {
     m_countSource = requireNonNullParam(countSource, "countSource", "ExternalDirectionCounter");
     m_directionSource =
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/Tachometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/Tachometer.java
index 5bd8e7e..6bef24d 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/Tachometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/Tachometer.java
@@ -34,6 +34,7 @@
    *
    * @param source The DigitalSource (e.g. DigitalInput) of the Tachometer.
    */
+  @SuppressWarnings("this-escape")
   public Tachometer(DigitalSource source) {
     m_source = requireNonNullParam(source, "source", "Tachometer");
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/UpDownCounter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/UpDownCounter.java
index c89c118..ed9f35e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/UpDownCounter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/counter/UpDownCounter.java
@@ -32,6 +32,7 @@
    * @param upSource The up count source (can be null).
    * @param downSource The down count source (can be null).
    */
+  @SuppressWarnings("this-escape")
   public UpDownCounter(DigitalSource upSource, DigitalSource downSource) {
     ByteBuffer index = ByteBuffer.allocateDirect(4);
     // set the byte order
@@ -92,7 +93,7 @@
   }
 
   /** Resets the current count. */
-  public void reset() {
+  public final void reset() {
     CounterJNI.resetCounter(m_handle);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/DifferentialDrive.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/DifferentialDrive.java
index 9a1f2ba..2199aa8 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/DifferentialDrive.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/DifferentialDrive.java
@@ -14,49 +14,16 @@
 import edu.wpi.first.util.sendable.SendableBuilder;
 import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.motorcontrol.MotorController;
+import java.util.function.DoubleConsumer;
 
 /**
  * A class for driving differential drive/skid-steer drive platforms such as the Kit of Parts drive
  * base, "tank drive", or West Coast Drive.
  *
  * <p>These drive bases typically have drop-center / skid-steer with two or more wheels per side
- * (e.g., 6WD or 8WD). This class takes a MotorController per side. For four and six motor
- * drivetrains, construct and pass in {@link
- * edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup} instances as follows.
- *
- * <p>Four motor drivetrain:
- *
- * <pre><code>
- * public class Robot {
- *   MotorController m_frontLeft = new PWMVictorSPX(1);
- *   MotorController m_rearLeft = new PWMVictorSPX(2);
- *   MotorControllerGroup m_left = new MotorControllerGroup(m_frontLeft, m_rearLeft);
- *
- *   MotorController m_frontRight = new PWMVictorSPX(3);
- *   MotorController m_rearRight = new PWMVictorSPX(4);
- *   MotorControllerGroup m_right = new MotorControllerGroup(m_frontRight, m_rearRight);
- *
- *   DifferentialDrive m_drive = new DifferentialDrive(m_left, m_right);
- * }
- * </code></pre>
- *
- * <p>Six motor drivetrain:
- *
- * <pre><code>
- * public class Robot {
- *   MotorController m_frontLeft = new PWMVictorSPX(1);
- *   MotorController m_midLeft = new PWMVictorSPX(2);
- *   MotorController m_rearLeft = new PWMVictorSPX(3);
- *   MotorControllerGroup m_left = new MotorControllerGroup(m_frontLeft, m_midLeft, m_rearLeft);
- *
- *   MotorController m_frontRight = new PWMVictorSPX(4);
- *   MotorController m_midRight = new PWMVictorSPX(5);
- *   MotorController m_rearRight = new PWMVictorSPX(6);
- *   MotorControllerGroup m_right = new MotorControllerGroup(m_frontRight, m_midRight, m_rearRight);
- *
- *   DifferentialDrive m_drive = new DifferentialDrive(m_left, m_right);
- * }
- * </code></pre>
+ * (e.g., 6WD or 8WD). This class takes a setter per side. For four and six motor drivetrains, use
+ * CAN motor controller followers or {@link
+ * edu.wpi.first.wpilibj.motorcontrol.PWMMotorController#addFollower(PWMMotorController)}.
  *
  * <p>A differential drive robot has left and right wheels separated by an arbitrary width.
  *
@@ -88,8 +55,12 @@
 public class DifferentialDrive extends RobotDriveBase implements Sendable, AutoCloseable {
   private static int instances;
 
-  private final MotorController m_leftMotor;
-  private final MotorController m_rightMotor;
+  private final DoubleConsumer m_leftMotor;
+  private final DoubleConsumer m_rightMotor;
+
+  // Used for Sendable property getters
+  private double m_leftOutput;
+  private double m_rightOutput;
 
   private boolean m_reported;
 
@@ -100,7 +71,10 @@
    */
   @SuppressWarnings("MemberName")
   public static class WheelSpeeds {
+    /** Left wheel speed. */
     public double left;
+
+    /** Right wheel speed. */
     public double right;
 
     /** Constructs a WheelSpeeds with zeroes for left and right speeds. */
@@ -128,14 +102,30 @@
    * @param leftMotor Left motor.
    * @param rightMotor Right motor.
    */
+  @SuppressWarnings({"removal", "this-escape"})
   public DifferentialDrive(MotorController leftMotor, MotorController rightMotor) {
+    this((double output) -> leftMotor.set(output), (double output) -> rightMotor.set(output));
+    SendableRegistry.addChild(this, leftMotor);
+    SendableRegistry.addChild(this, rightMotor);
+  }
+
+  /**
+   * Construct a DifferentialDrive.
+   *
+   * <p>To pass multiple motors per side, use CAN motor controller followers or {@link
+   * edu.wpi.first.wpilibj.motorcontrol.PWMMotorController#addFollower(PWMMotorController)}. If a
+   * motor needs to be inverted, do so before passing it in.
+   *
+   * @param leftMotor Left motor setter.
+   * @param rightMotor Right motor setter.
+   */
+  @SuppressWarnings("this-escape")
+  public DifferentialDrive(DoubleConsumer leftMotor, DoubleConsumer rightMotor) {
     requireNonNullParam(leftMotor, "leftMotor", "DifferentialDrive");
     requireNonNullParam(rightMotor, "rightMotor", "DifferentialDrive");
 
     m_leftMotor = leftMotor;
     m_rightMotor = rightMotor;
-    SendableRegistry.addChild(this, m_leftMotor);
-    SendableRegistry.addChild(this, m_rightMotor);
     instances++;
     SendableRegistry.addLW(this, "DifferentialDrive", instances);
   }
@@ -177,8 +167,11 @@
 
     var speeds = arcadeDriveIK(xSpeed, zRotation, squareInputs);
 
-    m_leftMotor.set(speeds.left * m_maxOutput);
-    m_rightMotor.set(speeds.right * m_maxOutput);
+    m_leftOutput = speeds.left * m_maxOutput;
+    m_rightOutput = speeds.right * m_maxOutput;
+
+    m_leftMotor.accept(m_leftOutput);
+    m_rightMotor.accept(m_rightOutput);
 
     feed();
   }
@@ -206,8 +199,11 @@
 
     var speeds = curvatureDriveIK(xSpeed, zRotation, allowTurnInPlace);
 
-    m_leftMotor.set(speeds.left * m_maxOutput);
-    m_rightMotor.set(speeds.right * m_maxOutput);
+    m_leftOutput = speeds.left * m_maxOutput;
+    m_rightOutput = speeds.right * m_maxOutput;
+
+    m_leftMotor.accept(m_leftOutput);
+    m_rightMotor.accept(m_rightOutput);
 
     feed();
   }
@@ -244,8 +240,11 @@
 
     var speeds = tankDriveIK(leftSpeed, rightSpeed, squareInputs);
 
-    m_leftMotor.set(speeds.left * m_maxOutput);
-    m_rightMotor.set(speeds.right * m_maxOutput);
+    m_leftOutput = speeds.left * m_maxOutput;
+    m_rightOutput = speeds.right * m_maxOutput;
+
+    m_leftMotor.accept(m_leftOutput);
+    m_rightMotor.accept(m_rightOutput);
 
     feed();
   }
@@ -350,8 +349,12 @@
 
   @Override
   public void stopMotor() {
-    m_leftMotor.stopMotor();
-    m_rightMotor.stopMotor();
+    m_leftOutput = 0.0;
+    m_rightOutput = 0.0;
+
+    m_leftMotor.accept(0.0);
+    m_rightMotor.accept(0.0);
+
     feed();
   }
 
@@ -365,7 +368,7 @@
     builder.setSmartDashboardType("DifferentialDrive");
     builder.setActuator(true);
     builder.setSafeState(this::stopMotor);
-    builder.addDoubleProperty("Left Motor Speed", m_leftMotor::get, m_leftMotor::set);
-    builder.addDoubleProperty("Right Motor Speed", m_rightMotor::get, m_rightMotor::set);
+    builder.addDoubleProperty("Left Motor Speed", () -> m_leftOutput, m_leftMotor::accept);
+    builder.addDoubleProperty("Right Motor Speed", () -> m_rightOutput, m_rightMotor::accept);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/MecanumDrive.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/MecanumDrive.java
index 181e275..236a4fb 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/MecanumDrive.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/MecanumDrive.java
@@ -16,6 +16,7 @@
 import edu.wpi.first.util.sendable.SendableBuilder;
 import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.motorcontrol.MotorController;
+import java.util.function.DoubleConsumer;
 
 /**
  * A class for driving Mecanum drive platforms.
@@ -56,10 +57,16 @@
 public class MecanumDrive extends RobotDriveBase implements Sendable, AutoCloseable {
   private static int instances;
 
-  private final MotorController m_frontLeftMotor;
-  private final MotorController m_rearLeftMotor;
-  private final MotorController m_frontRightMotor;
-  private final MotorController m_rearRightMotor;
+  private final DoubleConsumer m_frontLeftMotor;
+  private final DoubleConsumer m_rearLeftMotor;
+  private final DoubleConsumer m_frontRightMotor;
+  private final DoubleConsumer m_rearRightMotor;
+
+  // Used for Sendable property getters
+  private double m_frontLeftOutput;
+  private double m_rearLeftOutput;
+  private double m_frontRightOutput;
+  private double m_rearRightOutput;
 
   private boolean m_reported;
 
@@ -70,9 +77,16 @@
    */
   @SuppressWarnings("MemberName")
   public static class WheelSpeeds {
+    /** Front-left wheel speed. */
     public double frontLeft;
+
+    /** Front-right wheel speed. */
     public double frontRight;
+
+    /** Rear-left wheel speed. */
     public double rearLeft;
+
+    /** Rear-right wheel speed. */
     public double rearRight;
 
     /** Constructs a WheelSpeeds with zeroes for all four speeds. */
@@ -104,11 +118,39 @@
    * @param frontRightMotor The motor on the front-right corner.
    * @param rearRightMotor The motor on the rear-right corner.
    */
+  @SuppressWarnings({"removal", "this-escape"})
   public MecanumDrive(
       MotorController frontLeftMotor,
       MotorController rearLeftMotor,
       MotorController frontRightMotor,
       MotorController rearRightMotor) {
+    this(
+        (double output) -> frontLeftMotor.set(output),
+        (double output) -> rearLeftMotor.set(output),
+        (double output) -> frontRightMotor.set(output),
+        (double output) -> rearRightMotor.set(output));
+    SendableRegistry.addChild(this, frontLeftMotor);
+    SendableRegistry.addChild(this, rearLeftMotor);
+    SendableRegistry.addChild(this, frontRightMotor);
+    SendableRegistry.addChild(this, rearRightMotor);
+  }
+
+  /**
+   * Construct a MecanumDrive.
+   *
+   * <p>If a motor needs to be inverted, do so before passing it in.
+   *
+   * @param frontLeftMotor The setter for the motor on the front-left corner.
+   * @param rearLeftMotor The setter for the motor on the rear-left corner.
+   * @param frontRightMotor The setter for the motor on the front-right corner.
+   * @param rearRightMotor The setter for the motor on the rear-right corner.
+   */
+  @SuppressWarnings("this-escape")
+  public MecanumDrive(
+      DoubleConsumer frontLeftMotor,
+      DoubleConsumer rearLeftMotor,
+      DoubleConsumer frontRightMotor,
+      DoubleConsumer rearRightMotor) {
     requireNonNullParam(frontLeftMotor, "frontLeftMotor", "MecanumDrive");
     requireNonNullParam(rearLeftMotor, "rearLeftMotor", "MecanumDrive");
     requireNonNullParam(frontRightMotor, "frontRightMotor", "MecanumDrive");
@@ -118,10 +160,6 @@
     m_rearLeftMotor = rearLeftMotor;
     m_frontRightMotor = frontRightMotor;
     m_rearRightMotor = rearRightMotor;
-    SendableRegistry.addChild(this, m_frontLeftMotor);
-    SendableRegistry.addChild(this, m_rearLeftMotor);
-    SendableRegistry.addChild(this, m_frontRightMotor);
-    SendableRegistry.addChild(this, m_rearRightMotor);
     instances++;
     SendableRegistry.addLW(this, "MecanumDrive", instances);
   }
@@ -171,10 +209,15 @@
 
     var speeds = driveCartesianIK(xSpeed, ySpeed, zRotation, gyroAngle);
 
-    m_frontLeftMotor.set(speeds.frontLeft * m_maxOutput);
-    m_frontRightMotor.set(speeds.frontRight * m_maxOutput);
-    m_rearLeftMotor.set(speeds.rearLeft * m_maxOutput);
-    m_rearRightMotor.set(speeds.rearRight * m_maxOutput);
+    m_frontLeftOutput = speeds.frontLeft * m_maxOutput;
+    m_rearLeftOutput = speeds.rearLeft * m_maxOutput;
+    m_frontRightOutput = speeds.frontRight * m_maxOutput;
+    m_rearRightOutput = speeds.rearRight * m_maxOutput;
+
+    m_frontLeftMotor.accept(m_frontLeftOutput);
+    m_frontRightMotor.accept(m_frontRightOutput);
+    m_rearLeftMotor.accept(m_rearLeftOutput);
+    m_rearRightMotor.accept(m_rearRightOutput);
 
     feed();
   }
@@ -255,10 +298,16 @@
 
   @Override
   public void stopMotor() {
-    m_frontLeftMotor.stopMotor();
-    m_frontRightMotor.stopMotor();
-    m_rearLeftMotor.stopMotor();
-    m_rearRightMotor.stopMotor();
+    m_frontLeftOutput = 0.0;
+    m_frontRightOutput = 0.0;
+    m_rearLeftOutput = 0.0;
+    m_rearRightOutput = 0.0;
+
+    m_frontLeftMotor.accept(0.0);
+    m_frontRightMotor.accept(0.0);
+    m_rearLeftMotor.accept(0.0);
+    m_rearRightMotor.accept(0.0);
+
     feed();
   }
 
@@ -273,11 +322,12 @@
     builder.setActuator(true);
     builder.setSafeState(this::stopMotor);
     builder.addDoubleProperty(
-        "Front Left Motor Speed", m_frontLeftMotor::get, m_frontLeftMotor::set);
+        "Front Left Motor Speed", () -> m_frontLeftOutput, m_frontLeftMotor::accept);
     builder.addDoubleProperty(
-        "Front Right Motor Speed", m_frontRightMotor::get, m_frontRightMotor::set);
-    builder.addDoubleProperty("Rear Left Motor Speed", m_rearLeftMotor::get, m_rearLeftMotor::set);
+        "Front Right Motor Speed", () -> m_frontRightOutput, m_frontRightMotor::accept);
     builder.addDoubleProperty(
-        "Rear Right Motor Speed", m_rearRightMotor::get, m_rearRightMotor::set);
+        "Rear Left Motor Speed", () -> m_rearLeftOutput, m_rearLeftMotor::accept);
+    builder.addDoubleProperty(
+        "Rear Right Motor Speed", () -> m_rearRightOutput, m_rearRightMotor::accept);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/RobotDriveBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/RobotDriveBase.java
index c4e931d..a952b2e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/RobotDriveBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/RobotDriveBase.java
@@ -12,22 +12,36 @@
  * <p>{@link edu.wpi.first.wpilibj.MotorSafety} is enabled by default.
  */
 public abstract class RobotDriveBase extends MotorSafety {
+  /** Default input deadband. */
   public static final double kDefaultDeadband = 0.02;
+
+  /** Default maximum output. */
   public static final double kDefaultMaxOutput = 1.0;
 
+  /** Input deadband. */
   protected double m_deadband = kDefaultDeadband;
+
+  /** Maximum output. */
   protected double m_maxOutput = kDefaultMaxOutput;
 
   /** The location of a motor on the robot for the purpose of driving. */
   public enum MotorType {
+    /** Front left motor. */
     kFrontLeft(0),
+    /** Front right motor. */
     kFrontRight(1),
+    /** Rear left motor. */
     kRearLeft(2),
+    /** Reat right motor. */
     kRearRight(3),
+    /** Left motor. */
     kLeft(0),
+    /** Right motor. */
     kRight(1),
+    /** Back motor. */
     kBack(2);
 
+    /** MotorType value. */
     public final int value;
 
     MotorType(int value) {
@@ -36,6 +50,7 @@
   }
 
   /** RobotDriveBase constructor. */
+  @SuppressWarnings("this-escape")
   public RobotDriveBase() {
     setSafetyEnabled(true);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/EventLoop.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/EventLoop.java
index 3e220e0..38d21cd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/EventLoop.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/EventLoop.java
@@ -5,6 +5,7 @@
 package edu.wpi.first.wpilibj.event;
 
 import java.util.Collection;
+import java.util.ConcurrentModificationException;
 import java.util.LinkedHashSet;
 
 /**
@@ -12,6 +13,10 @@
  */
 public final class EventLoop {
   private final Collection<Runnable> m_bindings = new LinkedHashSet<>();
+  private boolean m_running;
+
+  /** Default constructor. */
+  public EventLoop() {}
 
   /**
    * Bind a new action to run when the loop is polled.
@@ -19,16 +24,27 @@
    * @param action the action to run.
    */
   public void bind(Runnable action) {
+    if (m_running) {
+      throw new ConcurrentModificationException("Cannot bind EventLoop while it is running");
+    }
     m_bindings.add(action);
   }
 
   /** Poll all bindings. */
   public void poll() {
-    m_bindings.forEach(Runnable::run);
+    try {
+      m_running = true;
+      m_bindings.forEach(Runnable::run);
+    } finally {
+      m_running = false;
+    }
   }
 
   /** Clear all bindings. */
   public void clear() {
+    if (m_running) {
+      throw new ConcurrentModificationException("Cannot clear EventLoop while it is running");
+    }
     m_bindings.clear();
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Accelerometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Accelerometer.java
index d33b7ae..630d45c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Accelerometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Accelerometer.java
@@ -11,10 +11,15 @@
  */
 @Deprecated(since = "2024", forRemoval = true)
 public interface Accelerometer {
+  /** Accelerometer range. */
   enum Range {
+    /** 2 Gs max. */
     k2G,
+    /** 4 Gs max. */
     k4G,
+    /** 8 Gs max. */
     k8G,
+    /** 16 Gs max. */
     k16G
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/internal/DriverStationModeThread.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/internal/DriverStationModeThread.java
index 03d6397..57c6bf7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/internal/DriverStationModeThread.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/internal/DriverStationModeThread.java
@@ -9,9 +9,7 @@
 import edu.wpi.first.wpilibj.DriverStation;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-/*
- * For internal use only.
- */
+/** For internal use only. */
 public class DriverStationModeThread implements AutoCloseable {
   private final AtomicBoolean m_keepAlive = new AtomicBoolean();
   private final Thread m_thread;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java
index ac8ef1d..1b9d1ea 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java
@@ -50,7 +50,7 @@
   private static Runnable disabledListener;
 
   static {
-    SendableRegistry.setLiveWindowBuilderFactory(() -> new SendableBuilderImpl());
+    SendableRegistry.setLiveWindowBuilderFactory(SendableBuilderImpl::new);
     enabledPub.set(false);
   }
 
@@ -67,14 +67,29 @@
     throw new UnsupportedOperationException("This is a utility class!");
   }
 
+  /**
+   * Sets function to be called when LiveWindow is enabled.
+   *
+   * @param runnable function (or null for none)
+   */
   public static synchronized void setEnabledListener(Runnable runnable) {
     enabledListener = runnable;
   }
 
+  /**
+   * Sets function to be called when LiveWindow is disabled.
+   *
+   * @param runnable function (or null for none)
+   */
   public static synchronized void setDisabledListener(Runnable runnable) {
     disabledListener = runnable;
   }
 
+  /**
+   * Returns true if LiveWindow is enabled.
+   *
+   * @return True if LiveWindow is enabled.
+   */
   public static synchronized boolean isEnabled() {
     return liveWindowEnabled;
   }
@@ -105,10 +120,7 @@
       } else {
         System.out.println("stopping live window mode.");
         SendableRegistry.foreachLiveWindow(
-            dataHandle,
-            cbdata -> {
-              ((SendableBuilderImpl) cbdata.builder).stopLiveWindowMode();
-            });
+            dataHandle, cbdata -> ((SendableBuilderImpl) cbdata.builder).stopLiveWindowMode());
         if (disabledListener != null) {
           disabledListener.run();
         }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/DMC60.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/DMC60.java
index 5f0f26b..9d66907 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/DMC60.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/DMC60.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the DMC60 is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public DMC60(final int channel) {
     super("DMC60", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Jaguar.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Jaguar.java
index f072067..4a0ac85 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Jaguar.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Jaguar.java
@@ -31,6 +31,7 @@
    * @param channel The PWM channel that the Jaguar is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public Jaguar(final int channel) {
     super("Jaguar", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroup.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroup.java
index d9da3c8..c56496c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroup.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroup.java
@@ -9,7 +9,14 @@
 import edu.wpi.first.util.sendable.SendableRegistry;
 import java.util.Arrays;
 
-/** Allows multiple {@link MotorController} objects to be linked together. */
+/**
+ * Allows multiple {@link MotorController} objects to be linked together.
+ *
+ * @deprecated Use {@link PWMMotorController#addFollower(PWMMotorController)} or if using CAN motor
+ *     controllers, use their method of following.
+ */
+@SuppressWarnings("removal")
+@Deprecated(forRemoval = true, since = "2024")
 public class MotorControllerGroup implements MotorController, Sendable, AutoCloseable {
   private boolean m_isInverted;
   private final MotorController[] m_motorControllers;
@@ -21,6 +28,7 @@
    * @param motorController The first MotorController to add
    * @param motorControllers The MotorControllers to add
    */
+  @SuppressWarnings("this-escape")
   public MotorControllerGroup(
       MotorController motorController, MotorController... motorControllers) {
     m_motorControllers = new MotorController[motorControllers.length + 1];
@@ -29,6 +37,12 @@
     init();
   }
 
+  /**
+   * Create a new MotorControllerGroup with the provided MotorControllers.
+   *
+   * @param motorControllers The MotorControllers to add.
+   */
+  @SuppressWarnings("this-escape")
   public MotorControllerGroup(MotorController[] motorControllers) {
     m_motorControllers = Arrays.copyOf(motorControllers, motorControllers.length);
     init();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
index 25c29ac..01af765 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
@@ -14,6 +14,7 @@
 import edu.wpi.first.wpilibj.PWM;
 
 /** Nidec Brushless Motor. */
+@SuppressWarnings("removal")
 public class NidecBrushless extends MotorSafety
     implements MotorController, Sendable, AutoCloseable {
   private boolean m_isInverted;
@@ -30,6 +31,7 @@
    * @param dioChannel The DIO channel that the Nidec Brushless controller is attached to. 0-9 are
    *     on-board, 10-25 are on the MXP port
    */
+  @SuppressWarnings("this-escape")
   public NidecBrushless(final int pwmChannel, final int dioChannel) {
     setSafetyEnabled(false);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMMotorController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMMotorController.java
index 68817a1..b72cee3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMMotorController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMMotorController.java
@@ -9,11 +9,16 @@
 import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.MotorSafety;
 import edu.wpi.first.wpilibj.PWM;
+import java.util.ArrayList;
 
 /** Common base class for all PWM Motor Controllers. */
+@SuppressWarnings("removal")
 public abstract class PWMMotorController extends MotorSafety
     implements MotorController, Sendable, AutoCloseable {
   private boolean m_isInverted;
+  private final ArrayList<PWMMotorController> m_followers = new ArrayList<>();
+
+  /** PWM instances for motor controller. */
   protected PWM m_pwm;
 
   /**
@@ -23,6 +28,7 @@
    * @param channel The PWM channel that the controller is attached to. 0-9 are on-board, 10-19 are
    *     on the MXP port
    */
+  @SuppressWarnings("this-escape")
   protected PWMMotorController(final String name, final int channel) {
     m_pwm = new PWM(channel, false);
     SendableRegistry.addLW(this, name, channel);
@@ -45,7 +51,15 @@
    */
   @Override
   public void set(double speed) {
-    m_pwm.setSpeed(m_isInverted ? -speed : speed);
+    if (m_isInverted) {
+      speed = -speed;
+    }
+    m_pwm.setSpeed(speed);
+
+    for (var follower : m_followers) {
+      follower.set(speed);
+    }
+
     feed();
   }
 
@@ -116,6 +130,15 @@
     m_pwm.enableDeadbandElimination(eliminateDeadband);
   }
 
+  /**
+   * Make the given PWM motor controller follow the output of this one.
+   *
+   * @param follower The motor controller follower.
+   */
+  public void addFollower(PWMMotorController follower) {
+    m_followers.add(follower);
+  }
+
   @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("Motor Controller");
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkFlex.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkFlex.java
new file mode 100644
index 0000000..059b18a
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkFlex.java
@@ -0,0 +1,45 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.motorcontrol;
+
+import edu.wpi.first.hal.FRCNetComm.tResourceType;
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.PWM;
+
+/**
+ * REV Robotics SPARK Flex Motor Controller with PWM control.
+ *
+ * <p>Note that the SPARK Flex uses the following bounds for PWM values. These values should work
+ * reasonably well for most controllers, but if users experience issues such as asymmetric behavior
+ * around the deadband or inability to saturate the controller in either direction, calibration is
+ * recommended. The calibration procedure can be found in the SPARK Flex User Manual available from
+ * REV Robotics.
+ *
+ * <ul>
+ *   <li>2.003ms = full "forward"
+ *   <li>1.550ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.460ms = the "low end" of the deadband range
+ *   <li>0.999ms = full "reverse"
+ * </ul>
+ */
+public class PWMSparkFlex extends PWMMotorController {
+  /**
+   * Common initialization code called by all constructors.
+   *
+   * @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
+   */
+  @SuppressWarnings("this-escape")
+  public PWMSparkFlex(final int channel) {
+    super("PWMSparkFlex", channel);
+
+    m_pwm.setBoundsMicroseconds(2003, 1550, 1500, 1460, 999);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_RevSparkFlexPWM, getChannel() + 1);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkMax.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkMax.java
index ede5f5d..205d70a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkMax.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkMax.java
@@ -31,6 +31,7 @@
    *
    * @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
    */
+  @SuppressWarnings("this-escape")
   public PWMSparkMax(final int channel) {
     super("PWMSparkMax", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonFX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonFX.java
index 29ac766..2211fa5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonFX.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonFX.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the Talon FX is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public PWMTalonFX(final int channel) {
     super("PWMTalonFX", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonSRX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonSRX.java
index 085cd86..4677627 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonSRX.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonSRX.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the Talon SRX is attached to. 0-9 are on-board, 10-19 are
    *     on the MXP port
    */
+  @SuppressWarnings("this-escape")
   public PWMTalonSRX(final int channel) {
     super("PWMTalonSRX", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVenom.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVenom.java
index a38e936..f9960b0 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVenom.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVenom.java
@@ -31,6 +31,7 @@
    * @param channel The PWM channel that the Venom is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public PWMVenom(final int channel) {
     super("PWMVenom", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVictorSPX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVictorSPX.java
index 555614a..daa70f3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVictorSPX.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVictorSPX.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the PWMVictorSPX is attached to. 0-9 are on-board, 10-19
    *     are on the MXP port
    */
+  @SuppressWarnings("this-escape")
   public PWMVictorSPX(final int channel) {
     super("PWMVictorSPX", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/SD540.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/SD540.java
index 7cdb035..31246bc 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/SD540.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/SD540.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the SD540 is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public SD540(final int channel) {
     super("SD540", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Spark.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Spark.java
index cc92621..d57b626 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Spark.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Spark.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the SPARK is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public Spark(final int channel) {
     super("Spark", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Talon.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Talon.java
index dd9e473..bcbad60 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Talon.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Talon.java
@@ -31,6 +31,7 @@
    * @param channel The PWM channel that the Talon is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public Talon(final int channel) {
     super("Talon", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Victor.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Victor.java
index 5265a8e..1a816ad 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Victor.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Victor.java
@@ -17,7 +17,8 @@
  * 884 controllers also but if users experience issues such as asymmetric behavior around the
  * deadband or inability to saturate the controller in either direction, calibration is recommended.
  * The calibration procedure can be found in the Victor 884 User Manual available from VEX Robotics:
- * http://content.vexrobotics.com/docs/ifi-v884-users-manual-9-25-06.pdf
+ * <a
+ * href="http://content.vexrobotics.com/docs/ifi-v884-users-manual-9-25-06.pdf">http://content.vexrobotics.com/docs/ifi-v884-users-manual-9-25-06.pdf</a>
  *
  * <ul>
  *   <li>2.027ms = full "forward"
@@ -34,6 +35,7 @@
    * @param channel The PWM channel that the Victor is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public Victor(final int channel) {
     super("Victor", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/VictorSP.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/VictorSP.java
index aa262d0..09ba3c6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/VictorSP.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/VictorSP.java
@@ -32,6 +32,7 @@
    * @param channel The PWM channel that the VictorSP is attached to. 0-9 are on-board, 10-19 are on
    *     the MXP port
    */
+  @SuppressWarnings("this-escape")
   public VictorSP(final int channel) {
     super("VictorSP", channel);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java
index 4d64efe..0d85ce6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/EventImportance.java
@@ -38,6 +38,11 @@
     m_simpleName = simpleName;
   }
 
+  /**
+   * Returns name of the given enum.
+   *
+   * @return Name of the given enum.
+   */
   public String getSimpleName() {
     return m_simpleName;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
index 150f0db..bd428cd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
@@ -74,7 +74,7 @@
     }
   }
 
-  /*
+  /**
    * Sets NetworkTable instance used for camera publisher entries.
    *
    * @param inst NetworkTable instance
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
index 39078f4..3e1f75a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
@@ -26,25 +26,53 @@
   private int m_width = -1;
   private int m_height = -1;
 
+  /**
+   * Constructs a ShuffleboardComponent.
+   *
+   * @param parent The parent container.
+   * @param title The component title.
+   * @param type The component type.
+   */
   protected ShuffleboardComponent(ShuffleboardContainer parent, String title, String type) {
     m_parent = requireNonNullParam(parent, "parent", "ShuffleboardComponent");
     m_title = requireNonNullParam(title, "title", "ShuffleboardComponent");
     m_type = type;
   }
 
+  /**
+   * Constructs a ShuffleboardComponent.
+   *
+   * @param parent The parent container.
+   * @param title The component title.
+   */
   protected ShuffleboardComponent(ShuffleboardContainer parent, String title) {
     this(parent, title, null);
   }
 
+  /**
+   * Returns the parent container.
+   *
+   * @return The parent container.
+   */
   public final ShuffleboardContainer getParent() {
     return m_parent;
   }
 
+  /**
+   * Sets the component type.
+   *
+   * @param type The component type.
+   */
   protected final void setType(String type) {
     m_type = type;
     m_metadataDirty = true;
   }
 
+  /**
+   * Returns the component type.
+   *
+   * @return The component type.
+   */
   public final String getType() {
     return m_type;
   }
@@ -109,6 +137,11 @@
     return (C) this;
   }
 
+  /**
+   * Builds NT metadata.
+   *
+   * @param metaTable The NT metadata table.
+   */
   protected final void buildMetadata(NetworkTable metaTable) {
     if (!m_metadataDirty) {
       return;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
index df2f117..88e1e49 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
@@ -18,7 +18,7 @@
 
 final class ShuffleboardInstance implements ShuffleboardRoot {
   private final Map<String, ShuffleboardTab> m_tabs = new LinkedHashMap<>();
-
+  private boolean m_reported = false; // NOPMD redundant field initializer
   private boolean m_tabsChanged = false; // NOPMD redundant field initializer
   private final NetworkTable m_rootTable;
   private final NetworkTable m_rootMetaTable;
@@ -35,12 +35,15 @@
     m_rootMetaTable = m_rootTable.getSubTable(".metadata");
     m_selectedTabPub =
         m_rootMetaTable.getStringTopic("Selected").publish(PubSubOption.keepDuplicates(true));
-    HAL.report(tResourceType.kResourceType_Shuffleboard, 0);
   }
 
   @Override
   public ShuffleboardTab getTab(String title) {
     requireNonNullParam(title, "title", "getTab");
+    if (!m_reported) {
+      HAL.report(tResourceType.kResourceType_Shuffleboard, 0);
+      m_reported = true;
+    }
     if (!m_tabs.containsKey(title)) {
       m_tabs.put(title, new ShuffleboardTab(this, title));
       m_tabsChanged = true;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL345Sim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL345Sim.java
index c50c2f8..bcbf3e2 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL345Sim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL345Sim.java
@@ -9,10 +9,11 @@
 import edu.wpi.first.wpilibj.ADXL345_SPI;
 import java.util.Objects;
 
+/** Class to control a simulated ADXL345. */
 public class ADXL345Sim {
-  protected SimDouble m_simX;
-  protected SimDouble m_simY;
-  protected SimDouble m_simZ;
+  private SimDouble m_simX;
+  private SimDouble m_simY;
+  private SimDouble m_simZ;
 
   /**
    * Constructor.
@@ -48,15 +49,30 @@
     Objects.requireNonNull(m_simZ);
   }
 
-  public void setX(double x) {
-    m_simX.set(x);
+  /**
+   * Sets the X acceleration.
+   *
+   * @param accel The X acceleration.
+   */
+  public void setX(double accel) {
+    m_simX.set(accel);
   }
 
-  public void setY(double y) {
-    m_simY.set(y);
+  /**
+   * Sets the Y acceleration.
+   *
+   * @param accel The Y acceleration.
+   */
+  public void setY(double accel) {
+    m_simY.set(accel);
   }
 
-  public void setZ(double z) {
-    m_simZ.set(z);
+  /**
+   * Sets the Z acceleration.
+   *
+   * @param accel The Z acceleration.
+   */
+  public void setZ(double accel) {
+    m_simZ.set(accel);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL362Sim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL362Sim.java
index 76df8bd..148f914 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL362Sim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL362Sim.java
@@ -8,10 +8,11 @@
 import edu.wpi.first.wpilibj.ADXL362;
 import java.util.Objects;
 
+/** Class to control a simulated ADXL362. */
 public class ADXL362Sim {
-  protected SimDouble m_simX;
-  protected SimDouble m_simY;
-  protected SimDouble m_simZ;
+  private SimDouble m_simX;
+  private SimDouble m_simY;
+  private SimDouble m_simZ;
 
   /**
    * Constructor.
@@ -34,15 +35,30 @@
     Objects.requireNonNull(m_simZ);
   }
 
-  public void setX(double x) {
-    m_simX.set(x);
+  /**
+   * Sets the X acceleration.
+   *
+   * @param accel The X acceleration.
+   */
+  public void setX(double accel) {
+    m_simX.set(accel);
   }
 
-  public void setY(double y) {
-    m_simY.set(y);
+  /**
+   * Sets the Y acceleration.
+   *
+   * @param accel The Y acceleration.
+   */
+  public void setY(double accel) {
+    m_simY.set(accel);
   }
 
-  public void setZ(double z) {
-    m_simZ.set(z);
+  /**
+   * Sets the Z acceleration.
+   *
+   * @param accel The Z acceleration.
+   */
+  public void setZ(double accel) {
+    m_simZ.set(accel);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSim.java
index 94db6f6..a121659 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSim.java
@@ -148,8 +148,7 @@
    * @param dtSeconds the time difference
    */
   public void update(double dtSeconds) {
-    // Update state estimate with RK4
-    m_x = NumericalIntegration.rk4(this::getDynamics, m_x, m_u, dtSeconds);
+    m_x = NumericalIntegration.rkdp(this::getDynamics, m_x, m_u, dtSeconds);
     m_y = m_x;
     if (m_measurementStdDevs != null) {
       m_y = m_y.plus(StateSpaceUtil.makeWhiteNoiseVector(m_measurementStdDevs));
@@ -237,12 +236,9 @@
    * @return the drivetrain's left side current draw, in amps
    */
   public double getLeftCurrentDrawAmps() {
-    var loadIleft =
-        m_motor.getCurrent(
-                getState(State.kLeftVelocity) * m_currentGearing / m_wheelRadiusMeters,
-                m_u.get(0, 0))
-            * Math.signum(m_u.get(0, 0));
-    return loadIleft;
+    return m_motor.getCurrent(
+            getState(State.kLeftVelocity) * m_currentGearing / m_wheelRadiusMeters, m_u.get(0, 0))
+        * Math.signum(m_u.get(0, 0));
   }
 
   /**
@@ -251,13 +247,9 @@
    * @return the drivetrain's right side current draw, in amps
    */
   public double getRightCurrentDrawAmps() {
-    var loadIright =
-        m_motor.getCurrent(
-                getState(State.kRightVelocity) * m_currentGearing / m_wheelRadiusMeters,
-                m_u.get(1, 0))
-            * Math.signum(m_u.get(1, 0));
-
-    return loadIright;
+    return m_motor.getCurrent(
+            getState(State.kRightVelocity) * m_currentGearing / m_wheelRadiusMeters, m_u.get(1, 0))
+        * Math.signum(m_u.get(1, 0));
   }
 
   /**
@@ -309,6 +301,13 @@
     m_x.set(State.kRightPosition.value, 0, 0);
   }
 
+  /**
+   * The differential drive dynamics function.
+   *
+   * @param x The state.
+   * @param u The input.
+   * @return The state derivative with respect to time.
+   */
   protected Matrix<N7, N1> getDynamics(Matrix<N7, N1> x, Matrix<N2, N1> u) {
     // Because G can be factored out of B, we can divide by the old ratio and multiply
     // by the new ratio to get a new drivetrain model.
@@ -377,12 +376,18 @@
    * and 16:48 8.45:1 -- 14:50 and 19:45 7.31:1 -- 14:50 and 21:43 5.95:1 -- 14:50 and 24:40
    */
   public enum KitbotGearing {
+    /** Gear ratio of 12.75:1. */
     k12p75(12.75),
+    /** Gear ratio of 10.71:1. */
     k10p71(10.71),
+    /** Gear ratio of 8.45:1. */
     k8p45(8.45),
+    /** Gear ratio of 7.31:1. */
     k7p31(7.31),
+    /** Gear ratio of 5.95:1. */
     k5p95(5.95);
 
+    /** KitbotGearing value. */
     public final double value;
 
     KitbotGearing(double i) {
@@ -392,15 +397,24 @@
 
   /** Represents common motor layouts of the kit drivetrain. */
   public enum KitbotMotor {
+    /** One CIM motor per drive side. */
     kSingleCIMPerSide(DCMotor.getCIM(1)),
+    /** Two CIM motors per drive side. */
     kDualCIMPerSide(DCMotor.getCIM(2)),
+    /** One Mini CIM motor per drive side. */
     kSingleMiniCIMPerSide(DCMotor.getMiniCIM(1)),
+    /** Two Mini CIM motors per drive side. */
     kDualMiniCIMPerSide(DCMotor.getMiniCIM(2)),
+    /** One Falcon 500 motor per drive side. */
     kSingleFalcon500PerSide(DCMotor.getFalcon500(1)),
+    /** Two Falcon 500 motors per drive side. */
     kDoubleFalcon500PerSide(DCMotor.getFalcon500(2)),
+    /** One NEO motor per drive side. */
     kSingleNEOPerSide(DCMotor.getNEO(1)),
+    /** Two NEO motors per drive side. */
     kDoubleNEOPerSide(DCMotor.getNEO(2));
 
+    /** KitbotMotor value. */
     public final DCMotor value;
 
     KitbotMotor(DCMotor i) {
@@ -410,10 +424,14 @@
 
   /** Represents common wheel sizes of the kit drivetrain. */
   public enum KitbotWheelSize {
+    /** Six inch diameter wheels. */
     kSixInch(Units.inchesToMeters(6)),
+    /** Eight inch diameter wheels. */
     kEightInch(Units.inchesToMeters(8)),
+    /** Ten inch diameter wheels. */
     kTenInch(Units.inchesToMeters(10));
 
+    /** KitbotWheelSize value. */
     public final double value;
 
     KitbotWheelSize(double i) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ElevatorSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ElevatorSim.java
index 322d897..6ec8002 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ElevatorSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ElevatorSim.java
@@ -41,6 +41,7 @@
    * @param startingHeightMeters The starting height of the elevator.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
+  @SuppressWarnings("this-escape")
   public ElevatorSim(
       LinearSystem<N2, N1, N1> plant,
       DCMotor gearbox,
@@ -221,7 +222,7 @@
    * @param positionMeters The new position in meters.
    * @param velocityMetersPerSecond New velocity in meters per second.
    */
-  public void setState(double positionMeters, double velocityMetersPerSecond) {
+  public final void setState(double positionMeters, double velocityMetersPerSecond) {
     setState(
         VecBuilder.fill(
             MathUtil.clamp(positionMeters, m_minHeight, m_maxHeight), velocityMetersPerSecond));
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/GenericHIDSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/GenericHIDSim.java
index 8a81f53..9c81be8 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/GenericHIDSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/GenericHIDSim.java
@@ -8,6 +8,7 @@
 
 /** Class to control a simulated generic joystick. */
 public class GenericHIDSim {
+  /** GenericHID port. */
   protected final int m_port;
 
   /**
@@ -135,7 +136,7 @@
    */
   public boolean getOutput(int outputNumber) {
     long outputs = getOutputs();
-    return (outputs & (1 << (outputNumber - 1))) != 0;
+    return (outputs & (1L << (outputNumber - 1))) != 0;
   }
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/JoystickSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/JoystickSim.java
index f55b038..b6f21e3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/JoystickSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/JoystickSim.java
@@ -15,6 +15,7 @@
    *
    * @param joystick joystick to simulate
    */
+  @SuppressWarnings("this-escape")
   public JoystickSim(Joystick joystick) {
     super(joystick);
     m_joystick = joystick;
@@ -29,6 +30,7 @@
    *
    * @param port port number
    */
+  @SuppressWarnings("this-escape")
   public JoystickSim(int port) {
     super(port);
     // default to a reasonable joystick configuration
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/LinearSystemSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/LinearSystemSim.java
index 73b1170..cc4db9c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/LinearSystemSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/LinearSystemSim.java
@@ -23,21 +23,24 @@
  *
  * <p>Set simulated sensor readings with the simulated positions in {@link #getOutput()}
  *
- * @param <States> The number of states of the system.
- * @param <Inputs> The number of inputs to the system.
- * @param <Outputs> The number of outputs of the system.
+ * @param <States> Number of states of the system.
+ * @param <Inputs> Number of inputs to the system.
+ * @param <Outputs> Number of outputs of the system.
  */
 public class LinearSystemSim<States extends Num, Inputs extends Num, Outputs extends Num> {
-  // The plant that represents the linear system.
+  /** The plant that represents the linear system. */
   protected final LinearSystem<States, Inputs, Outputs> m_plant;
 
-  // Variables for state, output, and input.
+  /** State vector. */
   protected Matrix<States, N1> m_x;
-  protected Matrix<Outputs, N1> m_y;
+
+  /** Input vector. */
   protected Matrix<Inputs, N1> m_u;
 
-  // The standard deviations of measurements, used for adding noise
-  // to the measurements.
+  /** Output vector. */
+  protected Matrix<Outputs, N1> m_y;
+
+  /** The standard deviations of measurements, used for adding noise to the measurements. */
   protected final Matrix<Outputs, N1> m_measurementStdDevs;
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS4ControllerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS4ControllerSim.java
index fa19760..822b1db 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS4ControllerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS4ControllerSim.java
@@ -13,6 +13,7 @@
    *
    * @param joystick controller to simulate
    */
+  @SuppressWarnings("this-escape")
   public PS4ControllerSim(PS4Controller joystick) {
     super(joystick);
     setAxisCount(6);
@@ -25,6 +26,7 @@
    *
    * @param port port number
    */
+  @SuppressWarnings("this-escape")
   public PS4ControllerSim(int port) {
     super(port);
     setAxisCount(6);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS5ControllerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS5ControllerSim.java
index d886bd6..e11cdd0 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS5ControllerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS5ControllerSim.java
@@ -13,6 +13,7 @@
    *
    * @param joystick controller to simulate
    */
+  @SuppressWarnings("this-escape")
   public PS5ControllerSim(PS5Controller joystick) {
     super(joystick);
     setAxisCount(6);
@@ -25,6 +26,7 @@
    *
    * @param port port number
    */
+  @SuppressWarnings("this-escape")
   public PS5ControllerSim(int port) {
     super(port);
     setAxisCount(6);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PneumaticsBaseSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PneumaticsBaseSim.java
index 156562b..dde69c1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PneumaticsBaseSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PneumaticsBaseSim.java
@@ -10,6 +10,7 @@
 
 /** Common base class for pneumatics module simulation classes. */
 public abstract class PneumaticsBaseSim {
+  /** PneumaticsBase index. */
   protected final int m_index;
 
   /**
@@ -30,10 +31,20 @@
     }
   }
 
+  /**
+   * Constructs a PneumaticsBaseSim with the given index.
+   *
+   * @param index The index.
+   */
   protected PneumaticsBaseSim(int index) {
     m_index = index;
   }
 
+  /**
+   * Constructs a PneumaticsBaseSim for the given module.
+   *
+   * @param module The module.
+   */
   protected PneumaticsBaseSim(PneumaticsBase module) {
     this(module.getModuleNumber());
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RoboRioSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RoboRioSim.java
index 8b28b6d..e2bc895 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RoboRioSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RoboRioSim.java
@@ -6,6 +6,8 @@
 
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.hal.simulation.RoboRioDataJNI;
+import edu.wpi.first.wpilibj.RobotController;
+import edu.wpi.first.wpilibj.RobotController.RadioLEDState;
 
 /** A utility class to control a simulated RoboRIO. */
 public final class RoboRioSim {
@@ -625,6 +627,38 @@
     RoboRioDataJNI.setComments(comments);
   }
 
+  /**
+   * Register a callback to be run whenever the Radio led state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to call the callback with the initial state
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public static CallbackStore registerRadioLEDStateCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = RoboRioDataJNI.registerRadioLEDStateCallback(callback, initialNotify);
+    return new CallbackStore(uid, RoboRioDataJNI::cancelRadioLEDStateCallback);
+  }
+
+  /**
+   * Get the state of the radio led.
+   *
+   * @return The state of the radio led.
+   */
+  public static RadioLEDState getRadioLEDState() {
+    return RadioLEDState.fromValue(RoboRioDataJNI.getRadioLEDState());
+  }
+
+  /**
+   * Set the state of the radio led.
+   *
+   * @param state The state of the radio led.
+   */
+  public static void setRadioLEDState(RobotController.RadioLEDState state) {
+    RoboRioDataJNI.setRadioLEDState(state.value);
+  }
+
   /** Reset all simulation data. */
   public static void resetData() {
     RoboRioDataJNI.resetData();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPISim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPISim.java
index 6c0b9e9..8ce9554 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPISim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPISim.java
@@ -74,6 +74,13 @@
     return new CallbackStore(m_index, uid, SPIDataJNI::cancelWriteCallback);
   }
 
+  /**
+   * Register a callback to be run whenever an auto receive buffer is received.
+   *
+   * @param callback the callback
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerReadAutoReceiveBufferCallback(
       SpiReadAutoReceiveBufferCallback callback) {
     int uid = SPIDataJNI.registerReadAutoReceiveBufferCallback(m_index, callback);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimHooks.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimHooks.java
index 1664081..f637b01 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimHooks.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimHooks.java
@@ -6,6 +6,7 @@
 
 import edu.wpi.first.hal.simulation.SimulatorJNI;
 
+/** Simulation hooks. */
 public final class SimHooks {
   private SimHooks() {}
 
@@ -18,14 +19,21 @@
     SimulatorJNI.setRuntimeType(type);
   }
 
+  /** Waits until the user program has started. */
   public static void waitForProgramStart() {
     SimulatorJNI.waitForProgramStart();
   }
 
+  /** Sets that the user program has started. */
   public static void setProgramStarted() {
     SimulatorJNI.setProgramStarted();
   }
 
+  /**
+   * Returns true if the user program has started.
+   *
+   * @return True if the user program has started.
+   */
   public static boolean getProgramStarted() {
     return SimulatorJNI.getProgramStarted();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java
index af2ee85..46e0441 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSim.java
@@ -49,6 +49,7 @@
    * @param startingAngleRads The initial position of the Arm simulation in radians.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
+  @SuppressWarnings("this-escape")
   public SingleJointedArmSim(
       LinearSystem<N2, N1, N1> plant,
       DCMotor gearbox,
@@ -180,7 +181,7 @@
    * @param angleRadians The new angle in radians.
    * @param velocityRadPerSec The new angular velocity in radians per second.
    */
-  public void setState(double angleRadians, double velocityRadPerSec) {
+  public final void setState(double angleRadians, double velocityRadPerSec) {
     setState(
         VecBuilder.fill(MathUtil.clamp(angleRadians, m_minAngle, m_maxAngle), velocityRadPerSec));
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/UltrasonicSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/UltrasonicSim.java
index dc4ebaa..2ad42e7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/UltrasonicSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/UltrasonicSim.java
@@ -36,14 +36,29 @@
     m_simRange = simDevice.getDouble("Range (in)");
   }
 
+  /**
+   * Sets if the range measurement is valid.
+   *
+   * @param valid True if valid
+   */
   public void setRangeValid(boolean valid) {
     m_simRangeValid.set(valid);
   }
 
+  /**
+   * Sets the range measurement.
+   *
+   * @param inches The range in inches.
+   */
   public void setRangeInches(double inches) {
     m_simRange.set(inches);
   }
 
+  /**
+   * Sets the range measurement.
+   *
+   * @param meters The range in meters.
+   */
   public void setRangeMeters(double meters) {
     m_simRange.set(Units.metersToInches(meters));
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/XboxControllerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/XboxControllerSim.java
index 41d200e..37e0bda 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/XboxControllerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/XboxControllerSim.java
@@ -13,6 +13,7 @@
    *
    * @param joystick controller to simulate
    */
+  @SuppressWarnings("this-escape")
   public XboxControllerSim(XboxController joystick) {
     super(joystick);
     setAxisCount(6);
@@ -25,6 +26,7 @@
    *
    * @param port port number
    */
+  @SuppressWarnings("this-escape")
   public XboxControllerSim(int port) {
     super(port);
     setAxisCount(6);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
index bfacb42..4b02c4f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
@@ -31,6 +31,7 @@
  */
 public class Field2d implements NTSendable, AutoCloseable {
   /** Constructor. */
+  @SuppressWarnings("this-escape")
   public Field2d() {
     FieldObject2d obj = new FieldObject2d("Robot");
     obj.setPose(new Pose2d());
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
index 9ffccde..0fecbb6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
@@ -10,6 +10,7 @@
 import edu.wpi.first.math.trajectory.Trajectory;
 import edu.wpi.first.networktables.DoubleArrayEntry;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /** Game field object on a Field2d. */
@@ -70,9 +71,7 @@
    */
   public synchronized void setPoses(List<Pose2d> poses) {
     m_poses.clear();
-    for (Pose2d pose : poses) {
-      m_poses.add(pose);
-    }
+    m_poses.addAll(poses);
     updateEntry();
   }
 
@@ -83,9 +82,7 @@
    */
   public synchronized void setPoses(Pose2d... poses) {
     m_poses.clear();
-    for (Pose2d pose : poses) {
-      m_poses.add(pose);
-    }
+    Collections.addAll(m_poses, poses);
     updateEntry();
   }
 
@@ -109,7 +106,7 @@
    */
   public synchronized List<Pose2d> getPoses() {
     updateFromEntry();
-    return new ArrayList<Pose2d>(m_poses);
+    return new ArrayList<>(m_poses);
   }
 
   void updateEntry() {
@@ -143,7 +140,7 @@
       return;
     }
 
-    double[] arr = m_entry.get((double[]) null);
+    double[] arr = m_entry.get(null);
     if (arr != null) {
       if ((arr.length % 3) != 0) {
         return;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/ListenerExecutor.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/ListenerExecutor.java
index 19a3fc1..9d0efa2 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/ListenerExecutor.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/ListenerExecutor.java
@@ -31,9 +31,9 @@
   /** Runs all posted tasks. Called periodically from main thread. */
   public void runListenerTasks() {
     // Locally copy tasks from internal list; minimizes blocking time
-    Collection<Runnable> tasks = new ArrayList<>();
+    Collection<Runnable> tasks;
     synchronized (m_lock) {
-      tasks.addAll(m_tasks);
+      tasks = new ArrayList<>(m_tasks);
       m_tasks.clear();
     }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
index 28484a3..ea7fda3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
@@ -82,7 +82,7 @@
    *
    * @param degrees the angle in degrees
    */
-  public synchronized void setAngle(double degrees) {
+  public final synchronized void setAngle(double degrees) {
     m_angle = degrees;
     if (m_angleEntry != null) {
       m_angleEntry.set(degrees);
@@ -115,7 +115,7 @@
    *
    * @param length the line length
    */
-  public synchronized void setLength(double length) {
+  public final synchronized void setLength(double length) {
     m_length = length;
     if (m_lengthEntry != null) {
       m_lengthEntry.set(length);
@@ -139,7 +139,7 @@
    *
    * @param color the color of the line
    */
-  public synchronized void setColor(Color8Bit color) {
+  public final synchronized void setColor(Color8Bit color) {
     m_color = String.format("#%02X%02X%02X", color.red, color.green, color.blue);
     if (m_colorEntry != null) {
       m_colorEntry.set(m_color);
@@ -177,7 +177,7 @@
    *
    * @param weight the line thickness
    */
-  public synchronized void setLineWeight(double weight) {
+  public final synchronized void setLineWeight(double weight) {
     m_weight = weight;
     if (m_weightEntry != null) {
       m_weightEntry.set(weight);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
index 754bd92..3dbcf5c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
@@ -74,6 +74,11 @@
    */
   protected abstract void updateEntries(NetworkTable table);
 
+  /**
+   * Retrieve the object's name.
+   *
+   * @return the object's name relative to its parent.
+   */
   public final String getName() {
     return m_name;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java
index 2c409e8..f15198f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java
@@ -57,6 +57,7 @@
 import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
+/** Implementation detail for SendableBuilder. */
 @SuppressWarnings("PMD.CompareObjectsWithEquals")
 public class SendableBuilderImpl implements NTSendableBuilder {
   @FunctionalInterface
@@ -109,6 +110,9 @@
 
   private final List<AutoCloseable> m_closeables = new ArrayList<>();
 
+  /** Default constructor. */
+  public SendableBuilderImpl() {}
+
   @Override
   @SuppressWarnings("PMD.AvoidCatchingGenericException")
   public void close() {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java
index 7f3b93c..e905ebc 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java
@@ -53,6 +53,7 @@
   private static final AtomicInteger s_instances = new AtomicInteger();
 
   /** Instantiates a {@link SendableChooser}. */
+  @SuppressWarnings("this-escape")
   public SendableChooser() {
     m_instance = s_instances.getAndIncrement();
     SendableRegistry.add(this, "SendableChooser", m_instance);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java
index 8a8f147..7da170b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java
@@ -4,6 +4,7 @@
 
 package edu.wpi.first.wpilibj.smartdashboard;
 
+import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.networktables.NetworkTable;
@@ -35,9 +36,10 @@
   /** The executor for listener tasks; calls listener tasks synchronously from main thread. */
   private static final ListenerExecutor listenerExecutor = new ListenerExecutor();
 
+  private static boolean m_reported = false; // NOPMD redundant field initializer
+
   static {
     setNetworkTableInstance(NetworkTableInstance.getDefault());
-    HAL.report(tResourceType.kResourceType_SmartDashboard, 0);
   }
 
   private SmartDashboard() {
@@ -64,6 +66,10 @@
    */
   @SuppressWarnings("PMD.CompareObjectsWithEquals")
   public static synchronized void putData(String key, Sendable data) {
+    if (!m_reported) {
+      HAL.report(tResourceType.kResourceType_SmartDashboard, tInstances.kSmartDashboard_Instance);
+      m_reported = true;
+    }
     Sendable sddata = tablesToData.get(key);
     if (sddata == null || sddata != data) {
       tablesToData.put(key, data);
@@ -114,6 +120,10 @@
    * @return Network table entry.
    */
   public static NetworkTableEntry getEntry(String key) {
+    if (!m_reported) {
+      HAL.report(tResourceType.kResourceType_SmartDashboard, tInstances.kSmartDashboard_Instance);
+      m_reported = true;
+    }
     return table.getEntry(key);
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/SysIdRoutineLog.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/SysIdRoutineLog.java
new file mode 100644
index 0000000..38bc46f
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/sysid/SysIdRoutineLog.java
@@ -0,0 +1,220 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.sysid;
+
+import static edu.wpi.first.units.Units.Amps;
+import static edu.wpi.first.units.Units.Meters;
+import static edu.wpi.first.units.Units.MetersPerSecond;
+import static edu.wpi.first.units.Units.Rotations;
+import static edu.wpi.first.units.Units.RotationsPerSecond;
+import static edu.wpi.first.units.Units.Second;
+import static edu.wpi.first.units.Units.Volts;
+
+import edu.wpi.first.units.Angle;
+import edu.wpi.first.units.Current;
+import edu.wpi.first.units.Distance;
+import edu.wpi.first.units.Measure;
+import edu.wpi.first.units.Velocity;
+import edu.wpi.first.units.Voltage;
+import edu.wpi.first.util.datalog.DoubleLogEntry;
+import edu.wpi.first.util.datalog.StringLogEntry;
+import edu.wpi.first.wpilibj.DataLogManager;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility for logging data from a SysId test routine. Each complete routine (quasistatic and
+ * dynamic, forward and reverse) should have its own SysIdRoutineLog instance, with a unique log
+ * name.
+ */
+public class SysIdRoutineLog {
+  private final Map<String, Map<String, DoubleLogEntry>> m_logEntries = new HashMap<>();
+  private final String m_logName;
+  private final StringLogEntry m_state;
+
+  /**
+   * Create a new logging utility for a SysId test routine.
+   *
+   * @param logName The name for the test routine in the log. Should be unique between complete test
+   *     routines (quasistatic and dynamic, forward and reverse). The current state of this test
+   *     (e.g. "quasistatic-forward") will appear in WPILog under the "sysid-test-state-logName"
+   *     entry.
+   */
+  public SysIdRoutineLog(String logName) {
+    m_logName = logName;
+    m_state = new StringLogEntry(DataLogManager.getLog(), "sysid-test-state-" + logName);
+    m_state.append(State.kNone.toString());
+  }
+
+  /** Possible state of a SysId routine. */
+  public enum State {
+    /** Quasistatic forward test. */
+    kQuasistaticForward("quasistatic-forward"),
+    /** Quasistatic reverse test. */
+    kQuasistaticReverse("quasistatic-reverse"),
+    /** Dynamic forward test. */
+    kDynamicForward("dynamic-forward"),
+    /** Dynamic reverse test. */
+    kDynamicReverse("dynamic-reverse"),
+    /** No test. */
+    kNone("none");
+
+    private final String m_state;
+
+    State(String state) {
+      m_state = state;
+    }
+
+    @Override
+    public String toString() {
+      return m_state;
+    }
+  }
+
+  /** Logs data from a single motor during a SysIdRoutine. */
+  public class MotorLog {
+    private final String m_motorName;
+
+    /**
+     * Create a new SysId motor log handle.
+     *
+     * @param motorName The name of the motor whose data is being logged.
+     */
+    private MotorLog(String motorName) {
+      m_motorName = motorName;
+      m_logEntries.put(motorName, new HashMap<>());
+    }
+
+    /**
+     * Log a generic data value from the motor.
+     *
+     * @param name The name of the data field being recorded.
+     * @param value The numeric value of the data field.
+     * @param unit The unit string of the data field.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog value(String name, double value, String unit) {
+      var motorEntries = m_logEntries.get(m_motorName);
+      var entry = motorEntries.get(name);
+
+      if (entry == null) {
+        var log = DataLogManager.getLog();
+
+        entry = new DoubleLogEntry(log, name + "-" + m_motorName + "-" + m_logName, unit);
+        motorEntries.put(name, entry);
+      }
+
+      entry.append(value);
+      return this;
+    }
+
+    /**
+     * Log the voltage applied to the motor.
+     *
+     * @param voltage The voltage to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog voltage(Measure<Voltage> voltage) {
+      return value("voltage", voltage.in(Volts), Volts.name());
+    }
+
+    /**
+     * Log the linear position of the motor.
+     *
+     * @param position The linear position to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog linearPosition(Measure<Distance> position) {
+      return value("position", position.in(Meters), Meters.name());
+    }
+
+    /**
+     * Log the angular position of the motor.
+     *
+     * @param position The angular position to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog angularPosition(Measure<Angle> position) {
+      return value("position", position.in(Rotations), Rotations.name());
+    }
+
+    /**
+     * Log the linear velocity of the motor.
+     *
+     * @param velocity The linear velocity to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog linearVelocity(Measure<Velocity<Distance>> velocity) {
+      return value("velocity", velocity.in(MetersPerSecond), MetersPerSecond.name());
+    }
+
+    /**
+     * Log the angular velocity of the motor.
+     *
+     * @param velocity The angular velocity to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog angularVelocity(Measure<Velocity<Angle>> velocity) {
+      return value("velocity", velocity.in(RotationsPerSecond), RotationsPerSecond.name());
+    }
+
+    /**
+     * Log the linear acceleration of the motor.
+     *
+     * @param acceleration The linear acceleration to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog linearAcceleration(Measure<Velocity<Velocity<Distance>>> acceleration) {
+      return value(
+          "position",
+          acceleration.in(MetersPerSecond.per(Second)),
+          MetersPerSecond.per(Second).name());
+    }
+
+    /**
+     * Log the angular acceleration of the motor.
+     *
+     * @param acceleration The angular acceleration to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog angularAcceleration(Measure<Velocity<Velocity<Angle>>> acceleration) {
+      return value(
+          "position",
+          acceleration.in(RotationsPerSecond.per(Second)),
+          RotationsPerSecond.per(Second).name());
+    }
+
+    /**
+     * Log the current applied to the motor.
+     *
+     * @param current The current to record.
+     * @return The motor log (for call chaining).
+     */
+    public MotorLog current(Measure<Current> current) {
+      value("current", current.in(Amps), Amps.name());
+      return this;
+    }
+  }
+
+  /**
+   * Log data from a motor during a SysId routine.
+   *
+   * @param motorName The name of the motor.
+   * @return Handle with chainable callbacks to log individual data fields.
+   */
+  public MotorLog motor(String motorName) {
+    return new MotorLog(motorName);
+  }
+
+  /**
+   * Records the current state of the SysId test routine. Should be called once per iteration during
+   * tests with the type of the current test, and once upon test end with state `none`.
+   *
+   * @param state The current state of the SysId test routine.
+   */
+  public void recordState(State state) {
+    m_state.append(state.toString());
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java
index 2defbd8..5060567 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java
@@ -14,13 +14,24 @@
  */
 @SuppressWarnings("MemberName")
 public class Color {
-  private static final double kPrecision = Math.pow(2, -12);
-
+  /** Red component (0-1). */
   public final double red;
+
+  /** Green component (0-1). */
   public final double green;
+
+  /** Blue component (0-1). */
   public final double blue;
+
   private String m_name;
 
+  /** Constructs a default color (black). */
+  public Color() {
+    red = 0.0;
+    green = 0.0;
+    blue = 0.0;
+  }
+
   /**
    * Constructs a Color from doubles.
    *
@@ -70,6 +81,22 @@
   }
 
   /**
+   * Constructs a Color from a hex string.
+   *
+   * @param hexString a string of the format <code>#RRGGBB</code>
+   * @throws IllegalArgumentException if the hex string is invalid.
+   */
+  public Color(String hexString) {
+    if (hexString.length() != 7 || !hexString.startsWith("#")) {
+      throw new IllegalArgumentException("Invalid hex string \"" + hexString + "\"");
+    }
+
+    this.red = Integer.valueOf(hexString.substring(1, 3), 16) / 255.0;
+    this.green = Integer.valueOf(hexString.substring(3, 5), 16) / 255.0;
+    this.blue = Integer.valueOf(hexString.substring(5, 7), 16) / 255.0;
+  }
+
+  /**
    * Creates a Color from HSV values.
    *
    * @param h The h value [0-180)
@@ -155,8 +182,7 @@
   }
 
   private static double roundAndClamp(double value) {
-    final var rounded = Math.round((value + kPrecision / 2) / kPrecision) * kPrecision;
-    return MathUtil.clamp(rounded, 0.0, 1.0);
+    return MathUtil.clamp(Math.ceil(value * (1 << 12)) / (1 << 12), 0.0, 1.0);
   }
 
   /*
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java
index 3260673..132ea64 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java
@@ -10,10 +10,22 @@
 /** Represents colors with 8 bits of precision. */
 @SuppressWarnings("MemberName")
 public class Color8Bit {
+  /** Red component (0-255). */
   public final int red;
+
+  /** Green component (0-255). */
   public final int green;
+
+  /** Blue component (0-255). */
   public final int blue;
 
+  /** Constructs a default color (black). */
+  public Color8Bit() {
+    red = 0;
+    green = 0;
+    blue = 0;
+  }
+
   /**
    * Constructs a Color8Bit.
    *
@@ -36,6 +48,22 @@
     this((int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255));
   }
 
+  /**
+   * Constructs a Color8Bit from a hex string.
+   *
+   * @param hexString a string of the format <code>#RRGGBB</code>
+   * @throws IllegalArgumentException if the hex string is invalid.
+   */
+  public Color8Bit(String hexString) {
+    if (hexString.length() != 7 || !hexString.startsWith("#")) {
+      throw new IllegalArgumentException("Invalid hex string \"" + hexString + "\"");
+    }
+
+    this.red = Integer.valueOf(hexString.substring(1, 3), 16);
+    this.green = Integer.valueOf(hexString.substring(3, 5), 16);
+    this.blue = Integer.valueOf(hexString.substring(5, 7), 16);
+  }
+
   @Override
   public boolean equals(Object other) {
     if (this == other) {
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
index c1c5f3c..920fb54 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
@@ -144,11 +144,7 @@
   void disabledModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
@@ -231,11 +227,7 @@
   void autonomousModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
@@ -320,11 +312,7 @@
   void teleopModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
@@ -411,11 +399,7 @@
     MockRobot robot = new MockRobot();
     robot.enableLiveWindowInTest(isLW);
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
@@ -531,11 +515,7 @@
   void modeChangeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     // Start in disabled
@@ -655,17 +635,9 @@
     MockRobot robot = new MockRobot();
 
     final AtomicInteger callbackCount = new AtomicInteger(0);
-    robot.addPeriodic(
-        () -> {
-          callbackCount.addAndGet(1);
-        },
-        kPeriod / 2.0);
+    robot.addPeriodic(() -> callbackCount.addAndGet(1), kPeriod / 2.0);
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
@@ -704,12 +676,7 @@
     MockRobot robot = new MockRobot();
 
     final AtomicInteger callbackCount = new AtomicInteger(0);
-    robot.addPeriodic(
-        () -> {
-          callbackCount.addAndGet(1);
-        },
-        kPeriod / 2.0,
-        kPeriod / 4.0);
+    robot.addPeriodic(() -> callbackCount.addAndGet(1), kPeriod / 2.0, kPeriod / 4.0);
 
     // Expirations in this test (ms)
     //
@@ -720,11 +687,7 @@
     //     p |    0.75p
     //    2p |    1.25p
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java
index ee4860d..5cff0d0 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java
@@ -54,22 +54,10 @@
     // | RobotPeriodic() |           0 |              2 |
     // | Callback 1      |           2 |            0.5 |
     // | Callback 2      |         2.5 |              1 |
-    robot.schedule(
-        () -> {
-          callbackCount1.addAndGet(1);
-        },
-        0.0005);
-    robot.schedule(
-        () -> {
-          callbackCount2.addAndGet(1);
-        },
-        0.001);
+    robot.schedule(() -> callbackCount1.addAndGet(1), 0.0005);
+    robot.schedule(() -> callbackCount2.addAndGet(1), 0.001);
 
-    Thread robotThread =
-        new Thread(
-            () -> {
-              robot.startCompetition();
-            });
+    Thread robotThread = new Thread(robot::startCompetition);
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java
index c1676e0..5e9e122 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java
@@ -33,12 +33,7 @@
   void enableDisableTest() {
     final AtomicInteger watchdogCounter = new AtomicInteger(0);
 
-    try (Watchdog watchdog =
-        new Watchdog(
-            0.4,
-            () -> {
-              watchdogCounter.addAndGet(1);
-            })) {
+    try (Watchdog watchdog = new Watchdog(0.4, () -> watchdogCounter.addAndGet(1))) {
       // Run 1
       watchdog.enable();
       SimHooks.stepTiming(0.2);
@@ -71,12 +66,7 @@
   void resetTest() {
     final AtomicInteger watchdogCounter = new AtomicInteger(0);
 
-    try (Watchdog watchdog =
-        new Watchdog(
-            0.4,
-            () -> {
-              watchdogCounter.addAndGet(1);
-            })) {
+    try (Watchdog watchdog = new Watchdog(0.4, () -> watchdogCounter.addAndGet(1))) {
       watchdog.enable();
       SimHooks.stepTiming(0.2);
       watchdog.reset();
@@ -92,12 +82,7 @@
   void setTimeoutTest() {
     final AtomicInteger watchdogCounter = new AtomicInteger(0);
 
-    try (Watchdog watchdog =
-        new Watchdog(
-            1.0,
-            () -> {
-              watchdogCounter.addAndGet(1);
-            })) {
+    try (Watchdog watchdog = new Watchdog(1.0, () -> watchdogCounter.addAndGet(1))) {
       watchdog.enable();
       SimHooks.stepTiming(0.2);
       watchdog.setTimeout(0.2);
@@ -137,12 +122,7 @@
   void epochsTest() {
     final AtomicInteger watchdogCounter = new AtomicInteger(0);
 
-    try (Watchdog watchdog =
-        new Watchdog(
-            0.4,
-            () -> {
-              watchdogCounter.addAndGet(1);
-            })) {
+    try (Watchdog watchdog = new Watchdog(0.4, () -> watchdogCounter.addAndGet(1))) {
       // Run 1
       watchdog.enable();
       watchdog.addEpoch("Epoch 1");
@@ -173,18 +153,8 @@
     final AtomicInteger watchdogCounter1 = new AtomicInteger(0);
     final AtomicInteger watchdogCounter2 = new AtomicInteger(0);
 
-    try (Watchdog watchdog1 =
-            new Watchdog(
-                0.2,
-                () -> {
-                  watchdogCounter1.addAndGet(1);
-                });
-        Watchdog watchdog2 =
-            new Watchdog(
-                0.6,
-                () -> {
-                  watchdogCounter2.addAndGet(1);
-                })) {
+    try (Watchdog watchdog1 = new Watchdog(0.2, () -> watchdogCounter1.addAndGet(1));
+        Watchdog watchdog2 = new Watchdog(0.6, () -> watchdogCounter2.addAndGet(1))) {
       watchdog2.enable();
       SimHooks.stepTiming(0.25);
       assertEquals(0, watchdogCounter1.get(), "Watchdog triggered early");
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DifferentialDriveTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DifferentialDriveTest.java
index 4675a7f..474dc89 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DifferentialDriveTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DifferentialDriveTest.java
@@ -6,7 +6,7 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import edu.wpi.first.wpilibj.motorcontrol.MockMotorController;
+import edu.wpi.first.wpilibj.motorcontrol.MockPWMMotorController;
 import org.junit.jupiter.api.Test;
 
 class DifferentialDriveTest {
@@ -250,9 +250,9 @@
 
   @Test
   void testArcadeDrive() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -288,9 +288,9 @@
 
   @Test
   void testArcadeDriveSquared() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -326,9 +326,9 @@
 
   @Test
   void testCurvatureDrive() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -364,9 +364,9 @@
 
   @Test
   void testCurvatureDriveTurnInPlace() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -402,9 +402,9 @@
 
   @Test
   void testTankDrive() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -440,9 +440,9 @@
 
   @Test
   void testTankDriveSquared() {
-    var left = new MockMotorController();
-    var right = new MockMotorController();
-    var drive = new DifferentialDrive(left, right);
+    var left = new MockPWMMotorController();
+    var right = new MockPWMMotorController();
+    var drive = new DifferentialDrive(left::set, right::set);
     drive.setDeadband(0.0);
 
     // Forward
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/MecanumDriveTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/MecanumDriveTest.java
index 034ba10..a1017d5 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/MecanumDriveTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/MecanumDriveTest.java
@@ -7,7 +7,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import edu.wpi.first.math.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.motorcontrol.MockMotorController;
+import edu.wpi.first.wpilibj.motorcontrol.MockPWMMotorController;
 import org.junit.jupiter.api.Test;
 
 class MecanumDriveTest {
@@ -89,11 +89,11 @@
 
   @Test
   void testCartesian() {
-    var fl = new MockMotorController();
-    var fr = new MockMotorController();
-    var rl = new MockMotorController();
-    var rr = new MockMotorController();
-    var drive = new MecanumDrive(fl, rl, fr, rr);
+    var fl = new MockPWMMotorController();
+    var rl = new MockPWMMotorController();
+    var fr = new MockPWMMotorController();
+    var rr = new MockPWMMotorController();
+    var drive = new MecanumDrive(fl::set, rl::set, fr::set, rr::set);
     drive.setDeadband(0.0);
 
     // Forward
@@ -134,11 +134,11 @@
 
   @Test
   void testCartesianGyro90CW() {
-    var fl = new MockMotorController();
-    var fr = new MockMotorController();
-    var rl = new MockMotorController();
-    var rr = new MockMotorController();
-    var drive = new MecanumDrive(fl, rl, fr, rr);
+    var fl = new MockPWMMotorController();
+    var rl = new MockPWMMotorController();
+    var fr = new MockPWMMotorController();
+    var rr = new MockPWMMotorController();
+    var drive = new MecanumDrive(fl::set, rl::set, fr::set, rr::set);
     drive.setDeadband(0.0);
 
     // Forward in global frame; left in robot frame
@@ -179,11 +179,11 @@
 
   @Test
   void testPolar() {
-    var fl = new MockMotorController();
-    var fr = new MockMotorController();
-    var rl = new MockMotorController();
-    var rr = new MockMotorController();
-    var drive = new MecanumDrive(fl, rl, fr, rr);
+    var fl = new MockPWMMotorController();
+    var rl = new MockPWMMotorController();
+    var fr = new MockPWMMotorController();
+    var rr = new MockPWMMotorController();
+    var drive = new MecanumDrive(fl::set, rl::set, fr::set, rr::set);
     drive.setDeadband(0.0);
 
     // Forward
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/EventLoopTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/EventLoopTest.java
index ea3c569..644c18e 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/EventLoopTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/EventLoopTest.java
@@ -5,7 +5,9 @@
 package edu.wpi.first.wpilibj.event;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.ConcurrentModificationException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.jupiter.api.Test;
@@ -58,4 +60,33 @@
     // shouldn't change
     assertEquals(1, counter.get());
   }
+
+  @Test
+  void testConcurrentModification() {
+    var loop = new EventLoop();
+
+    loop.bind(
+        () -> {
+          assertThrows(
+              ConcurrentModificationException.class,
+              () -> {
+                loop.bind(() -> {});
+              });
+        });
+
+    loop.poll();
+
+    loop.clear();
+
+    loop.bind(
+        () -> {
+          assertThrows(
+              ConcurrentModificationException.class,
+              () -> {
+                loop.clear();
+              });
+        });
+
+    loop.poll();
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockMotorController.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockMotorController.java
index e7a6b8e..4ac3dc8 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockMotorController.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockMotorController.java
@@ -4,6 +4,7 @@
 
 package edu.wpi.first.wpilibj.motorcontrol;
 
+@SuppressWarnings("removal")
 public class MockMotorController implements MotorController {
   private double m_speed;
   private boolean m_isInverted;
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockPWMMotorController.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockPWMMotorController.java
new file mode 100644
index 0000000..bf2c287
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockPWMMotorController.java
@@ -0,0 +1,34 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.motorcontrol;
+
+public class MockPWMMotorController {
+  private double m_speed;
+  private boolean m_isInverted;
+
+  public void set(double speed) {
+    m_speed = m_isInverted ? -speed : speed;
+  }
+
+  public double get() {
+    return m_speed;
+  }
+
+  public void setInverted(boolean isInverted) {
+    m_isInverted = isInverted;
+  }
+
+  public boolean getInverted() {
+    return m_isInverted;
+  }
+
+  public void disable() {
+    m_speed = 0;
+  }
+
+  public void stopMotor() {
+    disable();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroupTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroupTest.java
index 6efb3de..a8f71f6 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroupTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroupTest.java
@@ -15,6 +15,7 @@
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
+@SuppressWarnings("removal")
 class MotorControllerGroupTest {
   private static Stream<Arguments> motorControllerArguments() {
     return IntStream.of(1, 2, 3)
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java
index 04397c2..a630c3e 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/MockActuatorSendable.java
@@ -10,6 +10,7 @@
 
 /** A mock sendable that marks itself as an actuator. */
 public class MockActuatorSendable implements Sendable {
+  @SuppressWarnings("this-escape")
   public MockActuatorSendable(String name) {
     SendableRegistry.add(this, name);
   }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java
index 6283c68..25b545b 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java
@@ -136,7 +136,7 @@
     try (StringSubscriber subscriber =
         m_ntInstance
             .getStringTopic("/Shuffleboard/.metadata/Selected")
-            .subscribe("", PubSubOption.keepDuplicates(true)); ) {
+            .subscribe("", PubSubOption.keepDuplicates(true))) {
       listener =
           m_ntInstance.addListener(
               subscriber,
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSimTest.java
index 096f77e..4ddadf1 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSimTest.java
@@ -79,7 +79,7 @@
     BufferCallback callback = new BufferCallback();
 
     try (AddressableLED led = new AddressableLED(0);
-        CallbackStore cb = sim.registerDataCallback(callback); ) {
+        CallbackStore cb = sim.registerDataCallback(callback)) {
       assertFalse(sim.getRunning());
 
       assertEquals(1, sim.getLength()); // Defaults to 1 led
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DCMotorSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DCMotorSimTest.java
index fbe3af0..681e6b4 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DCMotorSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DCMotorSimTest.java
@@ -63,7 +63,7 @@
 
     try (var motor = new PWMVictorSPX(0);
         var encoder = new Encoder(0, 1);
-        var controller = new PIDController(0.04, 0.0, 0.001); ) {
+        var controller = new PIDController(0.04, 0.0, 0.001)) {
       var encoderSim = new EncoderSim(encoder);
       encoderSim.resetData();
 
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSimTest.java
index 5baed3a..cbb8d18 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DifferentialDrivetrainSimTest.java
@@ -76,11 +76,11 @@
       sim.update(0.020);
 
       // Update our ground truth
-      groundTruthX = NumericalIntegration.rk4(sim::getDynamics, groundTruthX, voltages, 0.020);
+      groundTruthX = NumericalIntegration.rkdp(sim::getDynamics, groundTruthX, voltages, 0.020);
     }
 
     // 2 inch tolerance is OK since our ground truth is an approximation of the
-    // ODE solution using RK4 anyway
+    // ODE solution using RKDP anyway
     assertEquals(
         groundTruthX.get(DifferentialDrivetrainSim.State.kX.value, 0),
         sim.getState(DifferentialDrivetrainSim.State.kX),
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSimTest.java
index 9f29aac..8a0fc7c 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSimTest.java
@@ -25,7 +25,7 @@
       BooleanCallback initializeCallback = new BooleanCallback();
       DoubleCallback dutyCycleCallback = new DoubleCallback();
       try (CallbackStore initCb = sim.registerInitializedCallback(initializeCallback, false);
-          CallbackStore dutyCycleCb = sim.registerDutyCycleCallback(dutyCycleCallback, false); ) {
+          CallbackStore dutyCycleCb = sim.registerDutyCycleCallback(dutyCycleCallback, false)) {
         final double kTestDutyCycle = 0.191;
         output.enablePWM(kTestDutyCycle);
 
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RelaySimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RelaySimTest.java
index 3918361..c7538cc 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RelaySimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RelaySimTest.java
@@ -58,7 +58,7 @@
 
     try (CallbackStore fwdCb = sim.registerInitializedForwardCallback(forwardCallback, false);
         CallbackStore revCb = sim.registerInitializedReverseCallback(reverseCallback, false);
-        Relay relay = new Relay(0, Relay.Direction.kForward); ) {
+        Relay relay = new Relay(0, Relay.Direction.kForward)) {
       assertTrue(sim.getInitializedForward());
       assertFalse(sim.getInitializedReverse());
 
@@ -84,7 +84,7 @@
 
     try (CallbackStore fwdCb = sim.registerInitializedForwardCallback(forwardCallback, false);
         CallbackStore revCb = sim.registerInitializedReverseCallback(reverseCallback, false);
-        Relay relay = new Relay(0, Relay.Direction.kReverse); ) {
+        Relay relay = new Relay(0, Relay.Direction.kReverse)) {
       assertFalse(sim.getInitializedForward());
       assertTrue(sim.getInitializedReverse());
 
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RoboRioSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RoboRioSimTest.java
index c016ec4..01029cd 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RoboRioSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RoboRioSimTest.java
@@ -10,8 +10,10 @@
 
 import edu.wpi.first.hal.HALUtil;
 import edu.wpi.first.wpilibj.RobotController;
+import edu.wpi.first.wpilibj.RobotController.RadioLEDState;
 import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
 import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.EnumCallback;
 import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
 import org.junit.jupiter.api.Test;
 
@@ -279,4 +281,26 @@
     assertEquals(kCommentsTruncated, RoboRioSim.getComments());
     assertEquals(kCommentsTruncated, RobotController.getComments());
   }
+
+  @Test
+  void testRadioLEDState() {
+    RoboRioSim.resetData();
+
+    EnumCallback callback = new EnumCallback();
+    try (CallbackStore cb = RoboRioSim.registerRadioLEDStateCallback(callback, false)) {
+      RobotController.setRadioLEDState(RadioLEDState.kGreen);
+      assertTrue(callback.wasTriggered());
+      assertEquals(RadioLEDState.kGreen.value, callback.getSetValue());
+      assertEquals(RadioLEDState.kGreen, RoboRioSim.getRadioLEDState());
+      assertEquals(RadioLEDState.kGreen, RobotController.getRadioLEDState());
+
+      callback.reset();
+
+      RoboRioSim.setRadioLEDState(RadioLEDState.kOrange);
+      assertTrue(callback.wasTriggered());
+      assertEquals(RadioLEDState.kOrange.value, callback.getSetValue());
+      assertEquals(RadioLEDState.kOrange, RoboRioSim.getRadioLEDState());
+      assertEquals(RadioLEDState.kOrange, RobotController.getRadioLEDState());
+    }
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SimDeviceSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SimDeviceSimTest.java
index 62a24d1..89b4333 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SimDeviceSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SimDeviceSimTest.java
@@ -41,18 +41,10 @@
         SimDevice dev1 = SimDevice.create("testDC1")) {
       try (CallbackStore callback1 =
               SimDeviceSim.registerDeviceCreatedCallback(
-                  "testDC",
-                  (name, handle) -> {
-                    callback1Counter.addAndGet(1);
-                  },
-                  false);
+                  "testDC", (name, handle) -> callback1Counter.addAndGet(1), false);
           CallbackStore callback2 =
               SimDeviceSim.registerDeviceCreatedCallback(
-                  "testDC",
-                  (name, handle) -> {
-                    callback2Counter.addAndGet(1);
-                  },
-                  true)) {
+                  "testDC", (name, handle) -> callback2Counter.addAndGet(1), true)) {
         assertEquals(0, callback1Counter.get(), "Callback 1 called early");
         assertEquals(
             1,
@@ -82,11 +74,7 @@
     SimDevice dev1 = SimDevice.create("testDF1");
     try (CallbackStore callback =
         SimDeviceSim.registerDeviceFreedCallback(
-            "testDF",
-            (name, handle) -> {
-              counter.addAndGet(1);
-            },
-            false)) {
+            "testDF", (name, handle) -> counter.addAndGet(1), false)) {
       assertEquals(0, counter.get(), "Callback called early");
       dev1.close();
       assertEquals(1, counter.get(), "Callback called either more than once or not at all");
@@ -108,16 +96,10 @@
       SimDeviceSim sim = new SimDeviceSim("testVM1");
       try (CallbackStore callback1 =
               sim.registerValueCreatedCallback(
-                  (name, handle, readonly, value) -> {
-                    callback1Counter.addAndGet(1);
-                  },
-                  false);
+                  (name, handle, readonly, value) -> callback1Counter.addAndGet(1), false);
           CallbackStore callback2 =
               sim.registerValueCreatedCallback(
-                  (name, handle, readonly, value) -> {
-                    callback2Counter.addAndGet(1);
-                  },
-                  true)) {
+                  (name, handle, readonly, value) -> callback2Counter.addAndGet(1), true)) {
         assertEquals(0, callback1Counter.get(), "Callback 1 called early");
         assertEquals(
             1,
@@ -148,18 +130,10 @@
       SimValue simVal = sim.getValue("v1");
       try (CallbackStore callback1 =
               sim.registerValueChangedCallback(
-                  simVal,
-                  (name, handle, readonly, value) -> {
-                    callback1Counter.addAndGet(1);
-                  },
-                  false);
+                  simVal, (name, handle, readonly, value) -> callback1Counter.addAndGet(1), false);
           CallbackStore callback2 =
               sim.registerValueChangedCallback(
-                  simVal,
-                  (name, handle, readonly, value) -> {
-                    callback2Counter.addAndGet(1);
-                  },
-                  true)) {
+                  simVal, (name, handle, readonly, value) -> callback2Counter.addAndGet(1), true)) {
         assertEquals(0, callback1Counter.get(), "Callback 1 called early");
         assertEquals(
             1,
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java
index 07f09af..d97d54c 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java
@@ -75,7 +75,7 @@
         chooser.addOption(String.valueOf(i), i);
       }
       AtomicInteger currentVal = new AtomicInteger();
-      chooser.onChange(val -> currentVal.set(val));
+      chooser.onChange(currentVal::set);
 
       SmartDashboard.putData("changeListenerChooser", chooser);
       SmartDashboard.updateValues();
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java
new file mode 100644
index 0000000..499ecbd
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java
@@ -0,0 +1,65 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class Color8BitTest {
+  @Test
+  void testConstructDefault() {
+    var color = new Color8Bit();
+
+    assertEquals(0, color.red);
+    assertEquals(0, color.green);
+    assertEquals(0, color.blue);
+  }
+
+  @Test
+  void testConstructFromInts() {
+    var color = new Color8Bit(255, 128, 64);
+
+    assertEquals(255, color.red);
+    assertEquals(128, color.green);
+    assertEquals(64, color.blue);
+  }
+
+  @Test
+  void testConstructFromColor() {
+    var color = new Color8Bit(new Color(255, 128, 64));
+
+    assertEquals(255, color.red);
+    assertEquals(128, color.green);
+    assertEquals(64, color.blue);
+  }
+
+  @Test
+  void testConstructFromHexString() {
+    var color = new Color8Bit("#FF8040");
+
+    assertEquals(255, color.red);
+    assertEquals(128, color.green);
+    assertEquals(64, color.blue);
+
+    // No leading #
+    assertThrows(IllegalArgumentException.class, () -> new Color8Bit("112233"));
+
+    // Too long
+    assertThrows(IllegalArgumentException.class, () -> new Color8Bit("#11223344"));
+
+    // Invalid hex characters
+    assertThrows(IllegalArgumentException.class, () -> new Color8Bit("#$$$$$$"));
+  }
+
+  @Test
+  void testToHexString() {
+    var color = new Color8Bit(255, 128, 64);
+
+    assertEquals("#FF8040", color.toHexString());
+    assertEquals("#FF8040", color.toString());
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java
new file mode 100644
index 0000000..1b32afd
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class ColorTest {
+  @Test
+  void testConstructDefault() {
+    var color = new Color();
+
+    assertEquals(0.0, color.red);
+    assertEquals(0.0, color.green);
+    assertEquals(0.0, color.blue);
+  }
+
+  @Test
+  void testConstructFromDoubles() {
+    {
+      var color = new Color(1.0, 0.5, 0.25);
+
+      assertEquals(1.0, color.red, 1e-2);
+      assertEquals(0.5, color.green, 1e-2);
+      assertEquals(0.25, color.blue, 1e-2);
+    }
+
+    {
+      var color = new Color(1.0, 0.0, 0.0);
+
+      // Check for exact match to ensure round-and-clamp is correct
+      assertEquals(1.0, color.red);
+      assertEquals(0.0, color.green);
+      assertEquals(0.0, color.blue);
+    }
+  }
+
+  @Test
+  void testConstructFromInts() {
+    var color = new Color(255, 128, 64);
+
+    assertEquals(1.0, color.red, 1e-2);
+    assertEquals(0.5, color.green, 1e-2);
+    assertEquals(0.25, color.blue, 1e-2);
+  }
+
+  @Test
+  void testConstructFromHexString() {
+    var color = new Color("#FF8040");
+
+    assertEquals(1.0, color.red, 1e-2);
+    assertEquals(0.5, color.green, 1e-2);
+    assertEquals(0.25, color.blue, 1e-2);
+
+    // No leading #
+    assertThrows(IllegalArgumentException.class, () -> new Color("112233"));
+
+    // Too long
+    assertThrows(IllegalArgumentException.class, () -> new Color("#11223344"));
+
+    // Invalid hex characters
+    assertThrows(IllegalArgumentException.class, () -> new Color("#$$$$$$"));
+  }
+
+  @Test
+  void testFromHSV() {
+    var color = Color.fromHSV(90, 128, 64);
+
+    assertEquals(0.125732421875, color.red);
+    assertEquals(0.251220703125, color.green);
+    assertEquals(0.251220703125, color.blue);
+  }
+
+  @Test
+  void testToHexString() {
+    var color = new Color(255, 128, 64);
+
+    assertEquals("#FF8040", color.toHexString());
+    assertEquals("#FF8040", color.toString());
+  }
+}