Squashed 'third_party/allwpilib/' changes from 66b57f032..e473a00f9

e473a00f9 [wpiutil] Base64: Add unsigned span/vector variants (#3702)
52f2d580e [wpiutil] raw_uv_ostream: Add reset() (#3701)
d7b1e3576 [wpiutil] WebSocket: move std::function (#3700)
93799fbe9 [examples] Fix description of TrapezoidProfileSubsystem (#3699)
b84644740 [wpimath] Document pose estimator states, inputs, and outputs (#3698)
2dc35c139 [wpimath] Fix classpaths for JNI class loads (#3697)
2cb171f6f [docs] Set Doxygen extract_all to true and fix Doxygen failures (#3695)
a939cd9c8 [wpimath] Print uncontrollable/unobservable models in LQR and KF (#3694)
d5270d113 [wpimath] Clean up C++ StateSpaceUtil tests (#3692)
b20903960 [wpimath] Remove redundant discretization tests from StateSpaceUtilTest (#3689)
c0cb545b4 [wpilibc] Add deprecated Doxygen attribute to SpeedController (#3691)
35c9f66a7 [wpilib] Rename PneumaticsHub to PneumaticHub (#3686)
796d03d10 [wpiutil] Remove unused LLVM header (#3688)
8723caf78 [wpilibj] Make Java TrapezoidProfile.Constraints an immutable class (#3687)
187f50a34 [wpimath] Catch incorrect parameters to state-space models earlier (#3680)
8d04606c4 Replace instances of frc-characterization with SysId (NFC) (#3681)
b82d4f6e5 [hal, cscore, ntcore] Use WPI common handle type base
87e34967e [wpiutil] Add synchronization primitives
e32499c54 [wpiutil] Add ParallelTcpConnector (#3655)
aa0b49228 [wpilib] Remove redundant "quick turn" docs for curvature drive (NFC) (#3674)
57301a7f9 [hal] REVPH: Start closed-loop compressor control on init (#3673)
d1842ea8f [wpilib] Improve interrupt docs (NFC) (#3679)
558151061 [wpiutil] Add DsClient (#3654)
181723e57 Replace `.to<double>()` and `.template to<double>()` with `.value()` (#3667)
6bc1db44b [commands] Add pointer overload of AddRequirements (artf6003) (#3669)
737b57ed5 [wpimath] Update to drake v0.35.0 (#3665)
4d287d1ae [build] Upgrade WPIJREArtifact to JRE 2022-11.0.12u5 (#3666)
f26eb5ada [hal] Fix another typo (get -> gets) (NFC) (#3663)
94ed275ba [hal] Fix misspelling (numer -> number) (NFC) (#3662)
ac2f44da3 [wpiutil] uv: use move for std::function (#3653)
75fa1fbfb [wpiutil] json::serializer: Optimize construction (#3647)
5e689faea [wpiutil] Import MessagePack implementation (mpack) (#3650)
649a50b40 [wpiutil] Add LEB128 byte-by-byte reader (#3652)
e94397a97 [wpiutil] Move json_serializer.h to public headers (#3646)
4ec58724d [wpiutil] uv::Tcp: Clarify SetNoDelay documentation (#3649)
8cb294aa4 [wpiutil] WebSocket: Make Shutdown() public (#3651)
2b3a9a52b [wpiutil] json: Fix map iterator key() for std::string_view (#3645)
138cbb94b [wpiutil] uv::Async: Add direct call for no-parameter specialization (#3648)
e56d6dea8 [ci] Update testbench pool image to ubuntu-latest (#3643)
43f30e44e [build] Enable comments in doxygen source files (#3644)
9e6db17ef [build] Enable doxygen preprocessor expansion of WPI_DEPRECATED (#3642)
0e631ad2f Add WPILib version to issue template (#3641)
6229d8d2f [build] Docs: set case_sense_names to false (#3392)
4647d09b5 [docs] Fix Doxygen warnings, add CI docs lint job (#3639)
4ad3a5402 [hal] Fix PWM allocation channel (#3637)
05e5feac4 [docs] Fix brownout docs (NFC) (#3638)
67df469c5 [examples] Remove old command-based templates and examples (#3263)
689e9ccfb [hal, wpilib] Add brownout voltage configuration (#3632)
9cd4bc407 [docs] Add useLocal to avoid using installer artifacts (#3634)
61996c2bb [cscore] Fix Java direct callback notifications (#3631)
6d3dd99eb [build] Update to newest native-utils (#3633)
f0b484892 [wpiutil] Fix StringMap iterator equality check (#3629)
8352cbb7a Update development build instructions for 2022 (#3616)
6da08b71d [examples] Fix Intermediate Vision Java Example description (#3628)
5d99059bf [wpiutil] Remove optional.h (#3627)
fa41b106a [glass, wpiutil] Add missing format args (#3626)
4e3fd7d42 [build] Enable Zc:__cplusplus for Windows (#3625)
791d8354d [build] Suppress deprecation/removal warnings for old commands (#3618)
10f19e6fc [hal, wpilib] Add REV PneumaticsHub (#3600)
4c61a1305 [ntcore] Revert to per-element copy for toNative() (#3621)
7b3f62244 [wpiutil] SendableRegistry: Print exception stacktrace (#3620)
d347928e4 [hal] Use better error for when console out is enabled while attempting to use onboard serial port (#3622)
cc31079a1 [hal] Use setcap instead of setuid for setting thread priorities (#3613)
4676648b7 [wpimath] Upgrade to Drake v0.34.0 (#3607)
c7594c911 [build] Allow building wpilibc in cmake without cscore and opencv (#3605)
173cb7359 [wpilib] Add TimesliceRobot (#3502)
af295879f [hal] Set error status for I2C port out of range (#3603)
95dd20a15 [build] Enable spotbugs (#3601)
b65fce86b [wpilib] Remove Timer lock in wpilibj and update docs (#3602)
3b8d3bbcb Remove unused and add missing deprecated.h includes (#3599)
f9e976467 [examples] Rename DriveTrain classes to Drivetrain (#3594)
118a27be2 [wpilib] Add Timer tests (#3595)
59c89428e [wpilib] Deprecate Timer::HasPeriodPassed() (#3598)
202ca5e78 Force C++17 in .clang-format (#3597)
d6f185d8e Rename tests for consistency (#3592)
54ca474db [ci] Enable asan and tsan in CI for tests that pass (#3591)
1ca383b23 Add Debouncer (#3590)
179fde3a7 [build] Update to 2022 native utils and gradle 7 (#3588)
50198ffcf [examples] Add Mechanism2d visualization to Elevator Sim (#3587)
a446c2559 [examples] Synchronize C++ and Java Mechanism2d examples (#3589)
a7fb83103 [ci] clang-tidy: Generate compilation commands DB with Gradle (#3585)
4f5e0c9f8 [examples] Update ArmSimulation example to use Mechanism2d (#3572)
8164b91dc [CI] Print CMake test output on failure (#3583)
4d5fca27e [wpilib] Impove Mechanism2D documentation (NFC) (#3584)
fe59e4b9f Make C++ test names more consistent (#3586)
5c8868549 [wpilibc] Fix C++ MechanisimRoot2D to use same NT entries as Java/Glass (#3582)
9359431ba [wpimath] Clean up Eigen usage
72716f51c [wpimath] Upgrade to Eigen 3.4
382deef75 [wpimath] Explicitly export wpimath symbols
161e21173 [ntcore] Match standard handle layout, only allow 16 instances (#3577)
263a24811 [wpimath] Use jinja for codegen (#3574)
725251d29 [wpilib] Increase tolerances of DifferentialDriveSimTest (#3581)
4dff87301 [wpimath] Make LinearFilter::Factorial() constexpr (#3579)
60ede67ab [hal, wpilib] Switch PCM to be a single object that is allowed to be duplicated (#3475)
906bfc846 [build] Add CMake build support for sanitizers (#3576)
0d4f08ad9 [hal] Simplify string copy of joystick name (#3575)
a52bf87b7 [wpiutil] Add Java function package (#3570)
40c7645d6 [wpiutil] UidVector: Return old object from erase() (#3571)
5b886a23f [wpiutil] jni_util: Add size, operator[] to JArrayRef (#3569)
65797caa7 [sim] Fix halsim_ds_socket stringop overflow warning from GCC 10 (#3568)
66abb3988 [hal] Update runtime enum to allow selecting roborio 2 (#3565)
95a12e0ee [hal] UidSetter: Don't revert euid if its already current value (#3566)
27951442b [wpimath] Use external Eigen headers only (#3564)
c42e053ae [docs] Update to doxygen 1.9.2 (#3562)
e7048c8c8 [docs] Disable doxygen linking for common words that are also classes (#3563)
d8e0b6c97 [wpilibj] Fix java async interrupts (#3559)
5e6c34c61 Update to 2022 roborio image (#3537)
828f073eb [wpiutil] Fix uv::Buffer memory leaks caught by asan (#3555)
2dd5701ac [cscore] Fix mutex use-after-free in cscore test (#3557)
531439198 [ntcore] Fix NetworkTables memory leaks caught by asan (#3556)
3d9a4d585 [wpilibc] Fix AnalogTriggerOutput memory leak reported by asan (#3554)
54eda5928 [wpiutil] Ignore ubsan vptr upcast warning in SendableHelper moves (#3553)
5a4f75c9f [wpilib] Replace Speed controller comments with motor controller (NFC) (#3551)
7810f665f [wpiutil] Fix bug in uleb128 (#3540)
697e2dd33 [wpilib] Fix errant jaguar reference in comments (NFC) (#3550)
936c64ff5 [docs] Enable -linksource for javadocs (#3549)
1ea654954 [build] Upgrade CMake build to googletest 1.11.0 (#3548)
32d9949e4 [wpimath] Move controller tests to wpimath (#3541)
01ba56a8a [hal] Replace strncpy with memcpy (#3539)
e109c4251 [build] Rename makeSim flag to forceGazebo to better describe what it does (#3535)
e4c709164 [docs] Use a doxygen theme and add logo (#3533)
960b6e589 [wpimath] Fix Javadoc warning (#3532)
82eef8d5e [hal] Remove over current fault HAL functions from REV PDH (#3526)
aa3848b2c [wpimath] Move RobotDriveBase::ApplyDeadband() to MathUtil (#3529)
3b5d0d141 [wpimath] Add LinearFilter::BackwardFiniteDifference() (#3528)
c8fc715fe [wpimath] Upgrade drake files to v0.33.0 (#3531)
e5fe3a8e1 [build] Treat javadoc warnings as errors in CI and fix warnings (#3530)
e0c6cd3dc [wpimath] Add an operator for composing two Transform2ds (#3527)
2edd510ab [sim] Add sim wrappers for sensors that use SimDevice (#3517)
2b3e2ebc1 [hal] Fix HAL Notifier thread priority setting (#3522)
ab4cb5932 [gitignore] Update gitignore to ignore bazel / clion files (#3524)
57c8615af [build] Generate spotless patch on failure (#3523)
b90317321 Replace std::cout and std::cerr with fmt::print() (#3519)
10cc8b89c [hal] [wpilib] Add initial support for the REV PDH (#3503)
5d9ae3cdb [hal] Set HAL Notifier thread as RT by default (#3482)
192d251ee [wpilibcIntegrationTests] Properly disable DMA integration tests (#3514)
031962608 [wpilib] Add PS4Controller, remove Hand from GenericHID/XboxController (#3345)
25f6f478a [wpilib] Rename DriverStation::IsOperatorControl() to IsTeleop() (#3505)
e80f09f84 [wpilibj] Add unit tests (#3501)
c159f91f0 [wpilib] Only read DS control word once in IterativeRobotBase (#3504)
eb790a74d Add rio development docs documenting myRobot deploy tasks (#3508)
e47451f5a [wpimath] Replace auto with Eigen types (#3511)
252b8c83b Remove Java formatting from build task in CI (#3507)
09666ff29 Shorten Gazebo CI build (#3506)
baf2e501d Update myRobot to use 2021 java (#3509)
5ac60f0a2 [wpilib] Remove IterativeRobotBase mode init prints (#3500)
fb2ee8ec3 [wpilib] Add TimedRobot functions for running code on mode exit (#3499)
94e0db796 [wpilibc] Add more unit tests (#3494)
b25324695 [wpilibj] Add units to parameter names (NFC) (#3497)
1ac73a247 [hal] Rename PowerDistributionPanel to PowerDistribution (#3466)
2014115bc [examples] frisbeebot: Fix typo and reflow comments (NFC) (#3498)
4a944dc39 [examples] Consistently use 0 for controller port (#3496)
3838cc4ec Use unicode characters in docs equations (#3487)
85748f2e6 [examples] Add C++ TankDrive example (#3493)
d7b8aa56d [wpilibj] Rename DriverStation In[Mode] functions to follow style guide (#3488)
16e096cf8 [build] Fix CMake Windows CI (#3490)
50af74c38 [wpimath] Clean up NumericalIntegration and add Discretization tests (#3489)
bfc209b12 Automate fmt update (#3486)
e7f9331e4 [build] Update to Doxygen 1.9.1 (#3008)
ab8e8aa2a [wpimath] Update drake with upstream (#3484)
1ef826d1d [wpimath] Fix IOException path in WPIMath JNI (#3485)
52bddaa97 [wpimath] Disable iostream support for units and enable fmtlib (#3481)
e4dc3908b [wpiutil] Upgrade to fmtlib 8.0.1 (#3483)
1daadb812 [wpimath] Implement Dormand-Prince integration method (#3476)
9c2723391 [cscore] Add [[nodiscard]] to GrabFrame functions (#3479)
7a8796414 [wpilib] Add Notifier integration tests (#3480)
f8f13c536 [wpilibcExamples] Prefix decimal numbers with 0 (#3478)
1adb69c0f [ntcore] Use "NetworkTables" instead of "Network Tables" in NT specs (#3477)
5f5830b96 Upload wpiformat diff if one exists (#3474)
9fb4f35bb [wpimath] Add tests for DARE overload with Q, R, and N matrices (#3472)
c002e6f92 Run wpiformat (#3473)
c154e5262 [wpilib] Make solenoids exclusive use, PCM act like old sendable compressor (#3464)
6ddef1cca [hal] JNI setDIO: use a boolean and not a short (#3469)
9d68d9582 Remove extra newlines after open curly braces (NFC) (#3471)
a4233e1a1 [wpimath] Add script for updating Drake (#3470)
39373c6d2 Update README.md for new GCC version requirement (#3467)
d29acc90a [wpigui] Add option to reset UI on exit (#3463)
a371235b0 [ntcore] Fix dangling pointer in logger (#3465)
53b4891a5 [wpilibcintegrationtests] Fix deprecated Preferences usage (#3461)
646ded912 [wpimath] Remove incorrect discretization in pose estimators (#3460)
ea0b8f48e Fix some deprecation warnings due to fmtlib upgrade (#3459)
2067d7e30 [wpilibjexamples] Add wpimathjni, wpiutiljni to library path (#3455)
866571ab4 [wpiutil] Upgrade to fmtlib 8.0.0 (#3457)
4e1fa0308 [build] Skip PDB copy on windows build servers (#3458)
b45572167 [build] Change CI back to 18.04 docker images (#3456)
57a160f1b [wpilibc] Fix LiveWindow deprecation warning in RobotBase skeleton template (#3454)
29ae8640d [HLT] Implement duty cycle cross connect tests (#3453)
ee6377e54 [HLT] Add relay and analog cross connects (#3452)
b0f1ae7ea [build] CMake: Build the HAL even if WITH_CSCORE=OFF (#3449)
7aae2b72d Replace std::to_string() with fmt::format() (#3451)
73fcbbd74 [HLT] Add relay digital cross connect tests (#3450)
e7bedde83 [HLT] Add PWM tests that use DMA as the back end (#3447)
7253edb1e [wpilibc] Timer: Fix deprecated warning (#3446)
efa28125c [wpilibc] Add message to RobotBase on how to read stacktrace (#3444)
9832fcfe1 [hal] Fix DIO direction getter (#3445)
49c71f9f2 [wpilibj] Clarify robot quit message (#3364)
791770cf6 [wpimath] Move controller from wpilibj to wpimath (#3439)
9ce9188ff [wpimath] Add ReportWarning to MathShared (#3441)
362066a9b [wpilib] Deprecate getInstance() in favor of static functions (#3440)
26ff9371d Initial commit of cross connect integration test project (#3434)
4a36f86c8 [hal] Add support for DMA to Java (#3158)
85144e47f [commands] Unbreak build (#3438)
b417d961e Split Sendable into NT and non-NT portions (#3432)
ef4ea84cb [commands] Change grouping decorator impl to flatten nested group structures (#3335)
b422665a3 [examples] Invert right side of drive subsystems (#3437)
186dadf14 [hal] Error if attempting to set DIO output on an input port (#3436)
04e64db94 Remove redundant C++ lambda parentheses (NFC) (#3433)
f60994ad2 [wpiutil] Rename Java package to edu.wpi.first.util (#3431)
cfa1ca96f [wpilibc] Make ShuffleboardValue non-copyable (#3430)
4d9ff7643 Fix documentation warnings generated by JavaDoc (NFC) (#3428)
9e1b7e046 [build] Fix clang-tidy and clang-format (#3429)
a77c6ff3a [build] Upgrade clang-format and clang-tidy (NFC) (#3422)
099fde97d [wpilib] Improve PDP comments (NFC) (#3427)
f8fc2463e [wpilibc, wpiutil] Clean up includes (NFC) (#3426)
e246b7884 [wpimath] Clean up member initialization in feedforward classes (#3425)
c1e128bd5 Disable frivolous PMD warnings and enable PMD in ntcore (#3419)
8284075ee Run "Lint and Format" CI job on push as well as pull request (#3412)
f7db09a12 [wpimath] Move C++ filters into filter folder to match Java (#3417)
f9c3d54bd [wpimath] Reset error covariance in pose estimator ResetPosition() (#3418)
0773f4033 [hal] Ensure HAL status variables are initialized to zero (#3421)
d068fb321 [build] Upgrade CI to use 20.04 docker images (#3420)
8d054c940 [wpiutil] Remove STLExtras.h
80f1d7921 [wpiutil] Split function_ref to a separate header
64f541325 Use wpi::span instead of wpi::ArrayRef across all libraries (#3414)
2abbbd9e7 [build] clang-tidy: Remove bugprone-exception-escape (#3415)
a5c471af7 [wpimath] Add LQR template specialization for 2x2 system
edd2f0232 [wpimath] Add DARE solver for Q, R, and N with LQR ctor overloads
b2c3b2dd8 Use std::string_view and fmtlib across all libraries (#3402)
4f1cecb8e [wpiutil] Remove Path.h (#3413)
b336eac34 [build] Publish halsim_ws_core to Maven
2a09f6fa4 [build] Also build sim modules as static libraries
0e702eb79 [hal] Add a unified PCM object (#3331)
dea841103 [wpimath] Add fmtlib formatter overloads for Eigen::Matrix and units (#3409)
82856cf81 [wpiutil] Improve wpi::circular_buffer iterators (#3410)
8aecda03e [wpilib] Fix a documentation typo (#3408)
5c817082a [wpilib] Remove InterruptableSensorBase and replace with interrupt classes (#2410)
15c521a7f [wpimath] Fix drivetrain system identification (#3406)
989de4a1b [build] Force all linker warnings to be fatal for rio builds (#3407)
d9eeb45b0 [wpilibc] Add units to Ultrasonic class API (#3403)
fe570e000 [wpiutil] Replace llvm filesystem with C++17 filesystem (#3401)
01dc0249d [wpimath] Move SlewRateLimiter from wpilib to wpimath (#3399)
93523d572 [wpilibc] Clean up integration tests (#3400)
4f7a4464d [wpiutil] Rewrite StringExtras for std::string_view (#3394)
e09293a15 [wpilibc] Transition C++ classes to units::second_t (#3396)
827b17a52 [build] Create run tasks for Glass and OutlineViewer (#3397)
a61037996 [wpiutil] Avoid MSVC warning on span include (#3393)
4e2c3051b [wpilibc] Use std::string_view instead of Twine (#3380)
50915cb7e [wpilibc] MotorSafety::GetDescription(): Return std::string (#3390)
f4e2d26d5 [wpilibc] Move NullDeleter from frc/Base.h to wpi/NullDeleter.h (#3387)
cb0051ae6 [wpilibc] SimDeviceSim: use fmtlib (#3389)
a238cec12 [wpiutil] Deprecate wpi::math constants in favor of wpi::numbers (#3383)
393bf23c0 [ntcore, cscore, wpiutil] Standardize template impl files on .inc extension (NFC) (#3124)
e7d9ba135 [sim] Disable flaky web server integration tests (#3388)
0a0003c11 [wpilibjExamples] Fix name of Java swerve drive pose estimator example (#3382)
7e1b27554 [wpilibc] Use default copies and moves when possible (#3381)
fb2a56e2d [wpilibc] Remove START_ROBOT_CLASS macro (#3384)
84218bfb4 [wpilibc] Remove frc namespace shim (#3385)
dd7824340 [wpilibc] Remove C++ compiler version static asserts (#3386)
484cf9c0e [wpimath] Suppress the -Wmaybe-uninitialized warning in Eigen (#3378)
a04d1b4f9 [wpilibc] DriverStation: Remove ReportError and ReportWarning
831c10bdf [wpilibc] Errors: Use fmtlib
87603e400 [wpiutil] Import fmtlib (#3375)
442621672 [wpiutil] Add ArrayRef/std::span/wpi::span implicit conversions
bc15b953b [wpiutil] Add std::span implementation
6d20b1204 [wpiutil] StringRef, Twine, raw_ostream: Add std::string_view support (#3373)
2385c2a43 [wpilibc] Remove Utility.h (#3376)
87384ea68 [wpilib] Fix PIDController continuous range error calculations (#3170)
04dae799a [wpimath] Add SimpleMotorFeedforward::Calculate(velocity, nextVelocity) overload (#3183)
0768c3903 [wpilib] DifferentialDrive: Remove right side inversion (#3340)
8dd8d4d2d [wpimath] Fix redundant nested math package introduced by #3316 (#3368)
49b06beed [examples] Add Field2d to RamseteController example (#3371)
4c562a445 [wpimath] Fix typo in comment of update_eigen.py (#3369)
fdbbf1188 [wpimath] Add script for updating Eigen
f1e64b349 [wpimath] Move Eigen unsupported folder into eigeninclude
224f3a05c [sim] Fix build error when building with GCC 11.1 (#3361)
ff56d6861 [wpilibj] Fix SpeedController deprecated warnings (#3360)
1873fbefb [examples] Fix Swerve and Mecanum examples (#3359)
80b479e50 [examples] Fix SwerveBot example to use unique encoder ports (#3358)
1f7c9adee [wpilibjExamples] Fix pose estimator examples (#3356)
9ebc3b058 [outlineviewer] Change default size to 600x400 (#3353)
e21b443a4 [build] Gradle: Make C++ examples runnable (#3348)
da590120c [wpilibj] Add MotorController.setVoltage default (#3347)
561d53885 [build] Update opencv to 4.5.2, imgui/implot to latest (#3344)
44ad67ca8 [wpilibj] Preferences: Add missing Deprecated annotation (#3343)
3fe8fc75a [wpilibc] Revert "Return reference from GetInstance" (#3342)
3cc2da332 Merge branch '2022'
a3cd90dd7 [wpimath] Fix classpath used by generate_numbers.py (#3339)
d6cfdd3ba [wpilib] Preferences: Deprecate Put* in favor of Set* (#3337)
ba08baabb [wpimath] Update Drake DARE solver to v0.29.0 (#3336)
497b712f6 [wpilib] Make IterativeRobotBase::m_period private with getter
f00dfed7a [wpilib] Remove IterativeRobot base class
3c0846168 [hal] Use last error reporting instead of PARAMETER_OUT_OF_RANGE (#3328)
5ef2b4fdc [wpilibj] Fix @deprecated warning for SerialPort constructor (#3329)
23d2326d1 [hal] Report previous allocation location for indexed resource duplicates (#3322)
e338f9f19 [build] Fix wpilibc runCpp task (#3327)
c8ff626fe [wpimath] Move Java classes to edu.wpi.first.math (#3316)
4e424d51f [wpilibj] DifferentialDrivetrainSim: Rename constants to match the style guide (#3312)
6b50323b0 [cscore] Use Lock2DSize if possible for Windows USB cameras (#3326)
65c148536 [wpilibc] Fix "control reaches end of non-void function" warning (#3324)
f99f62bee [wpiutil] uv Handle: Use malloc/free instead of new/delete (#3325)
365f5449c [wpimath] Fix MecanumDriveKinematics (#3266)
ff52f207c [glass, wpilib] Rewrite Mechanism2d (#3281)
ee0eed143 [wpimath] Add DCMotor factory function for Romi motors (#3319)
512738072 [hal] Add HAL_GetLastError to enable better error messages from HAL calls (#3320)
ced654880 [glass, outlineviewer] Update Mac icons to macOS 11 style (#3313)
936d3b9f8 [templates] Add Java template for educational robot (#3309)
6e31230ad [examples] Fix odometry update in SwerveControllerCommand example (#3310)
05ebe9318 Merge branch 'main' into 2022
aaf24e255 [wpilib] Fix initial heading behavior in HolonomicDriveController (#3290)
8d961dfd2 [wpilibc] Remove ErrorBase (#3306)
659b37ef9 [wpiutil] StackTrace: Include offset on Linux (#3305)
0abf6c904 [wpilib] Move motor controllers to motorcontrol package (#3302)
4630191fa [wpiutil] circular_buffer: Use value initialization instead of passing zero (#3303)
b7b178f49 [wpilib] Remove Potentiometer interface
687066af3 [wpilib] Remove GyroBase
6b168ab0c [wpilib] Remove PIDController, PIDOutput, PIDSource
948625de9 [wpimath] Document conversion from filter cutoff frequency to time constant (#3299)
3848eb8b1 [wpilibc] Fix flywhel -> flywheel typo in FlywheelSim (#3298)
3abe0b9d4 [cscore] Move java package to edu.wpi.first.cscore (#3294)
d7fabe81f [wpilib] Remove RobotDrive (#3295)
1dc81669c [wpilib] Remove GearTooth (#3293)
01d0e1260 [wpilib] Revert move of RomiGyro into main wpilibc/j (#3296)
397e569aa [ntcore] Remove "using wpi" from nt namespace
79267f9e6 [ntcore] Remove NetworkTable -> nt::NetworkTable shim
48ebe5736 [ntcore] Remove deprecated Java interfaces and classes
c2064c78b [ntcore] Remove deprecated ITable interfaces
36608a283 [ntcore] Remove deprecated C++ APIs
a1c87e1e1 [glass] LogView: Add "copy to clipboard" button (#3274)
fa7240a50 [wpimath] Fix typo in quintic spline basis matrix
ffb4d38e2 [wpimath] Add derivation for spline basis matrices
f57c188f2 [wpilib] Add AnalogEncoder(int) ctor (#3273)
8471c4fb2 [wpilib] FieldObject2d: Add setTrajectory() method (#3277)
c97acd18e [glass] Field2d enhancements (#3234)
ffb590bfc [wpilib] Fix Compressor sendable properties (#3269)
6137f98eb [hal] Rename SimValueCallback2 to SimValueCallback (#3212)
a6f653969 [hal] Move registerSimPeriodic functions to HAL package (#3211)
10c038d9b [glass] Plot: Fix window creation after removal (#3264)
2d2eaa3ef [wpigui] Ensure window will be initially visible (#3256)
4d28b1f0c [wpimath] Use JNI for trajectory serialization (#3257)
3de800a60 [wpimath] TrajectoryUtil.h: Comment formatting (NFC) (#3262)
eff592377 [glass] Plot: Don't overwrite series ID (#3260)
a79faace1 [wpilibc] Return reference from GetInstance (#3247)
9550777b9 [wpilib] PWMSpeedController: Use PWM by composition (#3248)
c8521a3c3 [glass] Plot: Set reasonable default window size (#3261)
d71eb2cf3 [glass] Plot: Show full source name as tooltip and in popup (#3255)
160fb740f [hal] Use std::lround() instead of adding 0.5 and truncating (#3012)
48e9f3951 [wpilibj] Remove wpilibj package CameraServer (#3213)
8afa596fd [wpilib] Remove deprecated Sendable functions and SendableBase (#3210)
d3e45c297 [wpimath] Make C++ geometry classes immutable (#3249)
2c98939c1 [glass] StringChooser: Don't call SameLine() at end
a18a7409f [glass] NTStringChooser: Clear value of deleted entries
2f19cf452 [glass] NetworkTablesHelper: listen to delete events
da96707dc Merge branch 'main' into 2022
c3a8bdc24 [build] Fix clang-tidy action (#3246)
21624ef27 Add ImGui OutlineViewer (#3220)
1032c9b91 [wpiutil] Unbreak wpi::Format on Windows (#3242)
2e07902d7 [glass] NTField2D: Fix name lookup (#3233)
6e23e1840 [wpilibc] Remove WPILib.h (#3235)
3e22e4506 [wpilib] Make KoP drivetrain simulation weight 60 lbs (#3228)
79d1bd6c8 [glass] NetworkTablesSetting: Allow disable of server option (#3227)
fe341a16f [examples] Use more logical elevator setpoints in GearsBot (#3198)
62abf46b3 [glass] NetworkTablesSettings: Don't block GUI (#3226)
a95a5e0d9 [glass] Move NetworkTablesSettings to libglassnt (#3224)
d6f6ceaba [build] Run Spotless formatter (NFC) (#3221)
0922f8af5 [commands] CommandScheduler.requiring(): Note return can be null (NFC) (#2934)
6812302ff [examples] Make DriveDistanceOffboard example work in sim (#3199)
f3f86b8e7 [wpimath] Add pose estimator overload for vision + std dev measurement (#3200)
1a2680b9e [wpilibj] Change CommandBase.withName() to return CommandBase (#3209)
435bbb6a8 [command] RamseteCommand: Output 0 if interrupted (#3216)
3cf44e0a5 [hal] Add function for changing HAL Notifier thread priority (#3218)
40b367513 [wpimath] Units.java: Add kg-lb conversions (#3203)
9f563d584 [glass] NT: Fix return value in StringToDoubleArray (#3208)
af4adf537 [glass] Auto-size plots to fit window (#3193)
2560146da [sim] GUI: Add option to show prefix in Other Devices (#3186)
eae3a6397 gitignore: Ignore .cache directory (#3196)
959611420 [wpilib] Require non-zero positive value for PIDController.period (#3175)
9522f2e8c [wpimath] Add methods to concatenate trajectories (#3139)
e42a0b6cf [wpimath] Rotation2d comment formatting (NFC) (#3162)
d1c7032de [wpimath] Fix order of setting gyro offset in pose estimators (#3176)
d241bc81a [sim] Add DoubleSolenoidSim and SolenoidSim classes (#3177)
cb7f39afa [wpilibc] Add RobotController::GetBatteryVoltage() to C++ (#3179)
99b5ad9eb [wpilibj] Fix warnings that are not unused variables or deprecation (#3161)
c14b23775 [build] Fixup doxygen generated include dirs to match what users would need (#3154)
d447c7dc3 [sim] Add SimDeviceSim ctor overloads (#3134)
247420c9c [build] Remove jcenter repo (#3157)
04b112e00 [build] Include debug info in plugin published artifacts (#3149)
be0ce9900 [examples] Use PWMSparkMax instead of PWMVictorSPX (#3156)
69e8d0b65 [wpilib] Move RomiGyro into main wpilibc/j (#3143)
94e685e1b [wpimath] Add custom residual support to EKF (#3148)
5899f3dd2 [sim] GUI: Make keyboard settings loading more robust (#3167)
f82aa1d56 [wpilib] Fix HolonomicDriveController atReference() behavior (#3163)
fe5c2cf4b [wpimath] Remove ControllerUtil.java (#3169)
43d40c6e9 [wpiutil] Suppress unchecked cast in CombinedRuntimeLoader (#3155)
3d44d8f79 [wpimath] Fix argument order in UKF docs (NFC) (#3147)
ba6fe8ff2 [cscore] Add USB camera change event (#3123)
533725888 [build] Tweak OpenCV cmake search paths to work better on Linux (#3144)
29bf9d6ef [cscore] Add polled support to listener
483beb636 [ntcore] Move CallbackManager to wpiutil
fdaec7759 [examples] Instantiate m_ramseteController in example (#3142)
8494a5761 Rename default branch to main (#3140)
45590eea2 [wpigui] Hardcode window scale to 1 on macOS (#3135)
834a64920 [build] Publish libglass and libglassnt to Maven (#3127)
2c2ccb361 [wpimath] Fix Rotation2d equality operator (#3128)
fb5c8c39a [wpigui] clang-tidy: readability-braces-around-statements
f7d39193a [wpigui] Fix copyright in pfd and wpigui_metal.mm
aec796b21 [ntcore] Fix conditional jump on uninitialized value (#3125)
fb13bb239 [sim] GUI: Add right click popup for keyboard joystick settings (#3119)
c517ec677 [build] Update thirdparty-imgui to 1.79-2 (#3118)
e8cbf2a71 [wpimath] Fix typo in SwerveDrivePoseEstimator doc (NFC) (#3112)
e9c86df46 [wpimath] Add tests for swerve module optimization (#3100)
6ba8c289c [examples] Remove negative of ArcadeDrive(fwd, ..) in the C++ Getting Started Example (#3102)
3f1672e89 [hal] Add SimDevice createInt() and createLong() (#3110)
15be5cbf1 [examples] Fix segfault in GearsBot C++ example (#3111)
4cf0e5e6d Add quick links to API documentation in README (#3082)
6b1898f12 Fix RT priority docs (NFC) (#3098)
b3426e9c0 [wpimath] Fix missing whitespace in pose estimator doc (#3097)
38c1a1f3e [examples] Fix feildRelative -> fieldRelative typo in XControllerCommand examples (#3104)
4488e25f1 [glass] Shorten SmartDashboard window names (#3096)
cfdb3058e [wpilibj] Update SimDeviceSimTest (#3095)
64adff5fe [examples] Fix typo in ArcadeDrive constructor parameter name (#3092)
6efc58e3d [build] Fix issues with build on windows, deprecations, and native utils (#3090)
f393989a5 [wpimath, wpiutil] Add wpi::array for compile time size checking (#3087)
d6ed20c1e [build] Set macOS deployment target to 10.14 (#3088)
7c524014c [hal] Add [[nodiscard]] to HAL_WaitForNotifierAlarm() (#3085)
406d055f0 [wpilib] Fixup wouldHitLowerLimit in elevator and arm simulation classes. (#3076)
04a90b5dd [examples] Don't continually set setpoint in PotentiometerPID Examples (#3084)
8c5bfa013 [sim] GUI: Add max value setting for keyboard joysticks (#3083)
bc80c5535 [hal] Add SimValue reset() function (#3064)
9c3b51ca0 [wpilib] Document simulation APIs (#3079)
26584ff14 [wpimath] Add model description to LinearSystemId Javadocs (#3080)
42c3d5286 [examples] Sync Java and C++ trajectories in sim example (#3081)
64e72f710 [wpilibc] Add missing function RoboRioSim::ResetData (#3073)
e95503798 [wpimath] Add optimize() to SwerveModuleState (#3065)
fb99910c2 [hal] Add SimInt and SimLong wrappers for int/long SimValue (#3066)
e620bd4d3 [doc] Add machine-readable websocket specification (#3059)
a44e761d9 [glass] Add support for plot Y axis labels
ea1974d57 [wpigui] Update imgui and implot to latest
85a0bd43c [wpimath] Add RKF45 integration (#3047)
278e0f126 [glass] Use .controllable to set widgets' read-only state (#3035)
d8652cfd4 [wpimath] Make Java DCMotor API consistent with C++ and fix motor calcs (#3046)
377b7065a [build] Add toggleOffOn to Java spotless (#3053)
1e9c79c58 [sim] Use plant output to retrieve simulated position (#3043)
78147aa34 [sim] GUI: Fix Keyboard Joystick (#3052)
cd4a2265b [ntcore] Fix NetworkTableEntry::GetRaw() (#3051)
767ac1de1 [build] Use deploy key for doc publish (#3048)
d762215d1 [build] Add publish documentation script (#3040)
1fd09593c [examples] Add missing TestInit method to GettingStarted Example (#3039)
e45a0f6ce [examples] Add RomiGyro to the Romi Reference example (#3037)
94f852572 Update imaging link and fix typo (#3038)
d73cf64e5 [examples] Update RomiReference to match motor directions (#3036)
f945462ba Bump copyright year to 2021 (#3033)
b05946175 [wpimath] Catch Drake JNI exceptions and rethrow them (#3032)
62f0f8190 [wpimath] Deduplicate angle modulus functions (#2998)
bf8c0da4b [glass] Add "About" popup with version number (#3031)
dfdd6b389 [build] Increase Gradle heap size in Gazebo build (#3028)
f5e0fc3e9 Finish clang-tidy cleanups (#3003)
d741101fe [sim] Revert accidental commit of WSProvider_PDP.h (#3027)
e1620799c [examples] Add C++ RomiReference example (#2969)
749c7adb1 [command] Fix use-after-free in CommandScheduler (#3024)
921a73391 [sim] Add WS providers for AddressableLED, PCM, and Solenoid (#3026)
26d0004fe [build] Split Actions into different yml files (#3025)
948af6d5b [wpilib] PWMSpeedController.get(): Apply Inversion (#3016)
670a187a3 [wpilibc] SuppliedValueWidget.h: Forward declare ShuffleboardContainer (#3021)
be9f72502 [ntcore] NetworkTableValue: Use std::forward instead of std::move (#3022)
daf3f4cb1 [cscore] cscore_raw_cv.h: Fix error in PutFrame() (#3019)
5acda4cc7 [wpimath] ElevatorFeedforward.h: Add time.h include
8452af606 [wpimath] units/radiation.h: Add mass.h include
630d44952 [hal] ErrorsInternal.h: Add stdint.h include
7372cf7d9 [cscore] Windows NetworkUtil.cpp: Add missing include
b7e46c558 Include .h from .inc/.inl files (NFC) (#3017)
bf8f8710e [examples] Update Romi template and example (#2996)
6ffe5b775 [glass] Ensure NetworkTableTree parent context menu has an id (#3015)
be0805b85 [build] Update to WPILibVersioningPlugin 4.1.0 (#3014)
65b2359b2 [build] Add spotless for other files (#3007)
8651aa73e [examples] Enable NT Flush in Field2d examples (#3013)
78b542737 [build] Add Gazebo build to Actions CI (#3004)
fccf86532 [sim] DriverStationGui: Fix two bugs (#3010)
185741760 [sim] WSProvider_Joystick: Fix off-by-1 in incoming buttons (#3011)
ee7114a58 [glass] Add drive class widgets (#2975)
00fa91d0d [glass] Use ImGui style for gyro widget colors (#3009)
b7a25bfc3 ThirdPartyNotices: Add portable file dialogs license (#3005)
a2e46b9a1 [glass] modernize-use-nullptr (NFC) (#3006)
a751fa22d [build] Apply spotless for java formatting (#1768)
e563a0b7d [wpimath] Make LinearSystemLoop move-constructible and move-assignable (#2967)
49085ca94 [glass] Add context menus to remove and add NetworkTables values (#2979)
560a850a2 [glass] Add NetworkTables Log window (#2997)
66782e231 [sim] Create Left/Right drivetrain current accessors (#3001)
b60eb1544 clang-tidy: bugprone-virtual-near-miss
cbe59fa3b clang-tidy: google-explicit-constructor
c97c6dc06 clang-tidy: google-readability-casting (NFC)
32fa97d68 clang-tidy: modernize-use-nullptr (NFC)
aee460326 clang-tidy: modernize-pass-by-value
29c7da5f1 clang-tidy: modernize-make-unique
6131f4e32 clang-tidy: modernize-concat-nested-namespaces (NFC)
67e03e625 clang-tidy: modernize-use-equals-default
b124f9101 clang-tidy: modernize-use-default-member-init
d11a3a638 clang-tidy: modernize-use-override (NFC)
4cc0706b0 clang-tidy: modernize-use-using (NFC)
885f5a978 [wpilibc] Speed up ScopedTracerTest (#2999)
60b596457 [wpilibj] Fix typos (NFC) (#3000)
6e1919414 [build] Bring naming checkstyle rules up to date with Google Style guide (#1781)
8c8ec5e63 [wpilibj] Suppress unchecked cast warnings (#2995)
b8413ddd5 [wpiutil] Add noexcept to timestamp static functions (#2994)
5d976b6e1 [glass] Load NetworkTableView settings on first draw (#2993)
2b4317452 Replace NOLINT(runtime/explicit) comments with NOLINT (NFC) (#2992)
1c3011ba4 [glass] Fix handling of "/" NetworkTables key (#2991)
574a42f3b [hal] Fix UnsafeManipulateDIO status check (#2987)
9005cd59e [wpilib] Clamp input voltage in sim classes (#2955)
dd494d4ab [glass] NetworkTablesModel::Update(): Avoid use-after-move (#2988)
7cca469a1 [wpimath] NormalizeAngle: Make inline, remove unnamed namespace (#2986)
2aed432b4 Add braces to C++ single-line loops and conditionals (NFC) (#2973)
0291a3ff5 [wpiutil] StringRef: Add noexcept to several constructors (#2984)
5d7315280 [wpimath] Update UnitsTest.cpp copyright (#2985)
254931b9a [wpimath] Remove LinearSystem from LinearSystemLoop (#2968)
aa89744c9 Update OtherVersions.md to include wpimath info (#2983)
1cda3f5ad [glass] Fix styleguide (#2976)
8f1f64ffb Remove year from file copyright message (NFC) (#2972)
2bc0a7795 [examples] Fix wpiformat warning about utility include (#2971)
4204da6ad [glass] Add application icon
7ac39b10f [wpigui] Add icon support
6b567e006 [wpimath] Add support for varying vision standard deviations in pose estimators (#2956)
df299d6ed [wpimath] Add UnscentedKalmanFilter::Correct() overload (#2966)
4e34f0523 [examples] Use ADXRS450_GyroSim class in simulation example (#2964)
9962f6fd7 [wpilib] Give Field2d a default Sendable name (#2953)
f9d492f4b [sim] GUI: Show "Other Devices" window by default (#2961)
a8bb2ef1c [sim] Fix ADXRS450_GyroSim and DutyCycleEncoderSim (#2963)
240c629cd [sim] Try to guess "Map Gamepad" setting (#2960)
952567dd3 [wpilibc] Add missing move constructors and assignment operators (#2959)
10b396b4c [sim] Various WebSockets fixes and enhancements (#2952)
699bbe21a [examples] Fix comments in Gearsbot to match implementation (NFC) (#2957)
27b67deca [glass] Add more widgets (#2947)
581b7ec55 [wpilib] Add option to flush NetworkTables every iterative loop
acfbb1a44 [ntcore] DispatcherBase::Flush: Use wpi::Now()
d85a6d8fe [ntcore] Reduce limit on flush and update rate to 5 ms
20fbb5c63 [sim] Fix stringop truncation warning from GCC 10 (#2945)
1051a06a7 [glass] Show NT timestamps in seconds (#2944)
98dfc2620 [glass] Fix plots (#2943)
1ba0a2ced [sim] GUI: Add keyboard virtual joystick support (#2940)
4afb13f98 [examples] Replace M_PI with wpi::math::pi (#2938)
b27d33675 [examples] Enhance Romi templates (#2931)
00b9ae77f [sim] Change default WS port number to 3300 (#2932)
65219f309 [examples] Update Field2d position in periodic() (#2928)
f78d1d434 [sim] Process WS Encoder reset internally (#2927)
941edca59 [hal] Add Java SimDeviceDataJNI.getSimDeviceName (#2924)
a699435ed [wpilibj] Fix FlywheelSim argument order in constructor (#2922)
66d641718 [examples] Add tasks to run Java examples (#2920)
558e37c41 [examples] Add simple differential drive simulation example (#2918)
4f40d991e [glass] Switch name of Glass back to glass (#2919)
549af9900 [build] Update native-utils to 2021.0.6 (#2914)
b33693009 [glass] Change basename of glass to Glass (#2915)
c9a0edfb8 [glass] Package macOS application bundle
2c5668af4 [wpigui] Add platform-specific preferences save
751dea32a [wpilibc] Try to work around ABI break introduced in #2901 (#2917)
cd8f4bfb1 [build] Package up msvc runtime into maven artifact (#2913)
a6cfcc686 [wpilibc] Move SendableChooser Doxygen comments to header (NFC) (#2911)
b8c4f603d [wpimath] Upgrade to Eigen 3.3.9 (#2910)
0075e4b39 [wpilibj] Fix NPE in Field2d (#2909)
125af556c [simulation] Fix halsim_gui ntcore and wpiutil deps (#2908)
963ad5c25 [wpilib] Add noise to Differential Drive simulator (#2903)
387f56cb7 [examples] Add Romi reference Java example and templates (#2905)
b3deda38c [examples] Zero motors on disabledInit() in sim physics examples (#2906)
2a5ca7745 [glass] Add glass: an application for display of robot data
727940d84 [wpilib] Move Field2d to SmartDashboard
8cd42478e [wpilib] SendableBuilder: Make GetTable() visible
c11d34b26 [command] Use addCommands in command group templates (#2900)
339d7445b [sim] Add HAL hooks for simulationPeriodic (#2881)
d16f05f2c [wpilib] Fix SmartDashboard update order (#2896)
5427b32a4 [wpiutil] unique_function: Restrict implicit conversion (#2899)
f73701239 [ntcore] Add missing SetDefault initializer_list functions (#2898)
f5a6fc070 [sim] Add initialized flag for all solenoids on a PCM (#2897)
bdf5ba91a [wpilibj] Fix typo in ElevatorSim (#2895)
bc8f33877 [wpilib] Add pose estimators (#2867)
3413bfc06 [wpilib] PIDController: Recompute the error in AtSetpoint() (#2822)
2056f0ce0 [wpilib] Fix bugs in Hatchbot examples (#2893)
5eb8cfd69 [wpilibc] Fix MatchDataSender (#2892)
e6a425448 [build] Delete test folders after tests execute (#2891)
d478ad00d [imgui] Allow usage of imgui_stdlib (#2889)
53eda861d [build] Add unit-testing infrastructure to examples (#2863)
cc1d86ba6 [sim] Add title to simulator GUI window (#2888)
f0528f00e [build] CMake: Use project-specific binary and source dirs (#2886)
5cd2ad124 [wpilibc] Add Color::operator!= (#2887)
6c00e7a90 [build] CI CMake: build with GUI enabled (#2884)
53170bbb5 Update roboRIO toolchain installation instructions (#2883)
467258e05 [sim] GUI: Add option to not zero disconnected joysticks (#2876)
129be23c9 Clarify JDK installation instructions in readme (#2882)
8e9290e86 [build] Add separate CMake setting for wpimath (#2885)
7cf5bebf8 [wpilibj] Cache NT writes from DriverStation (#2780)
f7f9087fb [command] Fix timing issue in RamseteCommand (#2871)
256e7904f [wpilibj] SimDeviceSim: Fix sim value changed callback (#2880)
c8ea1b6c3 [wpilib] Add function to adjust LQR controller gain for pure time delay (#2878)
2816b06c0 [sim] HAL_GetControlWord: Fully zero output (#2873)
4c695ea08 Add toolchain installation instructions to README (#2875)
a14d51806 [wpimath] DCMotor: fix doc typo (NFC) (#2868)
017097791 [build] CMake: build sim extensions as shared libs (#2866)
f61726b5a [build] Fix cmake-config files (#2865)
fc27fdac5 [wpilibc] Cache NT values from driver station (#2768)
47c59859e [sim] Make SimDevice callbacks synchronous (#2861)
6e76ab9c0 [build] Turn on WITH_GUI for Windows cmake CI
5f78b7670 [build] Set GLFW_INSTALL to OFF
5e0808c84 [wpigui] Fix Windows cmake build
508f05a47 [imgui] Fix typo in Windows CMake target sources

Change-Id: I1737b45965f31803a96676bedc7dc40e337aa321
git-subtree-dir: third_party/allwpilib
git-subtree-split: e473a00f9785f9949e5ced30901baeaf426d2fc9
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/wpilibj/CMakeLists.txt b/wpilibj/CMakeLists.txt
index a07a563..cff7f22 100644
--- a/wpilibj/CMakeLists.txt
+++ b/wpilibj/CMakeLists.txt
@@ -1,12 +1,11 @@
 project (wpilibj)
 
-find_package( OpenCV REQUIRED )
-
 # Java bindings
 if (WITH_JAVA)
+    find_package( OpenCV REQUIRED )
     find_package(Java REQUIRED)
     include(UseJava)
-    set(CMAKE_JAVA_COMPILE_FLAGS "-Xlint:unchecked")
+    set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
 
     set(OPENCV_JAVA_INSTALL_DIR ${OpenCV_INSTALL_PATH}/share/OpenCV/java/)
 
@@ -15,8 +14,8 @@
     configure_file(src/generate/WPILibVersion.java.in WPILibVersion.java)
 
     file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
-    file(GLOB EJML_JARS "${CMAKE_BINARY_DIR}/wpimath/thirdparty/ejml/*.jar")
-    file(GLOB JACKSON_JARS "${CMAKE_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar")
+    file(GLOB EJML_JARS "${WPILIB_BINARY_DIR}/wpimath/thirdparty/ejml/*.jar")
+    file(GLOB JACKSON_JARS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar")
 
     add_jar(wpilibj_jar ${JAVA_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/WPILibVersion.java INCLUDE_JARS hal_jar ntcore_jar ${EJML_JARS} ${JACKSON_JARS} ${OPENCV_JAR_FILE} cscore_jar cameraserver_jar wpimath_jar wpiutil_jar OUTPUT_NAME wpilibj)
 
diff --git a/wpilibj/build.gradle b/wpilibj/build.gradle
index df16022..1004c20 100644
--- a/wpilibj/build.gradle
+++ b/wpilibj/build.gradle
@@ -56,7 +56,7 @@
 }
 
 repositories {
-    jcenter()
+    mavenCentral()
 }
 
 dependencies {
@@ -121,7 +121,7 @@
                 project(':hal').addHalDependency(it, 'shared')
                 project(':hal').addHalJniDependency(it)
                 if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                    nativeUtils.useRequiredLibrary(it, 'netcomm_shared', 'chipobject_shared', 'visa_shared', 'ni_runtime_shared')
+                    nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
                 }
             }
         }
diff --git a/wpilibj/src/dev/java/edu/wpi/first/wpilibj/DevMain.java b/wpilibj/src/dev/java/edu/wpi/first/wpilibj/DevMain.java
index 2c08b17..e0db62e 100644
--- a/wpilibj/src/dev/java/edu/wpi/first/wpilibj/DevMain.java
+++ b/wpilibj/src/dev/java/edu/wpi/first/wpilibj/DevMain.java
@@ -1,20 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.HALUtil;
 import edu.wpi.first.networktables.NetworkTablesJNI;
-import edu.wpi.first.wpiutil.RuntimeDetector;
+import edu.wpi.first.util.RuntimeDetector;
 
 public final class DevMain {
-  /**
-   * Main entry point.
-   */
+  /** Main entry point. */
   public static void main(String[] args) {
     System.out.println("Hello World!");
     System.out.println(RuntimeDetector.getPlatformPath());
@@ -22,6 +17,5 @@
     System.out.println(HALUtil.getHALRuntimeType());
   }
 
-  private DevMain() {
-  }
+  private DevMain() {}
 }
diff --git a/wpilibj/src/dev/native/cpp/main.cpp b/wpilibj/src/dev/native/cpp/main.cpp
index e324b44..a3e363e 100644
--- a/wpilibj/src/dev/native/cpp/main.cpp
+++ b/wpilibj/src/dev/native/cpp/main.cpp
@@ -1,8 +1,5 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.
 
 int main() {}
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 c31782c..29d3930 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
@@ -1,31 +1,26 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.hal.SimEnum;
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
 import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Accelerometer;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
-/**
- * ADXL345 I2C Accelerometer.
- */
+/** ADXL345 I2C Accelerometer. */
 @SuppressWarnings({"TypeName", "PMD.UnusedPrivateField"})
-public class ADXL345_I2C implements Accelerometer, Sendable, AutoCloseable {
+public class ADXL345_I2C implements Accelerometer, NTSendable, AutoCloseable {
   private static final byte kAddress = 0x1D;
   private static final byte kPowerCtlRegister = 0x2D;
   private static final byte kDataFormatRegister = 0x31;
@@ -47,9 +42,7 @@
     kY((byte) 0x02),
     kZ((byte) 0x04);
 
-    /**
-     * The integer value representing this enumeration.
-     */
+    /** The integer value representing this enumeration. */
     @SuppressWarnings("MemberName")
     public final byte value;
 
@@ -76,7 +69,7 @@
   /**
    * Constructs the ADXL345 Accelerometer with I2C address 0x1D.
    *
-   * @param port  The I2C port the accelerometer is attached to
+   * @param port The I2C port the accelerometer is attached to
    * @param range The range (+ or -) that the accelerometer will measure.
    */
   public ADXL345_I2C(I2C.Port port, Range range) {
@@ -86,20 +79,26 @@
   /**
    * Constructs the ADXL345 Accelerometer over I2C.
    *
-   * @param port          The I2C port the accelerometer is attached to
-   * @param range         The range (+ or -) that the accelerometer will measure.
+   * @param port The I2C port the accelerometer is attached to
+   * @param range The range (+ or -) that the accelerometer will measure.
    * @param deviceAddress I2C address of the accelerometer (0x1D or 0x53)
    */
   public ADXL345_I2C(I2C.Port port, Range range, int deviceAddress) {
     m_i2c = new I2C(port, deviceAddress);
 
     // simulation
-    m_simDevice = SimDevice.create("ADXL345_I2C", port.value, deviceAddress);
+    m_simDevice = SimDevice.create("Accel:ADXL345_I2C", port.value, deviceAddress);
     if (m_simDevice != null) {
-      m_simRange = m_simDevice.createEnum("Range", true, new String[] {"2G", "4G", "8G", "16G"}, 0);
-      m_simX = m_simDevice.createDouble("X Accel", false, 0.0);
-      m_simY = m_simDevice.createDouble("Y Accel", false, 0.0);
-      m_simZ = m_simDevice.createDouble("Z Accel", false, 0.0);
+      m_simRange =
+          m_simDevice.createEnumDouble(
+              "range",
+              SimDevice.Direction.kOutput,
+              new String[] {"2G", "4G", "8G", "16G"},
+              new double[] {2.0, 4.0, 8.0, 16.0},
+              0);
+      m_simX = m_simDevice.createDouble("x", SimDevice.Direction.kInput, 0.0);
+      m_simY = m_simDevice.createDouble("y", SimDevice.Direction.kInput, 0.0);
+      m_simZ = m_simDevice.createDouble("z", SimDevice.Direction.kInput, 0.0);
     }
 
     // Turn on the measurements
@@ -111,6 +110,14 @@
     SendableRegistry.addLW(this, "ADXL345_I2C", port.value);
   }
 
+  public int getPort() {
+    return m_i2c.getPort();
+  }
+
+  public int getDeviceAddress() {
+    return m_i2c.getDeviceAddress();
+  }
+
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -217,16 +224,17 @@
   }
 
   @Override
-  public void initSendable(SendableBuilder builder) {
+  public void initSendable(NTSendableBuilder builder) {
     builder.setSmartDashboardType("3AxisAccelerometer");
     NetworkTableEntry entryX = builder.getEntry("X");
     NetworkTableEntry entryY = builder.getEntry("Y");
     NetworkTableEntry entryZ = builder.getEntry("Z");
-    builder.setUpdateTable(() -> {
-      AllAxes data = getAccelerations();
-      entryX.setDouble(data.XAxis);
-      entryY.setDouble(data.YAxis);
-      entryZ.setDouble(data.ZAxis);
-    });
+    builder.setUpdateTable(
+        () -> {
+          AllAxes data = getAccelerations();
+          entryX.setDouble(data.XAxis);
+          entryY.setDouble(data.YAxis);
+          entryZ.setDouble(data.ZAxis);
+        });
   }
 }
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 842ffd6..fa8b7d8 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
@@ -1,31 +1,26 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.hal.SimEnum;
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
 import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Accelerometer;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
-/**
- * ADXL345 SPI Accelerometer.
- */
+/** ADXL345 SPI Accelerometer. */
 @SuppressWarnings({"TypeName", "PMD.UnusedPrivateField"})
-public class ADXL345_SPI implements Accelerometer, Sendable, AutoCloseable {
+public class ADXL345_SPI implements Accelerometer, NTSendable, AutoCloseable {
   private static final int kPowerCtlRegister = 0x2D;
   private static final int kDataFormatRegister = 0x31;
   private static final int kDataRegister = 0x32;
@@ -50,10 +45,7 @@
     kY((byte) 0x02),
     kZ((byte) 0x04);
 
-    /**
-     * The integer value representing this enumeration.
-     */
-    @SuppressWarnings("MemberName")
+    /** The integer value representing this enumeration. */
     public final byte value;
 
     Axes(byte value) {
@@ -79,23 +71,33 @@
   /**
    * Constructor.
    *
-   * @param port  The SPI port that the accelerometer is connected to
+   * @param port The SPI port that the accelerometer is connected to
    * @param range The range (+ or -) that the accelerometer will measure.
    */
   public ADXL345_SPI(SPI.Port port, Range range) {
     m_spi = new SPI(port);
     // simulation
-    m_simDevice = SimDevice.create("ADXL345_SPI", port.value);
+    m_simDevice = SimDevice.create("Accel:ADXL345_SPI", port.value);
     if (m_simDevice != null) {
-      m_simRange = m_simDevice.createEnum("Range", true, new String[] {"2G", "4G", "8G", "16G"}, 0);
-      m_simX = m_simDevice.createDouble("X Accel", false, 0.0);
-      m_simY = m_simDevice.createDouble("Y Accel", false, 0.0);
-      m_simZ = m_simDevice.createDouble("Z Accel", false, 0.0);
+      m_simRange =
+          m_simDevice.createEnumDouble(
+              "range",
+              SimDevice.Direction.kOutput,
+              new String[] {"2G", "4G", "8G", "16G"},
+              new double[] {2.0, 4.0, 8.0, 16.0},
+              0);
+      m_simX = m_simDevice.createDouble("x", SimDevice.Direction.kInput, 0.0);
+      m_simY = m_simDevice.createDouble("y", SimDevice.Direction.kInput, 0.0);
+      m_simZ = m_simDevice.createDouble("z", SimDevice.Direction.kInput, 0.0);
     }
     init(range);
     SendableRegistry.addLW(this, "ADXL345_SPI", port.value);
   }
 
+  public int getPort() {
+    return m_spi.getPort();
+  }
+
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -154,7 +156,7 @@
     }
 
     // Specify the data format to read
-    byte[] commands = new byte[]{kDataFormatRegister, (byte) (kDataFormat_FullRes | value)};
+    byte[] commands = new byte[] {kDataFormatRegister, (byte) (kDataFormat_FullRes | value)};
     m_spi.write(commands, commands.length);
 
     if (m_simRange != null) {
@@ -194,8 +196,8 @@
       return m_simZ.get();
     }
     ByteBuffer transferBuffer = ByteBuffer.allocate(3);
-    transferBuffer.put(0,
-        (byte) ((kAddress_Read | kAddress_MultiByte | kDataRegister) + axis.value));
+    transferBuffer.put(
+        0, (byte) ((kAddress_Read | kAddress_MultiByte | kDataRegister) + axis.value));
     m_spi.transaction(transferBuffer, transferBuffer, 3);
     // Sensor is little endian
     transferBuffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -232,16 +234,17 @@
   }
 
   @Override
-  public void initSendable(SendableBuilder builder) {
+  public void initSendable(NTSendableBuilder builder) {
     builder.setSmartDashboardType("3AxisAccelerometer");
     NetworkTableEntry entryX = builder.getEntry("X");
     NetworkTableEntry entryY = builder.getEntry("Y");
     NetworkTableEntry entryZ = builder.getEntry("Z");
-    builder.setUpdateTable(() -> {
-      AllAxes data = getAccelerations();
-      entryX.setDouble(data.XAxis);
-      entryY.setDouble(data.YAxis);
-      entryZ.setDouble(data.ZAxis);
-    });
+    builder.setUpdateTable(
+        () -> {
+          AllAxes data = getAccelerations();
+          entryX.setDouble(data.XAxis);
+          entryY.setDouble(data.YAxis);
+          entryZ.setDouble(data.ZAxis);
+        });
   }
 }
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 340a776..2a9c2fe 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java
@@ -1,32 +1,28 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.hal.SimEnum;
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
 import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Accelerometer;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * ADXL362 SPI Accelerometer.
  *
  * <p>This class allows access to an Analog Devices ADXL362 3-axis accelerometer.
  */
-@SuppressWarnings("PMD.UnusedPrivateField")
-public class ADXL362 implements Accelerometer, Sendable, AutoCloseable {
+public class ADXL362 implements Accelerometer, NTSendable, AutoCloseable {
   private static final byte kRegWrite = 0x0A;
   private static final byte kRegRead = 0x0B;
 
@@ -41,7 +37,10 @@
   private static final byte kFilterCtl_ODR_100Hz = 0x03;
 
   private static final byte kPowerCtl_UltraLowNoise = 0x20;
+
+  @SuppressWarnings("PMD.UnusedPrivateField")
   private static final byte kPowerCtl_AutoSleep = 0x04;
+
   private static final byte kPowerCtl_Measure = 0x02;
 
   public enum Axes {
@@ -49,7 +48,6 @@
     kY((byte) 0x02),
     kZ((byte) 0x04);
 
-    @SuppressWarnings("MemberName")
     public final byte value;
 
     Axes(byte value) {
@@ -75,7 +73,7 @@
   private double m_gsPerLSB;
 
   /**
-   * Constructor.  Uses the onboard CS1.
+   * Constructor. Uses the onboard CS1.
    *
    * @param range The range (+ or -) that the accelerometer will measure.
    */
@@ -86,19 +84,25 @@
   /**
    * Constructor.
    *
-   * @param port  The SPI port that the accelerometer is connected to
+   * @param port The SPI port that the accelerometer is connected to
    * @param range The range (+ or -) that the accelerometer will measure.
    */
   public ADXL362(SPI.Port port, Range range) {
     m_spi = new SPI(port);
 
     // simulation
-    m_simDevice = SimDevice.create("ADXL362", port.value);
+    m_simDevice = SimDevice.create("Accel:ADXL362", port.value);
     if (m_simDevice != null) {
-      m_simRange = m_simDevice.createEnum("Range", true, new String[] {"2G", "4G", "8G", "16G"}, 0);
-      m_simX = m_simDevice.createDouble("X Accel", false, 0.0);
-      m_simY = m_simDevice.createDouble("Y Accel", false, 0.0);
-      m_simZ = m_simDevice.createDouble("Z Accel", false, 0.0);
+      m_simRange =
+          m_simDevice.createEnumDouble(
+              "range",
+              SimDevice.Direction.kOutput,
+              new String[] {"2G", "4G", "8G", "16G"},
+              new double[] {2.0, 4.0, 8.0, 16.0},
+              0);
+      m_simX = m_simDevice.createDouble("x", SimDevice.Direction.kInput, 0.0);
+      m_simY = m_simDevice.createDouble("y", SimDevice.Direction.kInput, 0.0);
+      m_simZ = m_simDevice.createDouble("z", SimDevice.Direction.kInput, 0.0);
     }
 
     m_spi.setClockRate(3000000);
@@ -133,6 +137,10 @@
     SendableRegistry.addLW(this, "ADXL362", port.value);
   }
 
+  public int getPort() {
+    return m_spi.getPort();
+  }
+
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -163,18 +171,17 @@
         m_gsPerLSB = 0.002;
         break;
       case k8G:
-      case k16G:  // 16G not supported; treat as 8G
+      case k16G: // 16G not supported; treat as 8G
         value = kFilterCtl_Range8G;
         m_gsPerLSB = 0.004;
         break;
       default:
         throw new IllegalArgumentException(range + " unsupported");
-
     }
 
     // Specify the data format to read
-    byte[] commands = new byte[]{kRegWrite, kFilterCtlRegister, (byte) (kFilterCtl_ODR_100Hz
-        | value)};
+    byte[] commands =
+        new byte[] {kRegWrite, kFilterCtlRegister, (byte) (kFilterCtl_ODR_100Hz | value)};
     m_spi.write(commands, commands.length);
 
     if (m_simRange != null) {
@@ -182,7 +189,6 @@
     }
   }
 
-
   @Override
   public double getX() {
     return getAcceleration(Axes.kX);
@@ -257,16 +263,17 @@
   }
 
   @Override
-  public void initSendable(SendableBuilder builder) {
+  public void initSendable(NTSendableBuilder builder) {
     builder.setSmartDashboardType("3AxisAccelerometer");
     NetworkTableEntry entryX = builder.getEntry("X");
     NetworkTableEntry entryY = builder.getEntry("Y");
     NetworkTableEntry entryZ = builder.getEntry("Z");
-    builder.setUpdateTable(() -> {
-      AllAxes data = getAccelerations();
-      entryX.setDouble(data.XAxis);
-      entryY.setDouble(data.YAxis);
-      entryZ.setDouble(data.ZAxis);
-    });
+    builder.setUpdateTable(
+        () -> {
+          AllAxes data = getAccelerations();
+          entryX.setDouble(data.XAxis);
+          entryY.setDouble(data.YAxis);
+          entryZ.setDouble(data.ZAxis);
+        });
   }
 }
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 78ab8c4..5a826de 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
@@ -1,22 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimBoolean;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Gyro;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * Use a rate gyro to return the robots heading relative to a starting position. The Gyro class
@@ -29,7 +27,7 @@
  * an ADXRS Gyro is supported.
  */
 @SuppressWarnings({"TypeName", "AbbreviationAsWordInName", "PMD.UnusedPrivateField"})
-public class ADXRS450_Gyro extends GyroBase implements Gyro, PIDSource, Sendable, AutoCloseable {
+public class ADXRS450_Gyro implements Gyro, Sendable {
   private static final double kSamplePeriod = 0.0005;
   private static final double kCalibrationSampleTime = 5.0;
   private static final double kDegreePerSecondPerLSB = 0.0125;
@@ -45,16 +43,13 @@
   private static final int kSNLowRegister = 0x10;
 
   private SPI m_spi;
-  private SPI.Port m_port;
 
   private SimDevice m_simDevice;
   private SimBoolean m_simConnected;
   private SimDouble m_simAngle;
   private SimDouble m_simRate;
 
-  /**
-   * Constructor.  Uses the onboard CS0.
-   */
+  /** Constructor. Uses the onboard CS0. */
   public ADXRS450_Gyro() {
     this(SPI.Port.kOnboardCS0);
   }
@@ -66,14 +61,13 @@
    */
   public ADXRS450_Gyro(SPI.Port port) {
     m_spi = new SPI(port);
-    m_port = port;
 
     // simulation
-    m_simDevice = SimDevice.create("ADXRS450_Gyro", port.value);
+    m_simDevice = SimDevice.create("Gyro:ADXRS450", port.value);
     if (m_simDevice != null) {
-      m_simConnected = m_simDevice.createBoolean("Connected", false, true);
-      m_simAngle = m_simDevice.createDouble("Angle", false, 0.0);
-      m_simRate = m_simDevice.createDouble("Rate", false, 0.0);
+      m_simConnected = m_simDevice.createBoolean("connected", SimDevice.Direction.kInput, true);
+      m_simAngle = m_simDevice.createDouble("angle_x", SimDevice.Direction.kInput, 0.0);
+      m_simRate = m_simDevice.createDouble("rate_x", SimDevice.Direction.kInput, 0.0);
     }
 
     m_spi.setClockRate(3000000);
@@ -87,13 +81,12 @@
       if ((readRegister(kPIDRegister) & 0xff00) != 0x5200) {
         m_spi.close();
         m_spi = null;
-        DriverStation.reportError("could not find ADXRS450 gyro on SPI port " + port.value,
-            false);
+        DriverStation.reportError("could not find ADXRS450 gyro on SPI port " + port.value, false);
         return;
       }
 
-      m_spi.initAccumulator(kSamplePeriod, 0x20000000, 4, 0x0c00000e, 0x04000000, 10, 16,
-          true, true);
+      m_spi.initAccumulator(
+          kSamplePeriod, 0x20000000, 4, 0x0c00000e, 0x04000000, 10, 16, true, true);
 
       calibrate();
     }
@@ -137,7 +130,7 @@
    * @return The SPI port number.
    */
   public int getPort() {
-    return m_port.value;
+    return m_spi.getPort();
   }
 
   private boolean calcParity(int value) {
@@ -164,7 +157,7 @@
     m_spi.read(false, buf, 4);
 
     if ((buf.get(0) & 0xe0) == 0) {
-      return 0;  // error, return 0
+      return 0; // error, return 0
     }
     return (buf.getInt(0) >> 5) & 0xffff;
   }
@@ -172,19 +165,14 @@
   @Override
   public void reset() {
     if (m_simAngle != null) {
-      m_simAngle.set(0.0);
-    }
-    if (m_simRate != null) {
-      m_simRate.set(0.0);
+      m_simAngle.reset();
     }
     if (m_spi != null) {
       m_spi.resetAccumulator();
     }
   }
 
-  /**
-   * Delete (free) the spi port used for the gyro and stop accumulating.
-   */
+  /** Delete (free) the spi port used for the gyro and stop accumulating. */
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -219,4 +207,10 @@
     }
     return m_spi.getAccumulatorLastValue() * kDegreePerSecondPerLSB;
   }
+
+  @Override
+  public void initSendable(SendableBuilder builder) {
+    builder.setSmartDashboardType("Gyro");
+    builder.addDoubleProperty("Value", this::getAngle, null);
+  }
 }
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 5a333db..f4b367a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLED.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -58,8 +55,8 @@
   /**
    * Sets the led output data.
    *
-   * <p>If the output is enabled, this will start writing the next data cycle.
-   * It is safe to call, even while output is enabled.
+   * <p>If the output is enabled, this will start writing the next data cycle. It is safe to call,
+   * even while output is enabled.
    *
    * @param buffer the buffer to write
    */
@@ -77,10 +74,16 @@
    * @param lowTime1NanoSeconds low time for 1 bit
    * @param highTime1NanoSeconds high time for 1 bit
    */
-  public void setBitTiming(int lowTime0NanoSeconds, int highTime0NanoSeconds,
-      int lowTime1NanoSeconds, int highTime1NanoSeconds) {
-    AddressableLEDJNI.setBitTiming(m_handle, lowTime0NanoSeconds,
-        highTime0NanoSeconds, lowTime1NanoSeconds,
+  public void setBitTiming(
+      int lowTime0NanoSeconds,
+      int highTime0NanoSeconds,
+      int lowTime1NanoSeconds,
+      int highTime1NanoSeconds) {
+    AddressableLEDJNI.setBitTiming(
+        m_handle,
+        lowTime0NanoSeconds,
+        highTime0NanoSeconds,
+        lowTime1NanoSeconds,
         highTime1NanoSeconds);
   }
 
@@ -104,9 +107,7 @@
     AddressableLEDJNI.start(m_handle);
   }
 
-  /**
-   * Stops the output.
-   */
+  /** Stops the output. */
   public void stop() {
     AddressableLEDJNI.stop(m_handle);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLEDBuffer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLEDBuffer.java
index 8b76429..c9659a1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLEDBuffer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AddressableLEDBuffer.java
@@ -1,18 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.wpilibj.util.Color;
 import edu.wpi.first.wpilibj.util.Color8Bit;
 
-/**
- * Buffer storage for Addressable LEDs.
- */
+/** Buffer storage for Addressable LEDs. */
 public class AddressableLEDBuffer {
   byte[] m_buffer;
 
@@ -29,9 +24,9 @@
    * Sets a specific led in the buffer.
    *
    * @param index the index to write
-   * @param r     the r value [0-255]
-   * @param g     the g value [0-255]
-   * @param b     the b value [0-255]
+   * @param r the r value [0-255]
+   * @param g the g value [0-255]
+   * @param b the b value [0-255]
    */
   @SuppressWarnings("ParameterName")
   public void setRGB(int index, int r, int g, int b) {
@@ -45,9 +40,9 @@
    * Sets a specific led in the buffer.
    *
    * @param index the index to write
-   * @param h     the h value [0-180]
-   * @param s     the s value [0-255]
-   * @param v     the v value [0-255]
+   * @param h the h value [0-180]
+   * @param s the s value [0-255]
+   * @param v the v value [0-255]
    */
   @SuppressWarnings("ParameterName")
   public void setHSV(final int index, final int h, final int s, final int v) {
@@ -92,10 +87,7 @@
    * @param color The color of the LED
    */
   public void setLED(int index, Color color) {
-    setRGB(index,
-        (int) (color.red * 255),
-        (int) (color.green * 255),
-        (int) (color.blue * 255));
+    setRGB(index, (int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255));
   }
 
   /**
@@ -124,8 +116,8 @@
    * @return the LED color at the specified index
    */
   public Color8Bit getLED8Bit(int index) {
-    return new Color8Bit(m_buffer[index * 4 + 2] & 0xFF, m_buffer[index * 4 + 1] & 0xFF,
-                         m_buffer[index * 4] & 0xFF);
+    return new Color8Bit(
+        m_buffer[index * 4 + 2] & 0xFF, m_buffer[index * 4 + 1] & 0xFF, m_buffer[index * 4] & 0xFF);
   }
 
   /**
@@ -135,9 +127,9 @@
    * @return the LED color at the specified index
    */
   public Color getLED(int index) {
-    return new Color((m_buffer[index * 4 + 2] & 0xFF) / 255.0,
-                     (m_buffer[index * 4 + 1] & 0xFF) / 255.0,
-                     (m_buffer[index * 4] & 0xFF) / 255.0);
+    return new Color(
+        (m_buffer[index * 4 + 2] & 0xFF) / 255.0,
+        (m_buffer[index * 4 + 1] & 0xFF) / 255.0,
+        (m_buffer[index * 4] & 0xFF) / 255.0);
   }
-
 }
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 41c5b38..a2405b8 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogAccelerometer.java
@@ -1,37 +1,31 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Handle operation of an analog accelerometer. The accelerometer reads acceleration directly
  * through the sensor. Many sensors have multiple axis and can be treated as multiple devices. Each
  * is calibrated by finding the center value over a period of time.
  */
-public class AnalogAccelerometer implements PIDSource, Sendable, AutoCloseable {
+public class AnalogAccelerometer implements Sendable, AutoCloseable {
   private AnalogInput m_analogChannel;
   private double m_voltsPerG = 1.0;
   private double m_zeroGVoltage = 2.5;
   private final boolean m_allocatedChannel;
-  protected PIDSourceType m_pidSource = PIDSourceType.kDisplacement;
 
-  /**
-   * Common initialization.
-   */
+  /** Common initialization. */
   private void initAccelerometer() {
-    HAL.report(tResourceType.kResourceType_Accelerometer,
-                                   m_analogChannel.getChannel() + 1);
+    HAL.report(tResourceType.kResourceType_Accelerometer, m_analogChannel.getChannel() + 1);
     SendableRegistry.addLW(this, "Accelerometer", m_analogChannel.getChannel());
   }
 
@@ -53,7 +47,7 @@
    * read as an analog channel as well as through the Accelerometer class.
    *
    * @param channel The existing AnalogInput object for the analog input the accelerometer is
-   *                connected to
+   *     connected to
    */
   public AnalogAccelerometer(final AnalogInput channel) {
     this(channel, false);
@@ -66,9 +60,7 @@
     initAccelerometer();
   }
 
-  /**
-   * Delete the analog components used for the accelerometer.
-   */
+  /** Delete the analog components used for the accelerometer. */
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -117,26 +109,6 @@
   }
 
   @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Get the Acceleration for the PID Source parent.
-   *
-   * @return The current acceleration in Gs.
-   */
-  @Override
-  public double pidGet() {
-    return getAcceleration();
-  }
-
-  @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("Accelerometer");
     builder.addDoubleProperty("Value", this::getAcceleration, null);
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 bea4e6a..24989dd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogEncoder.java
@@ -1,21 +1,18 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.SimDevice;
+import edu.wpi.first.hal.SimDevice.Direction;
 import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.AnalogTriggerOutput.AnalogTriggerType;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
 
-/**
- * Class for supporting continuous analog encoders, such as the US Digital MA3.
- */
+/** Class for supporting continuous analog encoders, such as the US Digital MA3. */
 public class AnalogEncoder implements Sendable, AutoCloseable {
   private final AnalogInput m_analogInput;
   private AnalogTrigger m_analogTrigger;
@@ -28,6 +25,15 @@
   protected SimDouble m_simPosition;
 
   /**
+   * Construct a new AnalogEncoder attached to a specific AnalogIn channel.
+   *
+   * @param channel the analog input channel to attach to
+   */
+  public AnalogEncoder(int channel) {
+    this(new AnalogInput(channel));
+  }
+
+  /**
    * Construct a new AnalogEncoder attached to a specific AnalogInput.
    *
    * @param analogInput the analog input to attach to
@@ -44,7 +50,7 @@
     m_simDevice = SimDevice.create("AnalogEncoder", m_analogInput.getChannel());
 
     if (m_simDevice != null) {
-      m_simPosition = m_simDevice.createDouble("Position", false, 0.0);
+      m_simPosition = m_simDevice.createDouble("Position", Direction.kInput, 0.0);
     }
 
     // Limits need to be 25% from each end
@@ -89,9 +95,9 @@
   /**
    * Get the offset of position relative to the last reset.
    *
-   * <p>getPositionInRotation() - getPositionOffset() will give an encoder absolute
-   * position relative to the last reset. This could potentially be negative,
-   * which needs to be accounted for.
+   * <p>getPositionInRotation() - getPositionOffset() will give an encoder absolute position
+   * relative to the last reset. This could potentially be negative, which needs to be accounted
+   * for.
    *
    * @return the position offset
    */
@@ -100,11 +106,10 @@
   }
 
   /**
-   * Set the distance per rotation of the encoder. This sets the multiplier used
-   * to determine the distance driven based on the rotation value from the
-   * encoder. Set this value based on the how far the mechanism travels in 1
-   * rotation of the encoder, and factor in gearing reductions following the
-   * encoder shaft. This distance can be in any units you like, linear or angular.
+   * Set the distance per rotation of the encoder. This sets the multiplier used to determine the
+   * distance driven based on the rotation value from the encoder. Set this value based on the how
+   * far the mechanism travels in 1 rotation of the encoder, and factor in gearing reductions
+   * following the encoder shaft. This distance can be in any units you like, linear or angular.
    *
    * @param distancePerRotation the distance per rotation of the encoder
    */
@@ -115,16 +120,15 @@
   /**
    * Get the distance per rotation for this encoder.
    *
-   * @return The scale factor that will be used to convert rotation to useful
-   *         units.
+   * @return The scale factor that will be used to convert rotation to useful units.
    */
   public double getDistancePerRotation() {
     return m_distancePerRotation;
   }
 
   /**
-   * Get the distance the sensor has driven since the last reset as scaled by the
-   * value from {@link #setDistancePerRotation(double)}.
+   * Get the distance the sensor has driven since the last reset as scaled by the value from {@link
+   * #setDistancePerRotation(double)}.
    *
    * @return The distance driven since the last reset
    */
@@ -141,9 +145,7 @@
     return m_analogInput.getChannel();
   }
 
-  /**
-   * Reset the Encoder distance to zero.
-   */
+  /** Reset the Encoder distance to zero. */
   public void reset() {
     m_counter.reset();
     m_positionOffset = m_analogInput.getVoltage();
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 0ed22d9..c9b9126 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogGyro.java
@@ -1,19 +1,18 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
 import edu.wpi.first.hal.AnalogGyroJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Gyro;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 /**
  * Use a rate gyro to return the robots heading relative to a starting position. The Gyro class
@@ -24,16 +23,14 @@
  *
  * <p>This class is for gyro sensors that connect to an analog input.
  */
-public class AnalogGyro extends GyroBase implements Gyro, PIDSource, Sendable, AutoCloseable {
+public class AnalogGyro implements Gyro, Sendable {
   private static final double kDefaultVoltsPerDegreePerSecond = 0.007;
   protected AnalogInput m_analog;
   private boolean m_channelAllocated;
 
   private int m_gyroHandle;
 
-  /**
-   * Initialize the gyro. Calibration is handled by calibrate().
-   */
+  /** Initialize the gyro. Calibration is handled by calibrate(). */
   public void initGyro() {
     if (m_gyroHandle == 0) {
       m_gyroHandle = AnalogGyroJNI.initializeAnalogGyro(m_analog.m_port);
@@ -54,7 +51,7 @@
    * Gyro constructor using the channel number.
    *
    * @param channel The analog channel the gyro is connected to. Gyros can only be used on on-board
-   *                channels 0-1.
+   *     channels 0-1.
    */
   public AnalogGyro(int channel) {
     this(new AnalogInput(channel));
@@ -67,7 +64,7 @@
    * channel needs to be shared.
    *
    * @param channel The AnalogInput object that the gyro is connected to. Gyros can only be used on
-   *                on-board channels 0-1.
+   *     on-board channels 0-1.
    */
   public AnalogGyro(AnalogInput channel) {
     requireNonNullParam(channel, "channel", "AnalogGyro");
@@ -82,9 +79,9 @@
    * offset values. Bypasses calibration.
    *
    * @param channel The analog channel the gyro is connected to. Gyros can only be used on on-board
-   *                channels 0-1.
-   * @param center  Preset uncalibrated value to use as the accumulator center value.
-   * @param offset  Preset uncalibrated value to use as the gyro offset.
+   *     channels 0-1.
+   * @param center Preset uncalibrated value to use as the accumulator center value.
+   * @param offset Preset uncalibrated value to use as the gyro offset.
    */
   public AnalogGyro(int channel, int center, double offset) {
     this(new AnalogInput(channel), center, offset);
@@ -97,17 +94,18 @@
    * the center and offset values. Bypasses calibration.
    *
    * @param channel The analog channel the gyro is connected to. Gyros can only be used on on-board
-   *                channels 0-1.
-   * @param center  Preset uncalibrated value to use as the accumulator center value.
-   * @param offset  Preset uncalibrated value to use as the gyro offset.
+   *     channels 0-1.
+   * @param center Preset uncalibrated value to use as the accumulator center value.
+   * @param offset Preset uncalibrated value to use as the gyro offset.
    */
   public AnalogGyro(AnalogInput channel, int center, double offset) {
     requireNonNullParam(channel, "channel", "AnalogGyro");
 
     m_analog = channel;
     initGyro();
-    AnalogGyroJNI.setAnalogGyroParameters(m_gyroHandle, kDefaultVoltsPerDegreePerSecond,
-                                          offset, center);
+    AnalogGyroJNI.setAnalogGyroParameters(
+        m_gyroHandle, kDefaultVoltsPerDegreePerSecond,
+        offset, center);
     reset();
   }
 
@@ -116,9 +114,7 @@
     AnalogGyroJNI.resetAnalogGyro(m_gyroHandle);
   }
 
-  /**
-   * Delete (free) the accumulator and the analog components used for the gyro.
-   */
+  /** Delete (free) the accumulator and the analog components used for the gyro. */
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -173,8 +169,7 @@
    * @param voltsPerDegreePerSecond The sensitivity in Volts/degree/second.
    */
   public void setSensitivity(double voltsPerDegreePerSecond) {
-    AnalogGyroJNI.setAnalogGyroVoltsPerDegreePerSecond(m_gyroHandle,
-                                                       voltsPerDegreePerSecond);
+    AnalogGyroJNI.setAnalogGyroVoltsPerDegreePerSecond(m_gyroHandle, voltsPerDegreePerSecond);
   }
 
   /**
@@ -196,4 +191,10 @@
   public AnalogInput getAnalogInput() {
     return m_analog;
   }
+
+  @Override
+  public void initSendable(SendableBuilder builder) {
+    builder.setSmartDashboardType("Gyro");
+    builder.addDoubleProperty("Value", this::getAngle, null);
+  }
 }
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 3298058..e3bedcb 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogInput.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -13,8 +10,9 @@
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.util.AllocationException;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Analog channel class.
@@ -28,13 +26,12 @@
  * accumulated effectively increasing the resolution, while the averaged samples are divided by the
  * number of samples to retain the resolution, but get more stable values.
  */
-public class AnalogInput implements PIDSource, Sendable, AutoCloseable {
+public class AnalogInput implements Sendable, AutoCloseable {
   private static final int kAccumulatorSlot = 1;
   int m_port; // explicit no modifier, private and package accessible.
   private int m_channel;
   private static final int[] kAccumulatorChannels = {0, 1};
   private long m_accumulatorOffset;
-  protected PIDSourceType m_pidSource = PIDSourceType.kDisplacement;
 
   /**
    * Construct an analog channel.
@@ -183,13 +180,16 @@
     return AnalogJNI.getAnalogOversampleBits(m_port);
   }
 
-  /**
-   * Initialize the accumulator.
-   */
+  /** Initialize the accumulator. */
   public void initAccumulator() {
     if (!isAccumulatorChannel()) {
-      throw new AllocationException("Accumulators are only available on slot " + kAccumulatorSlot
-          + " on channels " + kAccumulatorChannels[0] + ", " + kAccumulatorChannels[1]);
+      throw new AllocationException(
+          "Accumulators are only available on slot "
+              + kAccumulatorSlot
+              + " on channels "
+              + kAccumulatorChannels[0]
+              + ", "
+              + kAccumulatorChannels[1]);
     }
     m_accumulatorOffset = 0;
     AnalogJNI.initAccumulator(m_port);
@@ -206,9 +206,7 @@
     m_accumulatorOffset = initialValue;
   }
 
-  /**
-   * Resets the accumulator to the initial value.
-   */
+  /** Resets the accumulator to the initial value. */
   public void resetAccumulator() {
     AnalogJNI.resetAccumulator(m_port);
 
@@ -218,7 +216,6 @@
     final double overSamples = 1 << getOversampleBits();
     final double averageSamples = 1 << getAverageBits();
     Timer.delay(sampleTime * overSamples * averageSamples);
-
   }
 
   /**
@@ -231,6 +228,8 @@
    * <p>This center value is based on the output of the oversampled and averaged source the
    * accumulator channel. Because of this, any non-zero oversample bits will affect the size of the
    * value for this field.
+   *
+   * @param center The accumulator's center value.
    */
   public void setAccumulatorCenter(int center) {
     AnalogJNI.setAccumulatorCenter(m_port, center);
@@ -325,26 +324,6 @@
     return AnalogJNI.getAnalogSampleRate();
   }
 
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Get the average voltage for use with PIDController.
-   *
-   * @return the average voltage
-   */
-  @Override
-  public double pidGet() {
-    return getAverageVoltage();
-  }
-
   /**
    * Indicates this input is used by a simulated device.
    *
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 feb6e1e..299d89c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogOutput.java
@@ -1,21 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.AnalogJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
-/**
- * Analog output class.
- */
+/** Analog output class. */
 public class AnalogOutput implements Sendable, AutoCloseable {
   private int m_port;
   private int m_channel;
@@ -46,6 +42,8 @@
 
   /**
    * Get the channel of this AnalogOutput.
+   *
+   * @return The channel of this AnalogOutput.
    */
   public int getChannel() {
     return m_channel;
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 9d5d49d..4a5bd75 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogPotentiometer.java
@@ -1,27 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
-import edu.wpi.first.wpilibj.interfaces.Potentiometer;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class for reading analog potentiometers. Analog potentiometers read in an analog voltage that
  * corresponds to a position. The position is in whichever units you choose, by way of the scaling
  * and offset constants passed to the constructor.
  */
-public class AnalogPotentiometer implements Potentiometer, Sendable, AutoCloseable {
+public class AnalogPotentiometer implements Sendable, AutoCloseable {
   private AnalogInput m_analogInput;
   private boolean m_initAnalogInput;
   private double m_fullRange;
   private double m_offset;
-  protected PIDSourceType m_pidSource = PIDSourceType.kDisplacement;
 
   /**
    * AnalogPotentiometer constructor.
@@ -29,13 +25,13 @@
    * <p>Use the fullRange and offset values so that the output produces meaningful values. I.E: you
    * have a 270 degree potentiometer and you want the output to be degrees with the halfway point as
    * 0 degrees. The fullRange value is 270.0(degrees) and the offset is -135.0 since the halfway
-   * point after scaling is 135 degrees. This will calculate the result from the fullRange times
-   * the fraction of the supply voltage, plus the offset.
+   * point after scaling is 135 degrees. This will calculate the result from the fullRange times the
+   * fraction of the supply voltage, plus the offset.
    *
-   * @param channel   The analog input channel this potentiometer is plugged into. 0-3 are
-   *                  on-board and 4-7 are on the MXP port.
+   * @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
+   *     and 4-7 are on the MXP port.
    * @param fullRange The scaling to multiply the fraction by to get a meaningful unit.
-   * @param offset    The offset to add to the scaled value for controlling the zero value
+   * @param offset The offset to add to the scaled value for controlling the zero value
    */
   public AnalogPotentiometer(final int channel, double fullRange, double offset) {
     this(new AnalogInput(channel), fullRange, offset);
@@ -47,16 +43,15 @@
    * AnalogPotentiometer constructor.
    *
    * <p>Use the fullRange and offset values so that the output produces meaningful values. I.E: you
-   * have a 270 degree potentiometer and you want the output to be degrees with the halfway point
-   * as 0 degrees. The fullRange value is 270.0(degrees) and the offset is -135.0 since the halfway
-   * point after scaling is 135 degrees. This will calculate the result from the fullRange times
-   * the fraction of the supply voltage, plus the offset.
+   * have a 270 degree potentiometer and you want the output to be degrees with the halfway point as
+   * 0 degrees. The fullRange value is 270.0(degrees) and the offset is -135.0 since the halfway
+   * point after scaling is 135 degrees. This will calculate the result from the fullRange times the
+   * fraction of the supply voltage, plus the offset.
    *
-   * @param input     The {@link AnalogInput} this potentiometer is plugged into.
-   * @param fullRange The angular value (in desired units) representing the full
-   *                  0-5V range of the input.
-   * @param offset    The angular value (in desired units) representing the
-   *                  angular output at 0V.
+   * @param input The {@link AnalogInput} this potentiometer is plugged into.
+   * @param fullRange The angular value (in desired units) representing the full 0-5V range of the
+   *     input.
+   * @param offset The angular value (in desired units) representing the angular output at 0V.
    */
   public AnalogPotentiometer(final AnalogInput input, double fullRange, double offset) {
     SendableRegistry.addLW(this, "AnalogPotentiometer", input.getChannel());
@@ -70,13 +65,13 @@
   /**
    * AnalogPotentiometer constructor.
    *
-   * <p>Use the scale value so that the output produces meaningful values. I.E: you
-   * have a 270 degree potentiometer and you want the output to be degrees with the starting point
-   * as 0 degrees. The scale value is 270.0(degrees).
+   * <p>Use the scale value so that the output produces meaningful values. I.E: you have a 270
+   * degree potentiometer and you want the output to be degrees with the starting point as 0
+   * degrees. The scale value is 270.0(degrees).
    *
-   * @param channel The analog input channel this potentiometer is plugged into. 0-3 are
-   *                on-board and 4-7 are on the MXP port.
-   * @param scale   The scaling to multiply the voltage by to get a meaningful unit.
+   * @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
+   *     and 4-7 are on the MXP port.
+   * @param scale The scaling to multiply the voltage by to get a meaningful unit.
    */
   public AnalogPotentiometer(final int channel, double scale) {
     this(channel, scale, 0);
@@ -101,8 +96,8 @@
    *
    * <p>The potentiometer will return a value between 0 and 1.0.
    *
-   * @param channel The analog input channel this potentiometer is plugged into. 0-3 are
-   *                  on-board and 4-7 are on the MXP port.
+   * @param channel The analog input channel this potentiometer is plugged into. 0-3 are on-board
+   *     and 4-7 are on the MXP port.
    */
   public AnalogPotentiometer(final int channel) {
     this(channel, 1, 0);
@@ -124,36 +119,12 @@
    *
    * @return The current position of the potentiometer.
    */
-  @Override
   public double get() {
     if (m_analogInput == null) {
       return m_offset;
     }
-    return (m_analogInput.getAverageVoltage() / RobotController.getVoltage5V())
-        * m_fullRange + m_offset;
-  }
-
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    if (!pidSource.equals(PIDSourceType.kDisplacement)) {
-      throw new IllegalArgumentException("Only displacement PID is allowed for potentiometers.");
-    }
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Implement the PIDSource interface.
-   *
-   * @return The current reading.
-   */
-  @Override
-  public double pidGet() {
-    return get();
+    return (m_analogInput.getAverageVoltage() / RobotController.getVoltage5V()) * m_fullRange
+        + m_offset;
   }
 
   @Override
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 282a346..d5825c1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTrigger.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,18 +8,14 @@
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.util.BoundaryException;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.AnalogTriggerOutput.AnalogTriggerType;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
 
-
-/**
- * Class for creating and configuring Analog Triggers.
- */
+/** Class for creating and configuring Analog Triggers. */
 public class AnalogTrigger implements Sendable, AutoCloseable {
-  /**
-   * Exceptions dealing with improper operation of the Analog trigger.
-   */
+  /** Exceptions dealing with improper operation of the Analog trigger. */
   @SuppressWarnings("serial")
   public static class AnalogTriggerException extends RuntimeException {
     /**
@@ -33,13 +26,11 @@
     public AnalogTriggerException(String message) {
       super(message);
     }
-
   }
 
-  /**
-   * Where the analog trigger is attached.
-   */
+  /** Where the analog trigger is attached. */
   protected int m_port;
+
   protected AnalogInput m_analogInput;
   protected DutyCycle m_dutyCycle;
   protected boolean m_ownsAnalog;
@@ -162,8 +153,7 @@
   }
 
   /**
-   * Return the index of the analog trigger. This is the FPGA index of this analog trigger
-   * instance.
+   * Return the index of the analog trigger. This is the FPGA index of this analog trigger instance.
    *
    * @return The index of the analog trigger.
    */
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 f9fe950..0f513b4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AnalogTriggerOutput.java
@@ -1,18 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
 import edu.wpi.first.hal.AnalogJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
 
 /**
  * Class to represent a specific output from an analog trigger. This class is used to get the
@@ -41,9 +39,7 @@
  * limited.
  */
 public class AnalogTriggerOutput extends DigitalSource implements Sendable {
-  /**
-   * Exceptions dealing with improper operation of the Analog trigger output.
-   */
+  /** Exceptions dealing with improper operation of the Analog trigger output. */
   @SuppressWarnings("serial")
   public static class AnalogTriggerOutputException extends RuntimeException {
     /**
@@ -65,7 +61,7 @@
    * <p>Because this class derives from DigitalSource, it can be passed into routing functions for
    * Counter, Encoder, etc.
    *
-   * @param trigger    The trigger for which this is an output.
+   * @param trigger The trigger for which this is an output.
    * @param outputType An enum that specifies the output on the trigger to represent.
    */
   public AnalogTriggerOutput(AnalogTrigger trigger, final AnalogTriggerType outputType) {
@@ -74,8 +70,10 @@
 
     m_trigger = trigger;
     m_outputType = outputType;
-    HAL.report(tResourceType.kResourceType_AnalogTriggerOutput,
-        trigger.getIndex() + 1, outputType.value + 1);
+    HAL.report(
+        tResourceType.kResourceType_AnalogTriggerOutput,
+        trigger.getIndex() + 1,
+        outputType.value + 1);
   }
 
   /**
@@ -107,15 +105,13 @@
     return true;
   }
 
-  /**
-   * Defines the state in which the AnalogTrigger triggers.
-   */
+  /** Defines the state in which the AnalogTrigger triggers. */
   public enum AnalogTriggerType {
-    kInWindow(AnalogJNI.AnalogTriggerType.kInWindow), kState(AnalogJNI.AnalogTriggerType.kState),
+    kInWindow(AnalogJNI.AnalogTriggerType.kInWindow),
+    kState(AnalogJNI.AnalogTriggerType.kState),
     kRisingPulse(AnalogJNI.AnalogTriggerType.kRisingPulse),
     kFallingPulse(AnalogJNI.AnalogTriggerType.kFallingPulse);
 
-    @SuppressWarnings("MemberName")
     private final int value;
 
     AnalogTriggerType(int value) {
@@ -124,6 +120,5 @@
   }
 
   @Override
-  public void initSendable(SendableBuilder builder) {
-  }
+  public void initSendable(SendableBuilder builder) {}
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/AsynchronousInterrupt.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AsynchronousInterrupt.java
new file mode 100644
index 0000000..1981671
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/AsynchronousInterrupt.java
@@ -0,0 +1,140 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+
+/**
+ * Class for handling asynchronous interrupts using a callback thread.
+ *
+ * <p>By default, interrupts will occur on rising edge. Callbacks are disabled by default, and
+ * enable() must be called before they will occur.
+ *
+ * <p>Both rising and falling edges can be indicated in one callback if both a rising and falling
+ * edge occurred since the previous callback.
+ *
+ * <p>Synchronous (blocking) interrupts are handled by the SynchronousInterrupt class.
+ */
+public class AsynchronousInterrupt implements AutoCloseable {
+  private final BiConsumer<Boolean, Boolean> m_callback;
+  private final SynchronousInterrupt m_interrupt;
+
+  private final AtomicBoolean m_keepRunning = new AtomicBoolean(false);
+  private Thread m_thread;
+
+  /**
+   * Construct a new asynchronous interrupt using a Digital Source.
+   *
+   * <p>At construction, the interrupt will trigger on the rising edge.
+   *
+   * <p>Callbacks will not be triggered until enable() is called.
+   *
+   * <p>The first bool in the callback indicates the rising edge triggered the interrupt, the second
+   * bool is falling edge.
+   *
+   * @param source The digital source to use.
+   * @param callback The callback to call on an interrupt
+   */
+  public AsynchronousInterrupt(DigitalSource source, BiConsumer<Boolean, Boolean> callback) {
+    m_callback = requireNonNullParam(callback, "callback", "AsynchronousInterrupt");
+    m_interrupt = new SynchronousInterrupt(source);
+  }
+
+  /**
+   * Closes the interrupt.
+   *
+   * <p>This does not close the associated digital source.
+   *
+   * <p>This will disable the interrupt if it is enabled.
+   */
+  @Override
+  public void close() {
+    disable();
+    m_interrupt.close();
+  }
+
+  /**
+   * Enables interrupt callbacks. Before this, callbacks will not occur. Does nothing if already
+   * enabled.
+   */
+  public void enable() {
+    if (m_keepRunning.get()) {
+      return;
+    }
+
+    m_keepRunning.set(true);
+    m_thread = new Thread(this::threadMain);
+    m_thread.start();
+  }
+
+  /** Disables interrupt callbacks. Does nothing if already disabled. */
+  public void disable() {
+    m_keepRunning.set(false);
+    m_interrupt.wakeupWaitingInterrupt();
+    if (m_thread != null) {
+      if (m_thread.isAlive()) {
+        try {
+          m_thread.interrupt();
+          m_thread.join();
+        } catch (InterruptedException ex) {
+          Thread.currentThread().interrupt();
+        }
+      }
+      m_thread = null;
+    }
+  }
+
+  /**
+   * Set which edges to trigger the interrupt on.
+   *
+   * @param risingEdge Trigger on rising edge
+   * @param fallingEdge Trigger on falling edge
+   */
+  public void setInterruptEdges(boolean risingEdge, boolean fallingEdge) {
+    m_interrupt.setInterruptEdges(risingEdge, fallingEdge);
+  }
+
+  /**
+   * Get the timestamp of the last rising edge.
+   *
+   * <p>This function does not require the interrupt to be enabled to work.
+   *
+   * <p>This only works if rising edge was configured using setInterruptEdges.
+   *
+   * @return the timestamp in seconds relative to getFPGATime
+   */
+  public double getRisingTimestamp() {
+    return m_interrupt.getRisingTimestamp();
+  }
+
+  /**
+   * Get the timestamp of the last falling edge.
+   *
+   * <p>This function does not require the interrupt to be enabled to work.
+   *
+   * <p>This only works if falling edge was configured using setInterruptEdges.
+   *
+   * @return the timestamp in seconds relative to getFPGATime
+   */
+  public double getFallingTimestamp() {
+    return m_interrupt.getFallingTimestamp();
+  }
+
+  private void threadMain() {
+    while (m_keepRunning.get()) {
+      var result = m_interrupt.waitForInterruptRaw(10, false);
+      if (!m_keepRunning.get()) {
+        break;
+      }
+      if (result == 0) {
+        continue;
+      }
+      m_callback.accept((result & 0x1) != 0, (result & 0x100) != 0);
+    }
+  }
+}
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 e367756..76c282a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/BuiltInAccelerometer.java
@@ -1,18 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.AccelerometerJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.interfaces.Accelerometer;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
 
 /**
  * Built-in accelerometer.
@@ -31,9 +29,7 @@
     SendableRegistry.addLW(this, "BuiltInAccel", 0);
   }
 
-  /**
-   * Constructor. The accelerometer will measure +/-8 g-forces
-   */
+  /** Constructor. The accelerometer will measure +/-8 g-forces */
   public BuiltInAccelerometer() {
     this(Range.k8G);
   }
@@ -60,7 +56,6 @@
       case k16G:
       default:
         throw new IllegalArgumentException(range + " range not supported (use k2G, k4G, or k8G)");
-
     }
 
     AccelerometerJNI.setAccelerometerActive(true);
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 2dde3ab..3dc6c1d 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CAN.java
@@ -1,29 +1,24 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.io.Closeable;
-
 import edu.wpi.first.hal.CANAPIJNI;
 import edu.wpi.first.hal.CANData;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
+import java.io.Closeable;
 
 /**
- * High level class for interfacing with CAN devices conforming to
- * the standard CAN spec.
+ * High level class for interfacing with CAN devices conforming to the standard CAN spec.
  *
- * <p>No packets that can be sent gets blocked by the RoboRIO, so all methods
- * work identically in all robot modes.
+ * <p>No packets that can be sent gets blocked by the RoboRIO, so all methods work identically in
+ * all robot modes.
  *
- * <p>All methods are thread safe, however the CANData object passed into the
- * read methods and the byte[] passed into the write methods need to not
- * be modified for the duration of their respective calls.
+ * <p>All methods are thread safe, however the CANData object passed into the read methods and the
+ * byte[] passed into the write methods need to not be modified for the duration of their respective
+ * calls.
  */
 public class CAN implements Closeable {
   public static final int kTeamManufacturer = 8;
@@ -32,9 +27,8 @@
   private final int m_handle;
 
   /**
-   * Create a new CAN communication interface with the specific device ID.
-   * This uses the team manufacturer and device types.
-   * The device ID is 6 bits (0-63).
+   * Create a new CAN communication interface with the specific device ID. This uses the team
+   * manufacturer and device types. The device ID is 6 bits (0-63).
    *
    * @param deviceId The device id
    */
@@ -44,22 +38,19 @@
   }
 
   /**
-   * Create a new CAN communication interface with a specific device ID,
-   * manufacturer and device type. The device ID is 6 bits, the
-   * manufacturer is 8 bits, and the device type is 5 bits.
+   * Create a new CAN communication interface with a specific device ID, manufacturer and device
+   * type. The device ID is 6 bits, the manufacturer is 8 bits, and the device type is 5 bits.
    *
-   * @param deviceId           The device ID
+   * @param deviceId The device ID
    * @param deviceManufacturer The device manufacturer
-   * @param deviceType         The device type
+   * @param deviceType The device type
    */
   public CAN(int deviceId, int deviceManufacturer, int deviceType) {
     m_handle = CANAPIJNI.initializeCAN(deviceManufacturer, deviceId, deviceType);
     HAL.report(tResourceType.kResourceType_CAN, deviceId + 1);
   }
 
-  /**
-   * Closes the CAN communication.
-   */
+  /** Closes the CAN communication. */
   @Override
   public void close() {
     if (m_handle != 0) {
@@ -78,8 +69,8 @@
   }
 
   /**
-   * Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits.
-   * The RoboRIO will automatically repeat the packet at the specified interval
+   * Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits. The RoboRIO
+   * will automatically repeat the packet at the specified interval
    *
    * @param data The data to write (8 bytes max)
    * @param apiId The API ID to write.
@@ -90,8 +81,8 @@
   }
 
   /**
-   * Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits.
-   * The length by spec must match what is returned by the responding device
+   * Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits. The length by spec
+   * must match what is returned by the responding device
    *
    * @param length The length to request (0 to 8)
    * @param apiId The API ID to write.
@@ -105,29 +96,32 @@
    *
    * @param data The data to write (8 bytes max)
    * @param apiId The API ID to write.
+   * @return TODO
    */
   public int writePacketNoThrow(byte[] data, int apiId) {
     return CANAPIJNI.writeCANPacketNoThrow(m_handle, data, apiId);
   }
 
   /**
-   * Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits.
-   * The RoboRIO will automatically repeat the packet at the specified interval
+   * Write a repeating packet to the CAN device with a specific ID. This ID is 10 bits. The RoboRIO
+   * will automatically repeat the packet at the specified interval
    *
    * @param data The data to write (8 bytes max)
    * @param apiId The API ID to write.
    * @param repeatMs The period to repeat the packet at.
+   * @return TODO
    */
   public int writePacketRepeatingNoThrow(byte[] data, int apiId, int repeatMs) {
     return CANAPIJNI.writeCANPacketRepeatingNoThrow(m_handle, data, apiId, repeatMs);
   }
 
   /**
-   * Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits.
-   * The length by spec must match what is returned by the responding device
+   * Write an RTR frame to the CAN device with a specific ID. This ID is 10 bits. The length by spec
+   * must match what is returned by the responding device
    *
    * @param length The length to request (0 to 8)
    * @param apiId The API ID to write.
+   * @return TODO
    */
   public int writeRTRFrameNoThrow(int length, int apiId) {
     return CANAPIJNI.writeCANRTRFrameNoThrow(m_handle, length, apiId);
@@ -143,8 +137,8 @@
   }
 
   /**
-   * Read a new CAN packet. This will only return properly once per packet
-   * received. Multiple calls without receiving another packet will return false.
+   * Read a new CAN packet. This will only return properly once per packet received. Multiple calls
+   * without receiving another packet will return false.
    *
    * @param apiId The API ID to read.
    * @param data Storage for the received data.
@@ -155,8 +149,8 @@
   }
 
   /**
-   * Read a CAN packet. The will continuously return the last packet received,
-   * without accounting for packet age.
+   * Read a CAN packet. The will continuously return the last packet received, without accounting
+   * for packet age.
    *
    * @param apiId The API ID to read.
    * @param data Storage for the received data.
@@ -167,8 +161,8 @@
   }
 
   /**
-   * Read a CAN packet. The will return the last packet received until the
-   * packet is older then the requested timeout. Then it will return false.
+   * Read a CAN packet. The will return the last packet received until the packet is older then the
+   * requested timeout. Then it will return false.
    *
    * @param apiId The API ID to read.
    * @param timeoutMs The timeout time for the packet
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java
deleted file mode 100644
index c8948b9..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java
+++ /dev/null
@@ -1,783 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import edu.wpi.cscore.AxisCamera;
-import edu.wpi.cscore.CameraServerJNI;
-import edu.wpi.cscore.CvSink;
-import edu.wpi.cscore.CvSource;
-import edu.wpi.cscore.MjpegServer;
-import edu.wpi.cscore.UsbCamera;
-import edu.wpi.cscore.VideoEvent;
-import edu.wpi.cscore.VideoException;
-import edu.wpi.cscore.VideoListener;
-import edu.wpi.cscore.VideoMode;
-import edu.wpi.cscore.VideoMode.PixelFormat;
-import edu.wpi.cscore.VideoProperty;
-import edu.wpi.cscore.VideoSink;
-import edu.wpi.cscore.VideoSource;
-import edu.wpi.first.cameraserver.CameraServerSharedStore;
-import edu.wpi.first.networktables.EntryListenerFlags;
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.networktables.NetworkTableInstance;
-
-/**
- * Singleton class for creating and keeping camera servers.
- * Also publishes camera information to NetworkTables.
- *
- * @deprecated Replaced with edu.wpi.first.cameraserver.CameraServer
- */
-@Deprecated
-public final class CameraServer {
-  public static final int kBasePort = 1181;
-
-  @Deprecated
-  public static final int kSize640x480 = 0;
-  @Deprecated
-  public static final int kSize320x240 = 1;
-  @Deprecated
-  public static final int kSize160x120 = 2;
-
-  private static final String kPublishName = "/CameraPublisher";
-  private static CameraServer server;
-
-  /**
-   * Get the CameraServer instance.
-   */
-  public static synchronized CameraServer getInstance() {
-    if (server == null) {
-      server = new CameraServer();
-    }
-    return server;
-  }
-
-  private final AtomicInteger m_defaultUsbDevice;
-  private String m_primarySourceName;
-  private final Map<String, VideoSource> m_sources;
-  private final Map<String, VideoSink> m_sinks;
-  private final Map<Integer, NetworkTable> m_tables;  // indexed by source handle
-  private final NetworkTable m_publishTable;
-  private final VideoListener m_videoListener; //NOPMD
-  private final int m_tableListener; //NOPMD
-  private int m_nextPort;
-  private String[] m_addresses;
-
-  @SuppressWarnings("JavadocMethod")
-  private static String makeSourceValue(int source) {
-    switch (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))) {
-      case kUsb:
-        return "usb:" + CameraServerJNI.getUsbCameraPath(source);
-      case kHttp: {
-        String[] urls = CameraServerJNI.getHttpCameraUrls(source);
-        if (urls.length > 0) {
-          return "ip:" + urls[0];
-        } else {
-          return "ip:";
-        }
-      }
-      case kCv:
-        // FIXME: Should be "cv:", but LabVIEW dashboard requires "usb:".
-        // https://github.com/wpilibsuite/allwpilib/issues/407
-        return "usb:";
-      default:
-        return "unknown:";
-    }
-  }
-
-  @SuppressWarnings("JavadocMethod")
-  private static String makeStreamValue(String address, int port) {
-    return "mjpg:http://" + address + ":" + port + "/?action=stream";
-  }
-
-  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
-  private synchronized String[] getSinkStreamValues(int sink) {
-    // Ignore all but MjpegServer
-    if (VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) != VideoSink.Kind.kMjpeg) {
-      return new String[0];
-    }
-
-    // Get port
-    int port = CameraServerJNI.getMjpegServerPort(sink);
-
-    // Generate values
-    ArrayList<String> values = new ArrayList<>(m_addresses.length + 1);
-    String listenAddress = CameraServerJNI.getMjpegServerListenAddress(sink);
-    if (!listenAddress.isEmpty()) {
-      // If a listen address is specified, only use that
-      values.add(makeStreamValue(listenAddress, port));
-    } else {
-      // Otherwise generate for hostname and all interface addresses
-      values.add(makeStreamValue(CameraServerJNI.getHostname() + ".local", port));
-      for (String addr : m_addresses) {
-        if ("127.0.0.1".equals(addr)) {
-          continue;  // ignore localhost
-        }
-        values.add(makeStreamValue(addr, port));
-      }
-    }
-
-    return values.toArray(new String[0]);
-  }
-
-  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
-  private synchronized String[] getSourceStreamValues(int source) {
-    // Ignore all but HttpCamera
-    if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))
-            != VideoSource.Kind.kHttp) {
-      return new String[0];
-    }
-
-    // Generate values
-    String[] values = CameraServerJNI.getHttpCameraUrls(source);
-    for (int j = 0; j < values.length; j++) {
-      values[j] = "mjpg:" + values[j];
-    }
-
-    if (CameraServerSharedStore.getCameraServerShared().isRoboRIO()) {
-      // Look to see if we have a passthrough server for this source
-      // Only do this on the roboRIO
-      for (VideoSink i : m_sinks.values()) {
-        int sink = i.getHandle();
-        int sinkSource = CameraServerJNI.getSinkSource(sink);
-        if (source == sinkSource
-            && VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink))
-            == VideoSink.Kind.kMjpeg) {
-          // Add USB-only passthrough
-          String[] finalValues = Arrays.copyOf(values, values.length + 1);
-          int port = CameraServerJNI.getMjpegServerPort(sink);
-          finalValues[values.length] = makeStreamValue("172.22.11.2", port);
-          return finalValues;
-        }
-      }
-    }
-
-    return values;
-  }
-
-  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
-  private synchronized void updateStreamValues() {
-    // Over all the sinks...
-    for (VideoSink i : m_sinks.values()) {
-      int sink = i.getHandle();
-
-      // Get the source's subtable (if none exists, we're done)
-      int source = CameraServerJNI.getSinkSource(sink);
-      if (source == 0) {
-        continue;
-      }
-      NetworkTable table = m_tables.get(source);
-      if (table != null) {
-        // Don't set stream values if this is a HttpCamera passthrough
-        if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))
-            == VideoSource.Kind.kHttp) {
-          continue;
-        }
-
-        // Set table value
-        String[] values = getSinkStreamValues(sink);
-        if (values.length > 0) {
-          table.getEntry("streams").setStringArray(values);
-        }
-      }
-    }
-
-    // Over all the sources...
-    for (VideoSource i : m_sources.values()) {
-      int source = i.getHandle();
-
-      // Get the source's subtable (if none exists, we're done)
-      NetworkTable table = m_tables.get(source);
-      if (table != null) {
-        // Set table value
-        String[] values = getSourceStreamValues(source);
-        if (values.length > 0) {
-          table.getEntry("streams").setStringArray(values);
-        }
-      }
-    }
-  }
-
-  @SuppressWarnings("JavadocMethod")
-  private static String pixelFormatToString(PixelFormat pixelFormat) {
-    switch (pixelFormat) {
-      case kMJPEG:
-        return "MJPEG";
-      case kYUYV:
-        return "YUYV";
-      case kRGB565:
-        return "RGB565";
-      case kBGR:
-        return "BGR";
-      case kGray:
-        return "Gray";
-      default:
-        return "Unknown";
-    }
-  }
-
-  /// Provide string description of video mode.
-  /// The returned string is "{width}x{height} {format} {fps} fps".
-  @SuppressWarnings("JavadocMethod")
-  private static String videoModeToString(VideoMode mode) {
-    return mode.width + "x" + mode.height + " " + pixelFormatToString(mode.pixelFormat)
-        + " " + mode.fps + " fps";
-  }
-
-  @SuppressWarnings("JavadocMethod")
-  private static String[] getSourceModeValues(int sourceHandle) {
-    VideoMode[] modes = CameraServerJNI.enumerateSourceVideoModes(sourceHandle);
-    String[] modeStrings = new String[modes.length];
-    for (int i = 0; i < modes.length; i++) {
-      modeStrings[i] = videoModeToString(modes[i]);
-    }
-    return modeStrings;
-  }
-
-  @SuppressWarnings({"JavadocMethod", "PMD.CyclomaticComplexity"})
-  private static void putSourcePropertyValue(NetworkTable table, VideoEvent event, boolean isNew) {
-    String name;
-    String infoName;
-    if (event.name.startsWith("raw_")) {
-      name = "RawProperty/" + event.name;
-      infoName = "RawPropertyInfo/" + event.name;
-    } else {
-      name = "Property/" + event.name;
-      infoName = "PropertyInfo/" + event.name;
-    }
-
-    NetworkTableEntry entry = table.getEntry(name);
-    try {
-      switch (event.propertyKind) {
-        case kBoolean:
-          if (isNew) {
-            entry.setDefaultBoolean(event.value != 0);
-          } else {
-            entry.setBoolean(event.value != 0);
-          }
-          break;
-        case kInteger:
-        case kEnum:
-          if (isNew) {
-            entry.setDefaultDouble(event.value);
-            table.getEntry(infoName + "/min").setDouble(
-                CameraServerJNI.getPropertyMin(event.propertyHandle));
-            table.getEntry(infoName + "/max").setDouble(
-                CameraServerJNI.getPropertyMax(event.propertyHandle));
-            table.getEntry(infoName + "/step").setDouble(
-                CameraServerJNI.getPropertyStep(event.propertyHandle));
-            table.getEntry(infoName + "/default").setDouble(
-                CameraServerJNI.getPropertyDefault(event.propertyHandle));
-          } else {
-            entry.setDouble(event.value);
-          }
-          break;
-        case kString:
-          if (isNew) {
-            entry.setDefaultString(event.valueStr);
-          } else {
-            entry.setString(event.valueStr);
-          }
-          break;
-        default:
-          break;
-      }
-    } catch (VideoException ignored) {
-      // ignore
-    }
-  }
-
-  @SuppressWarnings({"JavadocMethod", "PMD.UnusedLocalVariable", "PMD.ExcessiveMethodLength",
-      "PMD.NPathComplexity"})
-  private CameraServer() {
-    m_defaultUsbDevice = new AtomicInteger();
-    m_sources = new Hashtable<>();
-    m_sinks = new Hashtable<>();
-    m_tables = new Hashtable<>();
-    m_publishTable = NetworkTableInstance.getDefault().getTable(kPublishName);
-    m_nextPort = kBasePort;
-    m_addresses = new String[0];
-
-    // We publish sources to NetworkTables using the following structure:
-    // "/CameraPublisher/{Source.Name}/" - root
-    // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0")
-    // - "streams" (string array): URLs that can be used to stream data
-    // - "description" (string): Description of the source
-    // - "connected" (boolean): Whether source is connected
-    // - "mode" (string): Current video mode
-    // - "modes" (string array): Available video modes
-    // - "Property/{Property}" - Property values
-    // - "PropertyInfo/{Property}" - Property supporting information
-
-    // Listener for video events
-    m_videoListener = new VideoListener(event -> {
-      switch (event.kind) {
-        case kSourceCreated: {
-          // Create subtable for the camera
-          NetworkTable table = m_publishTable.getSubTable(event.name);
-          m_tables.put(event.sourceHandle, table);
-          table.getEntry("source").setString(makeSourceValue(event.sourceHandle));
-          table.getEntry("description").setString(
-              CameraServerJNI.getSourceDescription(event.sourceHandle));
-          table.getEntry("connected").setBoolean(
-              CameraServerJNI.isSourceConnected(event.sourceHandle));
-          table.getEntry("streams").setStringArray(getSourceStreamValues(event.sourceHandle));
-          try {
-            VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle);
-            table.getEntry("mode").setDefaultString(videoModeToString(mode));
-            table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle));
-          } catch (VideoException ignored) {
-            // Do nothing. Let the other event handlers update this if there is an error.
-          }
-          break;
-        }
-        case kSourceDestroyed: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            table.getEntry("source").setString("");
-            table.getEntry("streams").setStringArray(new String[0]);
-            table.getEntry("modes").setStringArray(new String[0]);
-          }
-          break;
-        }
-        case kSourceConnected: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            // update the description too (as it may have changed)
-            table.getEntry("description").setString(
-                CameraServerJNI.getSourceDescription(event.sourceHandle));
-            table.getEntry("connected").setBoolean(true);
-          }
-          break;
-        }
-        case kSourceDisconnected: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            table.getEntry("connected").setBoolean(false);
-          }
-          break;
-        }
-        case kSourceVideoModesUpdated: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle));
-          }
-          break;
-        }
-        case kSourceVideoModeChanged: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            table.getEntry("mode").setString(videoModeToString(event.mode));
-          }
-          break;
-        }
-        case kSourcePropertyCreated: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            putSourcePropertyValue(table, event, true);
-          }
-          break;
-        }
-        case kSourcePropertyValueUpdated: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            putSourcePropertyValue(table, event, false);
-          }
-          break;
-        }
-        case kSourcePropertyChoicesUpdated: {
-          NetworkTable table = m_tables.get(event.sourceHandle);
-          if (table != null) {
-            try {
-              String[] choices = CameraServerJNI.getEnumPropertyChoices(event.propertyHandle);
-              table.getEntry("PropertyInfo/" + event.name + "/choices").setStringArray(choices);
-            } catch (VideoException ignored) {
-              // ignore
-            }
-          }
-          break;
-        }
-        case kSinkSourceChanged:
-        case kSinkCreated:
-        case kSinkDestroyed:
-        case kNetworkInterfacesChanged: {
-          m_addresses = CameraServerJNI.getNetworkInterfaces();
-          updateStreamValues();
-          break;
-        }
-        default:
-          break;
-      }
-    }, 0x4fff, true);
-
-    // Listener for NetworkTable events
-    // We don't currently support changing settings via NT due to
-    // synchronization issues, so just update to current setting if someone
-    // else tries to change it.
-    m_tableListener = NetworkTableInstance.getDefault().addEntryListener(kPublishName + "/",
-      event -> {
-        String relativeKey = event.name.substring(kPublishName.length() + 1);
-
-        // get source (sourceName/...)
-        int subKeyIndex = relativeKey.indexOf('/');
-        if (subKeyIndex == -1) {
-          return;
-        }
-        String sourceName = relativeKey.substring(0, subKeyIndex);
-        VideoSource source = m_sources.get(sourceName);
-        if (source == null) {
-          return;
-        }
-
-        // get subkey
-        relativeKey = relativeKey.substring(subKeyIndex + 1);
-
-        // handle standard names
-        String propName;
-        if ("mode".equals(relativeKey)) {
-          // reset to current mode
-          event.getEntry().setString(videoModeToString(source.getVideoMode()));
-          return;
-        } else if (relativeKey.startsWith("Property/")) {
-          propName = relativeKey.substring(9);
-        } else if (relativeKey.startsWith("RawProperty/")) {
-          propName = relativeKey.substring(12);
-        } else {
-          return;  // ignore
-        }
-
-        // everything else is a property
-        VideoProperty property = source.getProperty(propName);
-        switch (property.getKind()) {
-          case kNone:
-            return;
-          case kBoolean:
-            // reset to current setting
-            event.getEntry().setBoolean(property.get() != 0);
-            return;
-          case kInteger:
-          case kEnum:
-            // reset to current setting
-            event.getEntry().setDouble(property.get());
-            return;
-          case kString:
-            // reset to current setting
-            event.getEntry().setString(property.getString());
-            return;
-          default:
-            return;
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kUpdate);
-  }
-
-  /**
-   * Start automatically capturing images to send to the dashboard.
-   *
-   * <p>You should call this method to see a camera feed on the dashboard.
-   * If you also want to perform vision processing on the roboRIO, use
-   * getVideo() to get access to the camera images.
-   *
-   * <p>The first time this overload is called, it calls
-   * {@link #startAutomaticCapture(int)} with device 0, creating a camera
-   * named "USB Camera 0".  Subsequent calls increment the device number
-   * (e.g. 1, 2, etc).
-   */
-  public UsbCamera startAutomaticCapture() {
-    UsbCamera camera = startAutomaticCapture(m_defaultUsbDevice.getAndIncrement());
-    CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Start automatically capturing images to send to the dashboard.
-   *
-   * <p>This overload calls {@link #startAutomaticCapture(String, int)} with
-   * a name of "USB Camera {dev}".
-   *
-   * @param dev The device number of the camera interface
-   */
-  public UsbCamera startAutomaticCapture(int dev) {
-    UsbCamera camera = new UsbCamera("USB Camera " + dev, dev);
-    startAutomaticCapture(camera);
-    CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Start automatically capturing images to send to the dashboard.
-   *
-   * @param name The name to give the camera
-   * @param dev The device number of the camera interface
-   */
-  public UsbCamera startAutomaticCapture(String name, int dev) {
-    UsbCamera camera = new UsbCamera(name, dev);
-    startAutomaticCapture(camera);
-    CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Start automatically capturing images to send to the dashboard.
-   *
-   * @param name The name to give the camera
-   * @param path The device path (e.g. "/dev/video0") of the camera
-   */
-  public UsbCamera startAutomaticCapture(String name, String path) {
-    UsbCamera camera = new UsbCamera(name, path);
-    startAutomaticCapture(camera);
-    CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Start automatically capturing images to send to the dashboard from
-   * an existing camera.
-   *
-   * @param camera Camera
-   */
-  public void startAutomaticCapture(VideoSource camera) {
-    addCamera(camera);
-    VideoSink server = addServer("serve_" + camera.getName());
-    server.setSource(camera);
-  }
-
-  /**
-   * Adds an Axis IP camera.
-   *
-   * <p>This overload calls {@link #addAxisCamera(String, String)} with
-   * name "Axis Camera".
-   *
-   * @param host Camera host IP or DNS name (e.g. "10.x.y.11")
-   */
-  public AxisCamera addAxisCamera(String host) {
-    return addAxisCamera("Axis Camera", host);
-  }
-
-  /**
-   * Adds an Axis IP camera.
-   *
-   * <p>This overload calls {@link #addAxisCamera(String, String[])} with
-   * name "Axis Camera".
-   *
-   * @param hosts Array of Camera host IPs/DNS names
-   */
-  public AxisCamera addAxisCamera(String[] hosts) {
-    return addAxisCamera("Axis Camera", hosts);
-  }
-
-  /**
-   * Adds an Axis IP camera.
-   *
-   * @param name The name to give the camera
-   * @param host Camera host IP or DNS name (e.g. "10.x.y.11")
-   */
-  public AxisCamera addAxisCamera(String name, String host) {
-    AxisCamera camera = new AxisCamera(name, host);
-    // Create a passthrough MJPEG server for USB access
-    startAutomaticCapture(camera);
-    CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Adds an Axis IP camera.
-   *
-   * @param name The name to give the camera
-   * @param hosts Array of Camera host IPs/DNS names
-   */
-  public AxisCamera addAxisCamera(String name, String[] hosts) {
-    AxisCamera camera = new AxisCamera(name, hosts);
-    // Create a passthrough MJPEG server for USB access
-    startAutomaticCapture(camera);
-    CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle());
-    return camera;
-  }
-
-  /**
-   * Get OpenCV access to the primary camera feed.  This allows you to
-   * get images from the camera for image processing on the roboRIO.
-   *
-   * <p>This is only valid to call after a camera feed has been added
-   * with startAutomaticCapture() or addServer().
-   */
-  public CvSink getVideo() {
-    VideoSource source;
-    synchronized (this) {
-      if (m_primarySourceName == null) {
-        throw new VideoException("no camera available");
-      }
-      source = m_sources.get(m_primarySourceName);
-    }
-    if (source == null) {
-      throw new VideoException("no camera available");
-    }
-    return getVideo(source);
-  }
-
-  /**
-   * Get OpenCV access to the specified camera.  This allows you to get
-   * images from the camera for image processing on the roboRIO.
-   *
-   * @param camera Camera (e.g. as returned by startAutomaticCapture).
-   */
-  public CvSink getVideo(VideoSource camera) {
-    String name = "opencv_" + camera.getName();
-
-    synchronized (this) {
-      VideoSink sink = m_sinks.get(name);
-      if (sink != null) {
-        VideoSink.Kind kind = sink.getKind();
-        if (kind != VideoSink.Kind.kCv) {
-          throw new VideoException("expected OpenCV sink, but got " + kind);
-        }
-        return (CvSink) sink;
-      }
-    }
-
-    CvSink newsink = new CvSink(name);
-    newsink.setSource(camera);
-    addServer(newsink);
-    return newsink;
-  }
-
-  /**
-   * Get OpenCV access to the specified camera.  This allows you to get
-   * images from the camera for image processing on the roboRIO.
-   *
-   * @param name Camera name
-   */
-  public CvSink getVideo(String name) {
-    VideoSource source;
-    synchronized (this) {
-      source = m_sources.get(name);
-      if (source == null) {
-        throw new VideoException("could not find camera " + name);
-      }
-    }
-    return getVideo(source);
-  }
-
-  /**
-   * Create a MJPEG stream with OpenCV input. This can be called to pass custom
-   * annotated images to the dashboard.
-   *
-   * @param name Name to give the stream
-   * @param width Width of the image being sent
-   * @param height Height of the image being sent
-   */
-  public CvSource putVideo(String name, int width, int height) {
-    CvSource source = new CvSource(name, VideoMode.PixelFormat.kMJPEG, width, height, 30);
-    startAutomaticCapture(source);
-    return source;
-  }
-
-  /**
-   * Adds a MJPEG server at the next available port.
-   *
-   * @param name Server name
-   */
-  public MjpegServer addServer(String name) {
-    int port;
-    synchronized (this) {
-      port = m_nextPort;
-      m_nextPort++;
-    }
-    return addServer(name, port);
-  }
-
-  /**
-   * Adds a MJPEG server.
-   *
-   * @param name Server name
-   */
-  public MjpegServer addServer(String name, int port) {
-    MjpegServer server = new MjpegServer(name, port);
-    addServer(server);
-    return server;
-  }
-
-  /**
-   * Adds an already created server.
-   *
-   * @param server Server
-   */
-  public void addServer(VideoSink server) {
-    synchronized (this) {
-      m_sinks.put(server.getName(), server);
-    }
-  }
-
-  /**
-   * Removes a server by name.
-   *
-   * @param name Server name
-   */
-  public void removeServer(String name) {
-    synchronized (this) {
-      m_sinks.remove(name);
-    }
-  }
-
-  /**
-   * Get server for the primary camera feed.
-   *
-   * <p>This is only valid to call after a camera feed has been added
-   * with startAutomaticCapture() or addServer().
-   */
-  public VideoSink getServer() {
-    synchronized (this) {
-      if (m_primarySourceName == null) {
-        throw new VideoException("no camera available");
-      }
-      return getServer("serve_" + m_primarySourceName);
-    }
-  }
-
-  /**
-   * Gets a server by name.
-   *
-   * @param name Server name
-   */
-  public VideoSink getServer(String name) {
-    synchronized (this) {
-      return m_sinks.get(name);
-    }
-  }
-
-  /**
-   * Adds an already created camera.
-   *
-   * @param camera Camera
-   */
-  public void addCamera(VideoSource camera) {
-    String name = camera.getName();
-    synchronized (this) {
-      if (m_primarySourceName == null) {
-        m_primarySourceName = name;
-      }
-      m_sources.put(name, camera);
-    }
-  }
-
-  /**
-   * Removes a camera by name.
-   *
-   * @param name Camera name
-   */
-  public void removeCamera(String name) {
-    synchronized (this) {
-      m_sources.remove(name);
-    }
-  }
-}
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 8fbefd8..f48fb63 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Compressor.java
@@ -1,61 +1,79 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.CompressorJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.hal.util.AllocationException;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
- * Class for operating a compressor connected to a PCM (Pneumatic Control Module). The PCM will
- * automatically run in closed loop mode by default whenever a {@link Solenoid} object is created.
- * For most cases, a Compressor object does not need to be instantiated or used in a robot program.
- * This class is only required in cases where the robot program needs a more detailed status of the
- * compressor or to enable/disable closed loop control.
+ * Class for operating a compressor connected to a pneumatics module. The module will automatically
+ * run in closed loop mode by default whenever a {@link Solenoid} object is created. For most cases,
+ * a Compressor object does not need to be instantiated or used in a robot program. This class is
+ * only required in cases where the robot program needs a more detailed status of the compressor or
+ * to enable/disable closed loop control.
  *
  * <p>Note: you cannot operate the compressor directly from this class as doing so would circumvent
  * the safety provided by using the pressure switch and closed loop control. You can only turn off
  * closed loop control, thereby stopping the compressor from operating.
  */
 public class Compressor implements Sendable, AutoCloseable {
-  private int m_compressorHandle;
-  private byte m_module;
+  private PneumaticsBase m_module;
 
   /**
-   * Makes a new instance of the compressor using the provided CAN device ID.  Use this constructor
-   * when you have more than one PCM.
+   * Constructs a compressor for a specified module and type.
    *
-   * @param module The PCM CAN device ID (0 - 62 inclusive)
+   * @param module The module ID to use.
+   * @param moduleType The module type to use.
    */
-  public Compressor(int module) {
-    m_module = (byte) module;
+  public Compressor(int module, PneumaticsModuleType moduleType) {
+    m_module = PneumaticsBase.getForType(module, moduleType);
+    boolean allocatedCompressor = false;
+    boolean successfulCompletion = false;
 
-    m_compressorHandle = CompressorJNI.initializeCompressor((byte) module);
+    try {
+      if (!m_module.reserveCompressor()) {
+        throw new AllocationException("Compressor already allocated");
+      }
 
-    HAL.report(tResourceType.kResourceType_Compressor, module + 1);
-    SendableRegistry.addLW(this, "Compressor", module);
+      allocatedCompressor = true;
+
+      m_module.setClosedLoopControl(true);
+
+      HAL.report(tResourceType.kResourceType_Compressor, module + 1);
+      SendableRegistry.addLW(this, "Compressor", module);
+      successfulCompletion = true;
+
+    } finally {
+      if (!successfulCompletion) {
+        if (allocatedCompressor) {
+          m_module.unreserveCompressor();
+        }
+        m_module.close();
+      }
+    }
   }
 
   /**
-   * Makes a new instance of the compressor using the default PCM ID of 0.
+   * Constructs a compressor for a default module and specified type.
    *
-   * <p>Additional modules can be supported by making a new instance and {@link #Compressor(int)
-   * specifying the CAN ID.}
+   * @param moduleType The module type to use.
    */
-  public Compressor() {
-    this(SensorUtil.getDefaultSolenoidModule());
+  public Compressor(PneumaticsModuleType moduleType) {
+    this(PneumaticsBase.getDefaultForType(moduleType), moduleType);
   }
 
   @Override
   public void close() {
     SendableRegistry.remove(this);
+    m_module.unreserveCompressor();
+    m_module.close();
+    m_module = null;
   }
 
   /**
@@ -86,7 +104,7 @@
    * @return true if the compressor is on
    */
   public boolean enabled() {
-    return CompressorJNI.getCompressor(m_compressorHandle);
+    return m_module.getCompressor();
   }
 
   /**
@@ -95,7 +113,7 @@
    * @return true if the pressure is low
    */
   public boolean getPressureSwitchValue() {
-    return CompressorJNI.getCompressorPressureSwitch(m_compressorHandle);
+    return m_module.getPressureSwitch();
   }
 
   /**
@@ -104,7 +122,7 @@
    * @return current consumed by the compressor in amps
    */
   public double getCompressorCurrent() {
-    return CompressorJNI.getCompressorCurrent(m_compressorHandle);
+    return m_module.getCompressorCurrent();
   }
 
   /**
@@ -113,7 +131,7 @@
    * @param on if true sets the compressor to be in closed loop control mode (default)
    */
   public void setClosedLoopControl(boolean on) {
-    CompressorJNI.setCompressorClosedLoopControl(m_compressorHandle, on);
+    m_module.setClosedLoopControl(on);
   }
 
   /**
@@ -122,99 +140,15 @@
    * @return true if compressor is operating on closed-loop mode
    */
   public boolean getClosedLoopControl() {
-    return CompressorJNI.getCompressorClosedLoopControl(m_compressorHandle);
-  }
-
-  /**
-   * If PCM is in fault state : Compressor Drive is disabled due to compressor current being too
-   * high.
-   *
-   * @return true if PCM is in fault state.
-   */
-  public boolean getCompressorCurrentTooHighFault() {
-    return CompressorJNI.getCompressorCurrentTooHighFault(m_compressorHandle);
-  }
-
-  /**
-   * If PCM sticky fault is set : Compressor is disabled due to compressor current being too
-   * high.
-   *
-   * @return true if PCM sticky fault is set.
-   */
-  public boolean getCompressorCurrentTooHighStickyFault() {
-    return CompressorJNI.getCompressorCurrentTooHighStickyFault(m_compressorHandle);
-  }
-
-  /**
-   * If PCM sticky fault is set : Compressor output appears to be shorted.
-   *
-   * @return true if PCM sticky fault is set.
-   */
-  public boolean getCompressorShortedStickyFault() {
-    return CompressorJNI.getCompressorShortedStickyFault(m_compressorHandle);
-  }
-
-  /**
-   * If PCM is in fault state : Compressor output appears to be shorted.
-   *
-   * @return true if PCM is in fault state.
-   */
-  public boolean getCompressorShortedFault() {
-    return CompressorJNI.getCompressorShortedFault(m_compressorHandle);
-  }
-
-  /**
-   * If PCM sticky fault is set : Compressor does not appear to be wired, i.e. compressor is not
-   * drawing enough current.
-   *
-   * @return true if PCM sticky fault is set.
-   */
-  public boolean getCompressorNotConnectedStickyFault() {
-    return CompressorJNI.getCompressorNotConnectedStickyFault(m_compressorHandle);
-  }
-
-  /**
-   * If PCM is in fault state : Compressor does not appear to be wired, i.e. compressor is not
-   * drawing enough current.
-   *
-   * @return true if PCM is in fault state.
-   */
-  public boolean getCompressorNotConnectedFault() {
-    return CompressorJNI.getCompressorNotConnectedFault(m_compressorHandle);
-  }
-
-  /**
-   * Clear ALL sticky faults inside PCM that Compressor is wired to.
-   *
-   * <p>If a sticky fault is set, then it will be persistently cleared. The compressor might
-   * momentarily disable while the flags are being cleared. Doo not call this method too
-   * frequently, otherwise normal compressor functionality may be prevented.
-   *
-   * <p>If no sticky faults are set then this call will have no effect.
-   */
-  public void clearAllPCMStickyFaults() {
-    CompressorJNI.clearAllPCMStickyFaults(m_module);
-  }
-
-  /**
-   * Gets the module number (CAN ID).
-   *
-   * @return Module number
-   */
-  public int getModule() {
-    return m_module;
+    return m_module.getClosedLoopControl();
   }
 
   @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("Compressor");
-    builder.addBooleanProperty("Enabled", this::enabled, value -> {
-      if (value) {
-        start();
-      } else {
-        stop();
-      }
-    });
+    builder.addBooleanProperty(
+        "Closed Loop Control", this::getClosedLoopControl, this::setClosedLoopControl);
+    builder.addBooleanProperty("Enabled", this::enabled, null);
     builder.addBooleanProperty("Pressure switch", this::getPressureSwitchValue, null);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Controller.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Controller.java
index 31d50d8..886050b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Controller.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Controller.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -16,9 +13,7 @@
  */
 @Deprecated(since = "2020", forRemoval = true)
 public interface Controller {
-  /**
-   * Allows the control loop to run.
-   */
+  /** Allows the control loop to run. */
   void enable();
 
   /**
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 0a26810..d445e9f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Counter.java
@@ -1,58 +1,44 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.CounterJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.AnalogTriggerOutput.AnalogTriggerType;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-import static java.util.Objects.requireNonNull;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * Class for counting the number of ticks on a digital input channel.
  *
  * <p>This is a general purpose class for counting repetitive events. It can return the number of
- * counts, the period of the most recent cycle, and detect when the signal being counted has
- * stopped by supplying a maximum cycle time.
+ * counts, the period of the most recent cycle, and detect when the signal being counted has stopped
+ * by supplying a maximum cycle time.
  *
  * <p>All counters will immediately start counting - reset() them if you need them to be zeroed
  * before use.
  */
-public class Counter implements CounterBase, PIDSource, Sendable, AutoCloseable {
-  /**
-   * Mode determines how and what the counter counts.
-   */
+public class Counter implements CounterBase, Sendable, AutoCloseable {
+  /** Mode determines how and what the counter counts. */
   public enum Mode {
-    /**
-     * mode: two pulse.
-     */
+    /** mode: two pulse. */
     kTwoPulse(0),
-    /**
-     * mode: semi period.
-     */
+    /** mode: semi period. */
     kSemiperiod(1),
-    /**
-     * mode: pulse length.
-     */
+    /** mode: pulse length. */
     kPulseLength(2),
-    /**
-     * mode: external direction.
-     */
+    /** mode: external direction. */
     kExternalDirection(3);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     Mode(int value) {
@@ -64,13 +50,14 @@
   protected DigitalSource m_downSource; // /< What makes the counter count down.
   private boolean m_allocatedUpSource;
   private boolean m_allocatedDownSource;
-  private int m_counter; // /< The FPGA counter object.
+  int m_counter; // /< The FPGA counter object.
   private int m_index; // /< The index of this counter.
-  private PIDSourceType m_pidSource;
   private double m_distancePerPulse; // distance of travel for each tick
 
   /**
    * Create an instance of a counter with the given mode.
+   *
+   * @param mode The counter mode.
    */
   public Counter(final Mode mode) {
     ByteBuffer index = ByteBuffer.allocateDirect(4);
@@ -135,12 +122,15 @@
    * <p>The counter will start counting immediately.
    *
    * @param encodingType which edges to count
-   * @param upSource     first source to count
-   * @param downSource   second source for direction
-   * @param inverted     true to invert the count
+   * @param upSource first source to count
+   * @param downSource second source for direction
+   * @param inverted true to invert the count
    */
-  public Counter(EncodingType encodingType, DigitalSource upSource, DigitalSource downSource,
-                 boolean inverted) {
+  public Counter(
+      EncodingType encodingType,
+      DigitalSource upSource,
+      DigitalSource downSource,
+      boolean inverted) {
     this(Mode.kExternalDirection);
 
     requireNonNullParam(encodingType, "encodingType", "Counter");
@@ -229,15 +219,15 @@
       m_allocatedUpSource = false;
     }
     m_upSource = source;
-    CounterJNI.setCounterUpSource(m_counter, source.getPortHandleForRouting(),
-        source.getAnalogTriggerTypeForRouting());
+    CounterJNI.setCounterUpSource(
+        m_counter, source.getPortHandleForRouting(), source.getAnalogTriggerTypeForRouting());
   }
 
   /**
    * Set the up counting source to be an analog trigger.
    *
    * @param analogTrigger The analog trigger object that is used for the Up Source
-   * @param triggerType   The analog trigger output that will trigger the counter.
+   * @param triggerType The analog trigger output that will trigger the counter.
    */
   public void setUpSource(AnalogTrigger analogTrigger, AnalogTriggerType triggerType) {
     requireNonNullParam(analogTrigger, "analogTrigger", "setUpSource");
@@ -251,7 +241,7 @@
    * Set the edge sensitivity on an up counting source. Set the up source to either detect rising
    * edges or falling edges.
    *
-   * @param risingEdge  true to count rising edge
+   * @param risingEdge true to count rising edge
    * @param fallingEdge true to count falling edge
    */
   public void setUpSourceEdge(boolean risingEdge, boolean fallingEdge) {
@@ -261,9 +251,7 @@
     CounterJNI.setCounterUpSourceEdge(m_counter, risingEdge, fallingEdge);
   }
 
-  /**
-   * Disable the up counting source to the counter.
-   */
+  /** Disable the up counting source to the counter. */
   public void clearUpSource() {
     if (m_upSource != null && m_allocatedUpSource) {
       m_upSource.close();
@@ -298,8 +286,8 @@
       m_downSource.close();
       m_allocatedDownSource = false;
     }
-    CounterJNI.setCounterDownSource(m_counter, source.getPortHandleForRouting(),
-        source.getAnalogTriggerTypeForRouting());
+    CounterJNI.setCounterDownSource(
+        m_counter, source.getPortHandleForRouting(), source.getAnalogTriggerTypeForRouting());
     m_downSource = source;
   }
 
@@ -307,7 +295,7 @@
    * Set the down counting source to be an analog trigger.
    *
    * @param analogTrigger The analog trigger object that is used for the Down Source
-   * @param triggerType   The analog trigger output that will trigger the counter.
+   * @param triggerType The analog trigger output that will trigger the counter.
    */
   public void setDownSource(AnalogTrigger analogTrigger, AnalogTriggerType triggerType) {
     requireNonNullParam(analogTrigger, "analogTrigger", "setDownSource");
@@ -321,7 +309,7 @@
    * Set the edge sensitivity on a down counting source. Set the down source to either detect rising
    * edges or falling edges.
    *
-   * @param risingEdge  true to count the rising edge
+   * @param risingEdge true to count the rising edge
    * @param fallingEdge true to count the falling edge
    */
   public void setDownSourceEdge(boolean risingEdge, boolean fallingEdge) {
@@ -330,9 +318,7 @@
     CounterJNI.setCounterDownSourceEdge(m_counter, risingEdge, fallingEdge);
   }
 
-  /**
-   * Disable the down counting source to the counter.
-   */
+  /** Disable the down counting source to the counter. */
   public void clearDownSource() {
     if (m_downSource != null && m_allocatedDownSource) {
       m_downSource.close();
@@ -373,7 +359,7 @@
    * is most useful for direction sensitive gear tooth sensors.
    *
    * @param threshold The pulse length beyond which the counter counts the opposite direction. Units
-   *                  are seconds.
+   *     are seconds.
    */
   public void setPulseLengthMode(double threshold) {
     CounterJNI.setCounterPulseLengthMode(m_counter, threshold);
@@ -525,39 +511,6 @@
     m_distancePerPulse = distancePerPulse;
   }
 
-  /**
-   * Set which parameter of the encoder you are using as a process control variable. The counter
-   * class supports the rate and distance parameters.
-   *
-   * @param pidSource An enum to select the parameter.
-   */
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    requireNonNullParam(pidSource, "pidSource", "setPIDSourceType");
-    if (pidSource != PIDSourceType.kDisplacement && pidSource != PIDSourceType.kRate) {
-      throw new IllegalArgumentException("PID Source parameter was not valid type: " + pidSource);
-    }
-
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  @Override
-  public double pidGet() {
-    switch (m_pidSource) {
-      case kDisplacement:
-        return getDistance();
-      case kRate:
-        return getRate();
-      default:
-        return 0.0;
-    }
-  }
-
   @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("Counter");
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 9d1d6b7..735b893 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CounterBase.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -16,24 +13,15 @@
  * before use.
  */
 public interface CounterBase {
-  /**
-   * The number of edges for the counterbase to increment or decrement on.
-   */
+  /** The number of edges for the counterbase to increment or decrement on. */
   enum EncodingType {
-    /**
-     * Count only the rising edge.
-     */
+    /** Count only the rising edge. */
     k1X(0),
-    /**
-     * Count both the rising and falling edge.
-     */
+    /** Count both the rising and falling edge. */
     k2X(1),
-    /**
-     * Count rising and falling on both channels.
-     */
+    /** Count rising and falling on both channels. */
     k4X(2);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     EncodingType(int value) {
@@ -48,9 +36,7 @@
    */
   int get();
 
-  /**
-   * Reset the count to zero.
-   */
+  /** Reset the count to zero. */
   void reset();
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java
new file mode 100644
index 0000000..a919da5
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMA.java
@@ -0,0 +1,110 @@
+// 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.DMAJNI;
+import edu.wpi.first.wpilibj.motorcontrol.PWMMotorController;
+
+public class DMA implements AutoCloseable {
+  final int m_dmaHandle;
+
+  public DMA() {
+    m_dmaHandle = DMAJNI.initialize();
+  }
+
+  @Override
+  public void close() {
+    DMAJNI.free(m_dmaHandle);
+  }
+
+  public void setPause(boolean pause) {
+    DMAJNI.setPause(m_dmaHandle, pause);
+  }
+
+  public void setTimedTrigger(double periodSeconds) {
+    DMAJNI.setTimedTrigger(m_dmaHandle, periodSeconds);
+  }
+
+  public void setTimedTriggerCycles(int cycles) {
+    DMAJNI.setTimedTriggerCycles(m_dmaHandle, cycles);
+  }
+
+  public void addEncoder(Encoder encoder) {
+    DMAJNI.addEncoder(m_dmaHandle, encoder.m_encoder);
+  }
+
+  public void addEncoderPeriod(Encoder encoder) {
+    DMAJNI.addEncoderPeriod(m_dmaHandle, encoder.m_encoder);
+  }
+
+  public void addCounter(Counter counter) {
+    DMAJNI.addCounter(m_dmaHandle, counter.m_counter);
+  }
+
+  public void addCounterPeriod(Counter counter) {
+    DMAJNI.addCounterPeriod(m_dmaHandle, counter.m_counter);
+  }
+
+  public void addDigitalSource(DigitalSource digitalSource) {
+    DMAJNI.addDigitalSource(m_dmaHandle, digitalSource.getPortHandleForRouting());
+  }
+
+  public void addDutyCycle(DutyCycle dutyCycle) {
+    DMAJNI.addDutyCycle(m_dmaHandle, dutyCycle.m_handle);
+  }
+
+  public void addAnalogInput(AnalogInput analogInput) {
+    DMAJNI.addAnalogInput(m_dmaHandle, analogInput.m_port);
+  }
+
+  public void addAveragedAnalogInput(AnalogInput analogInput) {
+    DMAJNI.addAveragedAnalogInput(m_dmaHandle, analogInput.m_port);
+  }
+
+  public void addAnalogAccumulator(AnalogInput analogInput) {
+    DMAJNI.addAnalogAccumulator(m_dmaHandle, analogInput.m_port);
+  }
+
+  /**
+   * Sets an external DMA trigger.
+   *
+   * @param source the source to trigger from.
+   * @param rising trigger on rising edge.
+   * @param falling trigger on falling edge.
+   * @return the index of the trigger
+   */
+  public int setExternalTrigger(DigitalSource source, boolean rising, boolean falling) {
+    return DMAJNI.setExternalTrigger(
+        m_dmaHandle,
+        source.getPortHandleForRouting(),
+        source.getAnalogTriggerTypeForRouting(),
+        rising,
+        falling);
+  }
+
+  public int setPwmEdgeTrigger(PWMMotorController pwm, boolean rising, boolean falling) {
+    return DMAJNI.setExternalTrigger(m_dmaHandle, pwm.getPwmHandle(), 0, rising, falling);
+  }
+
+  public int setPwmEdgeTrigger(PWM pwm, boolean rising, boolean falling) {
+    return DMAJNI.setExternalTrigger(m_dmaHandle, pwm.getHandle(), 0, rising, falling);
+  }
+
+  public void clearSensors() {
+    DMAJNI.clearSensors(m_dmaHandle);
+  }
+
+  public void clearExternalTriggers() {
+    DMAJNI.clearExternalTriggers(m_dmaHandle);
+  }
+
+  public void start(int queueDepth) {
+    DMAJNI.startDMA(m_dmaHandle, queueDepth);
+  }
+
+  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
new file mode 100644
index 0000000..1055dbc
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMASample.java
@@ -0,0 +1,126 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import edu.wpi.first.hal.AnalogJNI;
+import edu.wpi.first.hal.DMAJNISample;
+
+public class DMASample {
+  public enum DMAReadStatus {
+    kOk(1),
+    kTimeout(2),
+    kError(3);
+
+    private final int value;
+
+    DMAReadStatus(int value) {
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+
+    /**
+     * Constructs a DMAReadStatus from a raw value.
+     *
+     * @param value raw value
+     * @return enum value
+     */
+    public static DMAReadStatus getValue(int value) {
+      if (value == 1) {
+        return kOk;
+      } else if (value == 2) {
+        return kTimeout;
+      }
+      return kError;
+    }
+  }
+
+  private final DMAJNISample m_dmaSample = new DMAJNISample();
+
+  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();
+  }
+
+  public long getTime() {
+    return m_dmaSample.getTime();
+  }
+
+  public double getTimeStamp() {
+    return getTime() * 1.0e-6;
+  }
+
+  public int getEncoderRaw(Encoder encoder) {
+    return m_dmaSample.getEncoder(encoder.m_encoder);
+  }
+
+  /**
+   * Gets the scaled encoder distance for this sample.
+   *
+   * @param encoder the encoder to use to read
+   * @return the distance
+   */
+  public double getEncoderDistance(Encoder encoder) {
+    double val = getEncoderRaw(encoder);
+    val *= encoder.getDecodingScaleFactor();
+    val *= encoder.getDistancePerPulse();
+    return val;
+  }
+
+  public int getEncoderPeriodRaw(Encoder encoder) {
+    return m_dmaSample.getEncoderPeriod(encoder.m_encoder);
+  }
+
+  public int getCounter(Counter counter) {
+    return m_dmaSample.getCounter(counter.m_counter);
+  }
+
+  public int getCounterPeriod(Counter counter) {
+    return m_dmaSample.getCounterPeriod(counter.m_counter);
+  }
+
+  public boolean getDigitalSource(DigitalSource digitalSource) {
+    return m_dmaSample.getDigitalSource(digitalSource.getPortHandleForRouting());
+  }
+
+  public int getAnalogInputRaw(AnalogInput analogInput) {
+    return m_dmaSample.getAnalogInput(analogInput.m_port);
+  }
+
+  public double getAnalogInputVoltage(AnalogInput analogInput) {
+    return AnalogJNI.getAnalogValueToVolts(analogInput.m_port, getAnalogInputRaw(analogInput));
+  }
+
+  public int getAveragedAnalogInputRaw(AnalogInput analogInput) {
+    return m_dmaSample.getAnalogInputAveraged(analogInput.m_port);
+  }
+
+  public double getAveragedAnalogInputVoltage(AnalogInput analogInput) {
+    return AnalogJNI.getAnalogValueToVolts(
+        analogInput.m_port, getAveragedAnalogInputRaw(analogInput));
+  }
+
+  public int getDutyCycleOutputRaw(DutyCycle dutyCycle) {
+    return m_dmaSample.getDutyCycleOutput(dutyCycle.m_handle);
+  }
+
+  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/DMC60.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMC60.java
deleted file mode 100644
index 0feeebb..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DMC60.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Digilent DMC 60 Speed Controller.
- *
- * <p>Note that the DMC 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 DMC 60 User Manual
- * available from Digilent.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class DMC60 extends PWMSpeedController {
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the DMC60 is attached to. 0-9 are
-   *        on-board, 10-19 are on the MXP port
-   */
-  public DMC60(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_DigilentDMC60, getChannel() + 1);
-    SendableRegistry.setName(this, "DMC60", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java
new file mode 100644
index 0000000..016abc7
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DSControlWord.java
@@ -0,0 +1,127 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import edu.wpi.first.hal.ControlWord;
+import edu.wpi.first.hal.HAL;
+
+/** A wrapper around Driver Station control word. */
+public class DSControlWord {
+  private final ControlWord m_controlWord = new ControlWord();
+
+  /**
+   * DSControlWord constructor.
+   *
+   * <p>Upon construction, the current Driver Station control word is read and stored internally.
+   */
+  public DSControlWord() {
+    update();
+  }
+
+  /** Update internal Driver Station control word. */
+  public void update() {
+    HAL.getControlWord(m_controlWord);
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be enabled.
+   *
+   * @return True if the robot is enabled, false otherwise.
+   */
+  public boolean isEnabled() {
+    return m_controlWord.getEnabled() && m_controlWord.getDSAttached();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be disabled.
+   *
+   * @return True if the robot should be disabled, false otherwise.
+   */
+  public boolean isDisabled() {
+    return !isEnabled();
+  }
+
+  /**
+   * Gets a value indicating whether the Robot is e-stopped.
+   *
+   * @return True if the robot is e-stopped, false otherwise.
+   */
+  public boolean isEStopped() {
+    return m_controlWord.getEStop();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * autonomous mode.
+   *
+   * @return True if autonomous mode should be enabled, false otherwise.
+   */
+  public boolean isAutonomous() {
+    return m_controlWord.getAutonomous();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * autonomous mode and enabled.
+   *
+   * @return True if autonomous should be set and the robot should be enabled.
+   */
+  public boolean isAutonomousEnabled() {
+    return m_controlWord.getAutonomous()
+        && m_controlWord.getEnabled()
+        && m_controlWord.getDSAttached();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * operator-controlled mode.
+   *
+   * @return True if operator-controlled mode should be enabled, false otherwise.
+   */
+  public boolean isTeleop() {
+    return !(isAutonomous() || isTest());
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * operator-controller mode and enabled.
+   *
+   * @return True if operator-controlled mode should be set and the robot should be enabled.
+   */
+  public boolean isTeleopEnabled() {
+    return !m_controlWord.getAutonomous()
+        && !m_controlWord.getTest()
+        && m_controlWord.getEnabled()
+        && m_controlWord.getDSAttached();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in test
+   * mode.
+   *
+   * @return True if test mode should be enabled, false otherwise.
+   */
+  public boolean isTest() {
+    return m_controlWord.getTest();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station is attached.
+   *
+   * @return True if Driver Station is attached, false otherwise.
+   */
+  public boolean isDSAttached() {
+    return m_controlWord.getDSAttached();
+  }
+
+  /**
+   * Gets if the driver station attached to a Field Management System.
+   *
+   * @return true if the robot is competing on a field being controlled by a Field Management System
+   */
+  public boolean isFMSAttached() {
+    return m_controlWord.getFMSAttached();
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Debouncer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Debouncer.java
new file mode 100644
index 0000000..cef85aa
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Debouncer.java
@@ -0,0 +1,79 @@
+// 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;
+
+/**
+ * A simple debounce filter for boolean streams. Requires that the boolean change value from
+ * baseline for a specified period of time before the filtered value changes.
+ */
+public class Debouncer {
+  public enum DebounceType {
+    kRising,
+    kFalling,
+    kBoth
+  }
+
+  private final Timer m_timer = new Timer();
+  private final double m_debounceTime;
+  private final DebounceType m_debounceType;
+  private boolean m_baseline;
+
+  /**
+   * Creates a new Debouncer.
+   *
+   * @param debounceTime The number of seconds the value must change from baseline for the filtered
+   *     value to change.
+   * @param type Which type of state change the debouncing will be performed on.
+   */
+  public Debouncer(double debounceTime, DebounceType type) {
+    m_debounceTime = debounceTime;
+    m_debounceType = type;
+    m_timer.start();
+
+    switch (m_debounceType) {
+      case kBoth: // fall-through
+      case kRising:
+        m_baseline = false;
+        break;
+      case kFalling:
+        m_baseline = true;
+        break;
+      default:
+        throw new IllegalArgumentException("Invalid debounce type!");
+    }
+  }
+
+  /**
+   * Creates a new Debouncer. Baseline value defaulted to "false."
+   *
+   * @param debounceTime The number of seconds the value must change from baseline for the filtered
+   *     value to change.
+   */
+  public Debouncer(double debounceTime) {
+    this(debounceTime, DebounceType.kRising);
+  }
+
+  /**
+   * Applies the debouncer to the input stream.
+   *
+   * @param input The current value of the input stream.
+   * @return The debounced value of the input stream.
+   */
+  public boolean calculate(boolean input) {
+    if (input == m_baseline) {
+      m_timer.reset();
+    }
+
+    if (m_timer.hasElapsed(m_debounceTime)) {
+      if (m_debounceType == DebounceType.kBoth) {
+        m_baseline = input;
+        m_timer.reset();
+      }
+      return input;
+    } else {
+      return m_baseline;
+    }
+  }
+}
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 10155cc..abcc67b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalGlitchFilter.java
@@ -1,20 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
 import edu.wpi.first.hal.DigitalGlitchFilterJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Class to enable glitch filtering on a set of digital inputs. This class will manage adding and
@@ -22,11 +19,10 @@
  * that an input must remain high or low before it is classified as high or low.
  */
 public class DigitalGlitchFilter implements Sendable, AutoCloseable {
-  /**
-   * Configures the Digital Glitch Filter to its default settings.
-   */
+  /** Configures the Digital Glitch Filter to its default settings. */
   public DigitalGlitchFilter() {
-    synchronized (m_mutex) {
+    m_mutex.lock();
+    try {
       int index = 0;
       while (m_filterAllocated[index] && index < m_filterAllocated.length) {
         index++;
@@ -34,10 +30,11 @@
       if (index != m_filterAllocated.length) {
         m_channelIndex = index;
         m_filterAllocated[index] = true;
-        HAL.report(tResourceType.kResourceType_DigitalGlitchFilter,
-            m_channelIndex + 1, 0);
+        HAL.report(tResourceType.kResourceType_DigitalGlitchFilter, m_channelIndex + 1, 0);
         SendableRegistry.addLW(this, "DigitalGlitchFilter", index);
       }
+    } finally {
+      m_mutex.unlock();
     }
   }
 
@@ -45,9 +42,13 @@
   public void close() {
     SendableRegistry.remove(this);
     if (m_channelIndex >= 0) {
-      synchronized (m_mutex) {
+      m_mutex.lock();
+      try {
         m_filterAllocated[m_channelIndex] = false;
+      } finally {
+        m_mutex.unlock();
       }
+
       m_channelIndex = -1;
     }
   }
@@ -62,8 +63,8 @@
 
       int selected = DigitalGlitchFilterJNI.getFilterSelect(input.getPortHandleForRouting());
       if (selected != channelIndex) {
-        throw new IllegalStateException("DigitalGlitchFilterJNI.setFilterSelect("
-            + channelIndex + ") failed -> " + selected);
+        throw new IllegalStateException(
+            "DigitalGlitchFilterJNI.setFilterSelect(" + channelIndex + ") failed -> " + selected);
       }
     }
   }
@@ -143,8 +144,7 @@
    * @param nanoseconds The number of nanoseconds.
    */
   public void setPeriodNanoSeconds(long nanoseconds) {
-    int fpgaCycles = (int) (nanoseconds * SensorUtil.kSystemClockTicksPerMicrosecond / 4
-        / 1000);
+    int fpgaCycles = (int) (nanoseconds * SensorUtil.kSystemClockTicksPerMicrosecond / 4 / 1000);
     setPeriodCycles(fpgaCycles);
   }
 
@@ -167,14 +167,11 @@
   public long getPeriodNanoSeconds() {
     int fpgaCycles = getPeriodCycles();
 
-    return (long) fpgaCycles * 1000L
-        / (long) (SensorUtil.kSystemClockTicksPerMicrosecond / 4);
+    return (long) fpgaCycles * 1000L / (long) (SensorUtil.kSystemClockTicksPerMicrosecond / 4);
   }
 
   @Override
-  @SuppressWarnings("PMD.UnusedFormalParameter")
-  public void initSendable(SendableBuilder builder) {
-  }
+  public void initSendable(SendableBuilder builder) {}
 
   private int m_channelIndex = -1;
   private static final Lock m_mutex = new ReentrantLock(true);
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 7cbd1ed..5b4cf5e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalInput.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,8 +8,9 @@
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class to read a digital input. This class will read digital inputs and return the current value
@@ -20,7 +18,7 @@
  * elsewhere will automatically allocate digital inputs and outputs as required. This class is only
  * for devices like switches etc. that aren't implemented anywhere else.
  */
-public class DigitalInput extends DigitalSource implements Sendable, AutoCloseable {
+public class DigitalInput extends DigitalSource implements Sendable {
   private final int m_channel;
   private int m_handle;
 
@@ -43,9 +41,6 @@
   public void close() {
     super.close();
     SendableRegistry.remove(this);
-    if (m_interrupt != 0) {
-      cancelInterrupts();
-    }
     DIOJNI.freeDIOPort(m_handle);
     m_handle = 0;
   }
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 ec1a44c..2c87df5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalOutput.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,14 +8,15 @@
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class to write digital outputs. This class will write digital outputs. Other devices that are
  * implemented elsewhere will automatically allocate digital inputs and outputs as required.
  */
-public class DigitalOutput extends DigitalSource implements Sendable, AutoCloseable {
+public class DigitalOutput extends DigitalSource implements Sendable {
   private static final int invalidPwmGenerator = 0;
   private int m_pwmGenerator = invalidPwmGenerator;
 
@@ -26,11 +24,10 @@
   private int m_handle;
 
   /**
-   * Create an instance of a digital output. Create an instance of a digital output given a
-   * channel.
+   * Create an instance of a digital output. Create an instance of a digital output given a channel.
    *
    * @param channel the DIO channel to use for the digital output. 0-9 are on-board, 10-25 are on
-   *                the MXP
+   *     the MXP
    */
   public DigitalOutput(int channel) {
     SensorUtil.checkDigitalChannel(channel);
@@ -60,7 +57,7 @@
    * @param value true is on, off is false
    */
   public void set(boolean value) {
-    DIOJNI.setDIO(m_handle, (short) (value ? 1 : 0));
+    DIOJNI.setDIO(m_handle, value);
   }
 
   /**
@@ -153,8 +150,7 @@
    * Change the duty-cycle that is being generated on the line.
    *
    * <p>The resolution of the duty cycle is 8-bit for low frequencies (1kHz or less) but is reduced
-   * the
-   * higher the frequency of the PWM signal is.
+   * the higher the frequency of the PWM signal is.
    *
    * @param dutyCycle The duty-cycle to change to. [0..1]
    */
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 d31e191..cb8b024 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DigitalSource.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -13,8 +10,25 @@
  * just provides a channel, then a digital input will be constructed and freed when finished for the
  * source. The source can either be a digital input or analog trigger but not both.
  */
-public abstract class DigitalSource extends InterruptableSensorBase {
+public abstract class DigitalSource implements AutoCloseable {
   public abstract boolean isAnalogTrigger();
 
   public abstract int getChannel();
+
+  /**
+   * If this is an analog trigger.
+   *
+   * @return true if this is an analog trigger.
+   */
+  public abstract int getAnalogTriggerTypeForRouting();
+
+  /**
+   * The channel routing number.
+   *
+   * @return channel routing number
+   */
+  public abstract int getPortHandleForRouting();
+
+  @Override
+  public void close() {}
 }
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 d6d4908..68a1bca 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DoubleSolenoid.java
@@ -1,94 +1,118 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.hal.SolenoidJNI;
-import edu.wpi.first.hal.util.UncleanStatusException;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.hal.util.AllocationException;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
- * DoubleSolenoid class for running 2 channels of high voltage Digital Output on the PCM.
+ * DoubleSolenoid class for running 2 channels of high voltage Digital Output on the pneumatics
+ * module.
  *
  * <p>The DoubleSolenoid class is typically used for pneumatics solenoids that have two positions
  * controlled by two separate channels.
  */
-public class DoubleSolenoid extends SolenoidBase implements Sendable, AutoCloseable {
-  /**
-   * Possible values for a DoubleSolenoid.
-   */
+public class DoubleSolenoid implements Sendable, AutoCloseable {
+  /** Possible values for a DoubleSolenoid. */
   public enum Value {
     kOff,
     kForward,
     kReverse
   }
 
-  private byte m_forwardMask; // The mask for the forward channel.
-  private byte m_reverseMask; // The mask for the reverse channel.
-  private int m_forwardHandle;
-  private int m_reverseHandle;
+  private final int m_forwardMask; // The mask for the forward channel.
+  private final int m_reverseMask; // The mask for the reverse channel.
+  private final int m_mask; // The channel mask
+  private PneumaticsBase m_module;
+  private final int m_forwardChannel;
+  private final int m_reverseChannel;
 
   /**
-   * Constructor. Uses the default PCM ID (defaults to 0).
+   * Constructs a double solenoid for a default module of a specific module type.
    *
-   * @param forwardChannel The forward channel number on the PCM (0..7).
-   * @param reverseChannel The reverse channel number on the PCM (0..7).
+   * @param moduleType The module type to use.
+   * @param forwardChannel The forward channel on the module to control.
+   * @param reverseChannel The reverse channel on the module to control.
    */
-  public DoubleSolenoid(final int forwardChannel, final int reverseChannel) {
-    this(SensorUtil.getDefaultSolenoidModule(), forwardChannel, reverseChannel);
+  public DoubleSolenoid(
+      final PneumaticsModuleType moduleType, final int forwardChannel, final int reverseChannel) {
+    this(PneumaticsBase.getDefaultForType(moduleType), moduleType, forwardChannel, reverseChannel);
   }
 
   /**
-   * Constructor.
+   * Constructs a double solenoid for a specified module of a specific module type.
    *
-   * @param moduleNumber   The module number of the solenoid module to use.
-   * @param forwardChannel The forward channel on the module to control (0..7).
-   * @param reverseChannel The reverse channel on the module to control (0..7).
+   * @param module The module of the solenoid module to use.
+   * @param moduleType The module type to use.
+   * @param forwardChannel The forward channel on the module to control.
+   * @param reverseChannel The reverse channel on the module to control.
    */
-  public DoubleSolenoid(final int moduleNumber, final int forwardChannel,
-                        final int reverseChannel) {
-    super(moduleNumber);
+  public DoubleSolenoid(
+      final int module,
+      final PneumaticsModuleType moduleType,
+      final int forwardChannel,
+      final int reverseChannel) {
+    m_module = PneumaticsBase.getForType(module, moduleType);
+    boolean allocatedSolenoids = false;
+    boolean successfulCompletion = false;
 
-    SensorUtil.checkSolenoidModule(m_moduleNumber);
-    SensorUtil.checkSolenoidChannel(forwardChannel);
-    SensorUtil.checkSolenoidChannel(reverseChannel);
+    m_forwardChannel = forwardChannel;
+    m_reverseChannel = reverseChannel;
 
-    int portHandle = HAL.getPortWithModule((byte) m_moduleNumber, (byte) forwardChannel);
-    m_forwardHandle = SolenoidJNI.initializeSolenoidPort(portHandle);
+    m_forwardMask = 1 << forwardChannel;
+    m_reverseMask = 1 << reverseChannel;
+    m_mask = m_forwardMask | m_reverseMask;
 
     try {
-      portHandle = HAL.getPortWithModule((byte) m_moduleNumber, (byte) reverseChannel);
-      m_reverseHandle = SolenoidJNI.initializeSolenoidPort(portHandle);
-    } catch (UncleanStatusException ex) {
-      // free the forward handle on exception, then rethrow
-      SolenoidJNI.freeSolenoidPort(m_forwardHandle);
-      m_forwardHandle = 0;
-      m_reverseHandle = 0;
-      throw ex;
+      if (!m_module.checkSolenoidChannel(forwardChannel)) {
+        throw new IllegalArgumentException("Channel " + forwardChannel + " out of range");
+      }
+
+      if (!m_module.checkSolenoidChannel(reverseChannel)) {
+        throw new IllegalArgumentException("Channel " + reverseChannel + " out of range");
+      }
+
+      int allocMask = m_module.checkAndReserveSolenoids(m_mask);
+      if (allocMask != 0) {
+        if (allocMask == m_mask) {
+          throw new AllocationException(
+              "Channels " + forwardChannel + " and " + reverseChannel + " already allocated");
+        } else if (allocMask == m_forwardMask) {
+          throw new AllocationException("Channel " + forwardChannel + " already allocated");
+        } else {
+          throw new AllocationException("Channel " + reverseChannel + " already allocated");
+        }
+      }
+      allocatedSolenoids = true;
+
+      HAL.report(
+          tResourceType.kResourceType_Solenoid, forwardChannel + 1, m_module.getModuleNumber() + 1);
+      HAL.report(
+          tResourceType.kResourceType_Solenoid, reverseChannel + 1, m_module.getModuleNumber() + 1);
+      SendableRegistry.addLW(this, "DoubleSolenoid", m_module.getModuleNumber(), forwardChannel);
+      successfulCompletion = true;
+    } finally {
+      if (!successfulCompletion) {
+        if (allocatedSolenoids) {
+          m_module.unreserveSolenoids(m_mask);
+        }
+        m_module.close();
+      }
     }
-
-    m_forwardMask = (byte) (1 << forwardChannel);
-    m_reverseMask = (byte) (1 << reverseChannel);
-
-    HAL.report(tResourceType.kResourceType_Solenoid, forwardChannel + 1,
-                                   m_moduleNumber + 1);
-    HAL.report(tResourceType.kResourceType_Solenoid, reverseChannel + 1,
-                                   m_moduleNumber + 1);
-    SendableRegistry.addLW(this, "DoubleSolenoid", m_moduleNumber, forwardChannel);
   }
 
   @Override
   public synchronized void close() {
     SendableRegistry.remove(this);
-    SolenoidJNI.freeSolenoidPort(m_forwardHandle);
-    SolenoidJNI.freeSolenoidPort(m_reverseHandle);
+    m_module.unreserveSolenoids(m_mask);
+    m_module.close();
+    m_module = null;
   }
 
   /**
@@ -97,29 +121,23 @@
    * @param value The value to set (Off, Forward, Reverse)
    */
   public void set(final Value value) {
-    boolean forward = false;
-    boolean reverse = false;
+    int setValue;
 
     switch (value) {
       case kOff:
-        forward = false;
-        reverse = false;
+        setValue = 0;
         break;
       case kForward:
-        forward = true;
-        reverse = false;
+        setValue = m_forwardMask;
         break;
       case kReverse:
-        forward = false;
-        reverse = true;
+        setValue = m_reverseMask;
         break;
       default:
         throw new AssertionError("Illegal value: " + value);
-
     }
 
-    SolenoidJNI.setSolenoid(m_forwardHandle, forward);
-    SolenoidJNI.setSolenoid(m_reverseHandle, reverse);
+    m_module.setSolenoids(m_mask, setValue);
   }
 
   /**
@@ -128,12 +146,11 @@
    * @return The current value of the solenoid.
    */
   public Value get() {
-    boolean valueForward = SolenoidJNI.getSolenoid(m_forwardHandle);
-    boolean valueReverse = SolenoidJNI.getSolenoid(m_reverseHandle);
+    int values = m_module.getSolenoids();
 
-    if (valueForward) {
+    if ((values & m_forwardMask) != 0) {
       return Value.kForward;
-    } else if (valueReverse) {
+    } else if ((values & m_reverseMask) != 0) {
       return Value.kReverse;
     } else {
       return Value.kOff;
@@ -157,27 +174,41 @@
   }
 
   /**
-   * Check if the forward solenoid is blacklisted. If a solenoid is shorted, it is added to the
-   * blacklist and disabled until power cycle, or until faults are cleared.
+   * Get the forward channel.
    *
-   * @return If solenoid is disabled due to short.
-   * @see #clearAllPCMStickyFaults()
+   * @return the forward channel.
    */
-  public boolean isFwdSolenoidBlackListed() {
-    int blackList = getPCMSolenoidBlackList();
-    return (blackList & m_forwardMask) != 0;
+  public int getFwdChannel() {
+    return m_forwardChannel;
   }
 
   /**
-   * Check if the reverse solenoid is blacklisted. If a solenoid is shorted, it is added to the
-   * blacklist and disabled until power cycle, or until faults are cleared.
+   * Get the reverse channel.
+   *
+   * @return the reverse channel.
+   */
+  public int getRevChannel() {
+    return m_reverseChannel;
+  }
+
+  /**
+   * Check if the forward solenoid is Disabled. If a solenoid is shorted, it is added to the
+   * DisabledList and disabled until power cycle, or until faults are cleared.
    *
    * @return If solenoid is disabled due to short.
-   * @see #clearAllPCMStickyFaults()
    */
-  public boolean isRevSolenoidBlackListed() {
-    int blackList = getPCMSolenoidBlackList();
-    return (blackList & m_reverseMask) != 0;
+  public boolean isFwdSolenoidDisabled() {
+    return (m_module.getSolenoidDisabledList() & m_forwardMask) != 0;
+  }
+
+  /**
+   * Check if the reverse solenoid is Disabled. If a solenoid is shorted, it is added to the
+   * DisabledList and disabled until power cycle, or until faults are cleared.
+   *
+   * @return If solenoid is disabled due to short.
+   */
+  public boolean isRevSolenoidDisabled() {
+    return (m_module.getSolenoidDisabledList() & m_reverseMask) != 0;
   }
 
   @Override
@@ -185,14 +216,17 @@
     builder.setSmartDashboardType("Double Solenoid");
     builder.setActuator(true);
     builder.setSafeState(() -> set(Value.kOff));
-    builder.addStringProperty("Value", () -> get().name().substring(1), value -> {
-      if ("Forward".equals(value)) {
-        set(Value.kForward);
-      } else if ("Reverse".equals(value)) {
-        set(Value.kReverse);
-      } else {
-        set(Value.kOff);
-      }
-    });
+    builder.addStringProperty(
+        "Value",
+        () -> get().name().substring(1),
+        value -> {
+          if ("Forward".equals(value)) {
+            set(Value.kForward);
+          } else if ("Reverse".equals(value)) {
+            set(Value.kReverse);
+          } else {
+            set(Value.kOff);
+          }
+        });
   }
 }
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 fe3ca50..c450c5e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java
@@ -1,18 +1,9 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
 import edu.wpi.first.hal.AllianceStationID;
 import edu.wpi.first.hal.ControlWord;
 import edu.wpi.first.hal.HAL;
@@ -20,16 +11,15 @@
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
-/**
- * Provide access to the network communication data to / from the Driver Station.
- */
-@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveClassLength",
-                   "PMD.ExcessivePublicCount", "PMD.GodClass", "PMD.TooManyFields"})
+/** Provide access to the network communication data to / from the Driver Station. */
 public class DriverStation {
-  /**
-   * Number of Joystick Ports.
-   */
+  /** Number of Joystick Ports. */
   public static final int kJoystickPorts = 6;
 
   private static class HALJoystickButtons {
@@ -58,55 +48,87 @@
     }
   }
 
-  /**
-   * The robot alliance that the robot is a part of.
-   */
+  /** The robot alliance that the robot is a part of. */
   public enum Alliance {
-    Red, Blue, Invalid
+    Red,
+    Blue,
+    Invalid
   }
 
   public enum MatchType {
-    None, Practice, Qualification, Elimination
+    None,
+    Practice,
+    Qualification,
+    Elimination
   }
 
   private static final double JOYSTICK_UNPLUGGED_MESSAGE_INTERVAL = 1.0;
-  private double m_nextMessageTime;
+  private static double m_nextMessageTime;
 
   private static class DriverStationTask implements Runnable {
-    private final DriverStation m_ds;
-
-    DriverStationTask(DriverStation ds) {
-      m_ds = ds;
-    }
+    DriverStationTask() {}
 
     @Override
     public void run() {
-      m_ds.run();
+      DriverStation.run();
     }
   } /* DriverStationTask */
 
   private static class MatchDataSender {
     @SuppressWarnings("MemberName")
     NetworkTable table;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry typeMetadata;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry gameSpecificMessage;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry eventName;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry matchNumber;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry replayNumber;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry matchType;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry alliance;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry station;
+
     @SuppressWarnings("MemberName")
     NetworkTableEntry controlWord;
 
+    @SuppressWarnings("MemberName")
+    boolean oldIsRedAlliance = true;
+
+    @SuppressWarnings("MemberName")
+    int oldStationNumber = 1;
+
+    @SuppressWarnings("MemberName")
+    String oldEventName = "";
+
+    @SuppressWarnings("MemberName")
+    String oldGameSpecificMessage = "";
+
+    @SuppressWarnings("MemberName")
+    int oldMatchNumber;
+
+    @SuppressWarnings("MemberName")
+    int oldReplayNumber;
+
+    @SuppressWarnings("MemberName")
+    int oldMatchType;
+
+    @SuppressWarnings("MemberName")
+    int oldControlWord;
+
     MatchDataSender() {
       table = NetworkTableInstance.getDefault().getTable("FMSInfo");
       typeMetadata = table.getEntry(".type");
@@ -128,61 +150,147 @@
       controlWord = table.getEntry("FMSControlData");
       controlWord.forceSetDouble(0);
     }
+
+    private void sendMatchData() {
+      AllianceStationID allianceID = HAL.getAllianceStation();
+      boolean isRedAlliance = false;
+      int stationNumber = 1;
+      switch (allianceID) {
+        case Blue1:
+          isRedAlliance = false;
+          stationNumber = 1;
+          break;
+        case Blue2:
+          isRedAlliance = false;
+          stationNumber = 2;
+          break;
+        case Blue3:
+          isRedAlliance = false;
+          stationNumber = 3;
+          break;
+        case Red1:
+          isRedAlliance = true;
+          stationNumber = 1;
+          break;
+        case Red2:
+          isRedAlliance = true;
+          stationNumber = 2;
+          break;
+        default:
+          isRedAlliance = true;
+          stationNumber = 3;
+          break;
+      }
+
+      String currentEventName;
+      String currentGameSpecificMessage;
+      int currentMatchNumber;
+      int currentReplayNumber;
+      int currentMatchType;
+      int currentControlWord;
+      m_cacheDataMutex.lock();
+      try {
+        currentEventName = DriverStation.m_matchInfo.eventName;
+        currentGameSpecificMessage = DriverStation.m_matchInfo.gameSpecificMessage;
+        currentMatchNumber = DriverStation.m_matchInfo.matchNumber;
+        currentReplayNumber = DriverStation.m_matchInfo.replayNumber;
+        currentMatchType = DriverStation.m_matchInfo.matchType;
+      } finally {
+        m_cacheDataMutex.unlock();
+      }
+      currentControlWord = HAL.nativeGetControlWord();
+
+      if (oldIsRedAlliance != isRedAlliance) {
+        alliance.setBoolean(isRedAlliance);
+        oldIsRedAlliance = isRedAlliance;
+      }
+      if (oldStationNumber != stationNumber) {
+        station.setDouble(stationNumber);
+        oldStationNumber = stationNumber;
+      }
+      if (!oldEventName.equals(currentEventName)) {
+        eventName.setString(currentEventName);
+        oldEventName = currentEventName;
+      }
+      if (!oldGameSpecificMessage.equals(currentGameSpecificMessage)) {
+        gameSpecificMessage.setString(currentGameSpecificMessage);
+        oldGameSpecificMessage = currentGameSpecificMessage;
+      }
+      if (currentMatchNumber != oldMatchNumber) {
+        matchNumber.setDouble(currentMatchNumber);
+        oldMatchNumber = currentMatchNumber;
+      }
+      if (currentReplayNumber != oldReplayNumber) {
+        replayNumber.setDouble(currentReplayNumber);
+        oldReplayNumber = currentReplayNumber;
+      }
+      if (currentMatchType != oldMatchType) {
+        matchType.setDouble(currentMatchType);
+        oldMatchType = currentMatchType;
+      }
+      if (currentControlWord != oldControlWord) {
+        controlWord.setDouble(currentControlWord);
+        oldControlWord = currentControlWord;
+      }
+    }
   }
 
   private static DriverStation instance = new DriverStation();
 
   // Joystick User Data
-  private HALJoystickAxes[] m_joystickAxes = new HALJoystickAxes[kJoystickPorts];
-  private HALJoystickPOVs[] m_joystickPOVs = new HALJoystickPOVs[kJoystickPorts];
-  private HALJoystickButtons[] m_joystickButtons = new HALJoystickButtons[kJoystickPorts];
-  private MatchInfoData m_matchInfo = new MatchInfoData();
+  private static HALJoystickAxes[] m_joystickAxes = new HALJoystickAxes[kJoystickPorts];
+  private static HALJoystickPOVs[] m_joystickPOVs = new HALJoystickPOVs[kJoystickPorts];
+  private static HALJoystickButtons[] m_joystickButtons = new HALJoystickButtons[kJoystickPorts];
+  private static MatchInfoData m_matchInfo = new MatchInfoData();
 
   // Joystick Cached Data
-  private HALJoystickAxes[] m_joystickAxesCache = new HALJoystickAxes[kJoystickPorts];
-  private HALJoystickPOVs[] m_joystickPOVsCache = new HALJoystickPOVs[kJoystickPorts];
-  private HALJoystickButtons[] m_joystickButtonsCache = new HALJoystickButtons[kJoystickPorts];
-  private MatchInfoData m_matchInfoCache = new MatchInfoData();
+  private static HALJoystickAxes[] m_joystickAxesCache = new HALJoystickAxes[kJoystickPorts];
+  private static HALJoystickPOVs[] m_joystickPOVsCache = new HALJoystickPOVs[kJoystickPorts];
+  private static HALJoystickButtons[] m_joystickButtonsCache =
+      new HALJoystickButtons[kJoystickPorts];
+  private static MatchInfoData m_matchInfoCache = new MatchInfoData();
 
   // Joystick button rising/falling edge flags
-  private int[] m_joystickButtonsPressed = new int[kJoystickPorts];
-  private int[] m_joystickButtonsReleased = new int[kJoystickPorts];
+  private static int[] m_joystickButtonsPressed = new int[kJoystickPorts];
+  private static int[] m_joystickButtonsReleased = new int[kJoystickPorts];
 
   // preallocated byte buffer for button count
-  private final ByteBuffer m_buttonCountBuffer = ByteBuffer.allocateDirect(1);
+  private static final ByteBuffer m_buttonCountBuffer = ByteBuffer.allocateDirect(1);
 
-  private final MatchDataSender m_matchDataSender;
+  private static final MatchDataSender m_matchDataSender;
 
   // Internal Driver Station thread
-  @SuppressWarnings("PMD.SingularField")
-  private final Thread m_thread;
-  private volatile boolean m_threadKeepAlive = true;
+  private static Thread m_thread;
 
-  private final ReentrantLock m_cacheDataMutex = new ReentrantLock();
+  private static volatile boolean m_threadKeepAlive = true;
 
-  private final Lock m_waitForDataMutex;
-  private final Condition m_waitForDataCond;
-  private int m_waitForDataCount;
-  private final ThreadLocal<Integer> m_lastCount = ThreadLocal.withInitial(() -> 0);
+  private static final ReentrantLock m_cacheDataMutex = new ReentrantLock();
 
-  private boolean m_silenceJoystickWarning;
+  private static final Lock m_waitForDataMutex;
+  private static final Condition m_waitForDataCond;
+  private static int m_waitForDataCount;
+  private static final ThreadLocal<Integer> m_lastCount = ThreadLocal.withInitial(() -> 0);
+
+  private static boolean m_silenceJoystickWarning;
 
   // Robot state status variables
-  private boolean m_userInDisabled;
-  private boolean m_userInAutonomous;
-  private boolean m_userInTeleop;
-  private boolean m_userInTest;
+  private static boolean m_userInDisabled;
+  private static boolean m_userInAutonomous;
+  private static boolean m_userInTeleop;
+  private static boolean m_userInTest;
 
   // Control word variables
-  private final Object m_controlWordMutex;
-  private final ControlWord m_controlWordCache;
-  private long m_lastControlWordUpdate;
+  private static final Object m_controlWordMutex;
+  private static final ControlWord m_controlWordCache;
+  private static long m_lastControlWordUpdate;
 
   /**
    * Gets an instance of the DriverStation.
    *
    * @return The DriverStation.
+   * @deprecated Use the static methods
    */
+  @Deprecated
   public static DriverStation getInstance() {
     return DriverStation.instance;
   }
@@ -193,8 +301,9 @@
    * <p>The single DriverStation instance is created statically with the instance static member
    * variable.
    */
-  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
-  private DriverStation() {
+  private DriverStation() {}
+
+  static {
     HAL.initialize(500, 0);
     m_waitForDataCount = 0;
     m_waitForDataMutex = new ReentrantLock();
@@ -216,23 +325,29 @@
 
     m_matchDataSender = new MatchDataSender();
 
-    m_thread = new Thread(new DriverStationTask(this), "FRCDriverStation");
+    m_thread = new Thread(new DriverStationTask(), "FRCDriverStation");
     m_thread.setPriority((Thread.NORM_PRIORITY + Thread.MAX_PRIORITY) / 2);
 
     m_thread.start();
   }
 
-  /**
-   * Kill the thread.
-   */
-  public void release() {
+  /** Kill the thread. */
+  public static synchronized void release() {
     m_threadKeepAlive = false;
+    if (m_thread != null) {
+      try {
+        m_thread.join();
+      } catch (InterruptedException ex) {
+        Thread.currentThread().interrupt();
+      }
+      m_thread = null;
+    }
   }
 
   /**
-   * Report error to Driver Station. Optionally appends Stack trace
-   * to error message.
+   * Report error to Driver Station. Optionally appends Stack trace to error message.
    *
+   * @param error The error to report.
    * @param printTrace If true, append stack trace to error string
    */
   public static void reportError(String error, boolean printTrace) {
@@ -240,9 +355,9 @@
   }
 
   /**
-   * Report error to Driver Station. Appends provided stack trace
-   * to error message.
+   * Report error to Driver Station. Appends provided stack trace to error message.
    *
+   * @param error The error to report.
    * @param stackTrace The stack trace to append
    */
   public static void reportError(String error, StackTraceElement[] stackTrace) {
@@ -250,37 +365,41 @@
   }
 
   /**
-   * Report warning to Driver Station. Optionally appends Stack
-   * trace to warning message.
+   * Report warning to Driver Station. Optionally appends Stack trace to warning message.
    *
+   * @param warning The warning to report.
    * @param printTrace If true, append stack trace to warning string
    */
-  public static void reportWarning(String error, boolean printTrace) {
-    reportErrorImpl(false, 1, error, printTrace);
+  public static void reportWarning(String warning, boolean printTrace) {
+    reportErrorImpl(false, 1, warning, printTrace);
   }
 
   /**
-   * Report warning to Driver Station. Appends provided stack
-   * trace to warning message.
+   * Report warning to Driver Station. Appends provided stack trace to warning message.
    *
+   * @param warning The warning to report.
    * @param stackTrace The stack trace to append
    */
-  public static void reportWarning(String error, StackTraceElement[] stackTrace) {
-    reportErrorImpl(false, 1, error, stackTrace);
+  public static void reportWarning(String warning, StackTraceElement[] stackTrace) {
+    reportErrorImpl(false, 1, warning, stackTrace);
   }
 
-  private static void reportErrorImpl(boolean isError, int code, String error, boolean
-      printTrace) {
+  private static void reportErrorImpl(boolean isError, int code, String error, boolean printTrace) {
     reportErrorImpl(isError, code, error, printTrace, Thread.currentThread().getStackTrace(), 3);
   }
 
-  private static void reportErrorImpl(boolean isError, int code, String error,
-      StackTraceElement[] stackTrace) {
+  private static void reportErrorImpl(
+      boolean isError, int code, String error, StackTraceElement[] stackTrace) {
     reportErrorImpl(isError, code, error, true, stackTrace, 0);
   }
 
-  private static void reportErrorImpl(boolean isError, int code, String error,
-      boolean printTrace, StackTraceElement[] stackTrace, int stackTraceFirst) {
+  private static void reportErrorImpl(
+      boolean isError,
+      int code,
+      String error,
+      boolean printTrace,
+      StackTraceElement[] stackTrace,
+      int stackTraceFirst) {
     String locString;
     if (stackTrace.length >= stackTraceFirst + 1) {
       locString = stackTrace[stackTraceFirst].toString();
@@ -306,11 +425,11 @@
   /**
    * The state of one joystick button. Button indexes begin at 1.
    *
-   * @param stick  The joystick to read.
+   * @param stick The joystick to read.
    * @param button The button index, beginning at 1.
    * @return The state of the joystick button.
    */
-  public boolean getStickButton(final int stick, final int button) {
+  public static boolean getStickButton(final int stick, final int button) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-3");
     }
@@ -328,19 +447,23 @@
       m_cacheDataMutex.unlock();
     }
 
-    reportJoystickUnpluggedWarning("Joystick Button " + button + " on port " + stick
-        + " not available, check if controller is plugged in");
+    reportJoystickUnpluggedWarning(
+        "Joystick Button "
+            + button
+            + " on port "
+            + stick
+            + " not available, check if controller is plugged in");
     return false;
   }
 
   /**
    * Whether one joystick button was pressed since the last check. Button indexes begin at 1.
    *
-   * @param stick  The joystick to read.
+   * @param stick The joystick to read.
    * @param button The button index, beginning at 1.
    * @return Whether the joystick button was pressed since the last check.
    */
-  public boolean getStickButtonPressed(final int stick, final int button) {
+  public static boolean getStickButtonPressed(final int stick, final int button) {
     if (button <= 0) {
       reportJoystickUnpluggedError("Button indexes begin at 1 in WPILib for C++ and Java\n");
       return false;
@@ -364,20 +487,23 @@
       m_cacheDataMutex.unlock();
     }
 
-    reportJoystickUnpluggedWarning("Joystick Button " + button + " on port " + stick
-        + " not available, check if controller is plugged in");
+    reportJoystickUnpluggedWarning(
+        "Joystick Button "
+            + button
+            + " on port "
+            + stick
+            + " not available, check if controller is plugged in");
     return false;
   }
 
   /**
-   * Whether one joystick button was released since the last check. Button indexes
-   * begin at 1.
+   * Whether one joystick button was released since the last check. Button indexes begin at 1.
    *
-   * @param stick  The joystick to read.
+   * @param stick The joystick to read.
    * @param button The button index, beginning at 1.
    * @return Whether the joystick button was released since the last check.
    */
-  public boolean getStickButtonReleased(final int stick, final int button) {
+  public static boolean getStickButtonReleased(final int stick, final int button) {
     if (button <= 0) {
       reportJoystickUnpluggedError("Button indexes begin at 1 in WPILib for C++ and Java\n");
       return false;
@@ -401,8 +527,12 @@
       m_cacheDataMutex.unlock();
     }
 
-    reportJoystickUnpluggedWarning("Joystick Button " + button + " on port " + stick
-        + " not available, check if controller is plugged in");
+    reportJoystickUnpluggedWarning(
+        "Joystick Button "
+            + button
+            + " on port "
+            + stick
+            + " not available, check if controller is plugged in");
     return false;
   }
 
@@ -411,10 +541,10 @@
    * to the specified port.
    *
    * @param stick The joystick to read.
-   * @param axis  The analog axis value to read from the joystick.
+   * @param axis The analog axis value to read from the joystick.
    * @return The value of the axis on the joystick.
    */
-  public double getStickAxis(int stick, int axis) {
+  public static double getStickAxis(int stick, int axis) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -431,17 +561,23 @@
       m_cacheDataMutex.unlock();
     }
 
-    reportJoystickUnpluggedWarning("Joystick axis " + axis + " on port " + stick
-        + " not available, check if controller is plugged in");
+    reportJoystickUnpluggedWarning(
+        "Joystick axis "
+            + axis
+            + " on port "
+            + stick
+            + " not available, check if controller is plugged in");
     return 0.0;
   }
 
   /**
    * Get the state of a POV on the joystick.
    *
+   * @param stick The joystick to read.
+   * @param pov The POV to read.
    * @return the angle of the POV in degrees, or -1 if the POV is not pressed.
    */
-  public int getStickPOV(int stick, int pov) {
+  public static int getStickPOV(int stick, int pov) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -458,8 +594,12 @@
       m_cacheDataMutex.unlock();
     }
 
-    reportJoystickUnpluggedWarning("Joystick POV " + pov + " on port " + stick
-        + " not available, check if controller is plugged in");
+    reportJoystickUnpluggedWarning(
+        "Joystick POV "
+            + pov
+            + " on port "
+            + stick
+            + " not available, check if controller is plugged in");
     return -1;
   }
 
@@ -469,7 +609,7 @@
    * @param stick The joystick to read.
    * @return The state of the buttons on the joystick.
    */
-  public int getStickButtons(final int stick) {
+  public static int getStickButtons(final int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-3");
     }
@@ -488,7 +628,7 @@
    * @param stick The joystick port number
    * @return The number of axes on the indicated joystick
    */
-  public int getStickAxisCount(int stick) {
+  public static int getStickAxisCount(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -507,7 +647,7 @@
    * @param stick The joystick port number
    * @return The number of POVs on the indicated joystick
    */
-  public int getStickPOVCount(int stick) {
+  public static int getStickPOVCount(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -526,7 +666,7 @@
    * @param stick The joystick port number
    * @return The number of buttons on the indicated joystick
    */
-  public int getStickButtonCount(int stick) {
+  public static int getStickButtonCount(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -545,7 +685,7 @@
    * @param stick The joystick port number
    * @return A boolean that returns the value of isXbox
    */
-  public boolean getJoystickIsXbox(int stick) {
+  public static boolean getJoystickIsXbox(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -559,7 +699,7 @@
    * @param stick The joystick port number
    * @return The value of type
    */
-  public int getJoystickType(int stick) {
+  public static int getJoystickType(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -573,7 +713,7 @@
    * @param stick The joystick port number
    * @return The value of name
    */
-  public String getJoystickName(int stick) {
+  public static String getJoystickName(int stick) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -588,7 +728,7 @@
    * @param axis The target axis
    * @return What type of axis the axis is reporting to be
    */
-  public int getJoystickAxisType(int stick, int axis) {
+  public static int getJoystickAxisType(int stick, int axis) {
     if (stick < 0 || stick >= kJoystickPorts) {
       throw new IllegalArgumentException("Joystick index is out of range, should be 0-5");
     }
@@ -599,16 +739,16 @@
   /**
    * Returns if a joystick is connected to the Driver Station.
    *
-   * <p>This makes a best effort guess by looking at the reported number of axis,
-   * buttons, and POVs attached.
+   * <p>This makes a best effort guess by looking at the reported number of axis, buttons, and POVs
+   * attached.
    *
    * @param stick The joystick port number
    * @return true if a joystick is connected
    */
-  public boolean isJoystickConnected(int stick) {
+  public static boolean isJoystickConnected(int stick) {
     return getStickAxisCount(stick) > 0
-      || getStickButtonCount(stick) > 0
-      || getStickPOVCount(stick) > 0;
+        || getStickButtonCount(stick) > 0
+        || getStickPOVCount(stick) > 0;
   }
 
   /**
@@ -616,7 +756,7 @@
    *
    * @return True if the robot is enabled, false otherwise.
    */
-  public boolean isEnabled() {
+  public static boolean isEnabled() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getEnabled() && m_controlWordCache.getDSAttached();
@@ -628,7 +768,7 @@
    *
    * @return True if the robot should be disabled, false otherwise.
    */
-  public boolean isDisabled() {
+  public static boolean isDisabled() {
     return !isEnabled();
   }
 
@@ -637,7 +777,7 @@
    *
    * @return True if the robot is e-stopped, false otherwise.
    */
-  public boolean isEStopped() {
+  public static boolean isEStopped() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getEStop();
@@ -650,7 +790,7 @@
    *
    * @return True if autonomous mode should be enabled, false otherwise.
    */
-  public boolean isAutonomous() {
+  public static boolean isAutonomous() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getAutonomous();
@@ -663,7 +803,7 @@
    *
    * @return True if autonomous should be set and the robot should be enabled.
    */
-  public boolean isAutonomousEnabled() {
+  public static boolean isAutonomousEnabled() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getAutonomous() && m_controlWordCache.getEnabled();
@@ -675,8 +815,20 @@
    * operator-controlled mode.
    *
    * @return True if operator-controlled mode should be enabled, false otherwise.
+   * @deprecated Use isTeleop() instead.
    */
-  public boolean isOperatorControl() {
+  @Deprecated(since = "2022", forRemoval = true)
+  public static boolean isOperatorControl() {
+    return isTeleop();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * operator-controlled mode.
+   *
+   * @return True if operator-controlled mode should be enabled, false otherwise.
+   */
+  public static boolean isTeleop() {
     return !(isAutonomous() || isTest());
   }
 
@@ -685,11 +837,24 @@
    * operator-controller mode and enabled.
    *
    * @return True if operator-controlled mode should be set and the robot should be enabled.
+   * @deprecated Use isTeleopEnabled() instead.
    */
-  public boolean isOperatorControlEnabled() {
+  @Deprecated(since = "2022", forRemoval = true)
+  public static boolean isOperatorControlEnabled() {
+    return isTeleopEnabled();
+  }
+
+  /**
+   * Gets a value indicating whether the Driver Station requires the robot to be running in
+   * operator-controller mode and enabled.
+   *
+   * @return True if operator-controlled mode should be set and the robot should be enabled.
+   */
+  public static boolean isTeleopEnabled() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
-      return !m_controlWordCache.getAutonomous() && !m_controlWordCache.getTest()
+      return !m_controlWordCache.getAutonomous()
+          && !m_controlWordCache.getTest()
           && m_controlWordCache.getEnabled();
     }
   }
@@ -700,7 +865,7 @@
    *
    * @return True if test mode should be enabled, false otherwise.
    */
-  public boolean isTest() {
+  public static boolean isTest() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getTest();
@@ -712,7 +877,7 @@
    *
    * @return True if Driver Station is attached, false otherwise.
    */
-  public boolean isDSAttached() {
+  public static boolean isDSAttached() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getDSAttached();
@@ -725,7 +890,7 @@
    *
    * @return True if the control data has been updated since the last call.
    */
-  public boolean isNewControlData() {
+  public static boolean isNewControlData() {
     m_waitForDataMutex.lock();
     try {
       int currentCount = m_waitForDataCount;
@@ -744,7 +909,7 @@
    *
    * @return true if the robot is competing on a field being controlled by a Field Management System
    */
-  public boolean isFMSAttached() {
+  public static boolean isFMSAttached() {
     synchronized (m_controlWordMutex) {
       updateControlWord(false);
       return m_controlWordCache.getFMSAttached();
@@ -756,7 +921,7 @@
    *
    * @return the game specific message
    */
-  public String getGameSpecificMessage() {
+  public static String getGameSpecificMessage() {
     m_cacheDataMutex.lock();
     try {
       return m_matchInfo.gameSpecificMessage;
@@ -770,7 +935,7 @@
    *
    * @return the event name
    */
-  public String getEventName() {
+  public static String getEventName() {
     m_cacheDataMutex.lock();
     try {
       return m_matchInfo.eventName;
@@ -784,7 +949,7 @@
    *
    * @return the match type
    */
-  public MatchType getMatchType() {
+  public static MatchType getMatchType() {
     int matchType;
     m_cacheDataMutex.lock();
     try {
@@ -809,7 +974,7 @@
    *
    * @return the match number
    */
-  public int getMatchNumber() {
+  public static int getMatchNumber() {
     m_cacheDataMutex.lock();
     try {
       return m_matchInfo.matchNumber;
@@ -823,7 +988,7 @@
    *
    * @return the replay number
    */
-  public int getReplayNumber() {
+  public static int getReplayNumber() {
     m_cacheDataMutex.lock();
     try {
       return m_matchInfo.replayNumber;
@@ -837,7 +1002,7 @@
    *
    * @return the current alliance
    */
-  public Alliance getAlliance() {
+  public static Alliance getAlliance() {
     AllianceStationID allianceStationID = HAL.getAllianceStation();
     if (allianceStationID == null) {
       return Alliance.Invalid;
@@ -864,7 +1029,7 @@
    *
    * @return the location of the team's driver station controls: 1, 2, or 3
    */
-  public int getLocation() {
+  public static int getLocation() {
     AllianceStationID allianceStationID = HAL.getAllianceStation();
     if (allianceStationID == null) {
       return 0;
@@ -890,10 +1055,10 @@
   /**
    * Wait for new data from the driver station.
    *
-   * <p>Checks if new control data has arrived since the last waitForData call
-   * on the current thread. If new data has not arrived, returns immediately.
+   * <p>Checks if new control data has arrived since the last waitForData call on the current
+   * thread. If new data has not arrived, returns immediately.
    */
-  public void waitForData() {
+  public static void waitForData() {
     waitForData(0);
   }
 
@@ -901,15 +1066,15 @@
    * Wait for new data or for timeout, which ever comes first. If timeout is 0, wait for new data
    * only.
    *
-   * <p>Checks if new control data has arrived since the last waitForData call
-   * on the current thread. If new data has not arrived, returns immediately.
+   * <p>Checks if new control data has arrived since the last waitForData call on the current
+   * thread. If new data has not arrived, returns immediately.
    *
-   * @param timeout The maximum time in seconds to wait.
+   * @param timeoutSeconds The maximum time in seconds to wait.
    * @return true if there is new data, otherwise false
    */
-  public boolean waitForData(double timeout) {
-    long startTime = RobotController.getFPGATime();
-    long timeoutMicros = (long) (timeout * 1000000);
+  public static boolean waitForData(double timeoutSeconds) {
+    long startTimeMicroS = RobotController.getFPGATime();
+    long timeoutMicroS = (long) (timeoutSeconds * 1e6);
     m_waitForDataMutex.lock();
     try {
       int currentCount = m_waitForDataCount;
@@ -918,12 +1083,13 @@
         return true;
       }
       while (m_waitForDataCount == currentCount) {
-        if (timeout > 0) {
-          long now = RobotController.getFPGATime();
-          if (now < startTime + timeoutMicros) {
+        if (timeoutMicroS > 0) {
+          long nowMicroS = RobotController.getFPGATime();
+          if (nowMicroS < startTimeMicroS + timeoutMicroS) {
             // We still have time to wait
-            boolean signaled = m_waitForDataCond.await(startTime + timeoutMicros - now,
-                                                TimeUnit.MICROSECONDS);
+            boolean signaled =
+                m_waitForDataCond.await(
+                    startTimeMicroS + timeoutMicroS - nowMicroS, TimeUnit.MICROSECONDS);
             if (!signaled) {
               // Return false if a timeout happened
               return false;
@@ -951,12 +1117,12 @@
    * Return the approximate match time. The FMS does not send an official match time to the robots,
    * but does send an approximate match time. The value will count down the time remaining in the
    * current period (auto or teleop). Warning: This is not an official time (so it cannot be used to
-   * dispute ref calls or guarantee that a function will trigger before the match ends) The
-   * Practice Match function of the DS approximates the behavior seen on the field.
+   * dispute ref calls or guarantee that a function will trigger before the match ends) The Practice
+   * Match function of the DS approximates the behavior seen on the field.
    *
    * @return Time remaining in current match period (auto or teleop) in seconds
    */
-  public double getMatchTime() {
+  public static double getMatchTime() {
     return HAL.getMatchTime();
   }
 
@@ -966,8 +1132,7 @@
    *
    * @param entering If true, starting disabled code; if false, leaving disabled code
    */
-  @SuppressWarnings("MethodName")
-  public void InDisabled(boolean entering) {
+  public static void inDisabled(boolean entering) {
     m_userInDisabled = entering;
   }
 
@@ -977,8 +1142,7 @@
    *
    * @param entering If true, starting autonomous code; if false, leaving autonomous code
    */
-  @SuppressWarnings("MethodName")
-  public void InAutonomous(boolean entering) {
+  public static void inAutonomous(boolean entering) {
     m_userInAutonomous = entering;
   }
 
@@ -987,9 +1151,20 @@
    * purposes only.
    *
    * @param entering If true, starting teleop code; if false, leaving teleop code
+   * @deprecated Use {@link #inTeleop(boolean)} instead.
    */
-  @SuppressWarnings("MethodName")
-  public void InOperatorControl(boolean entering) {
+  @Deprecated(since = "2022", forRemoval = true)
+  public static void inOperatorControl(boolean entering) {
+    m_userInTeleop = entering;
+  }
+
+  /**
+   * Only to be used to tell the Driver Station what code you claim to be executing for diagnostic
+   * purposes only.
+   *
+   * @param entering If true, starting teleop code; if false, leaving teleop code
+   */
+  public static void inTeleop(boolean entering) {
     m_userInTeleop = entering;
   }
 
@@ -999,94 +1174,39 @@
    *
    * @param entering If true, starting test code; if false, leaving test code
    */
-  @SuppressWarnings("MethodName")
-  public void InTest(boolean entering) {
+  public static void inTest(boolean entering) {
     m_userInTest = entering;
   }
 
-  private void sendMatchData() {
-    AllianceStationID alliance = HAL.getAllianceStation();
-    boolean isRedAlliance = false;
-    int stationNumber = 1;
-    switch (alliance) {
-      case Blue1:
-        isRedAlliance = false;
-        stationNumber = 1;
-        break;
-      case Blue2:
-        isRedAlliance = false;
-        stationNumber = 2;
-        break;
-      case Blue3:
-        isRedAlliance = false;
-        stationNumber = 3;
-        break;
-      case Red1:
-        isRedAlliance = true;
-        stationNumber = 1;
-        break;
-      case Red2:
-        isRedAlliance = true;
-        stationNumber = 2;
-        break;
-      default:
-        isRedAlliance = true;
-        stationNumber = 3;
-        break;
-    }
-
-
-    String eventName;
-    String gameSpecificMessage;
-    int matchNumber;
-    int replayNumber;
-    int matchType;
-    synchronized (m_cacheDataMutex) {
-      eventName = m_matchInfo.eventName;
-      gameSpecificMessage = m_matchInfo.gameSpecificMessage;
-      matchNumber = m_matchInfo.matchNumber;
-      replayNumber = m_matchInfo.replayNumber;
-      matchType = m_matchInfo.matchType;
-    }
-
-    m_matchDataSender.alliance.setBoolean(isRedAlliance);
-    m_matchDataSender.station.setDouble(stationNumber);
-    m_matchDataSender.eventName.setString(eventName);
-    m_matchDataSender.gameSpecificMessage.setString(gameSpecificMessage);
-    m_matchDataSender.matchNumber.setDouble(matchNumber);
-    m_matchDataSender.replayNumber.setDouble(replayNumber);
-    m_matchDataSender.matchType.setDouble(matchType);
-    m_matchDataSender.controlWord.setDouble(HAL.nativeGetControlWord());
-  }
-
-  /**
-   * Forces waitForData() to return immediately.
-   */
-  public void wakeupWaitForData() {
+  /** Forces waitForData() to return immediately. */
+  public static void wakeupWaitForData() {
     m_waitForDataMutex.lock();
-    m_waitForDataCount++;
-    m_waitForDataCond.signalAll();
-    m_waitForDataMutex.unlock();
+    try {
+      m_waitForDataCount++;
+      m_waitForDataCond.signalAll();
+    } finally {
+      m_waitForDataMutex.unlock();
+    }
   }
 
   /**
-   * Allows the user to specify whether they want joystick connection warnings
-   * to be printed to the console. This setting is ignored when the FMS is
-   * connected -- warnings will always be on in that scenario.
+   * Allows the user to specify whether they want joystick connection warnings to be printed to the
+   * console. This setting is ignored when the FMS is connected -- warnings will always be on in
+   * that scenario.
    *
    * @param silence Whether warning messages should be silenced.
    */
-  public void silenceJoystickConnectionWarning(boolean silence) {
+  public static void silenceJoystickConnectionWarning(boolean silence) {
     m_silenceJoystickWarning = silence;
   }
 
   /**
-   * Returns whether joystick connection warnings are silenced. This will
-   * always return false when connected to the FMS.
+   * Returns whether joystick connection warnings are silenced. This will always return false when
+   * connected to the FMS.
    *
    * @return Whether joystick connection warnings are silenced.
    */
-  public boolean isJoystickConnectionWarningSilenced() {
+  public static boolean isJoystickConnectionWarningSilenced() {
     return !isFMSAttached() && m_silenceJoystickWarning;
   }
 
@@ -1094,7 +1214,7 @@
    * Copy data from the DS task for the user. If no new data exists, it will just be returned,
    * otherwise the data will be copied from the DS polling loop.
    */
-  protected void getData() {
+  protected static void getData() {
     // Get the status of all of the joysticks
     for (byte stick = 0; stick < kJoystickPorts; stick++) {
       m_joystickAxesCache[stick].m_count =
@@ -1144,14 +1264,14 @@
     }
 
     wakeupWaitForData();
-    sendMatchData();
+    m_matchDataSender.sendMatchData();
   }
 
   /**
    * Reports errors related to unplugged joysticks Throttles the errors so that they don't overwhelm
    * the DS.
    */
-  private void reportJoystickUnpluggedError(String message) {
+  private static void reportJoystickUnpluggedError(String message) {
     double currentTime = Timer.getFPGATimestamp();
     if (currentTime > m_nextMessageTime) {
       reportError(message, false);
@@ -1163,7 +1283,7 @@
    * Reports errors related to unplugged joysticks Throttles the errors so that they don't overwhelm
    * the DS.
    */
-  private void reportJoystickUnpluggedWarning(String message) {
+  private static void reportJoystickUnpluggedWarning(String message) {
     if (isFMSAttached() || !m_silenceJoystickWarning) {
       double currentTime = Timer.getFPGATimestamp();
       if (currentTime > m_nextMessageTime) {
@@ -1173,10 +1293,8 @@
     }
   }
 
-  /**
-   * Provides the service routine for the DS polling m_thread.
-   */
-  private void run() {
+  /** Provides the service routine for the DS polling m_thread. */
+  private static void run() {
     int safetyCounter = 0;
     while (m_threadKeepAlive) {
       HAL.waitForDSData();
@@ -1207,12 +1325,12 @@
   }
 
   /**
-   * Updates the data in the control word cache. Updates if the force parameter is set, or if
-   * 50ms have passed since the last update.
+   * Updates the data in the control word cache. Updates if the force parameter is set, or if 50ms
+   * have passed since the last update.
    *
    * @param force True to force an update to the cache, otherwise update if 50ms have passed.
    */
-  private void updateControlWord(boolean force) {
+  private static void updateControlWord(boolean force) {
     long now = System.currentTimeMillis();
     synchronized (m_controlWordMutex) {
       if (now - m_lastControlWordUpdate > 50 || force) {
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 0834fad..3332f49 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycle.java
@@ -1,28 +1,24 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.DutyCycleJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class to read a duty cycle PWM input.
  *
- * <p>PWM input signals are specified with a frequency and a ratio of high to low
- * in that frequency. There are 8 of these in the roboRIO, and they can be
- * attached to any {@link DigitalSource}.
+ * <p>PWM input signals are specified with a frequency and a ratio of high to low in that frequency.
+ * There are 8 of these in the roboRIO, and they can be attached to any {@link DigitalSource}.
  *
- * <p>These can be combined as the input of an AnalogTrigger to a Counter in order
- * to implement rollover checking.
- *
+ * <p>These can be combined as the input of an AnalogTrigger to a Counter in order to implement
+ * rollover checking.
  */
 public class DutyCycle implements Sendable, AutoCloseable {
   // Explicitly package private
@@ -38,8 +34,10 @@
    * @param digitalSource The DigitalSource to use.
    */
   public DutyCycle(DigitalSource digitalSource) {
-    m_handle = DutyCycleJNI.initialize(digitalSource.getPortHandleForRouting(),
-        digitalSource.getAnalogTriggerTypeForRouting());
+    m_handle =
+        DutyCycleJNI.initialize(
+            digitalSource.getPortHandleForRouting(),
+            digitalSource.getAnalogTriggerTypeForRouting());
 
     m_source = digitalSource;
     int index = getFPGAIndex();
@@ -47,9 +45,7 @@
     SendableRegistry.addLW(this, "Duty Cycle", index);
   }
 
-  /**
-   * Close the DutyCycle and free all resources.
-   */
+  /** Close the DutyCycle and free all resources. */
   @Override
   public void close() {
     DutyCycleJNI.free(m_handle);
@@ -78,8 +74,7 @@
   /**
    * Get the raw output ratio of the duty cycle signal.
    *
-   * <p>0 means always low, an output equal to getOutputScaleFactor() means always
-   * high.
+   * <p>0 means always low, an output equal to getOutputScaleFactor() means always high.
    *
    * @return output ratio in raw units
    */
@@ -90,9 +85,8 @@
   /**
    * Get the scale factor of the output.
    *
-   * <p>An output equal to this value is always high, and then linearly scales down
-   * to 0. Divide the result of getOutputRaw by this in order to get the
-   * percentage between 0 and 1.
+   * <p>An output equal to this value is always high, and then linearly scales down to 0. Divide the
+   * result of getOutputRaw by this in order to get the percentage between 0 and 1.
    *
    * @return the output scale factor
    */
@@ -119,6 +113,5 @@
     builder.setSmartDashboardType("Duty Cycle");
     builder.addDoubleProperty("Frequency", this::getFrequency, null);
     builder.addDoubleProperty("Output", this::getOutput, null);
-
   }
 }
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 f5bba5e..ce5b684 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DutyCycleEncoder.java
@@ -1,23 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.SimBoolean;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.AnalogTriggerOutput.AnalogTriggerType;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
 
 /**
- * Class for supporting duty cycle/PWM encoders, such as the US Digital MA3 with
- * PWM Output, the CTRE Mag Encoder, the Rev Hex Encoder, and the AM Mag
- * Encoder.
+ * Class for supporting duty cycle/PWM encoders, such as the US Digital MA3 with PWM Output, the
+ * CTRE Mag Encoder, the Rev Hex Encoder, and the AM Mag Encoder.
  */
 public class DutyCycleEncoder implements Sendable, AutoCloseable {
   private final DutyCycle m_dutyCycle;
@@ -69,12 +66,13 @@
   }
 
   private void init() {
-    m_simDevice = SimDevice.create("DutyCycleEncoder", m_dutyCycle.getFPGAIndex());
+    m_simDevice = SimDevice.create("DutyCycle:DutyCycleEncoder", m_dutyCycle.getSourceChannel());
 
     if (m_simDevice != null) {
-      m_simPosition = m_simDevice.createDouble("Position", false, 0.0);
-      m_simDistancePerRotation = m_simDevice.createDouble("DistancePerRotation", false, 1.0);
-      m_simIsConnected = m_simDevice.createBoolean("Connected", false, true);
+      m_simPosition = m_simDevice.createDouble("position", SimDevice.Direction.kInput, 0.0);
+      m_simDistancePerRotation =
+          m_simDevice.createDouble("distance_per_rot", SimDevice.Direction.kOutput, 1.0);
+      m_simIsConnected = m_simDevice.createBoolean("connected", SimDevice.Direction.kInput, true);
     } else {
       m_counter = new Counter();
       m_analogTrigger = new AnalogTrigger(m_dutyCycle);
@@ -121,9 +119,9 @@
   /**
    * Get the offset of position relative to the last reset.
    *
-   * <p>getPositionInRotation() - getPositionOffset() will give an encoder absolute
-   * position relative to the last reset. This could potentially be negative,
-   * which needs to be accounted for.
+   * <p>getPositionInRotation() - getPositionOffset() will give an encoder absolute position
+   * relative to the last reset. This could potentially be negative, which needs to be accounted
+   * for.
    *
    * @return the position offset
    */
@@ -132,35 +130,29 @@
   }
 
   /**
-   * Set the distance per rotation of the encoder. This sets the multiplier used
-   * to determine the distance driven based on the rotation value from the
-   * encoder. Set this value based on the how far the mechanism travels in 1
-   * rotation of the encoder, and factor in gearing reductions following the
-   * encoder shaft. This distance can be in any units you like, linear or angular.
+   * Set the distance per rotation of the encoder. This sets the multiplier used to determine the
+   * distance driven based on the rotation value from the encoder. Set this value based on the how
+   * far the mechanism travels in 1 rotation of the encoder, and factor in gearing reductions
+   * following the encoder shaft. This distance can be in any units you like, linear or angular.
    *
    * @param distancePerRotation the distance per rotation of the encoder
    */
   public void setDistancePerRotation(double distancePerRotation) {
     m_distancePerRotation = distancePerRotation;
-
-    if (m_simDistancePerRotation != null) {
-      m_simDistancePerRotation.set(distancePerRotation);
-    }
   }
 
   /**
    * Get the distance per rotation for this encoder.
    *
-   * @return The scale factor that will be used to convert rotation to useful
-   *         units.
+   * @return The scale factor that will be used to convert rotation to useful units.
    */
   public double getDistancePerRotation() {
     return m_distancePerRotation;
   }
 
   /**
-   * Get the distance the sensor has driven since the last reset as scaled by the
-   * value from {@link #setDistancePerRotation(double)}.
+   * Get the distance the sensor has driven since the last reset as scaled by the value from {@link
+   * #setDistancePerRotation(double)}.
    *
    * @return The distance driven since the last reset
    */
@@ -177,9 +169,7 @@
     return m_dutyCycle.getFrequency();
   }
 
-  /**
-   * Reset the Encoder distance to zero.
-   */
+  /** Reset the Encoder distance to zero. */
   public void reset() {
     if (m_counter != null) {
       m_counter.reset();
@@ -190,9 +180,9 @@
   /**
    * Get if the sensor is connected
    *
-   * <p>This uses the duty cycle frequency to determine if the sensor is connected.
-   * By default, a value of 100 Hz is used as the threshold, and this value can be
-   * changed with {@link #setConnectedFrequencyThreshold(int)}.
+   * <p>This uses the duty cycle frequency to determine if the sensor is connected. By default, a
+   * value of 100 Hz is used as the threshold, and this value can be changed with {@link
+   * #setConnectedFrequencyThreshold(int)}.
    *
    * @return true if the sensor is connected
    */
@@ -204,8 +194,7 @@
   }
 
   /**
-   * Change the frequency threshold for detecting connection used by
-   * {@link #isConnected()}.
+   * Change the frequency threshold for detecting connection used by {@link #isConnected()}.
    *
    * @param frequency the minimum frequency in Hz.
    */
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 125f769..200d713 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Encoder.java
@@ -1,21 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
 import edu.wpi.first.hal.EncoderJNI;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimDevice;
 import edu.wpi.first.hal.util.AllocationException;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class to read quadrature encoders.
@@ -30,11 +28,13 @@
  * <p>All encoders will immediately start counting - reset() them if you need them to be zeroed
  * before use.
  */
-public class Encoder implements CounterBase, PIDSource, Sendable, AutoCloseable {
+public class Encoder implements CounterBase, Sendable, AutoCloseable {
   public enum IndexingType {
-    kResetWhileHigh(0), kResetWhileLow(1), kResetOnFallingEdge(2), kResetOnRisingEdge(3);
+    kResetWhileHigh(0),
+    kResetWhileLow(1),
+    kResetOnFallingEdge(2),
+    kResetOnRisingEdge(3);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     IndexingType(int value) {
@@ -42,27 +42,19 @@
     }
   }
 
-  /**
-   * The a source.
-   */
-  @SuppressWarnings("MemberName")
+  /** The a source. */
   protected DigitalSource m_aSource; // the A phase of the quad encoder
-  /**
-   * The b source.
-   */
-  @SuppressWarnings("MemberName")
+  /** The b source. */
   protected DigitalSource m_bSource; // the B phase of the quad encoder
-  /**
-   * The index source.
-   */
+  /** The index source. */
   protected DigitalSource m_indexSource; // Index on some encoders
+
   private boolean m_allocatedA;
   private boolean m_allocatedB;
   private boolean m_allocatedI;
-  private PIDSourceType m_pidSource;
+  private final EncodingType m_encodingType;
 
-  private int m_encoder; // the HAL encoder object
-
+  int m_encoder; // the HAL encoder object
 
   /**
    * Common initialization code for Encoders. This code allocates resources for Encoders and is
@@ -73,11 +65,14 @@
    * @param reverseDirection If true, counts down instead of up (this is all relative)
    */
   private void initEncoder(boolean reverseDirection, final EncodingType type) {
-    m_encoder = EncoderJNI.initializeEncoder(m_aSource.getPortHandleForRouting(),
-        m_aSource.getAnalogTriggerTypeForRouting(), m_bSource.getPortHandleForRouting(),
-        m_bSource.getAnalogTriggerTypeForRouting(), reverseDirection, type.value);
-
-    m_pidSource = PIDSourceType.kDisplacement;
+    m_encoder =
+        EncoderJNI.initializeEncoder(
+            m_aSource.getPortHandleForRouting(),
+            m_aSource.getAnalogTriggerTypeForRouting(),
+            m_bSource.getPortHandleForRouting(),
+            m_bSource.getAnalogTriggerTypeForRouting(),
+            reverseDirection,
+            type.value);
 
     int fpgaIndex = getFPGAIndex();
     HAL.report(tResourceType.kResourceType_Encoder, fpgaIndex + 1, type.value + 1);
@@ -89,10 +84,10 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param channelA         The a channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
-   * @param channelB         The b channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
+   * @param channelA The a channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
+   * @param channelB The b channel DIO channel. 0-9 are on-board, 10-25 are on the MXP port
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
+   *     if necessary so forward represents positive values.
    */
   public Encoder(final int channelA, final int channelB, boolean reverseDirection) {
     this(channelA, channelB, reverseDirection, EncodingType.k4X);
@@ -115,19 +110,21 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param channelA         The a channel digital input channel.
-   * @param channelB         The b channel digital input channel.
+   * @param channelA The a channel digital input channel.
+   * @param channelB The b channel digital input channel.
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
-   * @param encodingType     either k1X, k2X, or k4X to indicate 1X, 2X or 4X decoding. If 4X is
-   *                         selected, then an encoder FPGA object is used and the returned counts
-   *                         will be 4x the encoder spec'd value since all rising and falling edges
-   *                         are counted. If 1X or 2X are selected then a m_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.
+   *     if necessary so forward represents positive values.
+   * @param encodingType either k1X, k2X, or k4X to indicate 1X, 2X or 4X decoding. If 4X is
+   *     selected, then an encoder FPGA object is used and the returned counts will be 4x the
+   *     encoder spec'd value since all rising and falling edges are counted. If 1X or 2X are
+   *     selected then a m_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.
    */
-  public Encoder(final int channelA, final int channelB, boolean reverseDirection,
-                 final EncodingType encodingType) {
+  public Encoder(
+      final int channelA,
+      final int channelB,
+      boolean reverseDirection,
+      final EncodingType encodingType) {
     requireNonNullParam(encodingType, "encodingType", "Encoder");
 
     m_allocatedA = true;
@@ -135,6 +132,7 @@
     m_allocatedI = false;
     m_aSource = new DigitalInput(channelA);
     m_bSource = new DigitalInput(channelB);
+    m_encodingType = encodingType;
     SendableRegistry.addChild(this, m_aSource);
     SendableRegistry.addChild(this, m_bSource);
     initEncoder(reverseDirection, encodingType);
@@ -146,14 +144,14 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param channelA         The a channel digital input channel.
-   * @param channelB         The b channel digital input channel.
-   * @param indexChannel     The index channel digital input channel.
+   * @param channelA The a channel digital input channel.
+   * @param channelB The b channel digital input channel.
+   * @param indexChannel The index channel digital input channel.
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
+   *     if necessary so forward represents positive values.
    */
-  public Encoder(final int channelA, final int channelB, final int indexChannel,
-                 boolean reverseDirection) {
+  public Encoder(
+      final int channelA, final int channelB, final int indexChannel, boolean reverseDirection) {
     this(channelA, channelB, reverseDirection);
     m_allocatedI = true;
     m_indexSource = new DigitalInput(indexChannel);
@@ -167,8 +165,8 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param channelA     The a channel digital input channel.
-   * @param channelB     The b channel digital input channel.
+   * @param channelA The a channel digital input channel.
+   * @param channelB The b channel digital input channel.
    * @param indexChannel The index channel digital input channel.
    */
   public Encoder(final int channelA, final int channelB, final int indexChannel) {
@@ -182,10 +180,10 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param sourceA          The source that should be used for the a channel.
-   * @param sourceB          the source that should be used for the b channel.
+   * @param sourceA The source that should be used for the a channel.
+   * @param sourceB the source that should be used for the b channel.
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
+   *     if necessary so forward represents positive values.
    */
   public Encoder(DigitalSource sourceA, DigitalSource sourceB, boolean reverseDirection) {
     this(sourceA, sourceB, reverseDirection, EncodingType.k4X);
@@ -212,19 +210,21 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param sourceA          The source that should be used for the a channel.
-   * @param sourceB          the source that should be used for the b channel.
+   * @param sourceA The source that should be used for the a channel.
+   * @param sourceB the source that should be used for the b channel.
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
-   * @param encodingType     either k1X, k2X, or k4X to indicate 1X, 2X or 4X decoding. If 4X is
-   *                         selected, then an encoder FPGA object is used and the returned counts
-   *                         will be 4x the encoder spec'd value since all rising and falling edges
-   *                         are counted. If 1X or 2X are selected then a m_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.
+   *     if necessary so forward represents positive values.
+   * @param encodingType either k1X, k2X, or k4X to indicate 1X, 2X or 4X decoding. If 4X is
+   *     selected, then an encoder FPGA object is used and the returned counts will be 4x the
+   *     encoder spec'd value since all rising and falling edges are counted. If 1X or 2X are
+   *     selected then a m_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.
    */
-  public Encoder(DigitalSource sourceA, DigitalSource sourceB, boolean reverseDirection,
-                 final EncodingType encodingType) {
+  public Encoder(
+      DigitalSource sourceA,
+      DigitalSource sourceB,
+      boolean reverseDirection,
+      final EncodingType encodingType) {
     requireNonNullParam(sourceA, "sourceA", "Encoder");
     requireNonNullParam(sourceB, "sourceB", "Encoder");
     requireNonNullParam(encodingType, "encodingType", "Encoder");
@@ -232,6 +232,7 @@
     m_allocatedA = false;
     m_allocatedB = false;
     m_allocatedI = false;
+    m_encodingType = encodingType;
     m_aSource = sourceA;
     m_bSource = sourceB;
     initEncoder(reverseDirection, encodingType);
@@ -244,14 +245,17 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param sourceA          The source that should be used for the a channel.
-   * @param sourceB          the source that should be used for the b channel.
-   * @param indexSource      the source that should be used for the index channel.
+   * @param sourceA The source that should be used for the a channel.
+   * @param sourceB the source that should be used for the b channel.
+   * @param indexSource the source that should be used for the index channel.
    * @param reverseDirection represents the orientation of the encoder and inverts the output values
-   *                         if necessary so forward represents positive values.
+   *     if necessary so forward represents positive values.
    */
-  public Encoder(DigitalSource sourceA, DigitalSource sourceB, DigitalSource indexSource,
-                 boolean reverseDirection) {
+  public Encoder(
+      DigitalSource sourceA,
+      DigitalSource sourceB,
+      DigitalSource indexSource,
+      boolean reverseDirection) {
     this(sourceA, sourceB, reverseDirection);
     m_allocatedI = false;
     m_indexSource = indexSource;
@@ -265,8 +269,8 @@
    *
    * <p>The encoder will start counting immediately.
    *
-   * @param sourceA     The source that should be used for the a channel.
-   * @param sourceB     the source that should be used for the b channel.
+   * @param sourceA The source that should be used for the a channel.
+   * @param sourceB the source that should be used for the b channel.
    * @param indexSource the source that should be used for the index channel.
    */
   public Encoder(DigitalSource sourceA, DigitalSource sourceB, DigitalSource indexSource) {
@@ -336,9 +340,7 @@
     return EncoderJNI.getEncoder(m_encoder);
   }
 
-  /**
-   * Reset the Encoder distance to zero. Resets the current count to zero on the encoder.
-   */
+  /** Reset the Encoder distance to zero. Resets the current count to zero on the encoder. */
   @Override
   public void reset() {
     EncoderJNI.resetEncoder(m_encoder);
@@ -367,7 +369,7 @@
    * compensates for the decoding type.
    *
    * @param maxPeriod The maximum time between rising and falling edges before the FPGA will report
-   *                  the device stopped. This is expressed in seconds.
+   *     the device stopped. This is expressed in seconds.
    */
   @Override
   public void setMaxPeriod(double maxPeriod) {
@@ -420,7 +422,7 @@
    * Set the minimum rate of the device before the hardware reports it stopped.
    *
    * @param minRate The minimum rate. The units are in distance per second as scaled by the value
-   *                from setDistancePerPulse().
+   *     from setDistancePerPulse().
    */
   public void setMinRate(double minRate) {
     EncoderJNI.setEncoderMinRate(m_encoder, minRate);
@@ -481,39 +483,6 @@
   }
 
   /**
-   * Set which parameter of the encoder you are using as a process control variable. The encoder
-   * class supports the rate and distance parameters.
-   *
-   * @param pidSource An enum to select the parameter.
-   */
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Implement the PIDSource interface.
-   *
-   * @return The current value of the selected source parameter.
-   */
-  @Override
-  public double pidGet() {
-    switch (m_pidSource) {
-      case kDisplacement:
-        return getDistance();
-      case kRate:
-        return getRate();
-      default:
-        return 0.0;
-    }
-  }
-
-  /**
    * Set the index source for the encoder. When this source is activated, the encoder count
    * automatically resets.
    *
@@ -538,7 +507,7 @@
    * resets.
    *
    * @param channel A DIO channel to set as the encoder index
-   * @param type    The state that will cause the encoder to reset
+   * @param type The state that will cause the encoder to reset
    */
   public void setIndexSource(int channel, IndexingType type) {
     if (m_allocatedI) {
@@ -555,11 +524,14 @@
    * resets.
    *
    * @param source A digital source to set as the encoder index
-   * @param type   The state that will cause the encoder to reset
+   * @param type The state that will cause the encoder to reset
    */
   public void setIndexSource(DigitalSource source, IndexingType type) {
-    EncoderJNI.setEncoderIndexSource(m_encoder, source.getPortHandleForRouting(),
-        source.getAnalogTriggerTypeForRouting(), type.value);
+    EncoderJNI.setEncoderIndexSource(
+        m_encoder,
+        source.getPortHandleForRouting(),
+        source.getAnalogTriggerTypeForRouting(),
+        type.value);
   }
 
   /**
@@ -571,6 +543,24 @@
     EncoderJNI.setEncoderSimDevice(m_encoder, device.getNativeHandle());
   }
 
+  /**
+   * Gets the decoding scale factor for scaling raw values to full counts.
+   *
+   * @return decoding scale factor
+   */
+  public double getDecodingScaleFactor() {
+    switch (m_encodingType) {
+      case k1X:
+        return 1.0;
+      case k2X:
+        return 0.5;
+      case k4X:
+        return 0.25;
+      default:
+        return 0.0;
+    }
+  }
+
   @Override
   public void initSendable(SendableBuilder builder) {
     if (EncoderJNI.getEncoderEncodingType(m_encoder) == EncodingType.k4X.value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Filesystem.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Filesystem.java
index c1327aa..98230db 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Filesystem.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Filesystem.java
@@ -1,27 +1,24 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.io.File;
 
 /**
- * Class for interacting with the Filesystem, particularly, interacting with
- * FRC-related paths on the system, such as the launch and deploy directories.
+ * Class for interacting with the Filesystem, particularly, interacting with FRC-related paths on
+ * the system, such as the launch and deploy directories.
  *
- * <p>This class is primarily used for obtaining resources in src/main/deploy, and
- * the RoboRIO path /home/lvuser in a simulation-compatible way.</p>
+ * <p>This class is primarily used for obtaining resources in src/main/deploy, and the RoboRIO path
+ * /home/lvuser in a simulation-compatible way.
  */
 public final class Filesystem {
-  private Filesystem() { }
+  private Filesystem() {}
 
   /**
-   * Obtains the current working path that the program was launched with.
-   * This is analogous to the `pwd` command on unix.
+   * Obtains the current working path that the program was launched with. This is analogous to the
+   * `pwd` command on unix.
    *
    * @return The current working directory (launch directory)
    */
@@ -30,9 +27,8 @@
   }
 
   /**
-   * Obtains the operating directory of the program. On the roboRIO, this is
-   * /home/lvuser. In simulation, it is where the simulation was launched from
-   * (`pwd`).
+   * Obtains the operating directory of the program. On the roboRIO, this is /home/lvuser. In
+   * simulation, it is where the simulation was launched from (`pwd`).
    *
    * @return The operating directory
    */
@@ -45,10 +41,10 @@
   }
 
   /**
-   * Obtains the deploy directory of the program, which is the remote location
-   * src/main/deploy is deployed to by default. On the roboRIO, this is
-   * /home/lvuser/deploy. In simulation, it is where the simulation was launched
-   * from, in the subdirectory "src/main/deploy" (`pwd`/src/main/deploy).
+   * Obtains the deploy directory of the program, which is the remote location src/main/deploy is
+   * deployed to by default. On the roboRIO, this is /home/lvuser/deploy. In simulation, it is where
+   * the simulation was launched from, in the subdirectory "src/main/deploy"
+   * (`pwd`/src/main/deploy).
    *
    * @return The deploy directory
    */
@@ -56,8 +52,8 @@
     if (RobotBase.isReal()) {
       return new File(getOperatingDirectory(), "deploy");
     } else {
-      return new File(getOperatingDirectory(), "src" + File.separator + "main"
-          + File.separator + "deploy");
+      return new File(
+          getOperatingDirectory(), "src" + File.separator + "main" + File.separator + "deploy");
     }
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GearTooth.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/GearTooth.java
deleted file mode 100644
index 41f9d09..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GearTooth.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Alias for counter class. Implement the gear tooth sensor supplied by FIRST. Currently there is no
- * reverse sensing on the gear tooth sensor, but in future versions we might implement the necessary
- * timing in the FPGA to sense direction.
- *
- * @deprecated The only sensor this works with is no longer available and no teams use it according
- *             to FMS usage reporting.
- */
-@Deprecated(since = "2020", forRemoval = true)
-public class GearTooth extends Counter {
-  private static final double kGearToothThreshold = 55e-6;
-
-  /**
-   * Common code called by the constructors.
-   */
-  public void enableDirectionSensing(boolean directionSensitive) {
-    if (directionSensitive) {
-      setPulseLengthMode(kGearToothThreshold);
-    }
-  }
-
-  /**
-   * Construct a GearTooth sensor given a channel.
-   *
-   * <p>No direction sensing is assumed.
-   *
-   * @param channel The GPIO channel that the sensor is connected to.
-   */
-  public GearTooth(final int channel) {
-    this(channel, false);
-  }
-
-  /**
-   * Construct a GearTooth sensor given a channel.
-   *
-   * @param channel            The DIO channel that the sensor is connected to. 0-9 are on-board,
-   *                           10-25 are on the MXP port
-   * @param directionSensitive True to enable the pulse length decoding in hardware to specify count
-   *                           direction.
-   */
-  public GearTooth(final int channel, boolean directionSensitive) {
-    super(channel);
-    enableDirectionSensing(directionSensitive);
-    if (directionSensitive) {
-      HAL.report(tResourceType.kResourceType_GearTooth, channel + 1, 0, "D");
-    } else {
-      HAL.report(tResourceType.kResourceType_GearTooth, channel + 1, 0);
-    }
-    SendableRegistry.setName(this, "GearTooth", channel);
-  }
-
-  /**
-   * Construct a GearTooth sensor given a digital input. This should be used when sharing digital
-   * inputs.
-   *
-   * @param source             An existing DigitalSource object (such as a DigitalInput)
-   * @param directionSensitive True to enable the pulse length decoding in hardware to specify count
-   *                           direction.
-   */
-  public GearTooth(DigitalSource source, boolean directionSensitive) {
-    super(source);
-    enableDirectionSensing(directionSensitive);
-    if (directionSensitive) {
-      HAL.report(tResourceType.kResourceType_GearTooth, source.getChannel() + 1, 0, "D");
-    } else {
-      HAL.report(tResourceType.kResourceType_GearTooth, source.getChannel() + 1, 0);
-    }
-    SendableRegistry.setName(this, "GearTooth", source.getChannel());
-  }
-
-  /**
-   * Construct a GearTooth sensor given a digital input. This should be used when sharing digital
-   * inputs.
-   *
-   * <p>No direction sensing is assumed.
-   *
-   * @param source An object that fully describes the input that the sensor is connected to.
-   */
-  public GearTooth(DigitalSource source) {
-    this(source, false);
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    super.initSendable(builder);
-    builder.setSmartDashboardType("Gear Tooth");
-  }
-}
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 f573cb2..4903e00 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/GenericHID.java
@@ -1,26 +1,25 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import edu.wpi.first.hal.HAL;
 import java.util.HashMap;
 import java.util.Map;
 
-import edu.wpi.first.hal.HAL;
-
 /**
- * GenericHID Interface.
+ * Handle input from standard HID devices connected to the Driver Station.
+ *
+ * <p>This class handles standard 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 device and
+ * the mapping of ports to hardware buttons depends on the code in the Driver Station.
  */
-public abstract class GenericHID {
-  /**
-   * Represents a rumble output on the JoyStick.
-   */
+public class GenericHID {
+  /** Represents a rumble output on the JoyStick. */
   public enum RumbleType {
-    kLeftRumble, kRightRumble
+    kLeftRumble,
+    kRightRumble
   }
 
   public enum HIDType {
@@ -42,8 +41,8 @@
     kHIDFlight(23),
     kHID1stPerson(24);
 
-    @SuppressWarnings("MemberName")
     public final int value;
+
     @SuppressWarnings("PMD.UseConcurrentHashMap")
     private static final Map<Integer, HIDType> map = new HashMap<>();
 
@@ -62,109 +61,62 @@
     }
   }
 
-  /**
-   * Which hand the Human Interface Device is associated with.
-   */
-  public enum Hand {
-    kLeft(0), kRight(1);
-
-    @SuppressWarnings("MemberName")
-    public final int value;
-
-    Hand(int value) {
-      this.value = value;
-    }
-  }
-
-  private DriverStation m_ds;
   private final int m_port;
   private int m_outputs;
   private short m_leftRumble;
   private short m_rightRumble;
 
+  /**
+   * Construct an instance of a device.
+   *
+   * @param port The port index on the Driver Station that the device is plugged into.
+   */
   public GenericHID(int port) {
-    m_ds = DriverStation.getInstance();
     m_port = port;
   }
 
   /**
-   * Get the x position of the HID.
-   *
-   * @return the x position of the HID
-   */
-  public final double getX() {
-    return getX(Hand.kRight);
-  }
-
-  /**
-   * Get the x position of HID.
-   *
-   * @param hand which hand, left or right
-   * @return the x position
-   */
-  public abstract double getX(Hand hand);
-
-  /**
-   * Get the y position of the HID.
-   *
-   * @return the y position
-   */
-  public final double getY() {
-    return getY(Hand.kRight);
-  }
-
-  /**
-   * Get the y position of the HID.
-   *
-   * @param hand which hand, left or right
-   * @return the y position
-   */
-  public abstract double getY(Hand hand);
-
-  /**
    * Get the button value (starting at button 1).
    *
    * <p>The buttons are returned in a single 16 bit value with one bit representing the state of
    * each button. The appropriate button is returned as a boolean value.
    *
-   * <p>This method returns true if the button is being held down at the time
-   * that this method is being called.
+   * <p>This method returns true if the button is being held down at the time that this method is
+   * being called.
    *
    * @param button The button number to be read (starting at 1)
    * @return The state of the button.
    */
   public boolean getRawButton(int button) {
-    return m_ds.getStickButton(m_port, (byte) button);
+    return DriverStation.getStickButton(m_port, (byte) button);
   }
 
   /**
-   * Whether the button was pressed since the last check. Button indexes begin at
-   * 1.
+   * Whether the button was pressed since the last check. Button indexes begin at 1.
    *
-   * <p>This method returns true if the button went from not pressed to held down
-   * since the last time this method was called. This is useful if you only
-   * want to call a function once when you press the button.
+   * <p>This method returns true if the button went from not pressed to held down since the last
+   * time this method was called. This is useful if you only want to call a function once when you
+   * press the button.
    *
    * @param button The button index, beginning at 1.
    * @return Whether the button was pressed since the last check.
    */
   public boolean getRawButtonPressed(int button) {
-    return m_ds.getStickButtonPressed(m_port, (byte) button);
+    return DriverStation.getStickButtonPressed(m_port, (byte) button);
   }
 
   /**
-   * Whether the button was released since the last check. Button indexes begin at
-   * 1.
+   * Whether the button was released since the last check. Button indexes begin at 1.
    *
-   * <p>This method returns true if the button went from held down to not pressed
-   * since the last time this method was called. This is useful if you only
-   * want to call a function once when you release the button.
+   * <p>This method returns true if the button went from held down to not pressed since the last
+   * time this method was called. This is useful if you only want to call a function once when you
+   * release the button.
    *
    * @param button The button index, beginning at 1.
    * @return Whether the button was released since the last check.
    */
   public boolean getRawButtonReleased(int button) {
-    return m_ds.getStickButtonReleased(m_port, button);
+    return DriverStation.getStickButtonReleased(m_port, button);
   }
 
   /**
@@ -174,7 +126,7 @@
    * @return The value of the axis.
    */
   public double getRawAxis(int axis) {
-    return m_ds.getStickAxis(m_port, axis);
+    return DriverStation.getStickAxis(m_port, axis);
   }
 
   /**
@@ -183,13 +135,21 @@
    * <p>The POV angles start at 0 in the up direction, and increase clockwise (eg right is 90,
    * upper-left is 315).
    *
-   * @param pov The index of the POV to read (starting at 0)
+   * @param pov The index of the POV to read (starting at 0). Defaults to 0.
    * @return the angle of the POV in degrees, or -1 if the POV is not pressed.
    */
   public int getPOV(int pov) {
-    return m_ds.getStickPOV(m_port, pov);
+    return DriverStation.getStickPOV(m_port, pov);
   }
 
+  /**
+   * Get the angle in degrees of the default POV (index 0) on the HID.
+   *
+   * <p>The POV angles start at 0 in the up direction, and increase clockwise (eg right is 90,
+   * upper-left is 315).
+   *
+   * @return the angle of the POV in degrees, or -1 if the POV is not pressed.
+   */
   public int getPOV() {
     return getPOV(0);
   }
@@ -200,21 +160,25 @@
    * @return the number of axis for the current HID
    */
   public int getAxisCount() {
-    return m_ds.getStickAxisCount(m_port);
+    return DriverStation.getStickAxisCount(m_port);
   }
 
   /**
    * For the current HID, return the number of POVs.
+   *
+   * @return the number of POVs for the current HID
    */
   public int getPOVCount() {
-    return m_ds.getStickPOVCount(m_port);
+    return DriverStation.getStickPOVCount(m_port);
   }
 
   /**
    * For the current HID, return the number of buttons.
+   *
+   * @return the number of buttons for the current HID
    */
   public int getButtonCount() {
-    return m_ds.getStickButtonCount(m_port);
+    return DriverStation.getStickButtonCount(m_port);
   }
 
   /**
@@ -223,7 +187,7 @@
    * @return true if the HID is connected
    */
   public boolean isConnected() {
-    return m_ds.isJoystickConnected(m_port);
+    return DriverStation.isJoystickConnected(m_port);
   }
 
   /**
@@ -232,7 +196,7 @@
    * @return the type of the HID.
    */
   public HIDType getType() {
-    return HIDType.of(m_ds.getJoystickType(m_port));
+    return HIDType.of(DriverStation.getJoystickType(m_port));
   }
 
   /**
@@ -241,16 +205,17 @@
    * @return the name of the HID.
    */
   public String getName() {
-    return m_ds.getJoystickName(m_port);
+    return DriverStation.getJoystickName(m_port);
   }
 
   /**
    * Get the axis type of a joystick axis.
    *
+   * @param axis The axis to read, starting at 0.
    * @return the axis type of a joystick axis.
    */
   public int getAxisType(int axis) {
-    return m_ds.getJoystickAxisType(m_port, axis);
+    return DriverStation.getJoystickAxisType(m_port, axis);
   }
 
   /**
@@ -266,7 +231,7 @@
    * Set a single HID output value for the HID.
    *
    * @param outputNumber The index of the output to set (1-32)
-   * @param value        The value to set the output to
+   * @param value The value to set the output to
    */
   public void setOutput(int outputNumber, boolean value) {
     m_outputs = (m_outputs & ~(1 << (outputNumber - 1))) | ((value ? 1 : 0) << (outputNumber - 1));
@@ -287,7 +252,7 @@
    * Set the rumble output for the HID. The DS currently supports 2 rumble values, left rumble and
    * right rumble.
    *
-   * @param type  Which rumble value to set
+   * @param type Which rumble value to set
    * @param value The normalized value (0 to 1) to set the rumble to
    */
   public void setRumble(RumbleType type, double value) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GyroBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/GyroBase.java
deleted file mode 100644
index 21330ce..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/GyroBase.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpilibj.interfaces.Gyro;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-
-/**
- * GyroBase is the common base class for Gyro implementations such as AnalogGyro.
- */
-public abstract class GyroBase implements Gyro, PIDSource, Sendable {
-  private PIDSourceType m_pidSource = PIDSourceType.kDisplacement;
-
-  /**
-   * Set which parameter of the gyro you are using as a process control variable. The Gyro class
-   * supports the rate and displacement parameters
-   *
-   * @param pidSource An enum to select the parameter.
-   */
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Get the output of the gyro for use with PIDControllers. May be the angle or rate depending on
-   * the set PIDSourceType
-   *
-   * @return the output according to the gyro
-   */
-  @Override
-  public double pidGet() {
-    switch (m_pidSource) {
-      case kRate:
-        return getRate();
-      case kDisplacement:
-        return getAngle();
-      default:
-        return 0.0;
-    }
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("Gyro");
-    builder.addDoubleProperty("Value", this::getAngle, null);
-  }
-}
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 58b96aa..1d148a7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/I2C.java
@@ -1,20 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.I2CJNI;
 import edu.wpi.first.hal.util.BoundaryException;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import java.nio.ByteBuffer;
 
 /**
  * I2C bus interface class.
@@ -22,12 +18,11 @@
  * <p>This class is intended to be used by sensor (and other I2C device) drivers. It probably should
  * not be used directly.
  */
-@SuppressWarnings("PMD.GodClass")
 public class I2C implements AutoCloseable {
   public enum Port {
-    kOnboard(0), kMXP(1);
+    kOnboard(0),
+    kMXP(1);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     Port(int value) {
@@ -41,7 +36,7 @@
   /**
    * Constructor.
    *
-   * @param port          The I2C port the device is connected to.
+   * @param port The I2C port the device is connected to.
    * @param deviceAddress The address of the device on the I2C bus.
    */
   public I2C(Port port, int deviceAddress) {
@@ -53,6 +48,14 @@
     HAL.report(tResourceType.kResourceType_I2C, deviceAddress);
   }
 
+  public int getPort() {
+    return m_port;
+  }
+
+  public int getDeviceAddress() {
+    return m_deviceAddress;
+  }
+
   @Override
   public void close() {
     I2CJNI.i2CClose(m_port);
@@ -62,18 +65,18 @@
    * Generic transaction.
    *
    * <p>This is a lower-level interface to the I2C hardware giving you more control over each
-   * transaction. If you intend to write multiple bytes in the same transaction and do not
-   * plan to receive anything back, use writeBulk() instead. Calling this with a receiveSize
-   * of 0 will result in an error.
+   * transaction. If you intend to write multiple bytes in the same transaction and do not plan to
+   * receive anything back, use writeBulk() instead. Calling this with a receiveSize of 0 will
+   * result in an error.
    *
-   * @param dataToSend   Buffer of data to send as part of the transaction.
-   * @param sendSize     Number of bytes to send as part of the transaction.
+   * @param dataToSend Buffer of data to send as part of the transaction.
+   * @param sendSize Number of bytes to send as part of the transaction.
    * @param dataReceived Buffer to read data into.
-   * @param receiveSize  Number of bytes to read from the device.
+   * @param receiveSize Number of bytes to read from the device.
    * @return Transfer Aborted... false for success, true for aborted.
    */
-  public synchronized boolean transaction(byte[] dataToSend, int sendSize,
-                                          byte[] dataReceived, int receiveSize) {
+  public synchronized boolean transaction(
+      byte[] dataToSend, int sendSize, byte[] dataReceived, int receiveSize) {
     if (dataToSend.length < sendSize) {
       throw new IllegalArgumentException("dataToSend is too small, must be at least " + sendSize);
     }
@@ -81,8 +84,14 @@
       throw new IllegalArgumentException(
           "dataReceived is too small, must be at least " + receiveSize);
     }
-    return I2CJNI.i2CTransactionB(m_port, (byte) m_deviceAddress, dataToSend,
-                                  (byte) sendSize, dataReceived, (byte) receiveSize) < 0;
+    return I2CJNI.i2CTransactionB(
+            m_port,
+            (byte) m_deviceAddress,
+            dataToSend,
+            (byte) sendSize,
+            dataReceived,
+            (byte) receiveSize)
+        < 0;
   }
 
   /**
@@ -91,15 +100,15 @@
    * <p>This is a lower-level interface to the I2C hardware giving you more control over each
    * transaction.
    *
-   * @param dataToSend   Buffer of data to send as part of the transaction.
-   * @param sendSize     Number of bytes to send as part of the transaction.
+   * @param dataToSend Buffer of data to send as part of the transaction.
+   * @param sendSize Number of bytes to send as part of the transaction.
    * @param dataReceived Buffer to read data into.
-   * @param receiveSize  Number of bytes to read from the device.
+   * @param receiveSize Number of bytes to read from the device.
    * @return Transfer Aborted... false for success, true for aborted.
    */
-  @SuppressWarnings({"PMD.CyclomaticComplexity", "ByteBufferBackingArray"})
-  public synchronized boolean transaction(ByteBuffer dataToSend, int sendSize,
-                                          ByteBuffer dataReceived, int receiveSize) {
+  @SuppressWarnings("ByteBufferBackingArray")
+  public synchronized boolean transaction(
+      ByteBuffer dataToSend, int sendSize, ByteBuffer dataReceived, int receiveSize) {
     if (dataToSend.hasArray() && dataReceived.hasArray()) {
       return transaction(dataToSend.array(), sendSize, dataReceived.array(), receiveSize);
     }
@@ -117,8 +126,14 @@
           "dataReceived is too small, must be at least " + receiveSize);
     }
 
-    return I2CJNI.i2CTransaction(m_port, (byte) m_deviceAddress, dataToSend,
-                                 (byte) sendSize, dataReceived, (byte) receiveSize) < 0;
+    return I2CJNI.i2CTransaction(
+            m_port,
+            (byte) m_deviceAddress,
+            dataToSend,
+            (byte) sendSize,
+            dataReceived,
+            (byte) receiveSize)
+        < 0;
   }
 
   /**
@@ -139,15 +154,14 @@
    * <p>Write a single byte to a register on a device and wait until the transaction is complete.
    *
    * @param registerAddress The address of the register on the device to be written.
-   * @param data            The byte to write to the register on the device.
+   * @param data The byte to write to the register on the device.
    * @return Transfer Aborted... false for success, true for aborted.
    */
   public synchronized boolean write(int registerAddress, int data) {
     byte[] buffer = new byte[2];
     buffer[0] = (byte) registerAddress;
     buffer[1] = (byte) data;
-    return I2CJNI.i2CWriteB(m_port, (byte) m_deviceAddress, buffer,
-                            (byte) buffer.length) < 0;
+    return I2CJNI.i2CWriteB(m_port, (byte) m_deviceAddress, buffer, (byte) buffer.length) < 0;
   }
 
   /**
@@ -173,8 +187,7 @@
    */
   public synchronized boolean writeBulk(byte[] data, int size) {
     if (data.length < size) {
-      throw new IllegalArgumentException(
-          "buffer is too small, must be at least " + size);
+      throw new IllegalArgumentException("buffer is too small, must be at least " + size);
     }
     return I2CJNI.i2CWriteB(m_port, (byte) m_deviceAddress, data, (byte) size) < 0;
   }
@@ -197,8 +210,7 @@
       throw new IllegalArgumentException("must be a direct buffer");
     }
     if (data.capacity() < size) {
-      throw new IllegalArgumentException(
-          "buffer is too small, must be at least " + size);
+      throw new IllegalArgumentException("buffer is too small, must be at least " + size);
     }
 
     return I2CJNI.i2CWrite(m_port, (byte) m_deviceAddress, data, (byte) size) < 0;
@@ -211,8 +223,8 @@
    * internally allowing you to read consecutive registers on a device in a single transaction.
    *
    * @param registerAddress The register to read first in the transaction.
-   * @param count           The number of bytes to read in the transaction.
-   * @param buffer          A pointer to the array of bytes to store the data read from the device.
+   * @param count The number of bytes to read in the transaction.
+   * @param buffer A pointer to the array of bytes to store the data read from the device.
    * @return Transfer Aborted... false for success, true for aborted.
    */
   public boolean read(int registerAddress, int count, byte[] buffer) {
@@ -240,8 +252,8 @@
    * internally allowing you to read consecutive registers on a device in a single transaction.
    *
    * @param registerAddress The register to read first in the transaction.
-   * @param count           The number of bytes to read in the transaction.
-   * @param buffer          A buffer to store the data read from the device.
+   * @param count The number of bytes to read in the transaction.
+   * @param buffer A buffer to store the data read from the device.
    * @return Transfer Aborted... false for success, true for aborted.
    */
   @SuppressWarnings("ByteBufferBackingArray")
@@ -277,7 +289,7 @@
    * <p>Read bytes from a device. This method does not write any data to prompt the device.
    *
    * @param buffer A pointer to the array of bytes to store the data read from the device.
-   * @param count  The number of bytes to read in the transaction.
+   * @param count The number of bytes to read in the transaction.
    * @return Transfer Aborted... false for success, true for aborted.
    */
   public boolean readOnly(byte[] buffer, int count) {
@@ -289,8 +301,7 @@
       throw new IllegalArgumentException("buffer is too small, must be at least " + count);
     }
 
-    return I2CJNI.i2CReadB(m_port, (byte) m_deviceAddress, buffer,
-                           (byte) count) < 0;
+    return I2CJNI.i2CReadB(m_port, (byte) m_deviceAddress, buffer, (byte) count) < 0;
   }
 
   /**
@@ -299,14 +310,13 @@
    * <p>Read bytes from a device. This method does not write any data to prompt the device.
    *
    * @param buffer A pointer to the array of bytes to store the data read from the device.
-   * @param count  The number of bytes to read in the transaction.
+   * @param count The number of bytes to read in the transaction.
    * @return Transfer Aborted... false for success, true for aborted.
    */
   @SuppressWarnings("ByteBufferBackingArray")
   public boolean readOnly(ByteBuffer buffer, int count) {
     if (count < 1) {
-      throw new BoundaryException("Value must be at least 1, " + count
-          + " given");
+      throw new BoundaryException("Value must be at least 1, " + count + " given");
     }
 
     if (buffer.hasArray()) {
@@ -320,8 +330,7 @@
       throw new IllegalArgumentException("buffer is too small, must be at least " + count);
     }
 
-    return I2CJNI.i2CRead(m_port, (byte) m_deviceAddress, buffer, (byte) count)
-        < 0;
+    return I2CJNI.i2CRead(m_port, (byte) m_deviceAddress, buffer, (byte) count) < 0;
   }
 
   /*
@@ -343,13 +352,12 @@
    * expected value.
    *
    * @param registerAddress The base register to start reading from the device.
-   * @param count           The size of the field to be verified.
-   * @param expected        A buffer containing the values expected from the device.
+   * @param count The size of the field to be verified.
+   * @param expected A buffer containing the values expected from the device.
    * @return true if the sensor was verified to be connected
    * @pre The device must support and be configured to use register auto-increment.
    */
-  public boolean verifySensor(int registerAddress, int count,
-                              byte[] expected) {
+  public boolean verifySensor(int registerAddress, int count, byte[] expected) {
     // TODO: Make use of all 7 read bytes
     byte[] dataToSend = new byte[1];
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptHandlerFunction.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptHandlerFunction.java
deleted file mode 100644
index 6fd807b..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptHandlerFunction.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.InterruptJNI.InterruptJNIHandlerFunction;
-
-
-/**
- * It is recommended that you use this class in conjunction with classes from {@link
- * java.util.concurrent.atomic} as these objects are all thread safe.
- *
- * @param <T> The type of the parameter that should be returned to the the method {@link
- *            #interruptFired(int, Object)}
- */
-public abstract class InterruptHandlerFunction<T> {
-  /**
-   * The entry point for the interrupt. When the interrupt fires the {@link #apply(int, Object)}
-   * method is called. The outer class is provided as an interface to allow the implementer to pass
-   * a generic object to the interrupt fired method.
-   */
-  private class Function implements InterruptJNIHandlerFunction {
-    @SuppressWarnings("unchecked")
-    @Override
-    public void apply(int interruptAssertedMask, Object param) {
-      interruptFired(interruptAssertedMask, (T) param);
-    }
-  }
-
-  final Function m_function = new Function();
-
-  /**
-   * This method is run every time an interrupt is fired.
-   *
-   * @param interruptAssertedMask Interrupt Mask
-   * @param param                 The parameter provided by overriding the {@link
-   *                              #overridableParameter()} method.
-   */
-  public abstract void interruptFired(int interruptAssertedMask, T param);
-
-
-  /**
-   * Override this method if you would like to pass a specific parameter to the {@link
-   * #interruptFired(int, Object)} when it is fired by the interrupt. This method is called once
-   * when {@link InterruptableSensorBase#requestInterrupts(InterruptHandlerFunction)} is run.
-   *
-   * @return The object that should be passed to the interrupt when it runs
-   */
-  public T overridableParameter() {
-    return null;
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptableSensorBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptableSensorBase.java
deleted file mode 100644
index 8fb734b..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/InterruptableSensorBase.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import java.util.function.Consumer;
-
-import edu.wpi.first.hal.InterruptJNI;
-import edu.wpi.first.hal.util.AllocationException;
-
-
-/**
- * Base for sensors to be used with interrupts.
- */
-public abstract class InterruptableSensorBase implements AutoCloseable {
-  @SuppressWarnings("JavadocMethod")
-  public enum WaitResult {
-    kTimeout(0x0), kRisingEdge(0x1), kFallingEdge(0x100), kBoth(0x101);
-
-    @SuppressWarnings("MemberName")
-    public final int value;
-
-    WaitResult(int value) {
-      this.value = value;
-    }
-
-    public static WaitResult getValue(boolean rising, boolean falling) {
-      if (rising && falling) {
-        return kBoth;
-      } else if (rising) {
-        return kRisingEdge;
-      } else if (falling) {
-        return kFallingEdge;
-      } else {
-        return kTimeout;
-      }
-    }
-  }
-
-  /**
-   * The interrupt resource.
-   */
-  protected int m_interrupt = InterruptJNI.HalInvalidHandle;
-
-  /**
-   * Flags if the interrupt being allocated is synchronous.
-   */
-  protected boolean m_isSynchronousInterrupt;
-
-  /**
-   * Create a new InterrupatableSensorBase.
-   */
-  public InterruptableSensorBase() {
-    m_interrupt = 0;
-  }
-
-  @Override
-  public void close() {
-    if (m_interrupt != 0) {
-      cancelInterrupts();
-    }
-  }
-
-  /**
-   * If this is an analog trigger.
-   *
-   * @return true if this is an analog trigger.
-   */
-  public abstract int getAnalogTriggerTypeForRouting();
-
-  /**
-   * The channel routing number.
-   *
-   * @return channel routing number
-   */
-  public abstract int getPortHandleForRouting();
-
-  /**
-   * Request one of the 8 interrupts asynchronously on this digital input.
-   *
-   * @param handler The {@link Consumer} that will be called whenever there is an interrupt on this
-   *                device. Request interrupts in synchronous mode where the user program interrupt
-   *                handler will be called when an interrupt occurs. The default is interrupt on
-   *                rising edges only.
-   */
-  public void requestInterrupts(Consumer<WaitResult> handler) {
-    if (m_interrupt != 0) {
-      throw new AllocationException("The interrupt has already been allocated");
-    }
-
-    allocateInterrupts(false);
-
-    assert m_interrupt != 0;
-
-    InterruptJNI.requestInterrupts(m_interrupt, getPortHandleForRouting(),
-        getAnalogTriggerTypeForRouting());
-    setUpSourceEdge(true, false);
-    InterruptJNI.attachInterruptHandler(m_interrupt, (mask, obj) -> {
-      // Rising edge result is the interrupt bit set in the byte 0xFF
-      // Falling edge result is the interrupt bit set in the byte 0xFF00
-      boolean rising = (mask & 0xFF) != 0;
-      boolean falling = (mask & 0xFF00) != 0;
-      handler.accept(WaitResult.getValue(rising, falling));
-    }, null);
-  }
-
-  /**
-   * Request one of the 8 interrupts asynchronously on this digital input.
-   *
-   * @param handler The {@link InterruptHandlerFunction} that contains the method {@link
-   *                InterruptHandlerFunction#interruptFired(int, Object)} that will be called
-   *                whenever there is an interrupt on this device. Request interrupts in synchronous
-   *                mode where the user program interrupt handler will be called when an interrupt
-   *                occurs. The default is interrupt on rising edges only.
-   */
-  public void requestInterrupts(InterruptHandlerFunction<?> handler) {
-    if (m_interrupt != 0) {
-      throw new AllocationException("The interrupt has already been allocated");
-    }
-
-    allocateInterrupts(false);
-
-    assert m_interrupt != 0;
-
-    InterruptJNI.requestInterrupts(m_interrupt, getPortHandleForRouting(),
-        getAnalogTriggerTypeForRouting());
-    setUpSourceEdge(true, false);
-    InterruptJNI.attachInterruptHandler(m_interrupt, handler.m_function,
-        handler.overridableParameter());
-  }
-
-  /**
-   * Request one of the 8 interrupts synchronously on this digital input. Request interrupts in
-   * synchronous mode where the user program will have to explicitly wait for the interrupt to occur
-   * using {@link #waitForInterrupt}. The default is interrupt on rising edges only.
-   */
-  public void requestInterrupts() {
-    if (m_interrupt != 0) {
-      throw new AllocationException("The interrupt has already been allocated");
-    }
-
-    allocateInterrupts(true);
-
-    assert m_interrupt != 0;
-
-    InterruptJNI.requestInterrupts(m_interrupt, getPortHandleForRouting(),
-        getAnalogTriggerTypeForRouting());
-    setUpSourceEdge(true, false);
-
-  }
-
-  /**
-   * Allocate the interrupt.
-   *
-   * @param watcher true if the interrupt should be in synchronous mode where the user program will
-   *                have to explicitly wait for the interrupt to occur.
-   */
-  protected void allocateInterrupts(boolean watcher) {
-    m_isSynchronousInterrupt = watcher;
-
-    m_interrupt = InterruptJNI.initializeInterrupts(watcher);
-  }
-
-  /**
-   * Cancel interrupts on this device. This deallocates all the chipobject structures and disables
-   * any interrupts.
-   */
-  public void cancelInterrupts() {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    InterruptJNI.cleanInterrupts(m_interrupt);
-    m_interrupt = 0;
-  }
-
-  /**
-   * In synchronous mode, wait for the defined interrupt to occur.
-   *
-   * @param timeout        Timeout in seconds
-   * @param ignorePrevious If true, ignore interrupts that happened before waitForInterrupt was
-   *                       called.
-   * @return Result of the wait.
-   */
-  public WaitResult waitForInterrupt(double timeout, boolean ignorePrevious) {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    int result = InterruptJNI.waitForInterrupt(m_interrupt, timeout, ignorePrevious);
-
-    // Rising edge result is the interrupt bit set in the byte 0xFF
-    // Falling edge result is the interrupt bit set in the byte 0xFF00
-    // Set any bit set to be true for that edge, and AND the 2 results
-    // together to match the existing enum for all interrupts
-    boolean rising = (result & 0xFF) != 0;
-    boolean falling = (result & 0xFF00) != 0;
-    return WaitResult.getValue(rising, falling);
-  }
-
-  /**
-   * In synchronous mode, wait for the defined interrupt to occur.
-   *
-   * @param timeout Timeout in seconds
-   * @return Result of the wait.
-   */
-  public WaitResult waitForInterrupt(double timeout) {
-    return waitForInterrupt(timeout, true);
-  }
-
-  /**
-   * Enable interrupts to occur on this input. Interrupts are disabled when the RequestInterrupt
-   * call is made. This gives time to do the setup of the other options before starting to field
-   * interrupts.
-   */
-  public void enableInterrupts() {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    if (m_isSynchronousInterrupt) {
-      throw new IllegalStateException("You do not need to enable synchronous interrupts");
-    }
-    InterruptJNI.enableInterrupts(m_interrupt);
-  }
-
-  /**
-   * Disable Interrupts without without deallocating structures.
-   */
-  public void disableInterrupts() {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    if (m_isSynchronousInterrupt) {
-      throw new IllegalStateException("You can not disable synchronous interrupts");
-    }
-    InterruptJNI.disableInterrupts(m_interrupt);
-  }
-
-  /**
-   * Return the timestamp for the rising interrupt that occurred most recently. This is in the same
-   * time domain as getClock(). The rising-edge interrupt should be enabled with {@link
-   * #setUpSourceEdge}.
-   *
-   * @return Timestamp in seconds since boot.
-   */
-  public double readRisingTimestamp() {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    return InterruptJNI.readInterruptRisingTimestamp(m_interrupt) * 1e-6;
-  }
-
-  /**
-   * Return the timestamp for the falling interrupt that occurred most recently. This is in the same
-   * time domain as getClock(). The falling-edge interrupt should be enabled with {@link
-   * #setUpSourceEdge}.
-   *
-   * @return Timestamp in seconds since boot.
-   */
-  public double readFallingTimestamp() {
-    if (m_interrupt == 0) {
-      throw new IllegalStateException("The interrupt is not allocated.");
-    }
-    return InterruptJNI.readInterruptFallingTimestamp(m_interrupt) * 1e-6;
-  }
-
-  /**
-   * Set which edge to trigger interrupts on.
-   *
-   * @param risingEdge  true to interrupt on rising edge
-   * @param fallingEdge true to interrupt on falling edge
-   */
-  public void setUpSourceEdge(boolean risingEdge, boolean fallingEdge) {
-    if (m_interrupt != 0) {
-      InterruptJNI.setInterruptUpSourceEdge(m_interrupt, risingEdge,
-          fallingEdge);
-    } else {
-      throw new IllegalArgumentException("You must call RequestInterrupts before setUpSourceEdge");
-    }
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobot.java
deleted file mode 100644
index 66079a8..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobot.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tInstances;
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-
-/**
- * IterativeRobot implements the IterativeRobotBase robot program framework.
- *
- * <p>The IterativeRobot class is intended to be subclassed by a user creating a robot program.
- *
- * <p>periodic() functions from the base class are called each time a new packet is received from
- * the driver station.
- *
- * @deprecated Use TimedRobot instead. It's a drop-in replacement that provides more regular
- *     execution periods.
- */
-@Deprecated
-public class IterativeRobot extends IterativeRobotBase {
-  private static final double kPacketPeriod = 0.02;
-  private volatile boolean m_exit;
-
-  /**
-   * Create a new IterativeRobot.
-   */
-  public IterativeRobot() {
-    super(kPacketPeriod);
-
-    HAL.report(tResourceType.kResourceType_Framework, tInstances.kFramework_Iterative);
-  }
-
-  /**
-   * Provide an alternate "main loop" via startCompetition().
-   */
-  @Override
-  public void startCompetition() {
-    robotInit();
-
-    if (isSimulation()) {
-      simulationInit();
-    }
-
-    // Tell the DS that the robot is ready to be enabled
-    HAL.observeUserProgramStarting();
-
-    // Loop forever, calling the appropriate mode-dependent function
-    while (!Thread.currentThread().isInterrupted()) {
-      // Wait for new data to arrive
-      m_ds.waitForData();
-      if (m_exit) {
-        break;
-      }
-
-      loopFunc();
-    }
-  }
-
-  /**
-   * Ends the main loop in startCompetition().
-   */
-  @Override
-  public void endCompetition() {
-    m_exit = true;
-    m_ds.wakeupWaitForData();
-  }
-}
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 daa5bdb..a8c92a6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java
@@ -1,13 +1,11 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.HAL;
+import edu.wpi.first.networktables.NetworkTableInstance;
 import edu.wpi.first.wpilibj.livewindow.LiveWindow;
 import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard;
 import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
@@ -24,27 +22,37 @@
  *
  * <p>robotInit() -- provide for initialization at robot power-on
  *
- * <p>init() functions -- each of the following functions is called once when the
- * appropriate mode is entered:
- *   - disabledInit()   -- called each and every time disabled is entered from
- *                         another mode
- *   - autonomousInit() -- called each and every time autonomous is entered from
- *                         another mode
- *   - teleopInit()     -- called each and every time teleop is entered from
- *                         another mode
- *   - testInit()       -- called each and every time test is entered from
- *                         another mode
+ * <p>init() functions -- each of the following functions is called once when the appropriate mode
+ * is entered:
+ *
+ * <ul>
+ *   <li>disabledInit() -- called each and every time disabled is entered from another mode
+ *   <li>autonomousInit() -- called each and every time autonomous is entered from another mode
+ *   <li>teleopInit() -- called each and every time teleop is entered from another mode
+ *   <li>testInit() -- called each and every time test is entered from another mode
+ * </ul>
  *
  * <p>periodic() functions -- each of these functions is called on an interval:
- *   - robotPeriodic()
- *   - disabledPeriodic()
- *   - autonomousPeriodic()
- *   - teleopPeriodic()
- *   - testPeriodic()
+ *
+ * <ul>
+ *   <li>robotPeriodic()
+ *   <li>disabledPeriodic()
+ *   <li>autonomousPeriodic()
+ *   <li>teleopPeriodic()
+ *   <li>testPeriodic()
+ * </ul>
+ *
+ * <p>final() functions -- each of the following functions is called once when the appropriate mode
+ * is exited:
+ *
+ * <ul>
+ *   <li>disabledExit() -- called each and every time disabled is exited
+ *   <li>autonomousExit() -- called each and every time autonomous is exited
+ *   <li>teleopExit() -- called each and every time teleop is exited
+ *   <li>testExit() -- called each and every time test is exited
+ * </ul>
  */
 public abstract class IterativeRobotBase extends RobotBase {
-  protected double m_period;
-
   private enum Mode {
     kNone,
     kDisabled,
@@ -53,8 +61,11 @@
     kTest
   }
 
+  private final DSControlWord m_word = new DSControlWord();
   private Mode m_lastMode = Mode.kNone;
+  private final double m_period;
   private final Watchdog m_watchdog;
+  private boolean m_ntFlushEnabled;
 
   /**
    * Constructor for IterativeRobotBase.
@@ -66,9 +77,7 @@
     m_watchdog = new Watchdog(period, this::printLoopOverrunMessage);
   }
 
-  /**
-   * Provide an alternate "main loop" via startCompetition().
-   */
+  /** Provide an alternate "main loop" via startCompetition(). */
   @Override
   public abstract void startCompetition();
 
@@ -84,21 +93,16 @@
    * until RobotInit() exits. Code in RobotInit() that waits for enable will cause the robot to
    * never indicate that the code is ready, causing the robot to be bypassed in a match.
    */
-  public void robotInit() {
-    System.out.println("Default robotInit() method... Override me!");
-  }
+  public void robotInit() {}
 
   /**
    * Robot-wide simulation initialization code should go here.
    *
-   * <p>Users should override this method for default Robot-wide simulation
-   * related initialization which will be called when the robot is first
-   * started. It will be called exactly one time after RobotInit is called
-   * only when the robot is in simulation.
+   * <p>Users should override this method for default Robot-wide simulation related initialization
+   * which will be called when the robot is first started. It will be called exactly one time after
+   * RobotInit is called only when the robot is in simulation.
    */
-  public void simulationInit() {
-    System.out.println("Default simulationInit() method... Override me!");
-  }
+  public void simulationInit() {}
 
   /**
    * Initialization code for disabled mode should go here.
@@ -106,9 +110,7 @@
    * <p>Users should override this method for initialization code which will be called each time the
    * robot enters disabled mode.
    */
-  public void disabledInit() {
-    System.out.println("Default disabledInit() method... Override me!");
-  }
+  public void disabledInit() {}
 
   /**
    * Initialization code for autonomous mode should go here.
@@ -116,9 +118,7 @@
    * <p>Users should override this method for initialization code which will be called each time the
    * robot enters autonomous mode.
    */
-  public void autonomousInit() {
-    System.out.println("Default autonomousInit() method... Override me!");
-  }
+  public void autonomousInit() {}
 
   /**
    * Initialization code for teleop mode should go here.
@@ -126,9 +126,7 @@
    * <p>Users should override this method for initialization code which will be called each time the
    * robot enters teleop mode.
    */
-  public void teleopInit() {
-    System.out.println("Default teleopInit() method... Override me!");
-  }
+  public void teleopInit() {}
 
   /**
    * Initialization code for test mode should go here.
@@ -137,17 +135,13 @@
    * robot enters test mode.
    */
   @SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
-  public void testInit() {
-    System.out.println("Default testInit() method... Override me!");
-  }
+  public void testInit() {}
 
   /* ----------- Overridable periodic code ----------------- */
 
   private boolean m_rpFirstRun = true;
 
-  /**
-   * Periodic code for all robot modes should go here.
-   */
+  /** Periodic code for all robot modes should go here. */
   public void robotPeriodic() {
     if (m_rpFirstRun) {
       System.out.println("Default robotPeriodic() method... Override me!");
@@ -171,9 +165,7 @@
 
   private boolean m_dpFirstRun = true;
 
-  /**
-   * Periodic code for disabled mode should go here.
-   */
+  /** Periodic code for disabled mode should go here. */
   public void disabledPeriodic() {
     if (m_dpFirstRun) {
       System.out.println("Default disabledPeriodic() method... Override me!");
@@ -183,9 +175,7 @@
 
   private boolean m_apFirstRun = true;
 
-  /**
-   * Periodic code for autonomous mode should go here.
-   */
+  /** Periodic code for autonomous mode should go here. */
   public void autonomousPeriodic() {
     if (m_apFirstRun) {
       System.out.println("Default autonomousPeriodic() method... Override me!");
@@ -195,9 +185,7 @@
 
   private boolean m_tpFirstRun = true;
 
-  /**
-   * Periodic code for teleop mode should go here.
-   */
+  /** Periodic code for teleop mode should go here. */
   public void teleopPeriodic() {
     if (m_tpFirstRun) {
       System.out.println("Default teleopPeriodic() method... Override me!");
@@ -207,9 +195,7 @@
 
   private boolean m_tmpFirstRun = true;
 
-  /**
-   * Periodic code for test mode should go here.
-   */
+  /** Periodic code for test mode should go here. */
   @SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
   public void testPeriodic() {
     if (m_tmpFirstRun) {
@@ -218,64 +204,122 @@
     }
   }
 
-  @SuppressWarnings("PMD.CyclomaticComplexity")
+  /**
+   * Exit code for disabled mode should go here.
+   *
+   * <p>Users should override this method for code which will be called each time the robot exits
+   * disabled mode.
+   */
+  public void disabledExit() {}
+
+  /**
+   * Exit code for autonomous mode should go here.
+   *
+   * <p>Users should override this method for code which will be called each time the robot exits
+   * autonomous mode.
+   */
+  public void autonomousExit() {}
+
+  /**
+   * Exit code for teleop mode should go here.
+   *
+   * <p>Users should override this method for code which will be called each time the robot exits
+   * teleop mode.
+   */
+  public void teleopExit() {}
+
+  /**
+   * Exit code for test mode should go here.
+   *
+   * <p>Users should override this method for code which will be called each time the robot exits
+   * test mode.
+   */
+  @SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
+  public void testExit() {}
+
+  /**
+   * Enables or disables flushing NetworkTables every loop iteration. By default, this is disabled.
+   *
+   * @param enabled True to enable, false to disable
+   */
+  public void setNetworkTablesFlushEnabled(boolean enabled) {
+    m_ntFlushEnabled = enabled;
+  }
+
+  /**
+   * Gets time period between calls to Periodic() functions.
+   *
+   * @return The time period between calls to Periodic() functions.
+   */
+  public double getPeriod() {
+    return m_period;
+  }
+
   protected void loopFunc() {
     m_watchdog.reset();
 
-    // Call the appropriate function depending upon the current robot mode
-    if (isDisabled()) {
-      // Call DisabledInit() if we are now just entering disabled mode from either a different mode
-      // or from power-on.
-      if (m_lastMode != Mode.kDisabled) {
+    // Get current mode
+    m_word.update();
+    Mode mode = Mode.kNone;
+    if (m_word.isDisabled()) {
+      mode = Mode.kDisabled;
+    } else if (m_word.isAutonomous()) {
+      mode = Mode.kAutonomous;
+    } else if (m_word.isTeleop()) {
+      mode = Mode.kTeleop;
+    } else if (m_word.isTest()) {
+      mode = Mode.kTest;
+    }
+
+    // If mode changed, call mode exit and entry functions
+    if (m_lastMode != mode) {
+      // Call last mode's exit function
+      if (m_lastMode == Mode.kDisabled) {
+        disabledExit();
+      } else if (m_lastMode == Mode.kAutonomous) {
+        autonomousExit();
+      } else if (m_lastMode == Mode.kTeleop) {
+        teleopExit();
+      } else if (m_lastMode == Mode.kTest) {
         LiveWindow.setEnabled(false);
         Shuffleboard.disableActuatorWidgets();
+        testExit();
+      }
+
+      // Call current mode's entry function
+      if (mode == Mode.kDisabled) {
         disabledInit();
         m_watchdog.addEpoch("disabledInit()");
-        m_lastMode = Mode.kDisabled;
-      }
-
-      HAL.observeUserProgramDisabled();
-      disabledPeriodic();
-      m_watchdog.addEpoch("disablePeriodic()");
-    } else if (isAutonomous()) {
-      // Call AutonomousInit() if we are now just entering autonomous mode from either a different
-      // mode or from power-on.
-      if (m_lastMode != Mode.kAutonomous) {
-        LiveWindow.setEnabled(false);
-        Shuffleboard.disableActuatorWidgets();
+      } else if (mode == Mode.kAutonomous) {
         autonomousInit();
         m_watchdog.addEpoch("autonomousInit()");
-        m_lastMode = Mode.kAutonomous;
-      }
-
-      HAL.observeUserProgramAutonomous();
-      autonomousPeriodic();
-      m_watchdog.addEpoch("autonomousPeriodic()");
-    } else if (isOperatorControl()) {
-      // Call TeleopInit() if we are now just entering teleop mode from either a different mode or
-      // from power-on.
-      if (m_lastMode != Mode.kTeleop) {
-        LiveWindow.setEnabled(false);
-        Shuffleboard.disableActuatorWidgets();
+      } else if (mode == Mode.kTeleop) {
         teleopInit();
         m_watchdog.addEpoch("teleopInit()");
-        m_lastMode = Mode.kTeleop;
-      }
-
-      HAL.observeUserProgramTeleop();
-      teleopPeriodic();
-      m_watchdog.addEpoch("teleopPeriodic()");
-    } else {
-      // Call TestInit() if we are now just entering test mode from either a different mode or from
-      // power-on.
-      if (m_lastMode != Mode.kTest) {
+      } else if (mode == Mode.kTest) {
         LiveWindow.setEnabled(true);
         Shuffleboard.enableActuatorWidgets();
         testInit();
         m_watchdog.addEpoch("testInit()");
-        m_lastMode = Mode.kTest;
       }
 
+      m_lastMode = mode;
+    }
+
+    // Call the appropriate function depending upon the current robot mode
+    if (mode == Mode.kDisabled) {
+      HAL.observeUserProgramDisabled();
+      disabledPeriodic();
+      m_watchdog.addEpoch("disabledPeriodic()");
+    } else if (mode == Mode.kAutonomous) {
+      HAL.observeUserProgramAutonomous();
+      autonomousPeriodic();
+      m_watchdog.addEpoch("autonomousPeriodic()");
+    } else if (mode == Mode.kTeleop) {
+      HAL.observeUserProgramTeleop();
+      teleopPeriodic();
+      m_watchdog.addEpoch("teleopPeriodic()");
+    } else {
       HAL.observeUserProgramTest();
       testPeriodic();
       m_watchdog.addEpoch("testPeriodic()");
@@ -292,12 +336,19 @@
     m_watchdog.addEpoch("Shuffleboard.update()");
 
     if (isSimulation()) {
+      HAL.simPeriodicBefore();
       simulationPeriodic();
+      HAL.simPeriodicAfter();
       m_watchdog.addEpoch("simulationPeriodic()");
     }
 
     m_watchdog.disable();
 
+    // Flush NetworkTables
+    if (m_ntFlushEnabled) {
+      NetworkTableInstance.getDefault().flush();
+    }
+
     // Warn on loop time overruns
     if (m_watchdog.isExpired()) {
       m_watchdog.printEpochs();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Jaguar.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Jaguar.java
deleted file mode 100644
index 8ce7b7d..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Jaguar.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Texas Instruments / Vex Robotics Jaguar Speed Controller as a PWM device.
- *
- * <p>Note that the Jaguar 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 Jaguar User Manual
- * available from Vex.
- *
- * <p><ul>
- * <li>2.310ms = full "forward"
- * <li>1.550ms = the "high end" of the deadband range
- * <li>1.507ms = center of the deadband range (off)
- * <li>1.454ms = the "low end" of the deadband range
- * <li>0.697ms = full "reverse"
- * </ul>
- */
-public class Jaguar extends PWMSpeedController {
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the Jaguar is attached to. 0-9 are on-board, 10-19 are on
-   *                the MXP port
-   */
-  public Jaguar(final int channel) {
-    super(channel);
-
-    setBounds(2.31, 1.55, 1.507, 1.454, 0.697);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_Jaguar, getChannel() + 1);
-    SendableRegistry.setName(this, "Jaguar", getChannel());
-  }
-}
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 a83a346..e9b09a9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Joystick.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,7 +8,7 @@
 import edu.wpi.first.hal.HAL;
 
 /**
- * Handle input from standard Joysticks connected to the Driver Station.
+ * Handle input from Flight Joysticks connected to the Driver Station.
  *
  * <p>This class handles standard 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 joystick
@@ -24,13 +21,14 @@
   public static final byte kDefaultTwistChannel = 2;
   public static final byte kDefaultThrottleChannel = 3;
 
-  /**
-   * Represents an analog axis on a joystick.
-   */
+  /** Represents an analog axis on a joystick. */
   public enum AxisType {
-    kX(0), kY(1), kZ(2), kTwist(3), kThrottle(4);
+    kX(0),
+    kY(1),
+    kZ(2),
+    kTwist(3),
+    kThrottle(4);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     AxisType(int value) {
@@ -38,13 +36,11 @@
     }
   }
 
-  /**
-   * Represents a digital button on a joystick.
-   */
+  /** Represents a digital button on a joystick. */
   public enum ButtonType {
-    kTrigger(1), kTop(2);
+    kTrigger(1),
+    kTop(2);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     ButtonType(int value) {
@@ -52,50 +48,21 @@
     }
   }
 
-  /**
-   * Represents a digital button on a joystick.
-   */
-  private enum Button {
-    kTrigger(1), kTop(2);
-
-    @SuppressWarnings("MemberName")
-    public final int value;
-
-    Button(int value) {
-      this.value = value;
-    }
-  }
+  private final byte[] m_axes = new byte[AxisType.values().length];
 
   /**
-   * Represents an analog axis on a joystick.
-   */
-  private enum Axis {
-    kX(0), kY(1), kZ(2), kTwist(3), kThrottle(4), kNumAxes(5);
-
-    @SuppressWarnings("MemberName")
-    public final int value;
-
-    Axis(int value) {
-      this.value = value;
-    }
-  }
-
-  private final byte[] m_axes = new byte[Axis.kNumAxes.value];
-
-  /**
-   * Construct an instance of a joystick. The joystick index is the USB port on the drivers
-   * station.
+   * Construct an instance of a joystick.
    *
-   * @param port The port on the Driver Station that the joystick is plugged into.
+   * @param port The port index on the Driver Station that the joystick is plugged into.
    */
   public Joystick(final int port) {
     super(port);
 
-    m_axes[Axis.kX.value] = kDefaultXChannel;
-    m_axes[Axis.kY.value] = kDefaultYChannel;
-    m_axes[Axis.kZ.value] = kDefaultZChannel;
-    m_axes[Axis.kTwist.value] = kDefaultTwistChannel;
-    m_axes[Axis.kThrottle.value] = kDefaultThrottleChannel;
+    m_axes[AxisType.kX.value] = kDefaultXChannel;
+    m_axes[AxisType.kY.value] = kDefaultYChannel;
+    m_axes[AxisType.kZ.value] = kDefaultZChannel;
+    m_axes[AxisType.kTwist.value] = kDefaultTwistChannel;
+    m_axes[AxisType.kThrottle.value] = kDefaultThrottleChannel;
 
     HAL.report(tResourceType.kResourceType_Joystick, port + 1);
   }
@@ -106,7 +73,7 @@
    * @param channel The channel to set the axis to.
    */
   public void setXChannel(int channel) {
-    m_axes[Axis.kX.value] = (byte) channel;
+    m_axes[AxisType.kX.value] = (byte) channel;
   }
 
   /**
@@ -115,7 +82,7 @@
    * @param channel The channel to set the axis to.
    */
   public void setYChannel(int channel) {
-    m_axes[Axis.kY.value] = (byte) channel;
+    m_axes[AxisType.kY.value] = (byte) channel;
   }
 
   /**
@@ -124,7 +91,7 @@
    * @param channel The channel to set the axis to.
    */
   public void setZChannel(int channel) {
-    m_axes[Axis.kZ.value] = (byte) channel;
+    m_axes[AxisType.kZ.value] = (byte) channel;
   }
 
   /**
@@ -133,7 +100,7 @@
    * @param channel The channel to set the axis to.
    */
   public void setThrottleChannel(int channel) {
-    m_axes[Axis.kThrottle.value] = (byte) channel;
+    m_axes[AxisType.kThrottle.value] = (byte) channel;
   }
 
   /**
@@ -142,7 +109,7 @@
    * @param channel The channel to set the axis to.
    */
   public void setTwistChannel(int channel) {
-    m_axes[Axis.kTwist.value] = (byte) channel;
+    m_axes[AxisType.kTwist.value] = (byte) channel;
   }
 
   /**
@@ -151,7 +118,7 @@
    * @return The channel for the axis.
    */
   public int getXChannel() {
-    return m_axes[Axis.kX.value];
+    return m_axes[AxisType.kX.value];
   }
 
   /**
@@ -160,7 +127,7 @@
    * @return The channel for the axis.
    */
   public int getYChannel() {
-    return m_axes[Axis.kY.value];
+    return m_axes[AxisType.kY.value];
   }
 
   /**
@@ -169,7 +136,7 @@
    * @return The channel for the axis.
    */
   public int getZChannel() {
-    return m_axes[Axis.kZ.value];
+    return m_axes[AxisType.kZ.value];
   }
 
   /**
@@ -178,7 +145,7 @@
    * @return The channel for the axis.
    */
   public int getTwistChannel() {
-    return m_axes[Axis.kTwist.value];
+    return m_axes[AxisType.kTwist.value];
   }
 
   /**
@@ -187,31 +154,27 @@
    * @return The channel for the axis.
    */
   public int getThrottleChannel() {
-    return m_axes[Axis.kThrottle.value];
+    return m_axes[AxisType.kThrottle.value];
   }
 
   /**
    * Get the X value of the joystick. This depends on the mapping of the joystick connected to the
    * current port.
    *
-   * @param hand Unused
    * @return The X value of the joystick.
    */
-  @Override
-  public final double getX(Hand hand) {
-    return getRawAxis(m_axes[Axis.kX.value]);
+  public final double getX() {
+    return getRawAxis(m_axes[AxisType.kX.value]);
   }
 
   /**
    * Get the Y value of the joystick. This depends on the mapping of the joystick connected to the
    * current port.
    *
-   * @param hand Unused
    * @return The Y value of the joystick.
    */
-  @Override
-  public final double getY(Hand hand) {
-    return getRawAxis(m_axes[Axis.kY.value]);
+  public final double getY() {
+    return getRawAxis(m_axes[AxisType.kY.value]);
   }
 
   /**
@@ -220,7 +183,7 @@
    * @return the z position
    */
   public double getZ() {
-    return getRawAxis(m_axes[Axis.kZ.value]);
+    return getRawAxis(m_axes[AxisType.kZ.value]);
   }
 
   /**
@@ -230,7 +193,7 @@
    * @return The Twist value of the joystick.
    */
   public double getTwist() {
-    return getRawAxis(m_axes[Axis.kTwist.value]);
+    return getRawAxis(m_axes[AxisType.kTwist.value]);
   }
 
   /**
@@ -240,7 +203,7 @@
    * @return The Throttle value of the joystick.
    */
   public double getThrottle() {
-    return getRawAxis(m_axes[Axis.kThrottle.value]);
+    return getRawAxis(m_axes[AxisType.kThrottle.value]);
   }
 
   /**
@@ -249,7 +212,7 @@
    * @return The state of the trigger.
    */
   public boolean getTrigger() {
-    return getRawButton(Button.kTrigger.value);
+    return getRawButton(ButtonType.kTrigger.value);
   }
 
   /**
@@ -258,7 +221,7 @@
    * @return Whether the button was pressed since the last check.
    */
   public boolean getTriggerPressed() {
-    return getRawButtonPressed(Button.kTrigger.value);
+    return getRawButtonPressed(ButtonType.kTrigger.value);
   }
 
   /**
@@ -267,7 +230,7 @@
    * @return Whether the button was released since the last check.
    */
   public boolean getTriggerReleased() {
-    return getRawButtonReleased(Button.kTrigger.value);
+    return getRawButtonReleased(ButtonType.kTrigger.value);
   }
 
   /**
@@ -276,7 +239,7 @@
    * @return The state of the top button.
    */
   public boolean getTop() {
-    return getRawButton(Button.kTop.value);
+    return getRawButton(ButtonType.kTop.value);
   }
 
   /**
@@ -285,7 +248,7 @@
    * @return Whether the button was pressed since the last check.
    */
   public boolean getTopPressed() {
-    return getRawButtonPressed(Button.kTop.value);
+    return getRawButtonPressed(ButtonType.kTop.value);
   }
 
   /**
@@ -294,7 +257,7 @@
    * @return Whether the button was released since the last check.
    */
   public boolean getTopReleased() {
-    return getRawButtonReleased(Button.kTop.value);
+    return getRawButtonReleased(ButtonType.kTop.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 5dd3a4f..4e76936 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/MotorSafety.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,8 +8,8 @@
 import java.util.Set;
 
 /**
- * This base class runs a watchdog timer and calls the subclass's StopMotor()
- * function if the timeout expires.
+ * This base class runs a watchdog timer and calls the subclass's StopMotor() function if the
+ * timeout expires.
  *
  * <p>The subclass should call feed() whenever the motor value is updated.
  */
@@ -26,9 +23,7 @@
   private static final Set<MotorSafety> m_instanceList = new LinkedHashSet<>();
   private static final Object m_listMutex = new Object();
 
-  /**
-   * MotorSafety constructor.
-   */
+  /** MotorSafety constructor. */
   public MotorSafety() {
     synchronized (m_listMutex) {
       m_instanceList.add(this);
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 546da5b..40384ba 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Notifier.java
@@ -1,18 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.NotifierJNI;
-
-import static java.util.Objects.requireNonNull;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
 
 public class Notifier implements AutoCloseable {
   // The thread waiting on the HAL alarm.
@@ -22,9 +18,9 @@
   // The C pointer to the notifier object. We don't use it directly, it is
   // just passed to the JNI bindings.
   private final AtomicInteger m_notifier = new AtomicInteger();
-  // The time, in microseconds, at which the corresponding handler should be
-  // called. Has the same zero as Utility.getFPGATime().
-  private double m_expirationTime;
+  // The time, in seconds, at which the corresponding handler should be
+  // called. Has the same zero as RobotController.getFPGATime().
+  private double m_expirationTimeSeconds;
   // The handler passed in by the user which should be called at the
   // appropriate interval.
   private Runnable m_handler;
@@ -32,7 +28,7 @@
   private boolean m_periodic;
   // If periodic, the period of the calling; if just once, stores how long it
   // is until we call the handler.
-  private double m_period;
+  private double m_periodSeconds;
 
   @Override
   @SuppressWarnings("NoFinalizer")
@@ -63,28 +59,26 @@
   /**
    * Update the alarm hardware to reflect the next alarm.
    *
-   * @param triggerTime the time at which the next alarm will be triggered
+   * @param triggerTimeMicroS the time in microseconds at which the next alarm will be triggered
    */
-  private void updateAlarm(long triggerTime) {
+  private void updateAlarm(long triggerTimeMicroS) {
     int notifier = m_notifier.get();
     if (notifier == 0) {
       return;
     }
-    NotifierJNI.updateNotifierAlarm(notifier, triggerTime);
+    NotifierJNI.updateNotifierAlarm(notifier, triggerTimeMicroS);
   }
 
-  /**
-   * Update the alarm hardware to reflect the next alarm.
-   */
+  /** Update the alarm hardware to reflect the next alarm. */
   private void updateAlarm() {
-    updateAlarm((long) (m_expirationTime * 1e6));
+    updateAlarm((long) (m_expirationTimeSeconds * 1e6));
   }
 
   /**
    * Create a Notifier for timer event notification.
    *
-   * @param run The handler that is called at the notification time which is set
-   *            using StartSingle or StartPeriodic.
+   * @param run The handler that is called at the notification time which is set using StartSingle
+   *     or StartPeriodic.
    */
   public Notifier(Runnable run) {
     requireNonNull(run);
@@ -92,54 +86,59 @@
     m_handler = run;
     m_notifier.set(NotifierJNI.initializeNotifier());
 
-    m_thread = new Thread(() -> {
-      while (!Thread.interrupted()) {
-        int notifier = m_notifier.get();
-        if (notifier == 0) {
-          break;
-        }
-        long curTime = NotifierJNI.waitForNotifierAlarm(notifier);
-        if (curTime == 0) {
-          break;
-        }
+    m_thread =
+        new Thread(
+            () -> {
+              while (!Thread.interrupted()) {
+                int notifier = m_notifier.get();
+                if (notifier == 0) {
+                  break;
+                }
+                long curTime = NotifierJNI.waitForNotifierAlarm(notifier);
+                if (curTime == 0) {
+                  break;
+                }
 
-        Runnable handler = null;
-        m_processLock.lock();
-        try {
-          handler = m_handler;
-          if (m_periodic) {
-            m_expirationTime += m_period;
-            updateAlarm();
-          } else {
-            // need to update the alarm to cause it to wait again
-            updateAlarm((long) -1);
-          }
-        } finally {
-          m_processLock.unlock();
-        }
+                Runnable handler = null;
+                m_processLock.lock();
+                try {
+                  handler = m_handler;
+                  if (m_periodic) {
+                    m_expirationTimeSeconds += m_periodSeconds;
+                    updateAlarm();
+                  } else {
+                    // need to update the alarm to cause it to wait again
+                    updateAlarm((long) -1);
+                  }
+                } finally {
+                  m_processLock.unlock();
+                }
 
-        if (handler != null) {
-          handler.run();
-        }
-      }
-    });
+                if (handler != null) {
+                  handler.run();
+                }
+              }
+            });
     m_thread.setName("Notifier");
     m_thread.setDaemon(true);
-    m_thread.setUncaughtExceptionHandler((thread, error) -> {
-      Throwable cause = error.getCause();
-      if (cause != null) {
-        error = cause;
-      }
-      DriverStation.reportError("Unhandled exception: " + error.toString(), error.getStackTrace());
-      DriverStation.reportError(
-          "The loopFunc() method (or methods called by it) should have handled "
-              + "the exception above.", false);
-    });
+    m_thread.setUncaughtExceptionHandler(
+        (thread, error) -> {
+          Throwable cause = error.getCause();
+          if (cause != null) {
+            error = cause;
+          }
+          DriverStation.reportError(
+              "Unhandled exception: " + error.toString(), error.getStackTrace());
+          DriverStation.reportError(
+              "The loopFunc() method (or methods called by it) should have handled "
+                  + "the exception above.",
+              false);
+        });
     m_thread.start();
   }
 
   /**
-   * Sets the name of the notifier.  Used for debugging purposes only.
+   * Sets the name of the notifier. Used for debugging purposes only.
    *
    * @param name Name
    */
@@ -163,17 +162,17 @@
   }
 
   /**
-   * Register for single event notification. A timer event is queued for a single
-   * event after the specified delay.
+   * Register for single event notification. A timer event is queued for a single event after the
+   * specified delay.
    *
-   * @param delay Seconds to wait before the handler is called.
+   * @param delaySeconds Seconds to wait before the handler is called.
    */
-  public void startSingle(double delay) {
+  public void startSingle(double delaySeconds) {
     m_processLock.lock();
     try {
       m_periodic = false;
-      m_period = delay;
-      m_expirationTime = RobotController.getFPGATime() * 1e-6 + delay;
+      m_periodSeconds = delaySeconds;
+      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + delaySeconds;
       updateAlarm();
     } finally {
       m_processLock.unlock();
@@ -181,19 +180,19 @@
   }
 
   /**
-   * Register for periodic event notification. A timer event is queued for
-   * periodic event notification. Each time the interrupt occurs, the event will
-   * be immediately requeued for the same time interval.
+   * Register for periodic event notification. A timer event is queued for periodic event
+   * notification. Each time the interrupt occurs, the event will be immediately requeued for the
+   * same time interval.
    *
-   * @param period Period in seconds to call the handler starting one period after
-   *               the call to this method.
+   * @param periodSeconds Period in seconds to call the handler starting one period after the call
+   *     to this method.
    */
-  public void startPeriodic(double period) {
+  public void startPeriodic(double periodSeconds) {
     m_processLock.lock();
     try {
       m_periodic = true;
-      m_period = period;
-      m_expirationTime = RobotController.getFPGATime() * 1e-6 + period;
+      m_periodSeconds = periodSeconds;
+      m_expirationTimeSeconds = RobotController.getFPGATime() * 1e-6 + periodSeconds;
       updateAlarm();
     } finally {
       m_processLock.unlock();
@@ -201,10 +200,9 @@
   }
 
   /**
-   * Stop timer events from occurring. Stop any repeating timer events from
-   * occurring. This will also remove any single notification events from the
-   * queue. If a timer-based call to the registered handler is in progress, this
-   * function will block until the handler call is complete.
+   * Stop timer events from occurring. Stop any repeating timer events from occurring. This will
+   * also remove any single notification events from the queue. If a timer-based call to the
+   * registered handler is in progress, this function will block until the handler call is complete.
    */
   public void stop() {
     m_processLock.lock();
@@ -215,4 +213,21 @@
       m_processLock.unlock();
     }
   }
+
+  /**
+   * Sets the HAL notifier thread priority.
+   *
+   * <p>The HAL notifier thread is responsible for managing the FPGA's notifier interrupt and waking
+   * up user's Notifiers when it's their time to run. Giving the HAL notifier thread real-time
+   * priority helps ensure the user's real-time Notifiers, if any, are notified to run in a timely
+   * manner.
+   *
+   * @param realTime Set to true to set a real-time priority, false for standard priority.
+   * @param priority Priority to set the thread to. For real-time, this is 1-99 with 99 being
+   *     highest. For non-real-time, this is forced to 0. See "man 7 sched" for more details.
+   * @return True on success.
+   */
+  public static boolean setHALThreadPriority(boolean realTime, int priority) {
+    return NotifierJNI.setHALThreadPriority(realTime, priority);
+  }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDBase.java
deleted file mode 100644
index 40eb42f..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDBase.java
+++ /dev/null
@@ -1,829 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import java.util.concurrent.locks.ReentrantLock;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.hal.util.BoundaryException;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-
-/**
- * Class implements a PID Control Loop.
- *
- * <p>Creates a separate thread which reads the given PIDSource and takes care of the integral
- * calculations, as well as writing the given PIDOutput.
- *
- * <p>This feedback controller runs in discrete time, so time deltas are not used in the integral
- * and derivative calculations. Therefore, the sample rate affects the controller's behavior for a
- * given set of PID constants.
- *
- * @deprecated All APIs which use this have been deprecated.
- */
-@Deprecated(since = "2020", forRemoval = true)
-@SuppressWarnings("PMD.TooManyFields")
-public class PIDBase implements PIDInterface, PIDOutput, Sendable, AutoCloseable {
-  public static final double kDefaultPeriod = 0.05;
-  private static int instances;
-
-  // Factor for "proportional" control
-  @SuppressWarnings("MemberName")
-  private double m_P;
-
-  // Factor for "integral" control
-  @SuppressWarnings("MemberName")
-  private double m_I;
-
-  // Factor for "derivative" control
-  @SuppressWarnings("MemberName")
-  private double m_D;
-
-  // Factor for "feed forward" control
-  @SuppressWarnings("MemberName")
-  private double m_F;
-
-  // |maximum output|
-  private double m_maximumOutput = 1.0;
-
-  // |minimum output|
-  private double m_minimumOutput = -1.0;
-
-  // Maximum input - limit setpoint to this
-  private double m_maximumInput;
-
-  // Minimum input - limit setpoint to this
-  private double m_minimumInput;
-
-  // Input range - difference between maximum and minimum
-  private double m_inputRange;
-
-  // Do the endpoints wrap around? (e.g., absolute encoder)
-  private boolean m_continuous;
-
-  // Is the PID controller enabled
-  protected boolean m_enabled;
-
-  // The prior error (used to compute velocity)
-  private double m_prevError;
-
-  // The sum of the errors for use in the integral calc
-  private double m_totalError;
-
-  // The tolerance object used to check if on target
-  private Tolerance m_tolerance;
-
-  private double m_setpoint;
-  private double m_prevSetpoint;
-  @SuppressWarnings("PMD.UnusedPrivateField")
-  private double m_error;
-  private double m_result;
-
-  private LinearFilter m_filter;
-
-  protected ReentrantLock m_thisMutex = new ReentrantLock();
-
-  // Ensures when disable() is called, pidWrite() won't run if calculate()
-  // is already running at that time.
-  protected ReentrantLock m_pidWriteMutex = new ReentrantLock();
-
-  protected PIDSource m_pidInput;
-  protected PIDOutput m_pidOutput;
-  protected Timer m_setpointTimer;
-
-  /**
-   * Tolerance is the type of tolerance used to specify if the PID controller is on target.
-   *
-   * <p>The various implementations of this class such as PercentageTolerance and AbsoluteTolerance
-   * specify types of tolerance specifications to use.
-   */
-  public interface Tolerance {
-    boolean onTarget();
-  }
-
-  /**
-   * Used internally for when Tolerance hasn't been set.
-   */
-  public static class NullTolerance implements Tolerance {
-    @Override
-    public boolean onTarget() {
-      throw new IllegalStateException("No tolerance value set when calling onTarget().");
-    }
-  }
-
-  public class PercentageTolerance implements Tolerance {
-    private final double m_percentage;
-
-    PercentageTolerance(double value) {
-      m_percentage = value;
-    }
-
-    @Override
-    public boolean onTarget() {
-      return Math.abs(getError()) < m_percentage / 100 * m_inputRange;
-    }
-  }
-
-  public class AbsoluteTolerance implements Tolerance {
-    private final double m_value;
-
-    AbsoluteTolerance(double value) {
-      m_value = value;
-    }
-
-    @Override
-    public boolean onTarget() {
-      return Math.abs(getError()) < m_value;
-    }
-  }
-
-  /**
-   * Allocate a PID object with the given constants for P, I, D, and F.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param Kf     the feed forward term
-   * @param source The PIDSource object that is used to get values
-   * @param output The PIDOutput object that is set to the output percentage
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDBase(double Kp, double Ki, double Kd, double Kf, PIDSource source,
-                 PIDOutput output) {
-    requireNonNullParam(source, "PIDSource", "PIDBase");
-    requireNonNullParam(output, "output", "PIDBase");
-
-    m_setpointTimer = new Timer();
-    m_setpointTimer.start();
-
-    m_P = Kp;
-    m_I = Ki;
-    m_D = Kd;
-    m_F = Kf;
-
-    m_pidInput = source;
-    m_filter = LinearFilter.movingAverage(1);
-
-    m_pidOutput = output;
-
-    instances++;
-    HAL.report(tResourceType.kResourceType_PIDController, instances);
-    m_tolerance = new NullTolerance();
-    SendableRegistry.add(this, "PIDController", instances);
-  }
-
-  /**
-   * Allocate a PID object with the given constants for P, I, and D.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param source the PIDSource object that is used to get values
-   * @param output the PIDOutput object that is set to the output percentage
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDBase(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output) {
-    this(Kp, Ki, Kd, 0.0, source, output);
-  }
-
-  @Override
-  public void close() {
-    SendableRegistry.remove(this);
-  }
-
-  /**
-   * Read the input, calculate the output accordingly, and write to the output. This should only be
-   * called by the PIDTask and is created during initialization.
-   */
-  @SuppressWarnings({"LocalVariableName", "PMD.ExcessiveMethodLength", "PMD.NPathComplexity"})
-  protected void calculate() {
-    if (m_pidInput == null || m_pidOutput == null) {
-      return;
-    }
-
-    boolean enabled;
-
-    m_thisMutex.lock();
-    try {
-      enabled = m_enabled;
-    } finally {
-      m_thisMutex.unlock();
-    }
-
-    if (enabled) {
-      double input;
-
-      // Storage for function inputs
-      PIDSourceType pidSourceType;
-      double P;
-      double I;
-      double D;
-      double feedForward = calculateFeedForward();
-      double minimumOutput;
-      double maximumOutput;
-
-      // Storage for function input-outputs
-      double prevError;
-      double error;
-      double totalError;
-
-      m_thisMutex.lock();
-      try {
-        input = m_filter.calculate(m_pidInput.pidGet());
-
-        pidSourceType = m_pidInput.getPIDSourceType();
-        P = m_P;
-        I = m_I;
-        D = m_D;
-        minimumOutput = m_minimumOutput;
-        maximumOutput = m_maximumOutput;
-
-        prevError = m_prevError;
-        error = getContinuousError(m_setpoint - input);
-        totalError = m_totalError;
-      } finally {
-        m_thisMutex.unlock();
-      }
-
-      // Storage for function outputs
-      double result;
-
-      if (pidSourceType.equals(PIDSourceType.kRate)) {
-        if (P != 0) {
-          totalError = clamp(totalError + error, minimumOutput / P,
-              maximumOutput / P);
-        }
-
-        result = P * totalError + D * error + feedForward;
-      } else {
-        if (I != 0) {
-          totalError = clamp(totalError + error, minimumOutput / I,
-              maximumOutput / I);
-        }
-
-        result = P * error + I * totalError + D * (error - prevError)
-            + feedForward;
-      }
-
-      result = clamp(result, minimumOutput, maximumOutput);
-
-      // Ensures m_enabled check and pidWrite() call occur atomically
-      m_pidWriteMutex.lock();
-      try {
-        m_thisMutex.lock();
-        try {
-          if (m_enabled) {
-            // Don't block other PIDController operations on pidWrite()
-            m_thisMutex.unlock();
-
-            m_pidOutput.pidWrite(result);
-          }
-        } finally {
-          if (m_thisMutex.isHeldByCurrentThread()) {
-            m_thisMutex.unlock();
-          }
-        }
-      } finally {
-        m_pidWriteMutex.unlock();
-      }
-
-      m_thisMutex.lock();
-      try {
-        m_prevError = error;
-        m_error = error;
-        m_totalError = totalError;
-        m_result = result;
-      } finally {
-        m_thisMutex.unlock();
-      }
-    }
-  }
-
-  /**
-   * Calculate the feed forward term.
-   *
-   * <p>Both of the provided feed forward calculations are velocity feed forwards. If a different
-   * feed forward calculation is desired, the user can override this function and provide his or
-   * her own. This function  does no synchronization because the PIDController class only calls it
-   * in synchronized code, so be careful if calling it oneself.
-   *
-   * <p>If a velocity PID controller is being used, the F term should be set to 1 over the maximum
-   * setpoint for the output. If a position PID controller is being used, the F term should be set
-   * to 1 over the maximum speed for the output measured in setpoint units per this controller's
-   * update period (see the default period in this class's constructor).
-   */
-  protected double calculateFeedForward() {
-    if (m_pidInput.getPIDSourceType().equals(PIDSourceType.kRate)) {
-      return m_F * getSetpoint();
-    } else {
-      double temp = m_F * getDeltaSetpoint();
-      m_prevSetpoint = m_setpoint;
-      m_setpointTimer.reset();
-      return temp;
-    }
-  }
-
-  /**
-   * Set the PID Controller gain parameters. Set the proportional, integral, and differential
-   * coefficients.
-   *
-   * @param p Proportional coefficient
-   * @param i Integral coefficient
-   * @param d Differential coefficient
-   */
-  @Override
-  @SuppressWarnings("ParameterName")
-  public void setPID(double p, double i, double d) {
-    m_thisMutex.lock();
-    try {
-      m_P = p;
-      m_I = i;
-      m_D = d;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the PID Controller gain parameters. Set the proportional, integral, and differential
-   * coefficients.
-   *
-   * @param p Proportional coefficient
-   * @param i Integral coefficient
-   * @param d Differential coefficient
-   * @param f Feed forward coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setPID(double p, double i, double d, double f) {
-    m_thisMutex.lock();
-    try {
-      m_P = p;
-      m_I = i;
-      m_D = d;
-      m_F = f;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the Proportional coefficient of the PID controller gain.
-   *
-   * @param p Proportional coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setP(double p) {
-    m_thisMutex.lock();
-    try {
-      m_P = p;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the Integral coefficient of the PID controller gain.
-   *
-   * @param i Integral coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setI(double i) {
-    m_thisMutex.lock();
-    try {
-      m_I = i;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the Differential coefficient of the PID controller gain.
-   *
-   * @param d differential coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setD(double d) {
-    m_thisMutex.lock();
-    try {
-      m_D = d;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the Feed forward coefficient of the PID controller gain.
-   *
-   * @param f feed forward coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setF(double f) {
-    m_thisMutex.lock();
-    try {
-      m_F = f;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Get the Proportional coefficient.
-   *
-   * @return proportional coefficient
-   */
-  @Override
-  public double getP() {
-    m_thisMutex.lock();
-    try {
-      return m_P;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Get the Integral coefficient.
-   *
-   * @return integral coefficient
-   */
-  @Override
-  public double getI() {
-    m_thisMutex.lock();
-    try {
-      return m_I;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Get the Differential coefficient.
-   *
-   * @return differential coefficient
-   */
-  @Override
-  public double getD() {
-    m_thisMutex.lock();
-    try {
-      return m_D;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Get the Feed forward coefficient.
-   *
-   * @return feed forward coefficient
-   */
-  public double getF() {
-    m_thisMutex.lock();
-    try {
-      return m_F;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Return the current PID result This is always centered on zero and constrained the the max and
-   * min outs.
-   *
-   * @return the latest calculated output
-   */
-  public double get() {
-    m_thisMutex.lock();
-    try {
-      return m_result;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the PID controller to consider the input to be continuous, Rather then using the max and
-   * min input range as constraints, it considers them to be the same point and automatically
-   * calculates the shortest route to the setpoint.
-   *
-   * @param continuous Set to true turns on continuous, false turns off continuous
-   */
-  public void setContinuous(boolean continuous) {
-    if (continuous && m_inputRange <= 0) {
-      throw new IllegalStateException("No input range set when calling setContinuous().");
-    }
-    m_thisMutex.lock();
-    try {
-      m_continuous = continuous;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the PID controller to consider the input to be continuous, Rather then using the max and
-   * min input range as constraints, it considers them to be the same point and automatically
-   * calculates the shortest route to the setpoint.
-   */
-  public void setContinuous() {
-    setContinuous(true);
-  }
-
-  /**
-   * Sets the maximum and minimum values expected from the input and setpoint.
-   *
-   * @param minimumInput the minimum value expected from the input
-   * @param maximumInput the maximum value expected from the input
-   */
-  public void setInputRange(double minimumInput, double maximumInput) {
-    m_thisMutex.lock();
-    try {
-      if (minimumInput > maximumInput) {
-        throw new BoundaryException("Lower bound is greater than upper bound");
-      }
-      m_minimumInput = minimumInput;
-      m_maximumInput = maximumInput;
-      m_inputRange = maximumInput - minimumInput;
-    } finally {
-      m_thisMutex.unlock();
-    }
-
-    setSetpoint(m_setpoint);
-  }
-
-  /**
-   * Sets the minimum and maximum values to write.
-   *
-   * @param minimumOutput the minimum percentage to write to the output
-   * @param maximumOutput the maximum percentage to write to the output
-   */
-  public void setOutputRange(double minimumOutput, double maximumOutput) {
-    m_thisMutex.lock();
-    try {
-      if (minimumOutput > maximumOutput) {
-        throw new BoundaryException("Lower bound is greater than upper bound");
-      }
-      m_minimumOutput = minimumOutput;
-      m_maximumOutput = maximumOutput;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the setpoint for the PIDController.
-   *
-   * @param setpoint the desired setpoint
-   */
-  @Override
-  public void setSetpoint(double setpoint) {
-    m_thisMutex.lock();
-    try {
-      if (m_maximumInput > m_minimumInput) {
-        if (setpoint > m_maximumInput) {
-          m_setpoint = m_maximumInput;
-        } else if (setpoint < m_minimumInput) {
-          m_setpoint = m_minimumInput;
-        } else {
-          m_setpoint = setpoint;
-        }
-      } else {
-        m_setpoint = setpoint;
-      }
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Returns the current setpoint of the PIDController.
-   *
-   * @return the current setpoint
-   */
-  @Override
-  public double getSetpoint() {
-    m_thisMutex.lock();
-    try {
-      return m_setpoint;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Returns the change in setpoint over time of the PIDController.
-   *
-   * @return the change in setpoint over time
-   */
-  public double getDeltaSetpoint() {
-    m_thisMutex.lock();
-    try {
-      return (m_setpoint - m_prevSetpoint) / m_setpointTimer.get();
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Returns the current difference of the input from the setpoint.
-   *
-   * @return the current error
-   */
-  @Override
-  public double getError() {
-    m_thisMutex.lock();
-    try {
-      return getContinuousError(getSetpoint() - m_filter.calculate(m_pidInput.pidGet()));
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Returns the current difference of the error over the past few iterations. You can specify the
-   * number of iterations to average with setToleranceBuffer() (defaults to 1). getAvgError() is
-   * used for the onTarget() function.
-   *
-   * @deprecated Use getError(), which is now already filtered.
-   * @return     the current average of the error
-   */
-  @Deprecated
-  public double getAvgError() {
-    m_thisMutex.lock();
-    try {
-      return getError();
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Sets what type of input the PID controller will use.
-   *
-   * @param pidSource the type of input
-   */
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_pidInput.setPIDSourceType(pidSource);
-  }
-
-  /**
-   * Returns the type of input the PID controller is using.
-   *
-   * @return the PID controller input type
-   */
-  public PIDSourceType getPIDSourceType() {
-    return m_pidInput.getPIDSourceType();
-  }
-
-  /**
-   * Set the PID tolerance using a Tolerance object. Tolerance can be specified as a percentage of
-   * the range or as an absolute value. The Tolerance object encapsulates those options in an
-   * object. Use it by creating the type of tolerance that you want to use: setTolerance(new
-   * PIDController.AbsoluteTolerance(0.1))
-   *
-   * @deprecated      Use setPercentTolerance() instead.
-   * @param tolerance A tolerance object of the right type, e.g. PercentTolerance or
-   *                  AbsoluteTolerance
-   */
-  @Deprecated
-  public void setTolerance(Tolerance tolerance) {
-    m_tolerance = tolerance;
-  }
-
-  /**
-   * Set the absolute error which is considered tolerable for use with OnTarget.
-   *
-   * @param absvalue absolute error which is tolerable in the units of the input object
-   */
-  public void setAbsoluteTolerance(double absvalue) {
-    m_thisMutex.lock();
-    try {
-      m_tolerance = new AbsoluteTolerance(absvalue);
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the percentage error which is considered tolerable for use with OnTarget. (Input of 15.0 =
-   * 15 percent)
-   *
-   * @param percentage percent error which is tolerable
-   */
-  public void setPercentTolerance(double percentage) {
-    m_thisMutex.lock();
-    try {
-      m_tolerance = new PercentageTolerance(percentage);
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the number of previous error samples to average for tolerancing. When determining whether a
-   * mechanism is on target, the user may want to use a rolling average of previous measurements
-   * instead of a precise position or velocity. This is useful for noisy sensors which return a few
-   * erroneous measurements when the mechanism is on target. However, the mechanism will not
-   * register as on target for at least the specified bufLength cycles.
-   *
-   * @deprecated      Use a LinearFilter as the input.
-   * @param bufLength Number of previous cycles to average.
-   */
-  @Deprecated
-  public void setToleranceBuffer(int bufLength) {
-    m_thisMutex.lock();
-    try {
-      m_filter = LinearFilter.movingAverage(bufLength);
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Return true if the error is within the percentage of the total input range, determined by
-   * setTolerance. This assumes that the maximum and minimum input were set using setInput.
-   *
-   * @return true if the error is less than the tolerance
-   */
-  public boolean onTarget() {
-    m_thisMutex.lock();
-    try {
-      return m_tolerance.onTarget();
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Reset the previous error, the integral term, and disable the controller.
-   */
-  @Override
-  public void reset() {
-    m_thisMutex.lock();
-    try {
-      m_prevError = 0;
-      m_totalError = 0;
-      m_result = 0;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Passes the output directly to setSetpoint().
-   *
-   * <p>PIDControllers can be nested by passing a PIDController as another PIDController's output.
-   * In that case, the output of the parent controller becomes the input (i.e., the reference) of
-   * the child.
-   *
-   * <p>It is the caller's responsibility to put the data into a valid form for setSetpoint().
-   */
-  @Override
-  public void pidWrite(double output) {
-    setSetpoint(output);
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("PIDController");
-    builder.setSafeState(this::reset);
-    builder.addDoubleProperty("p", this::getP, this::setP);
-    builder.addDoubleProperty("i", this::getI, this::setI);
-    builder.addDoubleProperty("d", this::getD, this::setD);
-    builder.addDoubleProperty("f", this::getF, this::setF);
-    builder.addDoubleProperty("setpoint", this::getSetpoint, this::setSetpoint);
-  }
-
-  /**
-   * Wraps error around for continuous inputs. The original error is returned if continuous mode is
-   * disabled. This is an unsynchronized function.
-   *
-   * @param error The current error of the PID controller.
-   * @return Error for continuous inputs.
-   */
-  protected double getContinuousError(double error) {
-    if (m_continuous && m_inputRange > 0) {
-      error %= m_inputRange;
-      if (Math.abs(error) > m_inputRange / 2) {
-        if (error > 0) {
-          return error - m_inputRange;
-        } else {
-          return error + m_inputRange;
-        }
-      }
-    }
-
-    return error;
-  }
-
-  private static double clamp(double value, double low, double high) {
-    return Math.max(low, Math.min(value, high));
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDController.java
deleted file mode 100644
index 881723e..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDController.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-
-/**
- * Class implements a PID Control Loop.
- *
- * <p>Creates a separate thread which reads the given PIDSource and takes care of the integral
- * calculations, as well as writing the given PIDOutput.
- *
- * <p>This feedback controller runs in discrete time, so time deltas are not used in the integral
- * and derivative calculations. Therefore, the sample rate affects the controller's behavior for a
- * given set of PID constants.
- *
- * @deprecated Use {@link edu.wpi.first.wpilibj.controller.PIDController} instead.
- */
-@Deprecated(since = "2020", forRemoval = true)
-public class PIDController extends PIDBase implements Controller, AutoCloseable {
-  Notifier m_controlLoop = new Notifier(this::calculate);
-
-  /**
-   * Allocate a PID object with the given constants for P, I, D, and F.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param Kf     the feed forward term
-   * @param source The PIDSource object that is used to get values
-   * @param output The PIDOutput object that is set to the output percentage
-   * @param period the loop time for doing calculations in seconds.
-   *               This particularly affects calculations of
-   *               the integral and differential terms.
-   *               The default is 0.05 (50ms).
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd, double Kf, PIDSource source,
-                       PIDOutput output, double period) {
-    super(Kp, Ki, Kd, Kf, source, output);
-    m_controlLoop.startPeriodic(period);
-  }
-
-  /**
-   * Allocate a PID object with the given constants for P, I, D and period.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param source the PIDSource object that is used to get values
-   * @param output the PIDOutput object that is set to the output percentage
-   * @param period the loop time for doing calculations in seconds.
-   *               This particularly affects calculations of
-   *               the integral and differential terms.
-   *               The default is 0.05 (50ms).
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output,
-                       double period) {
-    this(Kp, Ki, Kd, 0.0, source, output, period);
-  }
-
-  /**
-   * Allocate a PID object with the given constants for P, I, D, using a 50ms period.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param source The PIDSource object that is used to get values
-   * @param output The PIDOutput object that is set to the output percentage
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output) {
-    this(Kp, Ki, Kd, source, output, kDefaultPeriod);
-  }
-
-  /**
-   * Allocate a PID object with the given constants for P, I, D, using a 50ms period.
-   *
-   * @param Kp     the proportional coefficient
-   * @param Ki     the integral coefficient
-   * @param Kd     the derivative coefficient
-   * @param Kf     the feed forward term
-   * @param source The PIDSource object that is used to get values
-   * @param output The PIDOutput object that is set to the output percentage
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd, double Kf, PIDSource source,
-                       PIDOutput output) {
-    this(Kp, Ki, Kd, Kf, source, output, kDefaultPeriod);
-  }
-
-  @Override
-  public void close() {
-    m_controlLoop.close();
-    m_thisMutex.lock();
-    try {
-      m_pidOutput = null;
-      m_pidInput = null;
-      m_controlLoop = null;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Begin running the PIDController.
-   */
-  @Override
-  public void enable() {
-    m_thisMutex.lock();
-    try {
-      m_enabled = true;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Stop running the PIDController, this sets the output to zero before stopping.
-   */
-  @Override
-  public void disable() {
-    // Ensures m_enabled check and pidWrite() call occur atomically
-    m_pidWriteMutex.lock();
-    try {
-      m_thisMutex.lock();
-      try {
-        m_enabled = false;
-      } finally {
-        m_thisMutex.unlock();
-      }
-
-      m_pidOutput.pidWrite(0);
-    } finally {
-      m_pidWriteMutex.unlock();
-    }
-  }
-
-  /**
-   * Set the enabled state of the PIDController.
-   */
-  public void setEnabled(boolean enable) {
-    if (enable) {
-      enable();
-    } else {
-      disable();
-    }
-  }
-
-  /**
-   * Return true if PIDController is enabled.
-   */
-  public boolean isEnabled() {
-    m_thisMutex.lock();
-    try {
-      return m_enabled;
-    } finally {
-      m_thisMutex.unlock();
-    }
-  }
-
-  /**
-   * Reset the previous error, the integral term, and disable the controller.
-   */
-  @Override
-  public void reset() {
-    disable();
-
-    super.reset();
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    super.initSendable(builder);
-    builder.addBooleanProperty("enabled", this::isEnabled, this::setEnabled);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDInterface.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDInterface.java
deleted file mode 100644
index 10e60d3..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDInterface.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-@Deprecated(since = "2020", forRemoval = true)
-@SuppressWarnings("SummaryJavadoc")
-public interface PIDInterface {
-  @SuppressWarnings("ParameterName")
-  void setPID(double p, double i, double d);
-
-  double getP();
-
-  double getI();
-
-  double getD();
-
-  void setSetpoint(double setpoint);
-
-  double getSetpoint();
-
-  double getError();
-
-  void reset();
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDOutput.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDOutput.java
deleted file mode 100644
index e9af2ff..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDOutput.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-/**
- * This interface allows PIDController to write it's results to its output.
- *
- * @deprecated Use DoubleConsumer and new PIDController class.
- */
-@FunctionalInterface
-@Deprecated(since = "2020", forRemoval = true)
-public interface PIDOutput {
-  /**
-   * Set the output to the value calculated by PIDController.
-   *
-   * @param output the value calculated by PIDController
-   */
-  void pidWrite(double output);
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSource.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSource.java
deleted file mode 100644
index 3c4f1f5..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSource.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-/**
- * This interface allows for PIDController to automatically read from this object.
- *
- * @deprecated Use DoubleSupplier and new PIDController class.
- */
-@Deprecated(since = "2020", forRemoval = true)
-public interface PIDSource {
-  /**
-   * Set which parameter of the device you are using as a process control variable.
-   *
-   * @param pidSource An enum to select the parameter.
-   */
-  void setPIDSourceType(PIDSourceType pidSource);
-
-  /**
-   * Get which parameter of the device you are using as a process control variable.
-   *
-   * @return the currently selected PID source parameter
-   */
-  PIDSourceType getPIDSourceType();
-
-  /**
-   * Get the result to use in PIDController.
-   *
-   * @return the result to use in PIDController
-   */
-  double pidGet();
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSourceType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSourceType.java
deleted file mode 100644
index 7508be5..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PIDSourceType.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-/**
- * A description for the type of output value to provide to a PIDController.
- */
-@Deprecated(since = "2020", forRemoval = true)
-public enum PIDSourceType {
-  kDisplacement,
-  kRate
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java
new file mode 100644
index 0000000..e3eb052
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PS4Controller.java
@@ -0,0 +1,535 @@
+// 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;
+
+/**
+ * Handle input from PS4 controllers connected to the Driver Station.
+ *
+ * <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.
+ */
+public class PS4Controller extends GenericHID {
+  /**
+   * Construct an instance of a device.
+   *
+   * @param port The port index on the Driver Station that the device is plugged into.
+   */
+  public PS4Controller(int port) {
+    super(port);
+    HAL.report(tResourceType.kResourceType_PS4Controller, port + 1);
+  }
+
+  /** Represents a digital button on a PS4Controller. */
+  public enum Button {
+    kSquare(1),
+    kCross(2),
+    kCircle(3),
+    kTriangle(4),
+    kL1(5),
+    kR1(6),
+    kL2(7),
+    kR2(8),
+    kShare(9),
+    kOptions(10),
+    kL3(11),
+    kR3(12),
+    kPS(13),
+    kTouchpad(14);
+
+    public final int value;
+
+    Button(int index) {
+      this.value = index;
+    }
+
+    /**
+     * Get the human-friendly name of the button, matching the relevant methods. This is done by
+     * stripping the leading `k`, and if not the touchpad 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 (this == kTouchpad) {
+        return name;
+      }
+      return name + "Button";
+    }
+  }
+
+  /** Represents an axis on a PS4Controller. */
+  public enum Axis {
+    kLeftX(0),
+    kLeftY(1),
+    kRightX(2),
+    kRightY(5),
+    kL2(3),
+    kR2(4);
+
+    public final int value;
+
+    Axis(int index) {
+      value = index;
+    }
+
+    /**
+     * Get the human-friendly name of the axis, matching the relevant methods. This is done by
+     * stripping the leading `k`, and if one of L2/R2 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("2")) {
+        return name + "Axis";
+      }
+      return name;
+    }
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Get the L2 axis value of the controller. Note that this axis is bound to the range of [0, 1] as
+   * opposed to the usual [-1, 1].
+   *
+   * @return the axis value.
+   */
+  public double getL2Axis() {
+    return getRawAxis(Axis.kL2.value);
+  }
+
+  /**
+   * Get the R2 axis value of the controller. Note that this axis is bound to the range of [0, 1] as
+   * opposed to the usual [-1, 1].
+   *
+   * @return the axis value.
+   */
+  public double getR2Axis() {
+    return getRawAxis(Axis.kR2.value);
+  }
+
+  /**
+   * Read the value of the left trigger button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getL2Button() {
+    return getRawButton(Button.kL2.value);
+  }
+
+  /**
+   * Read the value of the right trigger button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getR2Button() {
+    return getRawButton(Button.kR2.value);
+  }
+
+  /**
+   * Whether the L2 button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getL2ButtonPressed() {
+    return getRawButtonPressed(Button.kL2.value);
+  }
+
+  /**
+   * Whether the R2 button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getR2ButtonPressed() {
+    return getRawButtonPressed(Button.kR2.value);
+  }
+
+  /**
+   * Whether the L2 button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getL2ButtonReleased() {
+    return getRawButtonReleased(Button.kL2.value);
+  }
+
+  /**
+   * Whether the R2 button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getR2ButtonReleased() {
+    return getRawButtonReleased(Button.kR2.value);
+  }
+
+  /**
+   * Read the value of the L1 button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getL1Button() {
+    return getRawButton(Button.kL1.value);
+  }
+
+  /**
+   * Read the value of the R1 button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getR1Button() {
+    return getRawButton(Button.kR1.value);
+  }
+
+  /**
+   * Whether the L1 button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getL1ButtonPressed() {
+    return getRawButtonPressed(Button.kL1.value);
+  }
+
+  /**
+   * Whether the R1 button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getR1ButtonPressed() {
+    return getRawButtonPressed(Button.kR1.value);
+  }
+
+  /**
+   * Whether the L1 button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getL1ButtonReleased() {
+    return getRawButtonReleased(Button.kL1.value);
+  }
+
+  /**
+   * Whether the R1 button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getR1ButtonReleased() {
+    return getRawButtonReleased(Button.kR1.value);
+  }
+
+  /**
+   * Read the value of the L3 button (pressing the left analog stick) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getL3Button() {
+    return getRawButton(Button.kL3.value);
+  }
+
+  /**
+   * Read the value of the R3 button (pressing the right analog stick) on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getR3Button() {
+    return getRawButton(Button.kR3.value);
+  }
+
+  /**
+   * Whether the L3 (left stick) button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getL3ButtonPressed() {
+    return getRawButtonPressed(Button.kL3.value);
+  }
+
+  /**
+   * Whether the R3 (right stick) button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getR3ButtonPressed() {
+    return getRawButtonPressed(Button.kR3.value);
+  }
+
+  /**
+   * Whether the L3 (left stick) button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getL3ButtonReleased() {
+    return getRawButtonReleased(Button.kL3.value);
+  }
+
+  /**
+   * Whether the R3 (right stick) button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getR3ButtonReleased() {
+    return getRawButtonReleased(Button.kR3.value);
+  }
+
+  /**
+   * Read the value of the Square button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getSquareButton() {
+    return getRawButton(Button.kSquare.value);
+  }
+
+  /**
+   * Whether the Square button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getSquareButtonPressed() {
+    return getRawButtonPressed(Button.kSquare.value);
+  }
+
+  /**
+   * Whether the Square button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getSquareButtonReleased() {
+    return getRawButtonReleased(Button.kSquare.value);
+  }
+
+  /**
+   * Read the value of the Cross button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getCrossButton() {
+    return getRawButton(Button.kCross.value);
+  }
+
+  /**
+   * Whether the Cross button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getCrossButtonPressed() {
+    return getRawButtonPressed(Button.kCross.value);
+  }
+
+  /**
+   * Whether the Cross button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getCrossButtonReleased() {
+    return getRawButtonReleased(Button.kCross.value);
+  }
+
+  /**
+   * Read the value of the Triangle button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getTriangleButton() {
+    return getRawButton(Button.kTriangle.value);
+  }
+
+  /**
+   * Whether the Triangle button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getTriangleButtonPressed() {
+    return getRawButtonPressed(Button.kTriangle.value);
+  }
+
+  /**
+   * Whether the Triangle button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getTriangleButtonReleased() {
+    return getRawButtonReleased(Button.kTriangle.value);
+  }
+
+  /**
+   * Read the value of the Circle button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getCircleButton() {
+    return getRawButton(Button.kCircle.value);
+  }
+
+  /**
+   * Whether the Circle button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getCircleButtonPressed() {
+    return getRawButtonPressed(Button.kCircle.value);
+  }
+
+  /**
+   * Whether the Circle button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getCircleButtonReleased() {
+    return getRawButtonReleased(Button.kCircle.value);
+  }
+
+  /**
+   * Read the value of the share button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getShareButton() {
+    return getRawButton(Button.kShare.value);
+  }
+
+  /**
+   * Whether the share button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getShareButtonPressed() {
+    return getRawButtonPressed(Button.kShare.value);
+  }
+
+  /**
+   * Whether the share button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getShareButtonReleased() {
+    return getRawButtonReleased(Button.kShare.value);
+  }
+
+  /**
+   * Read the value of the PS button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getPSButton() {
+    return getRawButton(Button.kPS.value);
+  }
+
+  /**
+   * Whether the PS button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getPSButtonPressed() {
+    return getRawButtonPressed(Button.kPS.value);
+  }
+
+  /**
+   * Whether the PS button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getPSButtonReleased() {
+    return getRawButtonReleased(Button.kPS.value);
+  }
+
+  /**
+   * Read the value of the options button on the controller.
+   *
+   * @return The state of the button.
+   */
+  public boolean getOptionsButton() {
+    return getRawButton(Button.kOptions.value);
+  }
+
+  /**
+   * Whether the options button was pressed since the last check.
+   *
+   * @return Whether the button was pressed since the last check.
+   */
+  public boolean getOptionsButtonPressed() {
+    return getRawButtonPressed(Button.kOptions.value);
+  }
+
+  /**
+   * Whether the options button was released since the last check.
+   *
+   * @return Whether the button was released since the last check.
+   */
+  public boolean getOptionsButtonReleased() {
+    return getRawButtonReleased(Button.kOptions.value);
+  }
+
+  /**
+   * Read the value of the touchpad on the controller.
+   *
+   * @return The state of the touchpad.
+   */
+  public boolean getTouchpad() {
+    return getRawButton(Button.kTouchpad.value);
+  }
+
+  /**
+   * Whether the touchpad was pressed since the last check.
+   *
+   * @return Whether the touchpad was pressed since the last check.
+   */
+  public boolean getTouchpadPressed() {
+    return getRawButtonPressed(Button.kTouchpad.value);
+  }
+
+  /**
+   * Whether the touchpad was released since the last check.
+   *
+   * @return Whether the touchpad was released since the last check.
+   */
+  public boolean getTouchpadReleased() {
+    return getRawButtonReleased(Button.kTouchpad.value);
+  }
+}
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 8febd2c..9d86ab8 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWM.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,8 +8,9 @@
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.PWMConfigDataResult;
 import edu.wpi.first.hal.PWMJNI;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Class implements the PWM generation in the FPGA.
@@ -26,36 +24,42 @@
  * center value - 999 to 2 = linear scaling from "center" to "full reverse" - 1 = minimum pulse
  * width (currently .5ms) - 0 = disabled (i.e. PWM output is held low)
  */
-public class PWM extends MotorSafety implements Sendable, AutoCloseable {
-  /**
-   * Represents the amount to multiply the minimum servo-pulse pwm period by.
-   */
+public class PWM implements Sendable, AutoCloseable {
+  /** Represents the amount to multiply the minimum servo-pulse pwm period by. */
   public enum PeriodMultiplier {
-    /**
-     * Period Multiplier: don't skip pulses. PWM pulses occur every 5.005 ms
-     */
+    /** Period Multiplier: don't skip pulses. PWM pulses occur every 5.005 ms */
     k1X,
-    /**
-     * Period Multiplier: skip every other pulse. PWM pulses occur every 10.010 ms
-     */
+    /** Period Multiplier: skip every other pulse. PWM pulses occur every 10.010 ms */
     k2X,
-    /**
-     * Period Multiplier: skip three out of four pulses. PWM pulses occur every 20.020 ms
-     */
+    /** Period Multiplier: skip three out of four pulses. PWM pulses occur every 20.020 ms */
     k4X
   }
 
   private final int m_channel;
 
-  // Package private to use from AddressableLED
-  int m_handle;
+  private int m_handle;
+
+  /**
+   * Allocate a PWM given a channel.
+   *
+   * <p>Checks channel value range and allocates the appropriate channel. The allocation is only
+   * done to help users ensure that they don't double assign channels.
+   *
+   * <p>By default, adds itself to SendableRegistry and LiveWindow.
+   *
+   * @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
+   */
+  public PWM(final int channel) {
+    this(channel, true);
+  }
 
   /**
    * Allocate a PWM given a channel.
    *
    * @param channel The PWM channel number. 0-9 are on-board, 10-19 are on the MXP port
+   * @param registerSendable If true, adds this instance to SendableRegistry and LiveWindow
    */
-  public PWM(final int channel) {
+  public PWM(final int channel, final boolean registerSendable) {
     SensorUtil.checkPWMChannel(channel);
     m_channel = channel;
 
@@ -66,14 +70,12 @@
     PWMJNI.setPWMEliminateDeadband(m_handle, false);
 
     HAL.report(tResourceType.kResourceType_PWM, channel + 1);
-    SendableRegistry.addLW(this, "PWM", channel);
-
-    setSafetyEnabled(false);
+    if (registerSendable) {
+      SendableRegistry.addLW(this, "PWM", channel);
+    }
   }
 
-  /**
-   * Free the resource associated with the PWM channel and set the value to 0.
-   */
+  /** Free the resource associated with the PWM channel and set the value to 0. */
   @Override
   public void close() {
     SendableRegistry.remove(this);
@@ -85,22 +87,12 @@
     m_handle = 0;
   }
 
-  @Override
-  public void stopMotor() {
-    setDisabled();
-  }
-
-  @Override
-  public String getDescription() {
-    return "PWM " + getChannel();
-  }
-
   /**
-   * Optionally eliminate the deadband from a speed controller.
+   * Optionally eliminate the deadband from a motor controller.
    *
-   * @param eliminateDeadband If true, set the motor curve on the Jaguar to eliminate the deadband
-   *                          in the middle of the range. Otherwise, keep the full range without
-   *                          modifying any values.
+   * @param eliminateDeadband If true, set the motor curve for the motor controller to eliminate the
+   *     deadband in the middle of the range. Otherwise, keep the full range without modifying any
+   *     values.
    */
   public void enableDeadbandElimination(boolean eliminateDeadband) {
     PWMJNI.setPWMEliminateDeadband(m_handle, eliminateDeadband);
@@ -111,21 +103,23 @@
    * type of controller. The values determine the upper and lower speeds as well as the deadband
    * bracket.
    *
-   * @param max         The max PWM pulse width in ms
+   * @param max The max PWM pulse width in ms
    * @param deadbandMax The high end of the deadband range pulse width in ms
-   * @param center      The center (off) pulse width in ms
+   * @param center The center (off) pulse width in ms
    * @param deadbandMin The low end of the deadband pulse width in ms
-   * @param min         The minimum pulse width in ms
+   * @param min The minimum pulse width in ms
    */
-  public void setBounds(double max, double deadbandMax, double center, double deadbandMin,
-                           double min) {
+  public void setBounds(
+      double max, double deadbandMax, double center, double deadbandMin, double min) {
     PWMJNI.setPWMConfig(m_handle, max, deadbandMax, center, deadbandMin, min);
   }
 
   /**
-   * Gets the bounds on the PWM pulse widths. This Gets the bounds on the PWM values for a
-   * particular type of controller. The values determine the upper and lower speeds as well
-   * as the deadband bracket.
+   * Gets the bounds on the PWM pulse widths. This gets the bounds on the PWM values for a
+   * particular type of controller. The values determine the upper and lower speeds as well as the
+   * deadband bracket.
+   *
+   * @return The bounds on the PWM pulse widths.
    */
   public PWMConfigDataResult getRawBounds() {
     return PWMJNI.getPWMConfigRaw(m_handle);
@@ -169,9 +163,9 @@
   /**
    * Set the PWM value based on a speed.
    *
-   * <p>This is intended to be used by speed controllers.
+   * <p>This is intended to be used by motor controllers.
    *
-   * @param speed The speed to set the speed controller between -1.0 and 1.0.
+   * @param speed The speed to set the motor controller between -1.0 and 1.0.
    * @pre SetMaxPositivePwm() called.
    * @pre SetMinPositivePwm() called.
    * @pre SetCenterPwm() called.
@@ -185,7 +179,7 @@
   /**
    * Get the PWM value in terms of speed.
    *
-   * <p>This is intended to be used by speed controllers.
+   * <p>This is intended to be used by motor controllers.
    *
    * @return The most recently set speed between -1.0 and 1.0.
    * @pre SetMaxPositivePwm() called.
@@ -219,10 +213,7 @@
     return PWMJNI.getPWMRaw(m_handle);
   }
 
-  /**
-   * Temporarily disables the PWM output. The next set call will reenable
-   * the output.
-   */
+  /** Temporarily disables the PWM output. The next set call will reenable the output. */
   public void setDisabled() {
     PWMJNI.setPWMDisabled(m_handle);
   }
@@ -251,10 +242,19 @@
     }
   }
 
-  protected void setZeroLatch() {
+  public void setZeroLatch() {
     PWMJNI.latchPWMZero(m_handle);
   }
 
+  /**
+   * Get the underlying handle.
+   *
+   * @return Underlying PWM handle
+   */
+  public int getHandle() {
+    return m_handle;
+  }
+
   @Override
   public void initSendable(SendableBuilder builder) {
     builder.setSmartDashboardType("PWM");
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSparkMax.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSparkMax.java
deleted file mode 100644
index 0f97ea5..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSparkMax.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * REV Robotics SPARK MAX Speed Controller with PWM control.
- *
- * <P>Note that the SPARK MAX 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 MAX User Manual
- * available from REV Robotics.
- *
- * <p><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 PWMSparkMax extends PWMSpeedController {
-  /**
-   * Common initialization code called by all constructors.
-   *
-   */
-  public PWMSparkMax(final int channel) {
-    super(channel);
-
-    setBounds(2.003, 1.55, 1.50, 1.46, 0.999);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_RevSparkMaxPWM, getChannel() + 1);
-    SendableRegistry.setName(this, "PWMSparkMax", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSpeedController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSpeedController.java
deleted file mode 100644
index 0a33423..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMSpeedController.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-
-/**
- * Common base class for all PWM Speed Controllers.
- */
-public abstract class PWMSpeedController extends PWM implements SpeedController {
-  private boolean m_isInverted;
-
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the controller is attached to. 0-9 are on-board, 10-19 are
-   *                on the MXP port
-   */
-  protected PWMSpeedController(int channel) {
-    super(channel);
-  }
-
-  @Override
-  public String getDescription() {
-    return "PWM " + getChannel();
-  }
-
-  /**
-   * Set the PWM value.
-   *
-   * <p>The PWM value is set using a range of -1.0 to 1.0, appropriately scaling the value for the
-   * FPGA.
-   *
-   * @param speed The speed value between -1.0 and 1.0 to set.
-   */
-  @Override
-  public void set(double speed) {
-    setSpeed(m_isInverted ? -speed : speed);
-    feed();
-  }
-
-  /**
-   * Get the recently set value of the PWM.
-   *
-   * @return The most recently set value for the PWM between -1.0 and 1.0.
-   */
-  @Override
-  public double get() {
-    return getSpeed();
-  }
-
-  @Override
-  public void setInverted(boolean isInverted) {
-    m_isInverted = isInverted;
-  }
-
-  @Override
-  public boolean getInverted() {
-    return m_isInverted;
-  }
-
-  @Override
-  public void disable() {
-    setDisabled();
-  }
-
-  /**
-   * Write out the PID value as seen in the PIDOutput base object.
-   *
-   * @param output Write out the PWM value as was found in the PIDController
-   */
-  @Override
-  public void pidWrite(double output) {
-    set(output);
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("Speed Controller");
-    builder.setActuator(true);
-    builder.setSafeState(this::setDisabled);
-    builder.addDoubleProperty("Value", this::getSpeed, this::setSpeed);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonFX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonFX.java
deleted file mode 100644
index 03acab2..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonFX.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Cross the Road Electronics (CTRE) Talon FX Speed Controller with PWM control.
- *
- * <p>Note that the TalonFX 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 TalonFX User Manual
- * available from CTRE.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class PWMTalonFX extends PWMSpeedController {
-  /**
-   * Constructor for a TalonFX connected via PWM.
-   *
-   * @param channel The PWM channel that the Talon FX is attached to. 0-9 are on-board, 10-19 are
-   *                on the MXP port
-   */
-  public PWMTalonFX(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_TalonFX, getChannel() + 1);
-    SendableRegistry.setName(this, "PWMTalonFX", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonSRX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonSRX.java
deleted file mode 100644
index 1859e7f..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMTalonSRX.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Cross the Road Electronics (CTRE) Talon SRX Speed Controller with PWM control.
- *
- * <p>Note that the TalonSRX 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 TalonSRX User Manual
- * available from CTRE.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class PWMTalonSRX extends PWMSpeedController {
-  /**
-   * Constructor for a TalonSRX connected via PWM.
-   *
-   * @param channel The PWM channel that the Talon SRX is attached to. 0-9 are on-board, 10-19 are
-   *                on the MXP port
-   */
-  public PWMTalonSRX(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_PWMTalonSRX, getChannel() + 1);
-    SendableRegistry.setName(this, "PWMTalonSRX", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVenom.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVenom.java
deleted file mode 100644
index 9a1116d..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVenom.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Playing with Fusion Venom Smart Motor with PWM control.
- *
- * <p>Note that the Venom 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.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class PWMVenom extends PWMSpeedController {
-  /**
-   * Constructor for a Venom connected via PWM.
-   *
-   * @param channel The PWM channel that the Venom is attached to. 0-9 are on-board, 10-19 are
-   *                on the MXP port
-   */
-  public PWMVenom(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_FusionVenom, getChannel() + 1);
-    SendableRegistry.setName(this, "PWMVenom", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVictorSPX.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVictorSPX.java
deleted file mode 100644
index b51686a..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PWMVictorSPX.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Cross the Road Electronics (CTRE) Victor SPX Speed Controller with PWM control.
- *
- * <p>Note that the Victor SPX 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 Victor SPX User
- * Manual available from CTRE.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class PWMVictorSPX extends PWMSpeedController {
-  /**
-   * Constructor for a Victor SPX connected via PWM.
-   *
-   * @param channel The PWM channel that the PWMVictorSPX is attached to. 0-9 are on-board, 10-19
-   *                are on the MXP port
-   */
-  public PWMVictorSPX(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_PWMVictorSPX, getChannel() + 1);
-    SendableRegistry.setName(this, "PWMVictorSPX", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java
new file mode 100644
index 0000000..546c7c0
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticHub.java
@@ -0,0 +1,203 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import edu.wpi.first.hal.REVPHJNI;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Module class for controlling a REV Robotics Pneumatic Hub. */
+public class PneumaticHub implements PneumaticsBase {
+  private static class DataStore implements AutoCloseable {
+    public final int m_module;
+    public final int m_handle;
+    private int m_refCount;
+    private int m_reservedMask;
+    private boolean m_compressorReserved;
+    private final Object m_reserveLock = new Object();
+
+    DataStore(int module) {
+      m_handle = REVPHJNI.initialize(module);
+      m_module = module;
+      m_handleMap.put(module, this);
+    }
+
+    @Override
+    public void close() {
+      REVPHJNI.free(m_handle);
+      m_handleMap.remove(m_module);
+    }
+
+    public void addRef() {
+      m_refCount++;
+    }
+
+    public void removeRef() {
+      m_refCount--;
+      if (m_refCount == 0) {
+        this.close();
+      }
+    }
+  }
+
+  private static final Map<Integer, DataStore> m_handleMap = new HashMap<>();
+  private static final Object m_handleLock = new Object();
+
+  private static DataStore getForModule(int module) {
+    synchronized (m_handleLock) {
+      Integer moduleBoxed = module;
+      DataStore pcm = m_handleMap.get(moduleBoxed);
+      if (pcm == null) {
+        pcm = new DataStore(module);
+      }
+      pcm.addRef();
+      return pcm;
+    }
+  }
+
+  private static void freeModule(DataStore store) {
+    synchronized (m_handleLock) {
+      store.removeRef();
+    }
+  }
+
+  private final DataStore m_dataStore;
+  private final int m_handle;
+
+  /** Constructs a PneumaticHub with the default id (1). */
+  public PneumaticHub() {
+    this(SensorUtil.getDefaultREVPHModule());
+  }
+
+  /**
+   * Constructs a PneumaticHub.
+   *
+   * @param module module number to construct
+   */
+  public PneumaticHub(int module) {
+    m_dataStore = getForModule(module);
+    m_handle = m_dataStore.m_handle;
+  }
+
+  @Override
+  public void close() {
+    freeModule(m_dataStore);
+  }
+
+  @Override
+  public boolean getCompressor() {
+    return REVPHJNI.getCompressor(m_handle);
+  }
+
+  @Override
+  public void setClosedLoopControl(boolean enabled) {
+    REVPHJNI.setClosedLoopControl(m_handle, enabled);
+  }
+
+  @Override
+  public boolean getClosedLoopControl() {
+    return REVPHJNI.getClosedLoopControl(m_handle);
+  }
+
+  @Override
+  public boolean getPressureSwitch() {
+    return REVPHJNI.getPressureSwitch(m_handle);
+  }
+
+  @Override
+  public double getCompressorCurrent() {
+    return REVPHJNI.getCompressorCurrent(m_handle);
+  }
+
+  @Override
+  public void setSolenoids(int mask, int values) {
+    REVPHJNI.setSolenoids(m_handle, mask, values);
+  }
+
+  @Override
+  public int getSolenoids() {
+    return REVPHJNI.getSolenoids(m_handle);
+  }
+
+  @Override
+  public int getModuleNumber() {
+    return m_dataStore.m_module;
+  }
+
+  @Override
+  public void fireOneShot(int index) {
+    // TODO Combine APIs
+    // REVPHJNI.fireOneShot(m_handle, index, durMs);
+  }
+
+  @Override
+  public void setOneShotDuration(int index, int durMs) {
+    // TODO Combine APIs
+    // REVPHJNI.setOneShotDuration(m_handle, index, durMs);
+  }
+
+  @Override
+  public boolean checkSolenoidChannel(int channel) {
+    return REVPHJNI.checkSolenoidChannel(channel);
+  }
+
+  @Override
+  public int checkAndReserveSolenoids(int mask) {
+    synchronized (m_dataStore.m_reserveLock) {
+      if ((m_dataStore.m_reservedMask & mask) != 0) {
+        return m_dataStore.m_reservedMask & mask;
+      }
+      m_dataStore.m_reservedMask |= mask;
+      return 0;
+    }
+  }
+
+  @Override
+  public void unreserveSolenoids(int mask) {
+    synchronized (m_dataStore.m_reserveLock) {
+      m_dataStore.m_reservedMask &= ~mask;
+    }
+  }
+
+  @Override
+  public Solenoid makeSolenoid(int channel) {
+    return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.REVPH, channel);
+  }
+
+  @Override
+  public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
+    return new DoubleSolenoid(
+        m_dataStore.m_module, PneumaticsModuleType.REVPH, forwardChannel, reverseChannel);
+  }
+
+  @Override
+  public Compressor makeCompressor() {
+    return new Compressor(m_dataStore.m_module, PneumaticsModuleType.REVPH);
+  }
+
+  @Override
+  public boolean reserveCompressor() {
+    synchronized (m_dataStore.m_reserveLock) {
+      if (m_dataStore.m_compressorReserved) {
+        return false;
+      }
+      m_dataStore.m_compressorReserved = true;
+      return true;
+    }
+  }
+
+  @Override
+  public void unreserveCompressor() {
+    synchronized (m_dataStore.m_reserveLock) {
+      m_dataStore.m_compressorReserved = false;
+    }
+  }
+
+  @Override
+  public int getSolenoidDisabledList() {
+    // TODO Get this working
+    return 0;
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java
new file mode 100644
index 0000000..67c0f25
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsBase.java
@@ -0,0 +1,128 @@
+// 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;
+
+public interface PneumaticsBase extends AutoCloseable {
+  /**
+   * For internal use to get a module for a specific type.
+   *
+   * @param module module number
+   * @param type module type
+   * @return module
+   */
+  static PneumaticsBase getForType(int module, PneumaticsModuleType type) {
+    if (type == PneumaticsModuleType.CTREPCM) {
+      return new PneumaticsControlModule(module);
+    } else if (type == PneumaticsModuleType.REVPH) {
+      return new PneumaticHub(module);
+    }
+    throw new IllegalArgumentException("Unknown module type");
+  }
+
+  /**
+   * For internal use to get the default for a specific type.
+   *
+   * @param type module type
+   * @return module default
+   */
+  static int getDefaultForType(PneumaticsModuleType type) {
+    if (type == PneumaticsModuleType.CTREPCM) {
+      return SensorUtil.getDefaultCTREPCMModule();
+    } else if (type == PneumaticsModuleType.REVPH) {
+      return SensorUtil.getDefaultREVPHModule();
+    }
+    throw new IllegalArgumentException("Unknown module type");
+  }
+
+  /**
+   * Sets solenoids on a pneumatics module.
+   *
+   * @param mask mask
+   * @param values values
+   */
+  void setSolenoids(int mask, int values);
+
+  /**
+   * Gets solenoid values.
+   *
+   * @return values
+   */
+  int getSolenoids();
+
+  /**
+   * Get module number for this module.
+   *
+   * @return module number
+   */
+  int getModuleNumber();
+
+  /**
+   * Get the disabled solenoids.
+   *
+   * @return disabled list
+   */
+  int getSolenoidDisabledList();
+
+  /**
+   * Fire a single solenoid shot.
+   *
+   * @param index solenoid index
+   */
+  void fireOneShot(int index);
+
+  /**
+   * Set the duration for a single solenoid shot.
+   *
+   * @param index solenoid index
+   * @param durMs shot duration
+   */
+  void setOneShotDuration(int index, int durMs);
+
+  boolean getCompressor();
+
+  boolean getPressureSwitch();
+
+  double getCompressorCurrent();
+
+  void setClosedLoopControl(boolean on);
+
+  boolean getClosedLoopControl();
+
+  /**
+   * Check if a solenoid channel is valid.
+   *
+   * @param channel Channel to check
+   * @return True if channel exists
+   */
+  boolean checkSolenoidChannel(int channel);
+
+  /**
+   * Check to see if the masked solenoids can be reserved, and if not reserve them.
+   *
+   * @param mask The solenoid mask to reserve
+   * @return 0 if successful, mask of solenoids that couldn't be allocated otherwise
+   */
+  int checkAndReserveSolenoids(int mask);
+
+  /**
+   * Unreserve the masked solenoids.
+   *
+   * @param mask The solenoid mask to unreserve
+   */
+  void unreserveSolenoids(int mask);
+
+  boolean reserveCompressor();
+
+  void unreserveCompressor();
+
+  @Override
+  void close();
+
+  Solenoid makeSolenoid(int channel);
+
+  DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel);
+
+  Compressor makeCompressor();
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java
new file mode 100644
index 0000000..79f245b
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsControlModule.java
@@ -0,0 +1,236 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import edu.wpi.first.hal.CTREPCMJNI;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Module class for controlling a Cross The Road Electronics Pneumatics Control Module. */
+public class PneumaticsControlModule implements PneumaticsBase {
+  private static class DataStore implements AutoCloseable {
+    public final int m_module;
+    public final int m_handle;
+    private int m_refCount;
+    private int m_reservedMask;
+    private boolean m_compressorReserved;
+    private final Object m_reserveLock = new Object();
+
+    DataStore(int module) {
+      m_handle = CTREPCMJNI.initialize(module);
+      m_module = module;
+      m_handleMap.put(module, this);
+    }
+
+    @Override
+    public void close() {
+      CTREPCMJNI.free(m_handle);
+      m_handleMap.remove(m_module);
+    }
+
+    public void addRef() {
+      m_refCount++;
+    }
+
+    public void removeRef() {
+      m_refCount--;
+      if (m_refCount == 0) {
+        this.close();
+      }
+    }
+  }
+
+  private static final Map<Integer, DataStore> m_handleMap = new HashMap<>();
+  private static final Object m_handleLock = new Object();
+
+  private static DataStore getForModule(int module) {
+    synchronized (m_handleLock) {
+      Integer moduleBoxed = module;
+      DataStore pcm = m_handleMap.get(moduleBoxed);
+      if (pcm == null) {
+        pcm = new DataStore(module);
+      }
+      pcm.addRef();
+      return pcm;
+    }
+  }
+
+  private static void freeModule(DataStore store) {
+    synchronized (m_handleLock) {
+      store.removeRef();
+    }
+  }
+
+  private final DataStore m_dataStore;
+  private final int m_handle;
+
+  /** Constructs a PneumaticsControlModule with the default id (0). */
+  public PneumaticsControlModule() {
+    this(SensorUtil.getDefaultCTREPCMModule());
+  }
+
+  /**
+   * Constructs a PneumaticsControlModule.
+   *
+   * @param module module number to construct
+   */
+  public PneumaticsControlModule(int module) {
+    m_dataStore = getForModule(module);
+    m_handle = m_dataStore.m_handle;
+  }
+
+  @Override
+  public void close() {
+    freeModule(m_dataStore);
+  }
+
+  @Override
+  public boolean getCompressor() {
+    return CTREPCMJNI.getCompressor(m_handle);
+  }
+
+  @Override
+  public void setClosedLoopControl(boolean enabled) {
+    CTREPCMJNI.setClosedLoopControl(m_handle, enabled);
+  }
+
+  @Override
+  public boolean getClosedLoopControl() {
+    return CTREPCMJNI.getClosedLoopControl(m_handle);
+  }
+
+  @Override
+  public boolean getPressureSwitch() {
+    return CTREPCMJNI.getPressureSwitch(m_handle);
+  }
+
+  @Override
+  public double getCompressorCurrent() {
+    return CTREPCMJNI.getCompressorCurrent(m_handle);
+  }
+
+  public boolean getCompressorCurrentTooHighFault() {
+    return CTREPCMJNI.getCompressorCurrentTooHighFault(m_handle);
+  }
+
+  public boolean getCompressorCurrentTooHighStickyFault() {
+    return CTREPCMJNI.getCompressorCurrentTooHighStickyFault(m_handle);
+  }
+
+  public boolean getCompressorShortedFault() {
+    return CTREPCMJNI.getCompressorShortedFault(m_handle);
+  }
+
+  public boolean getCompressorShortedStickyFault() {
+    return CTREPCMJNI.getCompressorShortedStickyFault(m_handle);
+  }
+
+  public boolean getCompressorNotConnectedFault() {
+    return CTREPCMJNI.getCompressorNotConnectedFault(m_handle);
+  }
+
+  public boolean getCompressorNotConnectedStickyFault() {
+    return CTREPCMJNI.getCompressorNotConnectedStickyFault(m_handle);
+  }
+
+  @Override
+  public void setSolenoids(int mask, int values) {
+    CTREPCMJNI.setSolenoids(m_handle, mask, values);
+  }
+
+  @Override
+  public int getSolenoids() {
+    return CTREPCMJNI.getSolenoids(m_handle);
+  }
+
+  @Override
+  public int getModuleNumber() {
+    return m_dataStore.m_module;
+  }
+
+  @Override
+  public int getSolenoidDisabledList() {
+    return CTREPCMJNI.getSolenoidDisabledList(m_handle);
+  }
+
+  public boolean getSolenoidVoltageFault() {
+    return CTREPCMJNI.getSolenoidVoltageFault(m_handle);
+  }
+
+  public boolean getSolenoidVoltageStickyFault() {
+    return CTREPCMJNI.getSolenoidVoltageStickyFault(m_handle);
+  }
+
+  public void clearAllStickyFaults() {
+    CTREPCMJNI.clearAllStickyFaults(m_handle);
+  }
+
+  @Override
+  public void fireOneShot(int index) {
+    CTREPCMJNI.fireOneShot(m_handle, index);
+  }
+
+  @Override
+  public void setOneShotDuration(int index, int durMs) {
+    CTREPCMJNI.setOneShotDuration(m_handle, index, durMs);
+  }
+
+  @Override
+  public boolean checkSolenoidChannel(int channel) {
+    return CTREPCMJNI.checkSolenoidChannel(channel);
+  }
+
+  @Override
+  public int checkAndReserveSolenoids(int mask) {
+    synchronized (m_dataStore.m_reserveLock) {
+      if ((m_dataStore.m_reservedMask & mask) != 0) {
+        return m_dataStore.m_reservedMask & mask;
+      }
+      m_dataStore.m_reservedMask |= mask;
+      return 0;
+    }
+  }
+
+  @Override
+  public void unreserveSolenoids(int mask) {
+    synchronized (m_dataStore.m_reserveLock) {
+      m_dataStore.m_reservedMask &= ~mask;
+    }
+  }
+
+  @Override
+  public Solenoid makeSolenoid(int channel) {
+    return new Solenoid(m_dataStore.m_module, PneumaticsModuleType.CTREPCM, channel);
+  }
+
+  @Override
+  public DoubleSolenoid makeDoubleSolenoid(int forwardChannel, int reverseChannel) {
+    return new DoubleSolenoid(
+        m_dataStore.m_module, PneumaticsModuleType.CTREPCM, forwardChannel, reverseChannel);
+  }
+
+  @Override
+  public Compressor makeCompressor() {
+    return new Compressor(m_dataStore.m_module, PneumaticsModuleType.CTREPCM);
+  }
+
+  @Override
+  public boolean reserveCompressor() {
+    synchronized (m_dataStore.m_reserveLock) {
+      if (m_dataStore.m_compressorReserved) {
+        return false;
+      }
+      m_dataStore.m_compressorReserved = true;
+      return true;
+    }
+  }
+
+  @Override
+  public void unreserveCompressor() {
+    synchronized (m_dataStore.m_reserveLock) {
+      m_dataStore.m_compressorReserved = false;
+    }
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java
new file mode 100644
index 0000000..a7951e8
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PneumaticsModuleType.java
@@ -0,0 +1,10 @@
+// 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;
+
+public enum PneumaticsModuleType {
+  CTREPCM,
+  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
new file mode 100644
index 0000000..dd97efa
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistribution.java
@@ -0,0 +1,170 @@
+// 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.hal.PowerDistributionJNI;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+
+/**
+ * Class for getting voltage, current, temperature, power and energy from the Power Distribution
+ * Panel over CAN.
+ */
+public class PowerDistribution implements Sendable, AutoCloseable {
+  private final int m_handle;
+  private final int m_module;
+
+  public static final int kDefaultModule = PowerDistributionJNI.DEFAULT_MODULE;
+
+  public enum ModuleType {
+    kAutomatic(PowerDistributionJNI.AUTOMATIC_TYPE),
+    kCTRE(PowerDistributionJNI.CTRE_TYPE),
+    kRev(PowerDistributionJNI.REV_TYPE);
+
+    public final int value;
+
+    ModuleType(int value) {
+      this.value = value;
+    }
+  }
+
+  /**
+   * Constructs a PowerDistribution.
+   *
+   * @param module The CAN ID of the PDP.
+   * @param moduleType Module type (automatic, CTRE, or REV).
+   */
+  public PowerDistribution(int module, ModuleType moduleType) {
+    m_handle = PowerDistributionJNI.initialize(module, moduleType.value);
+    m_module = PowerDistributionJNI.getModuleNumber(m_handle);
+
+    HAL.report(tResourceType.kResourceType_PDP, m_module + 1);
+    SendableRegistry.addLW(this, "PowerDistribution", m_module);
+  }
+
+  /**
+   * Constructs a PowerDistribution.
+   *
+   * <p>Uses the default CAN ID.
+   */
+  public PowerDistribution() {
+    this(kDefaultModule, ModuleType.kAutomatic);
+  }
+
+  @Override
+  public void close() {
+    SendableRegistry.remove(this);
+  }
+
+  /**
+   * Gets the number of channel for this power distribution.
+   *
+   * @return Number of output channels.
+   */
+  public int getNumChannels() {
+    return PowerDistributionJNI.getNumChannels(m_handle);
+  }
+
+  /**
+   * Query the input voltage of the PDP.
+   *
+   * @return The voltage of the PDP in volts
+   */
+  public double getVoltage() {
+    return PowerDistributionJNI.getVoltage(m_handle);
+  }
+
+  /**
+   * Query the temperature of the PDP.
+   *
+   * @return The temperature of the PDP in degrees Celsius
+   */
+  public double getTemperature() {
+    return PowerDistributionJNI.getTemperature(m_handle);
+  }
+
+  /**
+   * Query the current of a single channel of the PDP.
+   *
+   * @param channel The PDP channel to query.
+   * @return The current of one of the PDP channels (channels 0-15) in Amperes
+   */
+  public double getCurrent(int channel) {
+    double current = PowerDistributionJNI.getChannelCurrent(m_handle, channel);
+
+    return current;
+  }
+
+  /**
+   * Query the current of all monitored PDP channels (0-15).
+   *
+   * @return The current of all the channels in Amperes
+   */
+  public double getTotalCurrent() {
+    return PowerDistributionJNI.getTotalCurrent(m_handle);
+  }
+
+  /**
+   * Query the total power drawn from the monitored PDP channels.
+   *
+   * @return the total power in Watts
+   */
+  public double getTotalPower() {
+    return PowerDistributionJNI.getTotalPower(m_handle);
+  }
+
+  /**
+   * Query the total energy drawn from the monitored PDP channels.
+   *
+   * @return the total energy in Joules
+   */
+  public double getTotalEnergy() {
+    return PowerDistributionJNI.getTotalEnergy(m_handle);
+  }
+
+  /** Reset the total energy to 0. */
+  public void resetTotalEnergy() {
+    PowerDistributionJNI.resetTotalEnergy(m_handle);
+  }
+
+  /** Clear all PDP sticky faults. */
+  public void clearStickyFaults() {
+    PowerDistributionJNI.clearStickyFaults(m_handle);
+  }
+
+  /**
+   * Gets module number (CAN ID).
+   *
+   * @return The module number (CAN ID).
+   */
+  public int getModule() {
+    return m_module;
+  }
+
+  public boolean getSwitchableChannel() {
+    return PowerDistributionJNI.getSwitchableChannel(m_handle);
+  }
+
+  public void setSwitchableChannel(boolean enabled) {
+    PowerDistributionJNI.setSwitchableChannel(m_handle, enabled);
+  }
+
+  @Override
+  public void initSendable(SendableBuilder builder) {
+    builder.setSmartDashboardType("PowerDistribution");
+    int numChannels = getNumChannels();
+    for (int i = 0; i < numChannels; ++i) {
+      final int chan = i;
+      builder.addDoubleProperty("Chan" + i, () -> getCurrent(chan), null);
+    }
+    builder.addDoubleProperty("Voltage", this::getVoltage, null);
+    builder.addDoubleProperty("TotalCurrent", this::getTotalCurrent, null);
+    builder.addBooleanProperty(
+        "SwitchableChannel", this::getSwitchableChannel, this::setSwitchableChannel);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistributionPanel.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistributionPanel.java
deleted file mode 100644
index ef46028..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/PowerDistributionPanel.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.hal.PDPJNI;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Class for getting voltage, current, temperature, power and energy from the Power Distribution
- * Panel over CAN.
- */
-public class PowerDistributionPanel implements Sendable, AutoCloseable {
-  private final int m_handle;
-  private final int m_module;
-
-  /**
-   * Constructor.
-   *
-   * @param module The CAN ID of the PDP
-   */
-  public PowerDistributionPanel(int module) {
-    SensorUtil.checkPDPModule(module);
-    m_handle = PDPJNI.initializePDP(module);
-    m_module = module;
-
-    HAL.report(tResourceType.kResourceType_PDP, module + 1);
-    SendableRegistry.addLW(this, "PowerDistributionPanel", module);
-  }
-
-  /**
-   * Constructor.  Uses the default CAN ID (0).
-   */
-  public PowerDistributionPanel() {
-    this(0);
-  }
-
-  @Override
-  public void close() {
-    SendableRegistry.remove(this);
-  }
-
-  /**
-   * Query the input voltage of the PDP.
-   *
-   * @return The voltage of the PDP in volts
-   */
-  public double getVoltage() {
-    return PDPJNI.getPDPVoltage(m_handle);
-  }
-
-  /**
-   * Query the temperature of the PDP.
-   *
-   * @return The temperature of the PDP in degrees Celsius
-   */
-  public double getTemperature() {
-    return PDPJNI.getPDPTemperature(m_handle);
-  }
-
-  /**
-   * Query the current of a single channel of the PDP.
-   *
-   * @return The current of one of the PDP channels (channels 0-15) in Amperes
-   */
-  public double getCurrent(int channel) {
-    double current = PDPJNI.getPDPChannelCurrent((byte) channel, m_handle);
-
-    SensorUtil.checkPDPChannel(channel);
-
-    return current;
-  }
-
-  /**
-   * Query the current of all monitored PDP channels (0-15).
-   *
-   * @return The current of all the channels in Amperes
-   */
-  public double getTotalCurrent() {
-    return PDPJNI.getPDPTotalCurrent(m_handle);
-  }
-
-  /**
-   * Query the total power drawn from the monitored PDP channels.
-   *
-   * @return the total power in Watts
-   */
-  public double getTotalPower() {
-    return PDPJNI.getPDPTotalPower(m_handle);
-  }
-
-  /**
-   * Query the total energy drawn from the monitored PDP channels.
-   *
-   * @return the total energy in Joules
-   */
-  public double getTotalEnergy() {
-    return PDPJNI.getPDPTotalEnergy(m_handle);
-  }
-
-  /**
-   * Reset the total energy to 0.
-   */
-  public void resetTotalEnergy() {
-    PDPJNI.resetPDPTotalEnergy(m_handle);
-  }
-
-  /**
-   * Clear all PDP sticky faults.
-   */
-  public void clearStickyFaults() {
-    PDPJNI.clearPDPStickyFaults(m_handle);
-  }
-
-  /**
-   * Gets module number (CAN ID).
-   */
-  public int getModule() {
-    return m_module;
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("PowerDistributionPanel");
-    for (int i = 0; i < SensorUtil.kPDPChannels; ++i) {
-      final int chan = i;
-      builder.addDoubleProperty("Chan" + i, () -> getCurrent(chan), null);
-    }
-    builder.addDoubleProperty("Voltage", this::getVoltage, null);
-    builder.addDoubleProperty("TotalCurrent", this::getTotalCurrent, null);
-  }
-}
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 0edbbf3..a15fb70 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java
@@ -1,13 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.Collection;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
@@ -15,41 +12,36 @@
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import java.util.Collection;
 
 /**
  * The preferences class provides a relatively simple way to save important values to the roboRIO to
  * access the next time the roboRIO is booted.
  *
- * <p> This class loads and saves from a file inside the roboRIO. The user can not access the file
+ * <p>This class loads and saves from a file inside the roboRIO. The user can not access the file
  * directly, but may modify values at specific fields which will then be automatically saved to the
- * file by the NetworkTables server. </p>
+ * file by the NetworkTables server.
  *
- * <p> This class is thread safe. </p>
+ * <p>This class is thread safe.
  *
- * <p> This will also interact with {@link NetworkTable} by creating a table called "Preferences"
- * with all the key-value pairs. </p>
+ * <p>This will also interact with {@link NetworkTable} by creating a table called "Preferences"
+ * with all the key-value pairs.
  */
 public final class Preferences {
-  /**
-   * The Preferences table name.
-   */
+  /** The Preferences table name. */
   private static final String TABLE_NAME = "Preferences";
-  /**
-   * The singleton instance.
-   */
+  /** The singleton instance. */
   private static Preferences instance;
-  /**
-   * The network table.
-   */
-  private final NetworkTable m_table;
+  /** The network table. */
+  private static final NetworkTable m_table;
 
   /**
    * Returns the preferences instance.
    *
    * @return the preferences instance
+   * @deprecated Use the static methods
    */
+  @Deprecated
   public static synchronized Preferences getInstance() {
     if (instance == null) {
       instance = new Preferences();
@@ -57,10 +49,10 @@
     return instance;
   }
 
-  /**
-   * Creates a preference class.
-   */
-  private Preferences() {
+  /** Creates a preference class. */
+  private Preferences() {}
+
+  static {
     m_table = NetworkTableInstance.getDefault().getTable(TABLE_NAME);
     m_table.getEntry(".type").setString("RobotPreferences");
     // Listener to set all Preferences values to persistent
@@ -73,21 +65,22 @@
 
   /**
    * Gets the preferences keys.
+   *
    * @return a collection of the keys
    */
-  public Collection<String> getKeys() {
+  public static Collection<String> getKeys() {
     return m_table.getKeys();
   }
 
   /**
    * Puts the given string into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    * @throws NullPointerException if value is null
    */
-  public void putString(String key, String value) {
-    requireNonNullParam(value, "value", "putString");
+  public static void setString(String key, String value) {
+    requireNonNullParam(value, "value", "setString");
 
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setString(value);
@@ -95,12 +88,25 @@
   }
 
   /**
+   * Puts the given string into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @throws NullPointerException if value is null
+   * @deprecated Use {@link #setString(String, String)}
+   */
+  @Deprecated
+  public static void putString(String key, String value) {
+    setString(key, value);
+  }
+
+  /**
    * Puts the given string into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initString(String key, String value) {
+  public static void initString(String key, String value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultString(value);
   }
@@ -108,22 +114,34 @@
   /**
    * Puts the given int into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    */
-  public void putInt(String key, int value) {
+  public static void setInt(String key, int value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDouble(value);
     entry.setPersistent();
   }
 
   /**
+   * Puts the given int into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @deprecated Use {@link #setInt(String, int)}
+   */
+  @Deprecated
+  public static void putInt(String key, int value) {
+    setInt(key, value);
+  }
+
+  /**
    * Puts the given int into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initInt(String key, int value) {
+  public static void initInt(String key, int value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultDouble(value);
   }
@@ -131,22 +149,34 @@
   /**
    * Puts the given double into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    */
-  public void putDouble(String key, double value) {
+  public static void setDouble(String key, double value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDouble(value);
     entry.setPersistent();
   }
 
   /**
+   * Puts the given double into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @deprecated Use {@link #setDouble(String, double)}
+   */
+  @Deprecated
+  public static void putDouble(String key, double value) {
+    setDouble(key, value);
+  }
+
+  /**
    * Puts the given double into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initDouble(String key, double value) {
+  public static void initDouble(String key, double value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultDouble(value);
   }
@@ -154,22 +184,34 @@
   /**
    * Puts the given float into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    */
-  public void putFloat(String key, float value) {
+  public static void setFloat(String key, float value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDouble(value);
     entry.setPersistent();
   }
 
   /**
+   * Puts the given float into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @deprecated Use {@link #setFloat(String, float)}
+   */
+  @Deprecated
+  public static void putFloat(String key, float value) {
+    setFloat(key, value);
+  }
+
+  /**
    * Puts the given float into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initFloat(String key, float value) {
+  public static void initFloat(String key, float value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultDouble(value);
   }
@@ -177,22 +219,34 @@
   /**
    * Puts the given boolean into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    */
-  public void putBoolean(String key, boolean value) {
+  public static void setBoolean(String key, boolean value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setBoolean(value);
     entry.setPersistent();
   }
 
   /**
+   * Puts the given boolean into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @deprecated Use {@link #setBoolean(String, boolean)}
+   */
+  @Deprecated
+  public static void putBoolean(String key, boolean value) {
+    setBoolean(key, value);
+  }
+
+  /**
    * Puts the given boolean into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initBoolean(String key, boolean value) {
+  public static void initBoolean(String key, boolean value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultBoolean(value);
   }
@@ -200,22 +254,34 @@
   /**
    * Puts the given long into the preferences table.
    *
-   * @param key   the key
+   * @param key the key
    * @param value the value
    */
-  public void putLong(String key, long value) {
+  public static void setLong(String key, long value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDouble(value);
     entry.setPersistent();
   }
 
   /**
+   * Puts the given long into the preferences table.
+   *
+   * @param key the key
+   * @param value the value
+   * @deprecated Use {@link #setLong(String, long)}
+   */
+  @Deprecated
+  public static void putLong(String key, long value) {
+    setLong(key, value);
+  }
+
+  /**
    * Puts the given long into the preferences table if it doesn't already exist.
    *
-   * @param key   The key
+   * @param key The key
    * @param value The value
    */
-  public void initLong(String key, long value) {
+  public static void initLong(String key, long value) {
     NetworkTableEntry entry = m_table.getEntry(key);
     entry.setDefaultDouble(value);
   }
@@ -226,7 +292,7 @@
    * @param key the key
    * @return if there is a value at the given key
    */
-  public boolean containsKey(String key) {
+  public static boolean containsKey(String key) {
     return m_table.containsKey(key);
   }
 
@@ -235,14 +301,12 @@
    *
    * @param key the key
    */
-  public void remove(String key) {
+  public static void remove(String key) {
     m_table.delete(key);
   }
 
-  /**
-   * Remove all preferences.
-   */
-  public void removeAll() {
+  /** Remove all preferences. */
+  public static void removeAll() {
     for (String key : m_table.getKeys()) {
       if (!".type".equals(key)) {
         remove(key);
@@ -254,11 +318,11 @@
    * Returns the string at the given key. If this table does not have a value for that position,
    * then the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public String getString(String key, String backup) {
+  public static String getString(String key, String backup) {
     return m_table.getEntry(key).getString(backup);
   }
 
@@ -266,11 +330,11 @@
    * Returns the int at the given key. If this table does not have a value for that position, then
    * the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public int getInt(String key, int backup) {
+  public static int getInt(String key, int backup) {
     return (int) m_table.getEntry(key).getDouble(backup);
   }
 
@@ -278,11 +342,11 @@
    * Returns the double at the given key. If this table does not have a value for that position,
    * then the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public double getDouble(String key, double backup) {
+  public static double getDouble(String key, double backup) {
     return m_table.getEntry(key).getDouble(backup);
   }
 
@@ -290,11 +354,11 @@
    * Returns the boolean at the given key. If this table does not have a value for that position,
    * then the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public boolean getBoolean(String key, boolean backup) {
+  public static boolean getBoolean(String key, boolean backup) {
     return m_table.getEntry(key).getBoolean(backup);
   }
 
@@ -302,11 +366,11 @@
    * Returns the float at the given key. If this table does not have a value for that position, then
    * the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public float getFloat(String key, float backup) {
+  public static float getFloat(String key, float backup) {
     return (float) m_table.getEntry(key).getDouble(backup);
   }
 
@@ -314,11 +378,11 @@
    * Returns the long at the given key. If this table does not have a value for that position, then
    * the given backup value will be returned.
    *
-   * @param key    the key
+   * @param key the key
    * @param backup the value to return if none exists in the table
    * @return either the value in the table, or the backup
    */
-  public long getLong(String key, long backup) {
+  public static long getLong(String key, long backup) {
     return (long) m_table.getEntry(key).getDouble(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 f379de4..05efebd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Relay.java
@@ -1,32 +1,30 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.Arrays;
-import java.util.Optional;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.RelayJNI;
+import edu.wpi.first.hal.util.HalHandleException;
 import edu.wpi.first.hal.util.UncleanStatusException;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.util.Arrays;
+import java.util.Optional;
 
 /**
  * Class for VEX Robotics Spike style relay outputs. Relays are intended to be connected to Spikes
  * or similar relays. The relay channels controls a pair of channels that are either both off, one
- * on, the other on, or both on. This translates into two Spike outputs at 0v, one at 12v and one
- * at 0v, one at 0v and the other at 12v, or two Spike outputs at 12V. This allows off, full
- * forward, or full reverse control of motors without variable speed. It also allows the two
- * channels (forward and reverse) to be used independently for something that does not care about
- * voltage polarity (like a solenoid).
+ * on, the other on, or both on. This translates into two Spike outputs at 0v, one at 12v and one at
+ * 0v, one at 0v and the other at 12v, or two Spike outputs at 12V. This allows off, full forward,
+ * or full reverse control of motors without variable speed. It also allows the two channels
+ * (forward and reverse) to be used independently for something that does not care about voltage
+ * polarity (like a solenoid).
  */
 public class Relay extends MotorSafety implements Sendable, AutoCloseable {
   /**
@@ -45,9 +43,7 @@
     }
   }
 
-  /**
-   * The state to drive a Relay to.
-   */
+  /** The state to drive a Relay to. */
   public enum Value {
     kOff("Off"),
     kOn("On"),
@@ -69,22 +65,13 @@
     }
   }
 
-  /**
-   * The Direction(s) that a relay is configured to operate in.
-   */
+  /** The Direction(s) that a relay is configured to operate in. */
   public enum Direction {
-    /**
-     * direction: both directions are valid.
-     */
-
+    /** direction: both directions are valid. */
     kBoth,
-    /**
-     * direction: Only forward is valid.
-     */
+    /** direction: Only forward is valid. */
     kForward,
-    /**
-     * direction: only reverse is valid.
-     */
+    /** direction: only reverse is valid. */
     kReverse
   }
 
@@ -149,12 +136,12 @@
   private void freeRelay() {
     try {
       RelayJNI.setRelay(m_forwardHandle, false);
-    } catch (UncleanStatusException ignored) {
+    } catch (UncleanStatusException | HalHandleException ignored) {
       // do nothing. Ignore
     }
     try {
       RelayJNI.setRelay(m_reverseHandle, false);
-    } catch (UncleanStatusException ignored) {
+    } catch (UncleanStatusException | HalHandleException ignored) {
       // do nothing. Ignore
     }
 
@@ -178,7 +165,6 @@
    *
    * @param value The state to set the relay.
    */
-  @SuppressWarnings("PMD.CyclomaticComplexity")
   public void set(Value value) {
     switch (value) {
       case kOff:
@@ -199,8 +185,8 @@
         break;
       case kForward:
         if (m_direction == Direction.kReverse) {
-          throw new InvalidValueException("A relay configured for reverse cannot be set to "
-              + "forward");
+          throw new InvalidValueException(
+              "A relay configured for reverse cannot be set to " + "forward");
         }
         if (m_direction == Direction.kBoth || m_direction == Direction.kForward) {
           RelayJNI.setRelay(m_forwardHandle, true);
@@ -211,8 +197,8 @@
         break;
       case kReverse:
         if (m_direction == Direction.kForward) {
-          throw new InvalidValueException("A relay configured for forward cannot be set to "
-              + "reverse");
+          throw new InvalidValueException(
+              "A relay configured for forward cannot be set to " + "reverse");
         }
         if (m_direction == Direction.kBoth) {
           RelayJNI.setRelay(m_forwardHandle, false);
@@ -310,7 +296,9 @@
     builder.setSmartDashboardType("Relay");
     builder.setActuator(true);
     builder.setSafeState(() -> set(Value.kOff));
-    builder.addStringProperty("Value", () -> get().getPrettyValue(),
+    builder.addStringProperty(
+        "Value",
+        () -> get().getPrettyValue(),
         value -> set(Value.getValueOf(value).orElse(Value.kOff)));
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Resource.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Resource.java
index 39a0eac..09137d6 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Resource.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Resource.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -29,9 +26,7 @@
   private final int m_size;
   private final Resource m_nextResource;
 
-  /**
-   * Clears all allocated resources.
-   */
+  /** Clears all allocated resources. */
   public static void restartProgram() {
     for (Resource r = Resource.resourceList; r != null; r = r.m_nextResource) {
       for (int i = 0; i < r.m_size; i++) {
@@ -42,8 +37,8 @@
 
   /**
    * Allocate storage for a new instance of Resource. Allocate a bool array of values that will get
-   * initialized to indicate that no resources have been allocated yet. The indices of the
-   * resources are 0..size-1.
+   * initialized to indicate that no resources have been allocated yet. The indices of the resources
+   * are 0..size-1.
    *
    * @param size The number of blocks to allocate
    */
@@ -106,5 +101,4 @@
     }
     m_numAllocated[index] = false;
   }
-
 }
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 9c3e45f..8d1fb07 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java
@@ -1,23 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Supplier;
-
-import edu.wpi.cscore.CameraServerJNI;
 import edu.wpi.first.cameraserver.CameraServerShared;
 import edu.wpi.first.cameraserver.CameraServerSharedStore;
+import edu.wpi.first.cscore.CameraServerJNI;
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
@@ -29,6 +18,13 @@
 import edu.wpi.first.wpilibj.livewindow.LiveWindow;
 import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard;
 import edu.wpi.first.wpilibj.util.WPILibVersion;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
 
 /**
  * Implement a Robot Program framework. The RobotBase class is intended to be subclassed by a user
@@ -38,106 +34,107 @@
  * might be spawned as a task, then killed at the end of the Autonomous period.
  */
 public abstract class RobotBase implements AutoCloseable {
-  /**
-   * The ID of the main Java thread.
-   */
+  /** The ID of the main Java thread. */
   // This is usually 1, but it is best to make sure
   private static long m_threadId = -1;
 
   private static void setupCameraServerShared() {
-    CameraServerShared shared = new CameraServerShared() {
+    CameraServerShared shared =
+        new CameraServerShared() {
+          @Override
+          public void reportVideoServer(int id) {
+            HAL.report(tResourceType.kResourceType_PCVideoServer, id + 1);
+          }
 
-      @Override
-      public void reportVideoServer(int id) {
-        HAL.report(tResourceType.kResourceType_PCVideoServer, id + 1);
-      }
+          @Override
+          public void reportUsbCamera(int id) {
+            HAL.report(tResourceType.kResourceType_UsbCamera, id + 1);
+          }
 
-      @Override
-      public void reportUsbCamera(int id) {
-        HAL.report(tResourceType.kResourceType_UsbCamera, id + 1);
-      }
+          @Override
+          public void reportDriverStationError(String error) {
+            DriverStation.reportError(error, true);
+          }
 
-      @Override
-      public void reportDriverStationError(String error) {
-        DriverStation.reportError(error, true);
-      }
+          @Override
+          public void reportAxisCamera(int id) {
+            HAL.report(tResourceType.kResourceType_AxisCamera, id + 1);
+          }
 
-      @Override
-      public void reportAxisCamera(int id) {
-        HAL.report(tResourceType.kResourceType_AxisCamera, id + 1);
-      }
+          @Override
+          public Long getRobotMainThreadId() {
+            return RobotBase.getMainThreadId();
+          }
 
-      @Override
-      public Long getRobotMainThreadId() {
-        return RobotBase.getMainThreadId();
-      }
-
-      @Override
-      public boolean isRoboRIO() {
-        return RobotBase.isReal();
-      }
-    };
+          @Override
+          public boolean isRoboRIO() {
+            return RobotBase.isReal();
+          }
+        };
 
     CameraServerSharedStore.setCameraServerShared(shared);
   }
 
   private static void setupMathShared() {
-    MathSharedStore.setMathShared(new MathShared() {
-      @Override
-      public void reportError(String error, StackTraceElement[] stackTrace) {
-        DriverStation.reportError(error, stackTrace);
-      }
+    MathSharedStore.setMathShared(
+        new MathShared() {
+          @Override
+          public void reportError(String error, StackTraceElement[] stackTrace) {
+            DriverStation.reportError(error, stackTrace);
+          }
 
-      @Override
-      public void reportUsage(MathUsageId id, int count) {
-        switch (id) {
-          case kKinematics_DifferentialDrive:
-            HAL.report(tResourceType.kResourceType_Kinematics,
-                tInstances.kKinematics_DifferentialDrive);
-            break;
-          case kKinematics_MecanumDrive:
-            HAL.report(tResourceType.kResourceType_Kinematics,
-                tInstances.kKinematics_MecanumDrive);
-            break;
-          case kKinematics_SwerveDrive:
-            HAL.report(tResourceType.kResourceType_Kinematics,
-                tInstances.kKinematics_SwerveDrive);
-            break;
-          case kTrajectory_TrapezoidProfile:
-            HAL.report(tResourceType.kResourceType_TrapezoidProfile, count);
-            break;
-          case kFilter_Linear:
-            HAL.report(tResourceType.kResourceType_LinearFilter, count);
-            break;
-          case kOdometry_DifferentialDrive:
-            HAL.report(tResourceType.kResourceType_Odometry,
-                tInstances.kOdometry_DifferentialDrive);
-            break;
-          case kOdometry_SwerveDrive:
-            HAL.report(tResourceType.kResourceType_Odometry,
-                tInstances.kOdometry_SwerveDrive);
-            break;
-          case kOdometry_MecanumDrive:
-            HAL.report(tResourceType.kResourceType_Odometry,
-                tInstances.kOdometry_MecanumDrive);
-            break;
-          default:
-            break;
-        }
-      }
-    });
+          @Override
+          public void reportUsage(MathUsageId id, int count) {
+            switch (id) {
+              case kKinematics_DifferentialDrive:
+                HAL.report(
+                    tResourceType.kResourceType_Kinematics,
+                    tInstances.kKinematics_DifferentialDrive);
+                break;
+              case kKinematics_MecanumDrive:
+                HAL.report(
+                    tResourceType.kResourceType_Kinematics, tInstances.kKinematics_MecanumDrive);
+                break;
+              case kKinematics_SwerveDrive:
+                HAL.report(
+                    tResourceType.kResourceType_Kinematics, tInstances.kKinematics_SwerveDrive);
+                break;
+              case kTrajectory_TrapezoidProfile:
+                HAL.report(tResourceType.kResourceType_TrapezoidProfile, count);
+                break;
+              case kFilter_Linear:
+                HAL.report(tResourceType.kResourceType_LinearFilter, count);
+                break;
+              case kOdometry_DifferentialDrive:
+                HAL.report(
+                    tResourceType.kResourceType_Odometry, tInstances.kOdometry_DifferentialDrive);
+                break;
+              case kOdometry_SwerveDrive:
+                HAL.report(tResourceType.kResourceType_Odometry, tInstances.kOdometry_SwerveDrive);
+                break;
+              case kOdometry_MecanumDrive:
+                HAL.report(tResourceType.kResourceType_Odometry, tInstances.kOdometry_MecanumDrive);
+                break;
+              case kController_PIDController2:
+                HAL.report(tResourceType.kResourceType_PIDController2, count);
+                break;
+              case kController_ProfiledPIDController:
+                HAL.report(tResourceType.kResourceType_ProfiledPIDController, count);
+                break;
+              default:
+                break;
+            }
+          }
+        });
   }
 
-  protected final DriverStation m_ds;
-
   /**
    * Constructor for a generic robot program. User code should be placed in the constructor that
    * runs before the Autonomous or Operator Control period starts. The constructor will run to
    * completion before Autonomous is entered.
    *
    * <p>This must be used to ensure that the communications code starts. In the future it would be
-   * nice
-   * to put this code into it's own task that loads on boot so ensure that it runs.
+   * nice to put this code into it's own task that loads on boot so ensure that it runs.
    */
   protected RobotBase() {
     final NetworkTableInstance inst = NetworkTableInstance.getDefault();
@@ -150,7 +147,6 @@
     } else {
       inst.startServer();
     }
-    m_ds = DriverStation.getInstance();
     inst.getTable("LiveWindow").getSubTable(".status").getEntry("LW Enabled").setBoolean(false);
 
     LiveWindow.setEnabled(false);
@@ -162,7 +158,15 @@
   }
 
   @Override
-  public void close() {
+  public void close() {}
+
+  /**
+   * Get the current runtime type.
+   *
+   * @return Current runtime type.
+   */
+  public static RuntimeType getRuntimeType() {
+    return RuntimeType.getValue(HALUtil.getHALRuntimeType());
   }
 
   /**
@@ -180,7 +184,8 @@
    * @return If the robot is running in the real world.
    */
   public static boolean isReal() {
-    return HALUtil.getHALRuntimeType() == 0;
+    RuntimeType runtimeType = getRuntimeType();
+    return runtimeType == RuntimeType.kRoboRIO || runtimeType == RuntimeType.kRoboRIO2;
   }
 
   /**
@@ -189,7 +194,7 @@
    * @return True if the Robot is currently disabled by the field controls.
    */
   public boolean isDisabled() {
-    return m_ds.isDisabled();
+    return DriverStation.isDisabled();
   }
 
   /**
@@ -198,37 +203,47 @@
    * @return True if the Robot is currently enabled by the field controls.
    */
   public boolean isEnabled() {
-    return m_ds.isEnabled();
+    return DriverStation.isEnabled();
   }
 
   /**
-   * Determine if the robot is currently in Autonomous mode as determined by the field
-   * controls.
+   * Determine if the robot is currently in Autonomous mode as determined by the field controls.
    *
    * @return True if the robot is currently operating Autonomously.
    */
   public boolean isAutonomous() {
-    return m_ds.isAutonomous();
+    return DriverStation.isAutonomous();
   }
 
   /**
-   * Determine if the robot is current in Autonomous mode and enabled as determined by
-   * the field controls.
+   * Determine if the robot is current in Autonomous mode and enabled as determined by the field
+   * controls.
    *
    * @return True if the robot is currently operating autonomously while enabled.
    */
   public boolean isAutonomousEnabled() {
-    return m_ds.isAutonomousEnabled();
+    return DriverStation.isAutonomousEnabled();
   }
 
   /**
-   * Determine if the robot is currently in Test mode as determined by the driver
-   * station.
+   * Determine if the robot is currently in Test mode as determined by the driver station.
    *
    * @return True if the robot is currently operating in Test mode.
    */
   public boolean isTest() {
-    return m_ds.isTest();
+    return DriverStation.isTest();
+  }
+
+  /**
+   * Determine if the robot is currently in Operator Control mode as determined by the field
+   * controls.
+   *
+   * @return True if the robot is currently operating in Tele-Op mode.
+   * @deprecated Use isTeleop() instead.
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public boolean isOperatorControl() {
+    return DriverStation.isTeleop();
   }
 
   /**
@@ -237,18 +252,30 @@
    *
    * @return True if the robot is currently operating in Tele-Op mode.
    */
-  public boolean isOperatorControl() {
-    return m_ds.isOperatorControl();
+  public boolean isTeleop() {
+    return DriverStation.isTeleop();
   }
 
   /**
-   * Determine if the robot is current in Operator Control mode and enabled as determined by
-   * the field controls.
+   * Determine if the robot is current in Operator Control mode and enabled as determined by the
+   * field controls.
+   *
+   * @return True if the robot is currently operating in Tele-Op mode while enabled.
+   * @deprecated Use isTeleopEnabled() instead.
+   */
+  @Deprecated(since = "2022", forRemoval = true)
+  public boolean isOperatorControlEnabled() {
+    return DriverStation.isTeleopEnabled();
+  }
+
+  /**
+   * Determine if the robot is current in Operator Control mode and enabled as determined by the
+   * field controls.
    *
    * @return True if the robot is currently operating in Tele-Op mode while enabled.
    */
-  public boolean isOperatorControlEnabled() {
-    return m_ds.isOperatorControlEnabled();
+  public boolean isTeleopEnabled() {
+    return DriverStation.isTeleopEnabled();
   }
 
   /**
@@ -257,20 +284,16 @@
    * @return Has new data arrived over the network since the last time this function was called?
    */
   public boolean isNewDataAvailable() {
-    return m_ds.isNewControlData();
+    return DriverStation.isNewControlData();
   }
 
-  /**
-   * Provide an alternate "main loop" via startCompetition().
-   */
+  /** Provide an alternate "main loop" via startCompetition(). */
   public abstract void startCompetition();
 
-  /**
-   * Ends the main loop in startCompetition().
-   */
+  /** Ends the main loop in startCompetition(). */
   public abstract void endCompetition();
 
-  @SuppressWarnings("JavadocMethod")
+  @SuppressWarnings("MissingJavadocMethod")
   public static boolean getBooleanProperty(String name, boolean defaultValue) {
     String propVal = System.getProperty(name);
     if (propVal == null) {
@@ -289,11 +312,8 @@
   private static RobotBase m_robotCopy;
   private static boolean m_suppressExitWarning;
 
-  /**
-   * Run the robot main loop.
-   */
-  @SuppressWarnings({"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidCatchingThrowable",
-                     "PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
+  /** Run the robot main loop. */
+  @SuppressWarnings("PMD.AvoidCatchingThrowable")
   private static <T extends RobotBase> void runRobot(Supplier<T> robotSupplier) {
     System.out.println("********** Robot program starting **********");
 
@@ -310,9 +330,15 @@
       if (elements.length > 0) {
         robotName = elements[0].getClassName();
       }
-      DriverStation.reportError("Unhandled exception instantiating robot " + robotName + " "
-          + throwable.toString(), elements);
-      DriverStation.reportWarning("Robots should not quit, but yours did!", false);
+      DriverStation.reportError(
+          "Unhandled exception instantiating robot " + robotName + " " + throwable.toString(),
+          elements);
+      DriverStation.reportError(
+          "The robot program quit unexpectedly."
+              + " This is usually due to a code error.\n"
+              + "  The above stacktrace can help determine where the error occurred.\n"
+              + "  See https://wpilib.org/stacktrace for more information.\n",
+          false);
       DriverStation.reportError("Could not instantiate robot " + robotName + "!", false);
       return;
     }
@@ -322,23 +348,23 @@
     m_runMutex.unlock();
 
     if (isReal()) {
+      final File file = new File("/tmp/frc_versions/FRC_Lib_Version.ini");
       try {
-        final File file = new File("/tmp/frc_versions/FRC_Lib_Version.ini");
-
-        if (file.exists()) {
-          file.delete();
+        if (file.exists() && !file.delete()) {
+          throw new IOException("Failed to delete FRC_Lib_Version.ini");
         }
 
-        file.createNewFile();
+        if (!file.createNewFile()) {
+          throw new IOException("Failed to create new FRC_Lib_Version.ini");
+        }
 
         try (OutputStream output = Files.newOutputStream(file.toPath())) {
           output.write("Java ".getBytes(StandardCharsets.UTF_8));
           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.toString(), ex.getStackTrace());
       }
     }
 
@@ -350,8 +376,8 @@
       if (cause != null) {
         throwable = cause;
       }
-      DriverStation.reportError("Unhandled exception: " + throwable.toString(),
-          throwable.getStackTrace());
+      DriverStation.reportError(
+          "Unhandled exception: " + throwable.toString(), throwable.getStackTrace());
       errorOnExit = true;
     } finally {
       m_runMutex.lock();
@@ -359,11 +385,17 @@
       m_runMutex.unlock();
       if (!suppressExitWarning) {
         // startCompetition never returns unless exception occurs....
-        DriverStation.reportWarning("Robots should not quit, but yours did!", false);
+        DriverStation.reportWarning(
+            "The robot program quit unexpectedly."
+                + " This is usually due to a code error.\n"
+                + "  The above stacktrace can help determine where the error occurred.\n"
+                + "  See https://wpilib.org/stacktrace for more information.",
+            false);
         if (errorOnExit) {
           DriverStation.reportError(
               "The startCompetition() method (or methods called by it) should have "
-                  + "handled the exception above.", false);
+                  + "handled the exception above.",
+              false);
         } else {
           DriverStation.reportError("Unexpected return from startCompetition() method.", false);
         }
@@ -372,7 +404,9 @@
   }
 
   /**
-   * Suppress the "Robots should not quit" message.
+   * Suppress the "The robot program quit unexpectedly." message.
+   *
+   * @param value True if exit warning should be suppressed.
    */
   public static void suppressExitWarning(boolean value) {
     m_runMutex.lock();
@@ -382,6 +416,9 @@
 
   /**
    * Starting point for the applications.
+   *
+   * @param <T> Robot subclass.
+   * @param robotSupplier Function that returns an instance of the robot subclass.
    */
   public static <T extends RobotBase> void startRobot(Supplier<T> robotSupplier) {
     if (!HAL.initialize(500, 0)) {
@@ -392,14 +429,21 @@
     // Needed because all the OpenCV JNI functions don't have built in loading
     CameraServerJNI.enumerateSinks();
 
-    HAL.report(tResourceType.kResourceType_Language, tInstances.kLanguage_Java, 0,
-        WPILibVersion.Version);
+    HAL.report(
+        tResourceType.kResourceType_Language, tInstances.kLanguage_Java, 0, WPILibVersion.Version);
+
+    if (!Notifier.setHALThreadPriority(true, 40)) {
+      DriverStation.reportWarning("Setting HAL Notifier RT priority to 40 failed", false);
+    }
 
     if (HAL.hasMain()) {
-      Thread thread = new Thread(() -> {
-        runRobot(robotSupplier);
-        HAL.exitMain();
-      }, "robot main");
+      Thread thread =
+          new Thread(
+              () -> {
+                runRobot(robotSupplier);
+                HAL.exitMain();
+              },
+              "robot main");
       thread.setDaemon(true);
       thread.start();
       HAL.runMain();
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 0623812..8a7460e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotController.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -13,17 +10,14 @@
 import edu.wpi.first.hal.can.CANJNI;
 import edu.wpi.first.hal.can.CANStatus;
 
-/**
- * Contains functions for roboRIO functionality.
- */
+/** Contains functions for roboRIO functionality. */
 public final class RobotController {
   private RobotController() {
     throw new UnsupportedOperationException("This is a utility class!");
   }
 
   /**
-   * Return the FPGA Version number. For now, expect this to be the current
-   * year.
+   * Return the FPGA Version number. For now, expect this to be the current year.
    *
    * @return FPGA Version number.
    */
@@ -120,7 +114,7 @@
   /**
    * Get the current output of the 3.3V rail.
    *
-   * @return The controller 3.3V rail output current value in Volts
+   * @return The controller 3.3V rail output current value in Amps
    */
   public static double getCurrent3V3() {
     return PowerJNI.getUserCurrent3V3();
@@ -220,13 +214,33 @@
   }
 
   /**
+   * Get the current brownout voltage setting.
+   *
+   * @return The brownout voltage
+   */
+  public static double getBrownoutVoltage() {
+    return PowerJNI.getBrownoutVoltage();
+  }
+
+  /**
+   * Set the voltage the roboRIO will brownout and disable all outputs.
+   *
+   * <p>Note that this only does anything on the roboRIO 2. On the roboRIO it is a no-op.
+   *
+   * @param brownoutVoltage The brownout voltage
+   */
+  public static void setBrownoutVoltage(double brownoutVoltage) {
+    PowerJNI.setBrownoutVoltage(brownoutVoltage);
+  }
+
+  /**
    * Get the current status of the CAN bus.
    *
    * @return The status of the CAN bus
    */
   public static CANStatus getCANStatus() {
     CANStatus status = new CANStatus();
-    CANJNI.GetCANStatus(status);
+    CANJNI.getCANStatus(status);
     return status;
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotDrive.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotDrive.java
deleted file mode 100644
index f79d74a..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotDrive.java
+++ /dev/null
@@ -1,716 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tInstances;
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-
-/**
- * Utility class for handling Robot drive based on a definition of the motor configuration. The
- * robot drive class handles basic driving for a robot. Currently, 2 and 4 motor tank and mecanum
- * drive trains are supported. In the future other drive types like swerve might be implemented.
- * Motor channel numbers are supplied on creation of the class. Those are used for either the drive
- * function (intended for hand created drive code, such as autonomous) or with the Tank/Arcade
- * functions intended to be used for Operator Control driving.
- *
- * @deprecated Use {@link edu.wpi.first.wpilibj.drive.DifferentialDrive}
- *             or {@link edu.wpi.first.wpilibj.drive.MecanumDrive} classes instead.
- */
-@Deprecated
-@SuppressWarnings("PMD.GodClass")
-public class RobotDrive extends MotorSafety implements AutoCloseable {
-  /**
-   * The location of a motor on the robot for the purpose of driving.
-   */
-  public enum MotorType {
-    kFrontLeft(0), kFrontRight(1), kRearLeft(2), kRearRight(3);
-
-    @SuppressWarnings("MemberName")
-    public final int value;
-
-    MotorType(int value) {
-      this.value = value;
-    }
-  }
-
-  public static final double kDefaultExpirationTime = 0.1;
-  public static final double kDefaultSensitivity = 0.5;
-  public static final double kDefaultMaxOutput = 1.0;
-  protected static final int kMaxNumberOfMotors = 4;
-  protected double m_sensitivity;
-  protected double m_maxOutput;
-  protected SpeedController m_frontLeftMotor;
-  protected SpeedController m_frontRightMotor;
-  protected SpeedController m_rearLeftMotor;
-  protected SpeedController m_rearRightMotor;
-  protected boolean m_allocatedSpeedControllers;
-  protected static boolean kArcadeRatioCurve_Reported;
-  protected static boolean kTank_Reported;
-  protected static boolean kArcadeStandard_Reported;
-  protected static boolean kMecanumCartesian_Reported;
-  protected static boolean kMecanumPolar_Reported;
-
-  /**
-   * Constructor for RobotDrive with 2 motors specified with channel numbers. Set up parameters for
-   * a two wheel drive system where the left and right motor pwm channels are specified in the call.
-   * This call assumes Talons for controlling the motors.
-   *
-   * @param leftMotorChannel  The PWM channel number that drives the left motor.
-   * @param rightMotorChannel The PWM channel number that drives the right motor.
-   */
-  public RobotDrive(final int leftMotorChannel, final int rightMotorChannel) {
-    m_sensitivity = kDefaultSensitivity;
-    m_maxOutput = kDefaultMaxOutput;
-    m_frontLeftMotor = null;
-    m_rearLeftMotor = new Talon(leftMotorChannel);
-    m_frontRightMotor = null;
-    m_rearRightMotor = new Talon(rightMotorChannel);
-    m_allocatedSpeedControllers = true;
-    setSafetyEnabled(true);
-    drive(0, 0);
-  }
-
-  /**
-   * Constructor for RobotDrive with 4 motors specified with channel numbers. Set up parameters for
-   * a four wheel drive system where all four motor pwm channels are specified in the call. This
-   * call assumes Talons for controlling the motors.
-   *
-   * @param frontLeftMotor  Front left motor channel number
-   * @param rearLeftMotor   Rear Left motor channel number
-   * @param frontRightMotor Front right motor channel number
-   * @param rearRightMotor  Rear Right motor channel number
-   */
-  public RobotDrive(final int frontLeftMotor, final int rearLeftMotor, final int frontRightMotor,
-                    final int rearRightMotor) {
-    m_sensitivity = kDefaultSensitivity;
-    m_maxOutput = kDefaultMaxOutput;
-    m_rearLeftMotor = new Talon(rearLeftMotor);
-    m_rearRightMotor = new Talon(rearRightMotor);
-    m_frontLeftMotor = new Talon(frontLeftMotor);
-    m_frontRightMotor = new Talon(frontRightMotor);
-    m_allocatedSpeedControllers = true;
-    setSafetyEnabled(true);
-    drive(0, 0);
-  }
-
-  /**
-   * Constructor for RobotDrive with 2 motors specified as SpeedController objects. The
-   * SpeedController version of the constructor enables programs to use the RobotDrive classes with
-   * subclasses of the SpeedController objects, for example, versions with ramping or reshaping of
-   * the curve to suit motor bias or dead-band elimination.
-   *
-   * @param leftMotor  The left SpeedController object used to drive the robot.
-   * @param rightMotor the right SpeedController object used to drive the robot.
-   */
-  public RobotDrive(SpeedController leftMotor, SpeedController rightMotor) {
-    requireNonNullParam(leftMotor, "leftMotor", "RobotDrive");
-    requireNonNullParam(rightMotor, "rightMotor", "RobotDrive");
-
-    m_frontLeftMotor = null;
-    m_rearLeftMotor = leftMotor;
-    m_frontRightMotor = null;
-    m_rearRightMotor = rightMotor;
-    m_sensitivity = kDefaultSensitivity;
-    m_maxOutput = kDefaultMaxOutput;
-    m_allocatedSpeedControllers = false;
-    setSafetyEnabled(true);
-    drive(0, 0);
-  }
-
-  /**
-   * Constructor for RobotDrive with 4 motors specified as SpeedController objects. Speed controller
-   * input version of RobotDrive (see previous comments).
-   *
-   * @param frontLeftMotor  The front left SpeedController object used to drive the robot
-   * @param rearLeftMotor   The back left SpeedController object used to drive the robot.
-   * @param frontRightMotor The front right SpeedController object used to drive the robot.
-   * @param rearRightMotor  The back right SpeedController object used to drive the robot.
-   */
-  public RobotDrive(SpeedController frontLeftMotor, SpeedController rearLeftMotor,
-                    SpeedController frontRightMotor, SpeedController rearRightMotor) {
-    m_frontLeftMotor = requireNonNullParam(frontLeftMotor, "frontLeftMotor", "RobotDrive");
-    m_rearLeftMotor = requireNonNullParam(rearLeftMotor, "rearLeftMotor", "RobotDrive");
-    m_frontRightMotor = requireNonNullParam(frontRightMotor, "frontRightMotor", "RobotDrive");
-    m_rearRightMotor = requireNonNullParam(rearRightMotor, "rearRightMotor", "RobotDrive");
-    m_sensitivity = kDefaultSensitivity;
-    m_maxOutput = kDefaultMaxOutput;
-    m_allocatedSpeedControllers = false;
-    setSafetyEnabled(true);
-    drive(0, 0);
-  }
-
-  /**
-   * Drive the motors at "outputMagnitude" and "curve". Both outputMagnitude and curve are -1.0 to
-   * +1.0 values, where 0.0 represents stopped and not turning. {@literal curve < 0 will turn left
-   * and curve > 0} will turn right.
-   *
-   * <p>The algorithm for steering provides a constant turn radius for any normal speed range, both
-   * forward and backward. Increasing sensitivity causes sharper turns for fixed values of curve.
-   *
-   * <p>This function will most likely be used in an autonomous routine.
-   *
-   * @param outputMagnitude The speed setting for the outside wheel in a turn, forward or backwards,
-   *                        +1 to -1.
-   * @param curve           The rate of turn, constant for different forward speeds. Set {@literal
-   *                        curve < 0 for left turn or curve > 0 for right turn.} Set curve =
-   *                        e^(-r/w) to get a turn radius r for wheelbase w of your robot.
-   *                        Conversely, turn radius r = -ln(curve)*w for a given value of curve and
-   *                        wheelbase w.
-   */
-  public void drive(double outputMagnitude, double curve) {
-    final double leftOutput;
-    final double rightOutput;
-
-    if (!kArcadeRatioCurve_Reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive_ArcadeRatioCurve,
-          getNumMotors());
-      kArcadeRatioCurve_Reported = true;
-    }
-    if (curve < 0) {
-      double value = Math.log(-curve);
-      double ratio = (value - m_sensitivity) / (value + m_sensitivity);
-      if (ratio == 0) {
-        ratio = 0.0000000001;
-      }
-      leftOutput = outputMagnitude / ratio;
-      rightOutput = outputMagnitude;
-    } else if (curve > 0) {
-      double value = Math.log(curve);
-      double ratio = (value - m_sensitivity) / (value + m_sensitivity);
-      if (ratio == 0) {
-        ratio = 0.0000000001;
-      }
-      leftOutput = outputMagnitude;
-      rightOutput = outputMagnitude / ratio;
-    } else {
-      leftOutput = outputMagnitude;
-      rightOutput = outputMagnitude;
-    }
-    setLeftRightMotorOutputs(leftOutput, rightOutput);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. drive the robot using two joystick
-   * inputs. The Y-axis will be selected from each Joystick object. The calculated values will be
-   * squared to decrease sensitivity at low speeds.
-   *
-   * @param leftStick  The joystick to control the left side of the robot.
-   * @param rightStick The joystick to control the right side of the robot.
-   */
-  public void tankDrive(GenericHID leftStick, GenericHID rightStick) {
-    requireNonNullParam(leftStick, "leftStick", "tankDrive");
-    requireNonNullParam(rightStick, "rightStick", "tankDrive");
-
-    tankDrive(leftStick.getY(), rightStick.getY(), true);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. drive the robot using two joystick
-   * inputs. The Y-axis will be selected from each Joystick object.
-   *
-   * @param leftStick     The joystick to control the left side of the robot.
-   * @param rightStick    The joystick to control the right side of the robot.
-   * @param squaredInputs Setting this parameter to true decreases the sensitivity at lower speeds
-   */
-  public void tankDrive(GenericHID leftStick, GenericHID rightStick, boolean squaredInputs) {
-    requireNonNullParam(leftStick, "leftStick", "tankDrive");
-    requireNonNullParam(rightStick, "rightStick", "tankDrive");
-
-    tankDrive(leftStick.getY(), rightStick.getY(), squaredInputs);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. This function lets you pick the
-   * axis to be used on each Joystick object for the left and right sides of the robot. The
-   * calculated values will be squared to decrease sensitivity at low speeds.
-   *
-   * @param leftStick  The Joystick object to use for the left side of the robot.
-   * @param leftAxis   The axis to select on the left side Joystick object.
-   * @param rightStick The Joystick object to use for the right side of the robot.
-   * @param rightAxis  The axis to select on the right side Joystick object.
-   */
-  public void tankDrive(GenericHID leftStick, final int leftAxis, GenericHID rightStick,
-                        final int rightAxis) {
-    requireNonNullParam(leftStick, "leftStick", "tankDrive");
-    requireNonNullParam(rightStick, "rightStick", "tankDrive");
-
-    tankDrive(leftStick.getRawAxis(leftAxis), rightStick.getRawAxis(rightAxis), true);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. This function lets you pick the
-   * axis to be used on each Joystick object for the left and right sides of the robot.
-   *
-   * @param leftStick     The Joystick object to use for the left side of the robot.
-   * @param leftAxis      The axis to select on the left side Joystick object.
-   * @param rightStick    The Joystick object to use for the right side of the robot.
-   * @param rightAxis     The axis to select on the right side Joystick object.
-   * @param squaredInputs Setting this parameter to true decreases the sensitivity at lower speeds
-   */
-  public void tankDrive(GenericHID leftStick, final int leftAxis, GenericHID rightStick,
-                        final int rightAxis, boolean squaredInputs) {
-    requireNonNullParam(leftStick, "leftStick", "tankDrive");
-    requireNonNullParam(rightStick, "rightStick", "tankDrive");
-
-    tankDrive(leftStick.getRawAxis(leftAxis), rightStick.getRawAxis(rightAxis), squaredInputs);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. This function lets you directly
-   * provide joystick values from any source.
-   *
-   * @param leftValue     The value of the left stick.
-   * @param rightValue    The value of the right stick.
-   * @param squaredInputs Setting this parameter to true decreases the sensitivity at lower speeds
-   */
-  public void tankDrive(double leftValue, double rightValue, boolean squaredInputs) {
-    if (!kTank_Reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive_Tank,
-          getNumMotors());
-      kTank_Reported = true;
-    }
-
-    leftValue = limit(leftValue);
-    rightValue = limit(rightValue);
-
-    // square the inputs (while preserving the sign) to increase fine control
-    // while permitting full power
-    if (squaredInputs) {
-      leftValue = Math.copySign(leftValue * leftValue, leftValue);
-      rightValue = Math.copySign(rightValue * rightValue, rightValue);
-    }
-    setLeftRightMotorOutputs(leftValue, rightValue);
-  }
-
-  /**
-   * Provide tank steering using the stored robot configuration. This function lets you directly
-   * provide joystick values from any source. The calculated values will be squared to decrease
-   * sensitivity at low speeds.
-   *
-   * @param leftValue  The value of the left stick.
-   * @param rightValue The value of the right stick.
-   */
-  public void tankDrive(double leftValue, double rightValue) {
-    tankDrive(leftValue, rightValue, true);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. Given a single Joystick, the class assumes the Y
-   * axis for the move value and the X axis for the rotate value. (Should add more information here
-   * regarding the way that arcade drive works.)
-   *
-   * @param stick         The joystick to use for Arcade single-stick driving. The Y-axis will be
-   *                      selected for forwards/backwards and the X-axis will be selected for
-   *                      rotation rate.
-   * @param squaredInputs If true, the sensitivity will be decreased for small values
-   */
-  public void arcadeDrive(GenericHID stick, boolean squaredInputs) {
-    // simply call the full-featured arcadeDrive with the appropriate values
-    arcadeDrive(stick.getY(), stick.getX(), squaredInputs);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. Given a single Joystick, the class assumes the Y
-   * axis for the move value and the X axis for the rotate value. (Should add more information here
-   * regarding the way that arcade drive works.) The calculated values will be squared to decrease
-   * sensitivity at low speeds.
-   *
-   * @param stick The joystick to use for Arcade single-stick driving. The Y-axis will be selected
-   *              for forwards/backwards and the X-axis will be selected for rotation rate.
-   */
-  public void arcadeDrive(GenericHID stick) {
-    arcadeDrive(stick, true);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. Given two joystick instances and two axis,
-   * compute the values to send to either two or four motors.
-   *
-   * @param moveStick     The Joystick object that represents the forward/backward direction
-   * @param moveAxis      The axis on the moveStick object to use for forwards/backwards (typically
-   *                      Y_AXIS)
-   * @param rotateStick   The Joystick object that represents the rotation value
-   * @param rotateAxis    The axis on the rotation object to use for the rotate right/left
-   *                      (typically X_AXIS)
-   * @param squaredInputs Setting this parameter to true decreases the sensitivity at lower speeds
-   */
-  public void arcadeDrive(GenericHID moveStick, final int moveAxis, GenericHID rotateStick,
-                          final int rotateAxis, boolean squaredInputs) {
-    double moveValue = moveStick.getRawAxis(moveAxis);
-    double rotateValue = rotateStick.getRawAxis(rotateAxis);
-
-    arcadeDrive(moveValue, rotateValue, squaredInputs);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. Given two joystick instances and two axis,
-   * compute the values to send to either two or four motors. The calculated values will be
-   * squared to decrease sensitivity at low speeds.
-   *
-   * @param moveStick   The Joystick object that represents the forward/backward direction
-   * @param moveAxis    The axis on the moveStick object to use for forwards/backwards (typically
-   *                    Y_AXIS)
-   * @param rotateStick The Joystick object that represents the rotation value
-   * @param rotateAxis  The axis on the rotation object to use for the rotate right/left (typically
-   *                    X_AXIS)
-   */
-  public void arcadeDrive(GenericHID moveStick, final int moveAxis, GenericHID rotateStick,
-                          final int rotateAxis) {
-    arcadeDrive(moveStick, moveAxis, rotateStick, rotateAxis, true);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. This function lets you directly provide
-   * joystick values from any source.
-   *
-   * @param moveValue     The value to use for forwards/backwards
-   * @param rotateValue   The value to use for the rotate right/left
-   * @param squaredInputs If set, decreases the sensitivity at low speeds
-   */
-  public void arcadeDrive(double moveValue, double rotateValue, boolean squaredInputs) {
-    // local variables to hold the computed PWM values for the motors
-    if (!kArcadeStandard_Reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive_ArcadeStandard,
-          getNumMotors());
-      kArcadeStandard_Reported = true;
-    }
-
-    double leftMotorSpeed;
-    double rightMotorSpeed;
-
-    moveValue = limit(moveValue);
-    rotateValue = limit(rotateValue);
-
-    // square the inputs (while preserving the sign) to increase fine control
-    // while permitting full power
-    if (squaredInputs) {
-      // square the inputs (while preserving the sign) to increase fine control
-      // while permitting full power
-      moveValue = Math.copySign(moveValue * moveValue, moveValue);
-      rotateValue = Math.copySign(rotateValue * rotateValue, rotateValue);
-    }
-
-    if (moveValue > 0.0) {
-      if (rotateValue > 0.0) {
-        leftMotorSpeed = moveValue - rotateValue;
-        rightMotorSpeed = Math.max(moveValue, rotateValue);
-      } else {
-        leftMotorSpeed = Math.max(moveValue, -rotateValue);
-        rightMotorSpeed = moveValue + rotateValue;
-      }
-    } else {
-      if (rotateValue > 0.0) {
-        leftMotorSpeed = -Math.max(-moveValue, rotateValue);
-        rightMotorSpeed = moveValue + rotateValue;
-      } else {
-        leftMotorSpeed = moveValue - rotateValue;
-        rightMotorSpeed = -Math.max(-moveValue, -rotateValue);
-      }
-    }
-
-    setLeftRightMotorOutputs(leftMotorSpeed, rightMotorSpeed);
-  }
-
-  /**
-   * Arcade drive implements single stick driving. This function lets you directly provide
-   * joystick values from any source. The calculated values will be squared to decrease
-   * sensitivity at low speeds.
-   *
-   * @param moveValue   The value to use for forwards/backwards
-   * @param rotateValue The value to use for the rotate right/left
-   */
-  public void arcadeDrive(double moveValue, double rotateValue) {
-    arcadeDrive(moveValue, rotateValue, true);
-  }
-
-  /**
-   * Drive method for Mecanum wheeled robots.
-   *
-   * <p>A method for driving with Mecanum wheeled robots. There are 4 wheels on the robot, arranged
-   * so that the front and back wheels are toed in 45 degrees. When looking at the wheels from the
-   * top, the roller axles should form an X across the robot.
-   *
-   * <p>This is designed to be directly driven by joystick axes.
-   *
-   * @param x         The speed that the robot should drive in the X direction. [-1.0..1.0]
-   * @param y         The speed that the robot should drive in the Y direction. This input is
-   *                  inverted to match the forward == -1.0 that joysticks produce. [-1.0..1.0]
-   * @param rotation  The rate of rotation for the robot that is completely independent of the
-   *                  translation. [-1.0..1.0]
-   * @param gyroAngle The current angle reading from the gyro. Use this to implement field-oriented
-   *                  controls.
-   */
-  @SuppressWarnings("ParameterName")
-  public void mecanumDrive_Cartesian(double x, double y, double rotation, double gyroAngle) {
-    if (!kMecanumCartesian_Reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive_MecanumCartesian,
-          getNumMotors());
-      kMecanumCartesian_Reported = true;
-    }
-    @SuppressWarnings("LocalVariableName")
-    double xIn = x;
-    @SuppressWarnings("LocalVariableName")
-    double yIn = y;
-    // Negate y for the joystick.
-    yIn = -yIn;
-    // Compensate for gyro angle.
-    double[] rotated = rotateVector(xIn, yIn, gyroAngle);
-    xIn = rotated[0];
-    yIn = rotated[1];
-
-    double[] wheelSpeeds = new double[kMaxNumberOfMotors];
-    wheelSpeeds[MotorType.kFrontLeft.value] = xIn + yIn + rotation;
-    wheelSpeeds[MotorType.kFrontRight.value] = -xIn + yIn - rotation;
-    wheelSpeeds[MotorType.kRearLeft.value] = -xIn + yIn + rotation;
-    wheelSpeeds[MotorType.kRearRight.value] = xIn + yIn - rotation;
-
-    normalize(wheelSpeeds);
-    m_frontLeftMotor.set(wheelSpeeds[MotorType.kFrontLeft.value] * m_maxOutput);
-    m_frontRightMotor.set(wheelSpeeds[MotorType.kFrontRight.value] * m_maxOutput);
-    m_rearLeftMotor.set(wheelSpeeds[MotorType.kRearLeft.value] * m_maxOutput);
-    m_rearRightMotor.set(wheelSpeeds[MotorType.kRearRight.value] * m_maxOutput);
-
-    feed();
-  }
-
-  /**
-   * Drive method for Mecanum wheeled robots.
-   *
-   * <p>A method for driving with Mecanum wheeled robots. There are 4 wheels on the robot, arranged
-   * so that the front and back wheels are toed in 45 degrees. When looking at the wheels from the
-   * top, the roller axles should form an X across the robot.
-   *
-   * @param magnitude The speed that the robot should drive in a given direction. [-1.0..1.0]
-   * @param direction The angle the robot should drive in degrees. The direction and magnitude
-   *                  are independent of the rotation rate. [-180.0..180.0]
-   * @param rotation  The rate of rotation for the robot that is completely independent of the
-   *                  magnitude or direction. [-1.0..1.0]
-   */
-  public void mecanumDrive_Polar(double magnitude, double direction, double rotation) {
-    if (!kMecanumPolar_Reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive_MecanumPolar,
-          getNumMotors());
-      kMecanumPolar_Reported = true;
-    }
-    // Normalized for full power along the Cartesian axes.
-    magnitude = limit(magnitude) * Math.sqrt(2.0);
-    // The rollers are at 45 degree angles.
-    double dirInRad = (direction + 45.0) * Math.PI / 180.0;
-    double cosD = Math.cos(dirInRad);
-    double sinD = Math.sin(dirInRad);
-
-    double[] wheelSpeeds = new double[kMaxNumberOfMotors];
-    wheelSpeeds[MotorType.kFrontLeft.value] = sinD * magnitude + rotation;
-    wheelSpeeds[MotorType.kFrontRight.value] = cosD * magnitude - rotation;
-    wheelSpeeds[MotorType.kRearLeft.value] = cosD * magnitude + rotation;
-    wheelSpeeds[MotorType.kRearRight.value] = sinD * magnitude - rotation;
-
-    normalize(wheelSpeeds);
-
-    m_frontLeftMotor.set(wheelSpeeds[MotorType.kFrontLeft.value] * m_maxOutput);
-    m_frontRightMotor.set(wheelSpeeds[MotorType.kFrontRight.value] * m_maxOutput);
-    m_rearLeftMotor.set(wheelSpeeds[MotorType.kRearLeft.value] * m_maxOutput);
-    m_rearRightMotor.set(wheelSpeeds[MotorType.kRearRight.value] * m_maxOutput);
-
-    feed();
-  }
-
-  /**
-   * Holonomic Drive method for Mecanum wheeled robots.
-   *
-   * <p>This is an alias to mecanumDrive_Polar() for backward compatibility
-   *
-   * @param magnitude The speed that the robot should drive in a given direction. [-1.0..1.0]
-   * @param direction The direction the robot should drive. The direction and maginitude are
-   *                  independent of the rotation rate.
-   * @param rotation  The rate of rotation for the robot that is completely independent of the
-   *                  magnitute or direction. [-1.0..1.0]
-   */
-  void holonomicDrive(double magnitude, double direction, double rotation) {
-    mecanumDrive_Polar(magnitude, direction, rotation);
-  }
-
-  /**
-   * Set the speed of the right and left motors. This is used once an appropriate drive setup
-   * function is called such as twoWheelDrive(). The motors are set to "leftSpeed" and
-   * "rightSpeed" and includes flipping the direction of one side for opposing motors.
-   *
-   * @param leftOutput  The speed to send to the left side of the robot.
-   * @param rightOutput The speed to send to the right side of the robot.
-   */
-  public void setLeftRightMotorOutputs(double leftOutput, double rightOutput) {
-
-    if (m_frontLeftMotor != null) {
-      m_frontLeftMotor.set(limit(leftOutput) * m_maxOutput);
-    }
-    m_rearLeftMotor.set(limit(leftOutput) * m_maxOutput);
-
-    if (m_frontRightMotor != null) {
-      m_frontRightMotor.set(-limit(rightOutput) * m_maxOutput);
-    }
-    m_rearRightMotor.set(-limit(rightOutput) * m_maxOutput);
-
-    feed();
-  }
-
-  /**
-   * Limit motor values to the -1.0 to +1.0 range.
-   */
-  protected static double limit(double number) {
-    if (number > 1.0) {
-      return 1.0;
-    }
-    if (number < -1.0) {
-      return -1.0;
-    }
-    return number;
-  }
-
-  /**
-   * Normalize all wheel speeds if the magnitude of any wheel is greater than 1.0.
-   */
-  protected static void normalize(double[] wheelSpeeds) {
-    double maxMagnitude = Math.abs(wheelSpeeds[0]);
-    for (int i = 1; i < kMaxNumberOfMotors; i++) {
-      double temp = Math.abs(wheelSpeeds[i]);
-      if (maxMagnitude < temp) {
-        maxMagnitude = temp;
-      }
-    }
-    if (maxMagnitude > 1.0) {
-      for (int i = 0; i < kMaxNumberOfMotors; i++) {
-        wheelSpeeds[i] = wheelSpeeds[i] / maxMagnitude;
-      }
-    }
-  }
-
-  /**
-   * Rotate a vector in Cartesian space.
-   */
-  @SuppressWarnings("ParameterName")
-  protected static double[] rotateVector(double x, double y, double angle) {
-    double cosA = Math.cos(angle * (Math.PI / 180.0));
-    double sinA = Math.sin(angle * (Math.PI / 180.0));
-    double[] out = new double[2];
-    out[0] = x * cosA - y * sinA;
-    out[1] = x * sinA + y * cosA;
-    return out;
-  }
-
-  /**
-   * Invert a motor direction. This is used when a motor should run in the opposite direction as the
-   * drive code would normally run it. Motors that are direct drive would be inverted, the drive
-   * code assumes that the motors are geared with one reversal.
-   *
-   * @param motor      The motor index to invert.
-   * @param isInverted True if the motor should be inverted when operated.
-   */
-  public void setInvertedMotor(MotorType motor, boolean isInverted) {
-    switch (motor) {
-      case kFrontLeft:
-        m_frontLeftMotor.setInverted(isInverted);
-        break;
-      case kFrontRight:
-        m_frontRightMotor.setInverted(isInverted);
-        break;
-      case kRearLeft:
-        m_rearLeftMotor.setInverted(isInverted);
-        break;
-      case kRearRight:
-        m_rearRightMotor.setInverted(isInverted);
-        break;
-      default:
-        throw new IllegalArgumentException("Illegal motor type: " + motor);
-    }
-  }
-
-  /**
-   * Set the turning sensitivity.
-   *
-   * <p>This only impacts the drive() entry-point.
-   *
-   * @param sensitivity Effectively sets the turning sensitivity (or turn radius for a given value)
-   */
-  public void setSensitivity(double sensitivity) {
-    m_sensitivity = sensitivity;
-  }
-
-  /**
-   * Configure the scaling factor for using RobotDrive with motor controllers in a mode other than
-   * PercentVbus.
-   *
-   * @param maxOutput Multiplied with the output percentage computed by the drive functions.
-   */
-  public void setMaxOutput(double maxOutput) {
-    m_maxOutput = maxOutput;
-  }
-
-  /**
-   * Free the speed controllers if they were allocated locally.
-   */
-  @Override
-  public void close() {
-    if (m_allocatedSpeedControllers) {
-      if (m_frontLeftMotor != null) {
-        ((PWM) m_frontLeftMotor).close();
-      }
-      if (m_frontRightMotor != null) {
-        ((PWM) m_frontRightMotor).close();
-      }
-      if (m_rearLeftMotor != null) {
-        ((PWM) m_rearLeftMotor).close();
-      }
-      if (m_rearRightMotor != null) {
-        ((PWM) m_rearRightMotor).close();
-      }
-    }
-  }
-
-  @Override
-  public String getDescription() {
-    return "Robot Drive";
-  }
-
-  @Override
-  public void stopMotor() {
-    if (m_frontLeftMotor != null) {
-      m_frontLeftMotor.stopMotor();
-    }
-    if (m_frontRightMotor != null) {
-      m_frontRightMotor.stopMotor();
-    }
-    if (m_rearLeftMotor != null) {
-      m_rearLeftMotor.stopMotor();
-    }
-    if (m_rearRightMotor != null) {
-      m_rearRightMotor.stopMotor();
-    }
-
-    feed();
-  }
-
-  protected int getNumMotors() {
-    int motors = 0;
-    if (m_frontLeftMotor != null) {
-      motors++;
-    }
-    if (m_frontRightMotor != null) {
-      motors++;
-    }
-    if (m_rearLeftMotor != null) {
-      motors++;
-    }
-    if (m_rearRightMotor != null) {
-      motors++;
-    }
-    return motors;
-  }
-}
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 58004d5..462051a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotState.java
@@ -1,38 +1,39 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
-@SuppressWarnings("JavadocMethod")
+@SuppressWarnings("MissingJavadocMethod")
 public final class RobotState {
   public static boolean isDisabled() {
-    return DriverStation.getInstance().isDisabled();
+    return DriverStation.isDisabled();
   }
 
   public static boolean isEnabled() {
-    return DriverStation.getInstance().isEnabled();
+    return DriverStation.isEnabled();
   }
 
   public static boolean isEStopped() {
-    return DriverStation.getInstance().isEStopped();
+    return DriverStation.isEStopped();
   }
 
+  @Deprecated
   public static boolean isOperatorControl() {
-    return DriverStation.getInstance().isOperatorControl();
+    return isTeleop();
+  }
+
+  public static boolean isTeleop() {
+    return DriverStation.isTeleop();
   }
 
   public static boolean isAutonomous() {
-    return DriverStation.getInstance().isAutonomous();
+    return DriverStation.isAutonomous();
   }
 
   public static boolean isTest() {
-    return DriverStation.getInstance().isTest();
+    return DriverStation.isTest();
   }
 
-  private RobotState() {
-  }
+  private RobotState() {}
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.java
new file mode 100644
index 0000000..e7af118
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RuntimeType.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;
+
+import edu.wpi.first.hal.HALUtil;
+
+public enum RuntimeType {
+  kRoboRIO(HALUtil.RUNTIME_ROBORIO),
+  kRoboRIO2(HALUtil.RUNTIME_ROBORIO2),
+  kSimulation(HALUtil.RUNTIME_SIMULATION);
+
+  public final int value;
+
+  RuntimeType(int value) {
+    this.value = value;
+  }
+
+  /**
+   * Construct a runtime type from an int value.
+   *
+   * @param type Runtime type as int
+   * @return Matching runtime type
+   */
+  public static RuntimeType getValue(int type) {
+    if (type == HALUtil.RUNTIME_ROBORIO) {
+      return RuntimeType.kRoboRIO;
+    } else if (type == HALUtil.RUNTIME_ROBORIO2) {
+      return RuntimeType.kRoboRIO2;
+    }
+    return RuntimeType.kSimulation;
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SD540.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SD540.java
deleted file mode 100644
index b2e98b6..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SD540.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Mindsensors SD540 Speed Controller.
- *
- * <p>Note that the SD540 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 SD540 User Manual
- * available from Mindsensors.
- *
- * <p><ul>
- * <li>2.05ms = full "forward"
- * <li>1.55ms = the "high end" of the deadband range
- * <li>1.50ms = center of the deadband range (off)
- * <li>1.44ms = the "low end" of the deadband range
- * <li>0.94ms = full "reverse"
- * </ul>
- */
-public class SD540 extends PWMSpeedController {
-  /**
-   * Common initialization code called by all constructors.
-   */
-  protected void initSD540() {
-    setBounds(2.05, 1.55, 1.50, 1.44, 0.94);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_MindsensorsSD540, getChannel() + 1);
-    SendableRegistry.setName(this, "SD540", getChannel());
-  }
-
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the SD540 is attached to. 0-9 are on-board, 10-19 are on
-   *                the MXP port
-   */
-  public SD540(final int channel) {
-    super(channel);
-    initSD540();
-  }
-}
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 aa3c679..2243140 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SPI.java
@@ -1,30 +1,26 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.IntBuffer;
-
 import edu.wpi.first.hal.AccumulatorResult;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SPIJNI;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
 
-/**
- * Represents a SPI bus port.
- */
-@SuppressWarnings("PMD.CyclomaticComplexity")
+/** Represents a SPI bus port. */
 public class SPI implements AutoCloseable {
   public enum Port {
-    kOnboardCS0(0), kOnboardCS1(1), kOnboardCS2(2), kOnboardCS3(3), kMXP(4);
+    kOnboardCS0(0),
+    kOnboardCS1(1),
+    kOnboardCS2(2),
+    kOnboardCS3(3),
+    kMXP(4);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     Port(int value) {
@@ -50,6 +46,10 @@
     HAL.report(tResourceType.kResourceType_SPI, port.value + 1);
   }
 
+  public int getPort() {
+    return m_port;
+  }
+
   @Override
   public void close() {
     if (m_accum != null) {
@@ -126,6 +126,7 @@
   /**
    * Configure that the data is stable on the falling edge and the data changes on the rising edge.
    * Note this gets reversed is setClockActiveLow is set.
+   *
    * @deprecated use {@link #setSampleDataOnTrailingEdge()} in most cases.
    */
   @Deprecated
@@ -137,6 +138,7 @@
   /**
    * Configure that the data is stable on the rising edge and the data changes on the falling edge.
    * Note this gets reversed is setClockActiveLow is set.
+   *
    * @deprecated use {@link #setSampleDataOnLeadingEdge()} in most cases.
    */
   @Deprecated
@@ -145,18 +147,12 @@
     SPIJNI.spiSetOpts(m_port, m_msbFirst, m_sampleOnTrailing, m_clockIdleHigh);
   }
 
-
-
-  /**
-   * Configure the chip select line to be active high.
-   */
+  /** Configure the chip select line to be active high. */
   public final void setChipSelectActiveHigh() {
     SPIJNI.spiSetChipSelectActiveHigh(m_port);
   }
 
-  /**
-   * Configure the chip select line to be active low.
-   */
+  /** Configure the chip select line to be active low. */
   public final void setChipSelectActiveLow() {
     SPIJNI.spiSetChipSelectActiveLow(m_port);
   }
@@ -166,6 +162,10 @@
    *
    * <p>If not running in output only mode, also saves the data received on the CIPO input during
    * the transfer into the receive FIFO.
+   *
+   * @param dataToSend The buffer containing the data to send.
+   * @param size The number of bytes to send.
+   * @return Number of bytes written or -1 on error.
    */
   public int write(byte[] dataToSend, int size) {
     if (dataToSend.length < size) {
@@ -181,6 +181,8 @@
    * the transfer into the receive FIFO.
    *
    * @param dataToSend The buffer containing the data to send.
+   * @param size The number of bytes to send.
+   * @return Number of bytes written or -1 on error.
    */
   @SuppressWarnings("ByteBufferBackingArray")
   public int write(ByteBuffer dataToSend, int size) {
@@ -204,8 +206,11 @@
    * <p>If the receive FIFO is empty, there is no active transfer, and initiate is false, errors.
    *
    * @param initiate If true, this function pushes "0" into the transmit buffer and initiates a
-   *                 transfer. If false, this function assumes that data is already in the receive
-   *                 FIFO from a previous write.
+   *     transfer. If false, this function assumes that data is already in the receive FIFO from a
+   *     previous write.
+   * @param dataReceived Buffer in which to store bytes read.
+   * @param size Number of bytes to read.
+   * @return Number of bytes read or -1 on error.
    */
   public int read(boolean initiate, byte[] dataReceived, int size) {
     if (dataReceived.length < size) {
@@ -221,11 +226,12 @@
    *
    * <p>If the receive FIFO is empty, there is no active transfer, and initiate is false, errors.
    *
-   * @param initiate     If true, this function pushes "0" into the transmit buffer and initiates
-   *                     a transfer. If false, this function assumes that data is already in the
-   *                     receive FIFO from a previous write.
+   * @param initiate If true, this function pushes "0" into the transmit buffer and initiates a
+   *     transfer. If false, this function assumes that data is already in the receive FIFO from a
+   *     previous write.
    * @param dataReceived The buffer to be filled with the received data.
-   * @param size         The length of the transaction, in bytes
+   * @param size The length of the transaction, in bytes
+   * @return Number of bytes read or -1 on error.
    */
   @SuppressWarnings("ByteBufferBackingArray")
   public int read(boolean initiate, ByteBuffer dataReceived, int size) {
@@ -244,9 +250,10 @@
   /**
    * Perform a simultaneous read/write transaction with the device.
    *
-   * @param dataToSend   The data to be written out to the device
+   * @param dataToSend The data to be written out to the device
    * @param dataReceived Buffer to receive data from the device
-   * @param size         The length of the transaction, in bytes
+   * @param size The length of the transaction, in bytes
+   * @return TODO
    */
   public int transaction(byte[] dataToSend, byte[] dataReceived, int size) {
     if (dataToSend.length < size) {
@@ -261,11 +268,12 @@
   /**
    * Perform a simultaneous read/write transaction with the device.
    *
-   * @param dataToSend   The data to be written out to the device.
+   * @param dataToSend The data to be written out to the device.
    * @param dataReceived Buffer to receive data from the device.
-   * @param size         The length of the transaction, in bytes
+   * @param size The length of the transaction, in bytes
+   * @return TODO
    */
-  @SuppressWarnings({"PMD.CyclomaticComplexity", "ByteBufferBackingArray"})
+  @SuppressWarnings("ByteBufferBackingArray")
   public int transaction(ByteBuffer dataToSend, ByteBuffer dataReceived, int size) {
     if (dataToSend.hasArray() && dataReceived.hasArray()) {
       return transaction(dataToSend.array(), dataReceived.array(), size);
@@ -288,8 +296,8 @@
   /**
    * Initialize automatic SPI transfer engine.
    *
-   * <p>Only a single engine is available, and use of it blocks use of all other
-   * chip select usage on the same physical SPI port while it is running.
+   * <p>Only a single engine is available, and use of it blocks use of all other chip select usage
+   * on the same physical SPI port while it is running.
    *
    * @param bufferSize buffer size in bytes
    */
@@ -297,9 +305,7 @@
     SPIJNI.spiInitAuto(m_port, bufferSize);
   }
 
-  /**
-   * Frees the automatic SPI transfer engine.
-   */
+  /** Frees the automatic SPI transfer engine. */
   public void freeAuto() {
     SPIJNI.spiFreeAuto(m_port);
   }
@@ -307,8 +313,7 @@
   /**
    * Set the data to be transmitted by the engine.
    *
-   * <p>Up to 16 bytes are configurable, and may be followed by up to 127 zero
-   * bytes.
+   * <p>Up to 16 bytes are configurable, and may be followed by up to 127 zero bytes.
    *
    * @param dataToSend data to send (maximum 16 bytes)
    * @param zeroSize number of zeros to send after the data
@@ -320,8 +325,8 @@
   /**
    * Start running the automatic SPI transfer engine at a periodic rate.
    *
-   * <p>{@link #initAuto(int)} and {@link #setAutoTransmitData(byte[], int)} must
-   * be called before calling this function.
+   * <p>{@link #initAuto(int)} and {@link #setAutoTransmitData(byte[], int)} must be called before
+   * calling this function.
    *
    * @param period period between transfers, in seconds (us resolution)
    */
@@ -332,28 +337,28 @@
   /**
    * Start running the automatic SPI transfer engine when a trigger occurs.
    *
-   * <p>{@link #initAuto(int)} and {@link #setAutoTransmitData(byte[], int)} must
-   * be called before calling this function.
+   * <p>{@link #initAuto(int)} and {@link #setAutoTransmitData(byte[], int)} must be called before
+   * calling this function.
    *
    * @param source digital source for the trigger (may be an analog trigger)
    * @param rising trigger on the rising edge
    * @param falling trigger on the falling edge
    */
   public void startAutoTrigger(DigitalSource source, boolean rising, boolean falling) {
-    SPIJNI.spiStartAutoTrigger(m_port, source.getPortHandleForRouting(),
-                               source.getAnalogTriggerTypeForRouting(), rising, falling);
+    SPIJNI.spiStartAutoTrigger(
+        m_port,
+        source.getPortHandleForRouting(),
+        source.getAnalogTriggerTypeForRouting(),
+        rising,
+        falling);
   }
 
-  /**
-   * Stop running the automatic SPI transfer engine.
-   */
+  /** Stop running the automatic SPI transfer engine. */
   public void stopAuto() {
     SPIJNI.spiStopAuto(m_port);
   }
 
-  /**
-   * Force the engine to make a single transfer.
-   */
+  /** Force the engine to make a single transfer. */
   public void forceAutoRead() {
     SPIJNI.spiForceAutoRead(m_port);
   }
@@ -361,16 +366,15 @@
   /**
    * Read data that has been transferred by the automatic SPI transfer engine.
    *
-   * <p>Transfers may be made a byte at a time, so it's necessary for the caller
-   * to handle cases where an entire transfer has not been completed.
+   * <p>Transfers may be made a byte at a time, so it's necessary for the caller to handle cases
+   * where an entire transfer has not been completed.
    *
-   * <p>Each received data sequence consists of a timestamp followed by the
-   * received data bytes, one byte per word (in the least significant byte).
-   * The length of each received data sequence is the same as the combined
-   * size of the data and zeroSize set in setAutoTransmitData().
+   * <p>Each received data sequence consists of a timestamp followed by the received data bytes, one
+   * byte per word (in the least significant byte). The length of each received data sequence is the
+   * same as the combined size of the data and zeroSize set in setAutoTransmitData().
    *
-   * <p>Blocks until numToRead words have been read or timeout expires.
-   * May be called with numToRead=0 to retrieve how many words are available.
+   * <p>Blocks until numToRead words have been read or timeout expires. May be called with
+   * numToRead=0 to retrieve how many words are available.
    *
    * @param buffer buffer where read words are stored
    * @param numToRead number of words to read
@@ -382,8 +386,8 @@
       throw new IllegalArgumentException("must be a direct buffer");
     }
     if (buffer.capacity() < numToRead * 4) {
-      throw new IllegalArgumentException("buffer is too small, must be at least "
-          + (numToRead * 4));
+      throw new IllegalArgumentException(
+          "buffer is too small, must be at least " + (numToRead * 4));
     }
     return SPIJNI.spiReadAutoReceivedData(m_port, buffer, numToRead, timeout);
   }
@@ -391,16 +395,15 @@
   /**
    * Read data that has been transferred by the automatic SPI transfer engine.
    *
-   * <p>Transfers may be made a byte at a time, so it's necessary for the caller
-   * to handle cases where an entire transfer has not been completed.
+   * <p>Transfers may be made a byte at a time, so it's necessary for the caller to handle cases
+   * where an entire transfer has not been completed.
    *
-   * <p>Each received data sequence consists of a timestamp followed by the
-   * received data bytes, one byte per word (in the least significant byte).
-   * The length of each received data sequence is the same as the combined
-   * size of the data and zeroSize set in setAutoTransmitData().
+   * <p>Each received data sequence consists of a timestamp followed by the received data bytes, one
+   * byte per word (in the least significant byte). The length of each received data sequence is the
+   * same as the combined size of the data and zeroSize set in setAutoTransmitData().
    *
-   * <p>Blocks until numToRead words have been read or timeout expires.
-   * May be called with numToRead=0 to retrieve how many words are available.
+   * <p>Blocks until numToRead words have been read or timeout expires. May be called with
+   * numToRead=0 to retrieve how many words are available.
    *
    * @param buffer array where read words are stored
    * @param numToRead number of words to read
@@ -415,8 +418,8 @@
   }
 
   /**
-   * Get the number of bytes dropped by the automatic SPI transfer engine due
-   * to the receive buffer being full.
+   * Get the number of bytes dropped by the automatic SPI transfer engine due to the receive buffer
+   * being full.
    *
    * @return Number of bytes dropped
    */
@@ -437,15 +440,22 @@
 
   private static final int kAccumulateDepth = 2048;
 
-  @SuppressWarnings("PMD.TooManyFields")
   private static class Accumulator implements AutoCloseable {
-    Accumulator(int port, int xferSize, int validMask, int validValue, int dataShift,
-                int dataSize, boolean isSigned, boolean bigEndian) {
+    Accumulator(
+        int port,
+        int xferSize,
+        int validMask,
+        int validValue,
+        int dataShift,
+        int dataSize,
+        boolean isSigned,
+        boolean bigEndian) {
       m_notifier = new Notifier(this::update);
-      m_buf = ByteBuffer.allocateDirect((xferSize + 1) * kAccumulateDepth * 4)
-          .order(ByteOrder.nativeOrder());
+      m_buf =
+          ByteBuffer.allocateDirect((xferSize + 1) * kAccumulateDepth * 4)
+              .order(ByteOrder.nativeOrder());
       m_intBuf = m_buf.asIntBuffer();
-      m_xferSize = xferSize + 1;  // +1 for timestamp
+      m_xferSize = xferSize + 1; // +1 for timestamp
       m_validMask = validMask;
       m_validValue = validValue;
       m_dataShift = dataShift;
@@ -478,15 +488,14 @@
 
     final int m_validMask;
     final int m_validValue;
-    final int m_dataMax;        // one more than max data value
-    final int m_dataMsbMask;    // data field MSB mask (for signed)
-    final int m_dataShift;      // data field shift right amount, in bits
-    final int m_xferSize;       // SPI transfer size, in bytes
-    final boolean m_isSigned;   // is data field signed?
-    final boolean m_bigEndian;  // is response big endian?
+    final int m_dataMax; // one more than max data value
+    final int m_dataMsbMask; // data field MSB mask (for signed)
+    final int m_dataShift; // data field shift right amount, in bits
+    final int m_xferSize; // SPI transfer size, in bytes
+    final boolean m_isSigned; // is data field signed?
+    final boolean m_bigEndian; // is response big endian?
     final int m_port;
 
-    @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
     void update() {
       synchronized (m_mutex) {
         boolean done = false;
@@ -503,7 +512,7 @@
             done = false;
           }
           if (numToRead == 0) {
-            return;  // no samples
+            return; // no samples
           }
 
           // read buffered data
@@ -546,11 +555,12 @@
                 if (m_count != 0) {
                   // timestamps use the 1us FPGA clock; also handle rollover
                   if (timestamp >= m_lastTimestamp) {
-                    m_integratedValue += dataNoCenter * (timestamp - m_lastTimestamp)
-                        * 1e-6 - m_integratedCenter;
+                    m_integratedValue +=
+                        dataNoCenter * (timestamp - m_lastTimestamp) * 1e-6 - m_integratedCenter;
                   } else {
-                    m_integratedValue += dataNoCenter * ((1L << 32) - m_lastTimestamp + timestamp)
-                        * 1e-6 - m_integratedCenter;
+                    m_integratedValue +=
+                        dataNoCenter * ((1L << 32) - m_lastTimestamp + timestamp) * 1e-6
+                            - m_integratedCenter;
                   }
                 }
               }
@@ -572,20 +582,26 @@
   /**
    * Initialize the accumulator.
    *
-   * @param period     Time between reads
-   * @param cmd        SPI command to send to request data
-   * @param xferSize   SPI transfer size, in bytes
-   * @param validMask  Mask to apply to received data for validity checking
+   * @param period Time between reads
+   * @param cmd SPI command to send to request data
+   * @param xferSize SPI transfer size, in bytes
+   * @param validMask Mask to apply to received data for validity checking
    * @param validValue After validMask is applied, required matching value for validity checking
-   * @param dataShift  Bit shift to apply to received data to get actual data value
-   * @param dataSize   Size (in bits) of data field
-   * @param isSigned   Is data field signed?
-   * @param bigEndian  Is device big endian?
+   * @param dataShift Bit shift to apply to received data to get actual data value
+   * @param dataSize Size (in bits) of data field
+   * @param isSigned Is data field signed?
+   * @param bigEndian Is device big endian?
    */
-  public void initAccumulator(double period, int cmd, int xferSize,
-                              int validMask, int validValue,
-                              int dataShift, int dataSize,
-                              boolean isSigned, boolean bigEndian) {
+  public void initAccumulator(
+      double period,
+      int cmd,
+      int xferSize,
+      int validMask,
+      int validValue,
+      int dataShift,
+      int dataSize,
+      boolean isSigned,
+      boolean bigEndian) {
     initAuto(xferSize * 2048);
     byte[] cmdBytes = new byte[] {0, 0, 0, 0};
     if (bigEndian) {
@@ -605,14 +621,13 @@
     setAutoTransmitData(cmdBytes, xferSize - 4);
     startAutoRate(period);
 
-    m_accum = new Accumulator(m_port, xferSize, validMask, validValue, dataShift, dataSize,
-                              isSigned, bigEndian);
+    m_accum =
+        new Accumulator(
+            m_port, xferSize, validMask, validValue, dataShift, dataSize, isSigned, bigEndian);
     m_accum.m_notifier.startPeriodic(period * 1024);
   }
 
-  /**
-   * Frees the accumulator.
-   */
+  /** Frees the accumulator. */
   public void freeAccumulator() {
     if (m_accum != null) {
       m_accum.close();
@@ -621,9 +636,7 @@
     freeAuto();
   }
 
-  /**
-   * Resets the accumulator to zero.
-   */
+  /** Resets the accumulator to zero. */
   public void resetAccumulator() {
     if (m_accum == null) {
       return;
@@ -643,6 +656,8 @@
    * <p>The center value is subtracted from each value before it is added to the accumulator. This
    * is used for the center value of devices like gyros and accelerometers to make integration work
    * and to take the device offset into account when integrating.
+   *
+   * @param center The accumulator's center value.
    */
   public void setAccumulatorCenter(int center) {
     if (m_accum == null) {
@@ -655,6 +670,8 @@
 
   /**
    * Set the accumulator's deadband.
+   *
+   * @param deadband The accumulator's deadband.
    */
   public void setAccumulatorDeadband(int deadband) {
     if (m_accum == null) {
@@ -667,6 +684,8 @@
 
   /**
    * Read the last value read by the accumulator engine.
+   *
+   * @return The last value read by the accumulator engine.
    */
   public int getAccumulatorLastValue() {
     if (m_accum == null) {
@@ -754,9 +773,11 @@
   /**
    * Set the center value of the accumulator integrator.
    *
-   * <p>The center value is subtracted from each value*dt before it is added to the
-   * integrated value. This is used for the center value of devices like gyros
-   * and accelerometers to take the device offset into account when integrating.
+   * <p>The center value is subtracted from each value*dt before it is added to the integrated
+   * value. This is used for the center value of devices like gyros and accelerometers to take the
+   * device offset into account when integrating.
+   *
+   * @param center The accumulator integrator's center value.
    */
   public void setAccumulatorIntegratedCenter(double center) {
     if (m_accum == null) {
@@ -768,8 +789,7 @@
   }
 
   /**
-   * Read the integrated value.  This is the sum of (each value * time between
-   * values).
+   * Read the integrated value. This is the sum of (each value * time between values).
    *
    * @return The integrated value accumulated since the last Reset().
    */
@@ -784,11 +804,10 @@
   }
 
   /**
-   * Read the average of the integrated value.  This is the sum of (each value
-   * times the time between values), divided by the count.
+   * Read the average of the integrated value. This is the sum of (each value times the time between
+   * values), divided by the count.
    *
-   * @return The average of the integrated value accumulated since the last
-   *         Reset().
+   * @return The average of the integrated value accumulated since the last Reset().
    */
   public double getAccumulatorIntegratedAverage() {
     if (m_accum == null) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Sendable.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Sendable.java
deleted file mode 100644
index 138f7f4..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Sendable.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-
-/**
- * The base interface for objects that can be sent over the network through network tables.
- */
-public interface Sendable {
-  /**
-   * Gets the name of this {@link Sendable} object.
-   *
-   * @return Name
-   * @deprecated Use SendableRegistry.getName()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default String getName() {
-    return SendableRegistry.getName(this);
-  }
-
-  /**
-   * Sets the name of this {@link Sendable} object.
-   *
-   * @param name name
-   * @deprecated Use SendableRegistry.setName()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void setName(String name) {
-    SendableRegistry.setName(this, name);
-  }
-
-  /**
-   * Sets both the subsystem name and device name of this {@link Sendable} object.
-   *
-   * @param subsystem subsystem name
-   * @param name device name
-   * @deprecated Use SendableRegistry.setName()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void setName(String subsystem, String name) {
-    SendableRegistry.setName(this, subsystem, name);
-  }
-
-  /**
-   * Sets the name of the sensor with a channel number.
-   *
-   * @param moduleType A string that defines the module name in the label for the value
-   * @param channel    The channel number the device is plugged into
-   * @deprecated Use SendableRegistry.setName()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void setName(String moduleType, int channel) {
-    SendableRegistry.setName(this, moduleType, channel);
-  }
-
-  /**
-   * Sets the name of the sensor with a module and channel number.
-   *
-   * @param moduleType   A string that defines the module name in the label for the value
-   * @param moduleNumber The number of the particular module type
-   * @param channel      The channel number the device is plugged into (usually PWM)
-   * @deprecated Use SendableRegistry.setName()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void setName(String moduleType, int moduleNumber, int channel) {
-    SendableRegistry.setName(this, moduleType, moduleNumber, channel);
-  }
-
-  /**
-   * Gets the subsystem name of this {@link Sendable} object.
-   *
-   * @return Subsystem name
-   * @deprecated Use SendableRegistry.getSubsystem()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default String getSubsystem() {
-    return SendableRegistry.getSubsystem(this);
-  }
-
-  /**
-   * Sets the subsystem name of this {@link Sendable} object.
-   *
-   * @param subsystem subsystem name
-   * @deprecated Use SendableRegistry.setSubsystem()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void setSubsystem(String subsystem) {
-    SendableRegistry.setSubsystem(this, subsystem);
-  }
-
-  /**
-   * Add a child component.
-   *
-   * @param child child component
-   * @deprecated Use SendableRegistry.addChild()
-   */
-  @Deprecated(since = "2020", forRemoval = true)
-  default void addChild(Object child) {
-    SendableRegistry.addChild(this, child);
-  }
-
-  /**
-   * Initializes this {@link Sendable} object.
-   *
-   * @param builder sendable builder
-   */
-  void initSendable(SendableBuilder builder);
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SendableBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SendableBase.java
deleted file mode 100644
index 4e2116c..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SendableBase.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Base class for all sensors. Stores most recent status information as well as containing utility
- * functions for checking channels and error processing.
- * @deprecated Use Sendable and SendableRegistry
- */
-@Deprecated(since = "2020", forRemoval = true)
-public abstract class SendableBase implements Sendable, AutoCloseable {
-  /**
-   * Creates an instance of the sensor base.
-   */
-  public SendableBase() {
-    this(true);
-  }
-
-  /**
-   * Creates an instance of the sensor base.
-   *
-   * @param addLiveWindow if true, add this Sendable to LiveWindow
-   */
-  public SendableBase(boolean addLiveWindow) {
-    if (addLiveWindow) {
-      SendableRegistry.addLW(this, "");
-    } else {
-      SendableRegistry.add(this, "");
-    }
-  }
-
-  @Override
-  public void close() {
-    SendableRegistry.remove(this);
-  }
-}
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 22aa7bd..0ac24de 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SensorUtil.java
@@ -1,92 +1,55 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 import edu.wpi.first.hal.AnalogJNI;
 import edu.wpi.first.hal.ConstantsJNI;
 import edu.wpi.first.hal.DIOJNI;
-import edu.wpi.first.hal.PDPJNI;
 import edu.wpi.first.hal.PWMJNI;
 import edu.wpi.first.hal.PortsJNI;
 import edu.wpi.first.hal.RelayJNI;
-import edu.wpi.first.hal.SolenoidJNI;
 
 /**
  * Stores most recent status information as well as containing utility functions for checking
  * channels and error processing.
  */
 public final class SensorUtil {
-  /**
-   * Ticks per microsecond.
-   */
-  public static final int kSystemClockTicksPerMicrosecond
-      = ConstantsJNI.getSystemClockTicksPerMicrosecond();
+  /** Ticks per microsecond. */
+  public static final int kSystemClockTicksPerMicrosecond =
+      ConstantsJNI.getSystemClockTicksPerMicrosecond();
 
-  /**
-   * Number of digital channels per roboRIO.
-   */
+  /** Number of digital channels per roboRIO. */
   public static final int kDigitalChannels = PortsJNI.getNumDigitalChannels();
 
-  /**
-   * Number of analog input channels per roboRIO.
-   */
+  /** Number of analog input channels per roboRIO. */
   public static final int kAnalogInputChannels = PortsJNI.getNumAnalogInputs();
 
-  /**
-   * Number of analog output channels per roboRIO.
-   */
+  /** Number of analog output channels per roboRIO. */
   public static final int kAnalogOutputChannels = PortsJNI.getNumAnalogOutputs();
 
-  /**
-   * Number of solenoid channels per module.
-   */
-  public static final int kSolenoidChannels = PortsJNI.getNumSolenoidChannels();
+  /** Number of solenoid channels per module. */
+  public static final int kCTRESolenoidChannels = PortsJNI.getNumCTRESolenoidChannels();
 
-  /**
-   * Number of PWM channels per roboRIO.
-   */
+  /** Number of PWM channels per roboRIO. */
   public static final int kPwmChannels = PortsJNI.getNumPWMChannels();
 
-  /**
-   * Number of relay channels per roboRIO.
-   */
+  /** Number of relay channels per roboRIO. */
   public static final int kRelayChannels = PortsJNI.getNumRelayHeaders();
 
-  /**
-   * Number of power distribution channels per PDP.
-   */
-  public static final int kPDPChannels = PortsJNI.getNumPDPChannels();
+  /** Number of power distribution channels per PDP. */
+  public static final int kCTREPDPChannels = PortsJNI.getNumCTREPDPChannels();
 
-  /**
-   * Number of power distribution modules per PDP.
-   */
-  public static final int kPDPModules = PortsJNI.getNumPDPModules();
+  /** Number of power distribution modules per PDP. */
+  public static final int kCTREPDPModules = PortsJNI.getNumCTREPDPModules();
 
-  /**
-   * Number of PCM Modules.
-   */
-  public static final int kPCMModules = PortsJNI.getNumPCMModules();
+  /** Number of PCM Modules. */
+  public static final int kCTREPCMModules = PortsJNI.getNumCTREPCMModules();
 
-  /**
-   * Verify that the solenoid module is correct.
-   *
-   * @param moduleNumber The solenoid module module number to check.
-   */
-  public static void checkSolenoidModule(final int moduleNumber) {
-    if (!SolenoidJNI.checkSolenoidModule(moduleNumber)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested solenoid module is out of range. Minimum: 0, Maximum: ")
-        .append(kPCMModules)
-        .append(", Requested: ")
-        .append(moduleNumber);
-      throw new IllegalArgumentException(buf.toString());
-    }
-  }
+  public static final int kREVPHChannels = PortsJNI.getNumREVPHChannels();
+
+  public static final int kREVPHModules = PortsJNI.getNumREVPHModules();
 
   /**
    * Check that the digital channel number is valid. Verify that the channel number is one of the
@@ -98,9 +61,9 @@
     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);
+          .append(kDigitalChannels)
+          .append(", Requested: ")
+          .append(channel);
       throw new IllegalArgumentException(buf.toString());
     }
   }
@@ -115,9 +78,9 @@
     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);
+          .append(kRelayChannels)
+          .append(", Requested: ")
+          .append(channel);
       throw new IllegalArgumentException(buf.toString());
     }
   }
@@ -132,9 +95,9 @@
     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);
+          .append(kPwmChannels)
+          .append(", Requested: ")
+          .append(channel);
       throw new IllegalArgumentException(buf.toString());
     }
   }
@@ -149,9 +112,9 @@
     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);
+          .append(kAnalogInputChannels)
+          .append(", Requested: ")
+          .append(channel);
       throw new IllegalArgumentException(buf.toString());
     }
   }
@@ -166,58 +129,9 @@
     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());
-    }
-  }
-
-  /**
-   * Verify that the solenoid channel number is within limits. Channel numbers are 0-based.
-   *
-   * @param channel The channel number to check.
-   */
-  public static void checkSolenoidChannel(final int channel) {
-    if (!SolenoidJNI.checkSolenoidChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested solenoid channel is out of range. Minimum: 0, Maximum: ")
-        .append(kSolenoidChannels)
-        .append(", Requested: ")
-        .append(channel);
-      throw new IllegalArgumentException(buf.toString());
-    }
-  }
-
-  /**
-   * Verify that the power distribution channel number is within limits. Channel numbers are
-   * 0-based.
-   *
-   * @param channel The channel number to check.
-   */
-  public static void checkPDPChannel(final int channel) {
-    if (!PDPJNI.checkPDPChannel(channel)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested PDP channel is out of range. Minimum: 0, Maximum: ")
-        .append(kPDPChannels)
-        .append(", Requested: ")
-        .append(channel);
-      throw new IllegalArgumentException(buf.toString());
-    }
-  }
-
-  /**
-   * Verify that the PDP module number is within limits. module numbers are 0-based.
-   *
-   * @param module The module number to check.
-   */
-  public static void checkPDPModule(final int module) {
-    if (!PDPJNI.checkPDPModule(module)) {
-      StringBuilder buf = new StringBuilder();
-      buf.append("Requested PDP module is out of range. Minimum: 0, Maximum: ")
-        .append(kPDPModules)
-        .append(", Requested: ")
-        .append(module);
+          .append(kAnalogOutputChannels)
+          .append(", Requested: ")
+          .append(channel);
       throw new IllegalArgumentException(buf.toString());
     }
   }
@@ -227,10 +141,20 @@
    *
    * @return The number of the default solenoid module.
    */
-  public static int getDefaultSolenoidModule() {
+  @SuppressWarnings("AbbreviationAsWordInName")
+  public static int getDefaultCTREPCMModule() {
     return 0;
   }
 
-  private SensorUtil() {
+  /**
+   * Get the number of the default solenoid module.
+   *
+   * @return The number of the default solenoid module.
+   */
+  @SuppressWarnings("AbbreviationAsWordInName")
+  public static int getDefaultREVPHModule() {
+    return 1;
   }
+
+  private SensorUtil() {}
 }
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 0f509c7..331bbf1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SerialPort.java
@@ -1,28 +1,25 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.nio.charset.StandardCharsets;
-
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SerialPortJNI;
+import java.nio.charset.StandardCharsets;
 
-/**
- * Driver for the serial ports (USB, MXP, Onboard) on the roboRIO.
- */
+/** Driver for the serial ports (USB, MXP, Onboard) on the roboRIO. */
 public class SerialPort implements AutoCloseable {
   private int m_portHandle;
 
   public enum Port {
-    kOnboard(0), kMXP(1), kUSB(2), kUSB1(2), kUSB2(3);
+    kOnboard(0),
+    kMXP(1),
+    kUSB(2),
+    kUSB1(2),
+    kUSB2(3);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     Port(int value) {
@@ -30,13 +27,14 @@
     }
   }
 
-  /**
-   * Represents the parity to use for serial communications.
-   */
+  /** Represents the parity to use for serial communications. */
   public enum Parity {
-    kNone(0), kOdd(1), kEven(2), kMark(3), kSpace(4);
+    kNone(0),
+    kOdd(1),
+    kEven(2),
+    kMark(3),
+    kSpace(4);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     Parity(int value) {
@@ -44,13 +42,12 @@
     }
   }
 
-  /**
-   * Represents the number of stop bits to use for Serial Communication.
-   */
+  /** Represents the number of stop bits to use for Serial Communication. */
   public enum StopBits {
-    kOne(10), kOnePointFive(15), kTwo(20);
+    kOne(10),
+    kOnePointFive(15),
+    kTwo(20);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     StopBits(int value) {
@@ -58,13 +55,13 @@
     }
   }
 
-  /**
-   * Represents what type of flow control to use for serial communication.
-   */
+  /** Represents what type of flow control to use for serial communication. */
   public enum FlowControl {
-    kNone(0), kXonXoff(1), kRtsCts(2), kDtsDsr(4);
+    kNone(0),
+    kXonXoff(1),
+    kRtsCts(2),
+    kDtsDsr(4);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     FlowControl(int value) {
@@ -72,13 +69,11 @@
     }
   }
 
-  /**
-   * Represents which type of buffer mode to use when writing to a serial port.
-   */
+  /** Represents which type of buffer mode to use when writing to a serial port. */
   public enum WriteBufferMode {
-    kFlushOnAccess(1), kFlushWhenFull(2);
+    kFlushOnAccess(1),
+    kFlushWhenFull(2);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     WriteBufferMode(int value) {
@@ -89,19 +84,25 @@
   /**
    * Create an instance of a Serial Port class.
    *
-   * <p>Prefer to use the constructor that doesn't take a port name, but in some
-   * cases the automatic detection might not work correctly.
+   * <p>Prefer to use the constructor that doesn't take a port name, but in some cases the automatic
+   * detection might not work correctly.
    *
    * @param baudRate The baud rate to configure the serial port.
-   * @param port     The Serial port to use
+   * @param port The Serial port to use
    * @param portName The direct portName to use
    * @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
-   * @param parity   Select the type of parity checking to use.
+   * @param parity Select the type of parity checking to use.
    * @param stopBits The number of stop bits to use as defined by the enum StopBits.
-   * @deprecated     Will be removed for 2019
+   * @deprecated Will be removed for 2019
    */
-  public SerialPort(final int baudRate, String portName, Port port, final int dataBits,
-                    Parity parity, StopBits stopBits) {
+  @Deprecated
+  public SerialPort(
+      final int baudRate,
+      String portName,
+      Port port,
+      final int dataBits,
+      Parity parity,
+      StopBits stopBits) {
     m_portHandle = SerialPortJNI.serialInitializePortDirect((byte) port.value, portName);
     SerialPortJNI.serialSetBaudRate(m_portHandle, baudRate);
     SerialPortJNI.serialSetDataBits(m_portHandle, (byte) dataBits);
@@ -126,13 +127,13 @@
    * Create an instance of a Serial Port class.
    *
    * @param baudRate The baud rate to configure the serial port.
-   * @param port     The Serial port to use
+   * @param port The Serial port to use
    * @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
-   * @param parity   Select the type of parity checking to use.
+   * @param parity Select the type of parity checking to use.
    * @param stopBits The number of stop bits to use as defined by the enum StopBits.
    */
-  public SerialPort(final int baudRate, Port port, final int dataBits, Parity parity,
-                    StopBits stopBits) {
+  public SerialPort(
+      final int baudRate, Port port, final int dataBits, Parity parity, StopBits stopBits) {
     m_portHandle = SerialPortJNI.serialInitializePort((byte) port.value);
     SerialPortJNI.serialSetBaudRate(m_portHandle, baudRate);
     SerialPortJNI.serialSetDataBits(m_portHandle, (byte) dataBits);
@@ -157,8 +158,9 @@
    * Create an instance of a Serial Port class. Defaults to one stop bit.
    *
    * @param baudRate The baud rate to configure the serial port.
+   * @param port The serial port to use.
    * @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
-   * @param parity   Select the type of parity checking to use.
+   * @param parity Select the type of parity checking to use.
    */
   public SerialPort(final int baudRate, Port port, final int dataBits, Parity parity) {
     this(baudRate, port, dataBits, parity, StopBits.kOne);
@@ -168,6 +170,7 @@
    * Create an instance of a Serial Port class. Defaults to no parity and one stop bit.
    *
    * @param baudRate The baud rate to configure the serial port.
+   * @param port The serial port to use.
    * @param dataBits The number of data bits per transfer. Valid values are between 5 and 8 bits.
    */
   public SerialPort(final int baudRate, Port port, final int dataBits) {
@@ -175,10 +178,10 @@
   }
 
   /**
-   * Create an instance of a Serial Port class. Defaults to 8 databits, no parity, and one stop
-   * bit.
+   * Create an instance of a Serial Port class. Defaults to 8 databits, no parity, and one stop bit.
    *
    * @param baudRate The baud rate to configure the serial port.
+   * @param port The serial port to use.
    */
   public SerialPort(final int baudRate, Port port) {
     this(baudRate, port, 8, Parity.kNone, StopBits.kOne);
@@ -226,9 +229,7 @@
     enableTermination('\n');
   }
 
-  /**
-   * Disable termination behavior.
-   */
+  /** Disable termination behavior. */
   public void disableTermination() {
     SerialPortJNI.serialDisableTermination(m_portHandle);
   }
@@ -283,7 +284,7 @@
    * Write raw bytes to the serial port.
    *
    * @param buffer The buffer of bytes to write.
-   * @param count  The maximum number of bytes to write.
+   * @param count The maximum number of bytes to write.
    * @return The number of bytes actually written into the port.
    */
   public int write(byte[] buffer, int count) {
@@ -344,11 +345,11 @@
   /**
    * Specify the flushing behavior of the output buffer.
    *
-   * <p>When set to kFlushOnAccess, data is synchronously written to the serial port after each
-   * call to either print() or write().
+   * <p>When set to kFlushOnAccess, data is synchronously written to the serial port after each call
+   * to either print() or write().
    *
-   * <p>When set to kFlushWhenFull, data will only be written to the serial port when the buffer
-   * is full or when flush() is called.
+   * <p>When set to kFlushWhenFull, data will only be written to the serial port when the buffer is
+   * full or when flush() is called.
    *
    * @param mode The write buffer mode.
    */
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 6b2eab6..f11f8f9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Servo.java
@@ -1,16 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
  * Standard hobby style servo.
@@ -28,11 +25,11 @@
   /**
    * Constructor.<br>
    *
-   * <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 maxPWM value<br>
+   * By default {@value #kDefaultMinServoPWM} ms is used as the minPWM value<br>
    *
    * @param channel The PWM channel to which the servo is attached. 0-9 are on-board, 10-19 are on
-   *                the MXP port
+   *     the MXP port
    */
   public Servo(final int channel) {
     super(channel);
@@ -43,7 +40,6 @@
     SendableRegistry.setName(this, "Servo", getChannel());
   }
 
-
   /**
    * Set the servo position.
    *
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SlewRateLimiter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SlewRateLimiter.java
deleted file mode 100644
index fe4dda7..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SlewRateLimiter.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.wpiutil.math.MathUtil;
-
-/**
- * A class that limits the rate of change of an input value.  Useful for implementing voltage,
- * setpoint, and/or output ramps.  A slew-rate limit is most appropriate when the quantity being
- * controlled is a velocity or a voltage; when controlling a position, consider using a
- * {@link edu.wpi.first.wpilibj.trajectory.TrapezoidProfile} instead.
- */
-public class SlewRateLimiter {
-  private final double m_rateLimit;
-  private double m_prevVal;
-  private double m_prevTime;
-
-  /**
-   * Creates a new SlewRateLimiter with the given rate limit and initial value.
-   *
-   * @param rateLimit The rate-of-change limit, in units per second.
-   * @param initialValue The initial value of the input.
-   */
-  public SlewRateLimiter(double rateLimit, double initialValue) {
-    m_rateLimit = rateLimit;
-    m_prevVal = initialValue;
-    m_prevTime = Timer.getFPGATimestamp();
-  }
-
-  /**
-   * Creates a new SlewRateLimiter with the given rate limit and an initial value of zero.
-   *
-   * @param rateLimit The rate-of-change limit, in units per second.
-   */
-  public SlewRateLimiter(double rateLimit) {
-    this(rateLimit, 0);
-  }
-
-  /**
-   * Filters the input to limit its slew rate.
-   *
-   * @param input The input value whose slew rate is to be limited.
-   * @return The filtered value, which will not change faster than the slew rate.
-   */
-  public double calculate(double input) {
-    double currentTime = Timer.getFPGATimestamp();
-    double elapsedTime = currentTime - m_prevTime;
-    m_prevVal += MathUtil.clamp(input - m_prevVal,
-                                -m_rateLimit * elapsedTime,
-                                m_rateLimit * elapsedTime);
-    m_prevTime = currentTime;
-    return m_prevVal;
-  }
-
-  /**
-   * Resets the slew rate limiter to the specified value; ignores the rate limit when doing so.
-   *
-   * @param value The value to reset to.
-   */
-  public void reset(double value) {
-    m_prevVal = value;
-    m_prevTime = Timer.getFPGATimestamp();
-  }
-}
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 6245c2b..e86042b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Solenoid.java
@@ -1,62 +1,83 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.hal.SolenoidJNI;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.hal.util.AllocationException;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
 /**
- * Solenoid class for running high voltage Digital Output on the PCM.
+ * Solenoid class for running high voltage Digital Output on a pneumatics module.
  *
- * <p>The Solenoid class is typically used for pneumatic solenoids, but could be used for any
- * device within the current spec of the PCM.
+ * <p>The Solenoid class is typically used for pneumatic solenoids, but could be used for any device
+ * within the current spec of the module.
  */
-public class Solenoid extends SolenoidBase implements Sendable, AutoCloseable {
-  private final int m_channel; // The channel to control.
-  private int m_solenoidHandle;
+public class Solenoid implements Sendable, AutoCloseable {
+  private final int m_mask; // The channel mask
+  private final int m_channel;
+  private PneumaticsBase m_module;
 
   /**
-   * Constructor using the default PCM ID (defaults to 0).
+   * Constructs a solenoid for a default module and specified type.
    *
-   * @param channel The channel on the PCM to control (0..7).
+   * @param moduleType The module type to use.
+   * @param channel The channel the solenoid is on.
    */
-  public Solenoid(final int channel) {
-    this(SensorUtil.getDefaultSolenoidModule(), channel);
+  public Solenoid(final PneumaticsModuleType moduleType, final int channel) {
+    this(PneumaticsBase.getDefaultForType(moduleType), moduleType, channel);
   }
 
   /**
-   * Constructor.
+   * Constructs a solenoid for a specified module and type.
    *
-   * @param moduleNumber The CAN ID of the PCM the solenoid is attached to.
-   * @param channel      The channel on the PCM to control (0..7).
+   * @param module The module ID to use.
+   * @param moduleType The module type to use.
+   * @param channel The channel the solenoid is on.
    */
-  public Solenoid(final int moduleNumber, final int channel) {
-    super(moduleNumber);
+  public Solenoid(final int module, final PneumaticsModuleType moduleType, final int channel) {
+    m_module = PneumaticsBase.getForType(module, moduleType);
+    boolean allocatedSolenoids = false;
+    boolean successfulCompletion = false;
+
+    m_mask = 1 << channel;
     m_channel = channel;
 
-    SensorUtil.checkSolenoidModule(m_moduleNumber);
-    SensorUtil.checkSolenoidChannel(m_channel);
+    try {
+      if (!m_module.checkSolenoidChannel(channel)) {
+        throw new IllegalArgumentException("Channel " + channel + " out of range");
+      }
 
-    int portHandle = HAL.getPortWithModule((byte) m_moduleNumber, (byte) m_channel);
-    m_solenoidHandle = SolenoidJNI.initializeSolenoidPort(portHandle);
+      if (m_module.checkAndReserveSolenoids(m_mask) != 0) {
+        throw new AllocationException("Solenoid already allocated");
+      }
 
-    HAL.report(tResourceType.kResourceType_Solenoid, m_channel + 1, m_moduleNumber + 1);
-    SendableRegistry.addLW(this, "Solenoid", m_moduleNumber, m_channel);
+      allocatedSolenoids = true;
+
+      HAL.report(tResourceType.kResourceType_Solenoid, channel + 1, m_module.getModuleNumber() + 1);
+      SendableRegistry.addLW(this, "Solenoid", m_module.getModuleNumber(), channel);
+      successfulCompletion = true;
+
+    } finally {
+      if (!successfulCompletion) {
+        if (allocatedSolenoids) {
+          m_module.unreserveSolenoids(m_mask);
+        }
+        m_module.close();
+      }
+    }
   }
 
   @Override
   public void close() {
     SendableRegistry.remove(this);
-    SolenoidJNI.freeSolenoidPort(m_solenoidHandle);
-    m_solenoidHandle = 0;
+    m_module.unreserveSolenoids(m_mask);
+    m_module.close();
+    m_module = null;
   }
 
   /**
@@ -65,7 +86,8 @@
    * @param on True will turn the solenoid output on. False will turn the solenoid output off.
    */
   public void set(boolean on) {
-    SolenoidJNI.setSolenoid(m_solenoidHandle, on);
+    int value = on ? (0xFFFF & m_mask) : 0;
+    m_module.setSolenoids(m_mask, value);
   }
 
   /**
@@ -74,7 +96,8 @@
    * @return True if the solenoid output is on or false if the solenoid output is off.
    */
   public boolean get() {
-    return SolenoidJNI.getSolenoid(m_solenoidHandle);
+    int currentAll = m_module.getSolenoids();
+    return (currentAll & m_mask) != 0;
   }
 
   /**
@@ -88,39 +111,44 @@
   }
 
   /**
-   * Check if solenoid is blacklisted. If a solenoid is shorted, it is added to the blacklist and
-   * disabled until power cycle, or until faults are cleared.
+   * Get the channel this solenoid is connected to.
    *
-   * @return If solenoid is disabled due to short.
-   * @see #clearAllPCMStickyFaults()
+   * @return The channel this solenoid is connected to.
    */
-  public boolean isBlackListed() {
-    int value = getPCMSolenoidBlackList() & (1 << m_channel);
-    return value != 0;
+  public int getChannel() {
+    return m_channel;
   }
 
   /**
-   * Set the pulse duration in the PCM. This is used in conjunction with
-   * the startPulse method to allow the PCM to control the timing of a pulse.
-   * The timing can be controlled in 0.01 second increments.
+   * Check if solenoid is DisabledListed. If a solenoid is shorted, it is added to the Disabled List
+   * and disabled until power cycle, or until faults are cleared.
+   *
+   * @return If solenoid is disabled due to short.
+   */
+  public boolean isDisabled() {
+    return (m_module.getSolenoidDisabledList() & m_mask) != 0;
+  }
+
+  /**
+   * Set the pulse duration in the PCM. This is used in conjunction with the startPulse method to
+   * allow the PCM to control the timing of a pulse. The timing can be controlled in 0.01 second
+   * increments.
    *
    * @param durationSeconds The duration of the pulse, from 0.01 to 2.55 seconds.
-   *
    * @see #startPulse()
    */
   public void setPulseDuration(double durationSeconds) {
     long durationMS = (long) (durationSeconds * 1000);
-    SolenoidJNI.setOneShotDuration(m_solenoidHandle, durationMS);
+    m_module.setOneShotDuration(m_channel, (int) durationMS);
   }
 
   /**
-   * Trigger the PCM to generate a pulse of the duration set in
-   * setPulseDuration.
+   * Trigger the PCM to generate a pulse of the duration set in setPulseDuration.
    *
    * @see #setPulseDuration(double)
    */
   public void startPulse() {
-    SolenoidJNI.fireOneShot(m_solenoidHandle);
+    m_module.fireOneShot(m_channel);
   }
 
   @Override
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SolenoidBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SolenoidBase.java
deleted file mode 100644
index a778d8c..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SolenoidBase.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.SolenoidJNI;
-
-/**
- * SolenoidBase class is the common base class for the {@link Solenoid} and {@link DoubleSolenoid}
- * classes.
- */
-public class SolenoidBase {
-  protected final int m_moduleNumber; // The number of the solenoid module being used.
-
-  /**
-   * Constructor.
-   *
-   * @param moduleNumber The PCM CAN ID
-   */
-  protected SolenoidBase(final int moduleNumber) {
-    m_moduleNumber = moduleNumber;
-  }
-
-  /**
-   * Read all 8 solenoids from the specified module as a single byte.
-   *
-   * @param moduleNumber the module number to read
-   * @return The current value of all 8 solenoids on the module.
-   */
-  public static int getAll(int moduleNumber) {
-    return SolenoidJNI.getAllSolenoids(moduleNumber);
-  }
-
-  /**
-   * Read all 8 solenoids from the module used by this solenoid as a single byte.
-   *
-   * @return The current value of all 8 solenoids on this module.
-   */
-  public int getAll() {
-    return SolenoidBase.getAll(m_moduleNumber);
-  }
-
-  /**
-   * Reads complete solenoid blacklist for all 8 solenoids as a single byte. If a solenoid is
-   * shorted, it is added to the blacklist and disabled until power cycle, or until faults are
-   * cleared.
-   *
-   * @param moduleNumber the module number to read
-   * @return The solenoid blacklist of all 8 solenoids on the module.
-   * @see #clearAllPCMStickyFaults()
-   */
-  public static int getPCMSolenoidBlackList(int moduleNumber) {
-    return SolenoidJNI.getPCMSolenoidBlackList(moduleNumber);
-  }
-
-  /**
-   * Reads complete solenoid blacklist for all 8 solenoids as a single byte. If a solenoid is
-   * shorted, it is added to the blacklist and disabled until power cycle, or until faults are
-   * cleared.
-   *
-   * @return The solenoid blacklist of all 8 solenoids on the module.
-   * @see #clearAllPCMStickyFaults()
-   */
-  public int getPCMSolenoidBlackList() {
-    return SolenoidBase.getPCMSolenoidBlackList(m_moduleNumber);
-  }
-
-  /**
-   * If true, the common highside solenoid voltage rail is too low, most likely a solenoid channel
-   * is shorted.
-   *
-   * @param moduleNumber the module number to read
-   * @return true if PCM sticky fault is set
-   */
-  public static boolean getPCMSolenoidVoltageStickyFault(int moduleNumber) {
-    return SolenoidJNI.getPCMSolenoidVoltageStickyFault(moduleNumber);
-  }
-
-  /**
-   * If true, the common highside solenoid voltage rail is too low, most likely a solenoid channel
-   * is shorted.
-   *
-   * @return true if PCM sticky fault is set
-   */
-  public boolean getPCMSolenoidVoltageStickyFault() {
-    return SolenoidBase.getPCMSolenoidVoltageStickyFault(m_moduleNumber);
-  }
-
-  /**
-   * The common highside solenoid voltage rail is too low, most likely a solenoid channel is
-   * shorted.
-   *
-   * @param moduleNumber the module number to read
-   * @return true if PCM is in fault state.
-   */
-  public static boolean getPCMSolenoidVoltageFault(int moduleNumber) {
-    return SolenoidJNI.getPCMSolenoidVoltageFault(moduleNumber);
-  }
-
-  /**
-   * The common highside solenoid voltage rail is too low, most likely a solenoid channel is
-   * shorted.
-   *
-   * @return true if PCM is in fault state.
-   */
-  public boolean getPCMSolenoidVoltageFault() {
-    return SolenoidBase.getPCMSolenoidVoltageFault(m_moduleNumber);
-  }
-
-  /**
-   * Clear ALL sticky faults inside PCM that Compressor is wired to.
-   *
-   * <p>If a sticky fault is set, then it will be persistently cleared. Compressor drive maybe
-   * momentarily disable while flags are being cleared. Care should be taken to not call this too
-   * frequently, otherwise normal compressor functionality may be prevented.
-   *
-   * <p>If no sticky faults are set then this call will have no effect.
-   *
-   * @param moduleNumber the module number to read
-   */
-  public static void clearAllPCMStickyFaults(int moduleNumber) {
-    SolenoidJNI.clearAllPCMStickyFaults(moduleNumber);
-  }
-
-  /**
-   * Clear ALL sticky faults inside PCM that Compressor is wired to.
-   *
-   * <p>If a sticky fault is set, then it will be persistently cleared. Compressor drive maybe
-   * momentarily disable while flags are being cleared. Care should be taken to not call this too
-   * frequently, otherwise normal compressor functionality may be prevented.
-   *
-   * <p>If no sticky faults are set then this call will have no effect.
-   */
-  public void clearAllPCMStickyFaults() {
-    SolenoidBase.clearAllPCMStickyFaults(m_moduleNumber);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Spark.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Spark.java
deleted file mode 100644
index c8bacbf..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Spark.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * REV Robotics SPARK Speed Controller.
- *
- * <p>Note that the SPARK 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 User Manual
- * available from REV Robotics.
- *
- * <p><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 Spark extends PWMSpeedController {
-  /**
-   * Common initialization code called by all constructors.
-   */
-  protected void initSpark() {
-    setBounds(2.003, 1.55, 1.50, 1.46, 0.999);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_RevSPARK, getChannel() + 1);
-    SendableRegistry.setName(this, "Spark", getChannel());
-  }
-
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the SPARK is attached to. 0-9 are on-board, 10-19 are on
-   *                the MXP port
-   */
-  public Spark(final int channel) {
-    super(channel);
-    initSpark();
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedController.java
index 4407ff0..490455c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedController.java
@@ -1,28 +1,28 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
 /**
- * Interface for speed controlling devices.
+ * Interface for motor controlling devices.
+ *
+ * @deprecated Use {@link edu.wpi.first.wpilibj.motorcontrol.MotorController}.
  */
-public interface SpeedController extends PIDOutput {
+@Deprecated(since = "2022", forRemoval = true)
+public interface SpeedController {
   /**
-   * Common interface for setting the speed of a speed controller.
+   * Common interface for setting the speed of a motor controller.
    *
    * @param speed The speed to set. Value should be between -1.0 and 1.0.
    */
   void set(double speed);
 
   /**
-   * Sets the voltage output of the SpeedController.  Compensates for the current bus
-   * voltage to ensure that the desired voltage is output even if the battery voltage is below
-   * 12V - highly useful when the voltage outputs are "meaningful" (e.g. they come from a
-   * feedforward calculation).
+   * Sets the voltage output of the MotorController. Compensates for the current bus voltage to
+   * ensure that the desired voltage is output even if the battery voltage is below 12V - highly
+   * useful when the voltage outputs are "meaningful" (e.g. they come from a feedforward
+   * calculation).
    *
    * <p>NOTE: This function *must* be called regularly in order for voltage compensation to work
    * properly - unlike the ordinary set function, it is not "set it and forget it."
@@ -34,29 +34,27 @@
   }
 
   /**
-   * Common interface for getting the current set speed of a speed controller.
+   * Common interface for getting the current set speed of a motor controller.
    *
    * @return The current set speed. Value is between -1.0 and 1.0.
    */
   double get();
 
   /**
-   * Common interface for inverting direction of a speed controller.
+   * Common interface for inverting direction of a motor controller.
    *
    * @param isInverted The state of inversion true is inverted.
    */
   void setInverted(boolean isInverted);
 
   /**
-   * Common interface for returning if a speed controller is in the inverted state or not.
+   * Common interface for returning if a motor controller is in the inverted state or not.
    *
    * @return isInverted The state of the inversion true is inverted.
    */
   boolean getInverted();
 
-  /**
-   * Disable the speed controller.
-   */
+  /** Disable the motor controller. */
   void disable();
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedControllerGroup.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedControllerGroup.java
index 81b5470..d2217b7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedControllerGroup.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SpeedControllerGroup.java
@@ -1,21 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import edu.wpi.first.wpilibj.motorcontrol.MotorController;
 import java.util.Arrays;
 
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
 /**
  * Allows multiple {@link SpeedController} objects to be linked together.
+ *
+ * @deprecated Use {@link edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup}.
  */
-public class SpeedControllerGroup implements SpeedController, Sendable, AutoCloseable {
+@Deprecated(since = "2022", forRemoval = true)
+@SuppressWarnings("removal")
+public class SpeedControllerGroup implements MotorController, Sendable, AutoCloseable {
   private boolean m_isInverted;
   private final SpeedController[] m_speedControllers;
   private static int instances;
@@ -23,16 +25,14 @@
   /**
    * Create a new SpeedControllerGroup with the provided SpeedControllers.
    *
+   * @param speedController The first SpeedController to add.
    * @param speedControllers The SpeedControllers to add
    */
-  @SuppressWarnings("PMD.AvoidArrayLoops")
-  public SpeedControllerGroup(SpeedController speedController,
-                              SpeedController... speedControllers) {
+  public SpeedControllerGroup(
+      SpeedController speedController, SpeedController... speedControllers) {
     m_speedControllers = new SpeedController[speedControllers.length + 1];
     m_speedControllers[0] = speedController;
-    for (int i = 0; i < speedControllers.length; i++) {
-      m_speedControllers[i + 1] = speedControllers[i];
-    }
+    System.arraycopy(speedControllers, 0, m_speedControllers, 1, speedControllers.length);
     init();
   }
 
@@ -46,7 +46,7 @@
       SendableRegistry.addChild(this, controller);
     }
     instances++;
-    SendableRegistry.addLW(this, "SpeedControllerGroup", instances);
+    SendableRegistry.addLW(this, "MotorControllerGroup", instances);
   }
 
   @Override
@@ -94,13 +94,8 @@
   }
 
   @Override
-  public void pidWrite(double output) {
-    set(output);
-  }
-
-  @Override
   public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("Speed Controller");
+    builder.setSmartDashboardType("Motor Controller");
     builder.setActuator(true);
     builder.setSafeState(this::stopMotor);
     builder.addDoubleProperty("Value", this::get, this::set);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java
new file mode 100644
index 0000000..1e7a54d
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/SynchronousInterrupt.java
@@ -0,0 +1,165 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
+import edu.wpi.first.hal.InterruptJNI;
+
+/**
+ * Class for handling synchronous (blocking) interrupts.
+ *
+ * <p>By default, interrupts will occur on rising edge.
+ *
+ * <p>Asynchronous interrupts are handled by the AsynchronousInterrupt class.
+ */
+public class SynchronousInterrupt implements AutoCloseable {
+  @SuppressWarnings("PMD.SingularField")
+  private final DigitalSource m_source;
+
+  private final int m_handle;
+
+  @SuppressWarnings("JavadocMethod")
+  public enum WaitResult {
+    kTimeout(0x0),
+    kRisingEdge(0x1),
+    kFallingEdge(0x100),
+    kBoth(0x101);
+
+    @SuppressWarnings("MemberName")
+    public final int value;
+
+    WaitResult(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Create a wait result enum.
+     *
+     * @param rising True if a rising edge occurred.
+     * @param falling True if a falling edge occurred.
+     * @return A wait result enum.
+     */
+    public static WaitResult getValue(boolean rising, boolean falling) {
+      if (rising && falling) {
+        return kBoth;
+      } else if (rising) {
+        return kRisingEdge;
+      } else if (falling) {
+        return kFallingEdge;
+      } else {
+        return kTimeout;
+      }
+    }
+  }
+
+  /**
+   * Constructs a new synchronous interrupt using a DigitalSource.
+   *
+   * <p>At construction, the interrupt will trigger on the rising edge.
+   *
+   * @param source The digital source to use.
+   */
+  public SynchronousInterrupt(DigitalSource source) {
+    m_source = requireNonNullParam(source, "source", "SynchronousInterrupt");
+    m_handle = InterruptJNI.initializeInterrupts();
+    InterruptJNI.requestInterrupts(
+        m_handle, m_source.getPortHandleForRouting(), m_source.getAnalogTriggerTypeForRouting());
+    InterruptJNI.setInterruptUpSourceEdge(m_handle, true, false);
+  }
+
+  /**
+   * Closes the interrupt.
+   *
+   * <p>This does not close the associated digital source.
+   */
+  @Override
+  public void close() {
+    InterruptJNI.cleanInterrupts(m_handle);
+  }
+
+  /**
+   * Wait for interrupt that returns the raw result value from the hardware.
+   *
+   * <p>Used by AsynchronousInterrupt. Users should use waitForInterrupt.
+   *
+   * @param timeoutSeconds The timeout in seconds. 0 or less will return immediately.
+   * @param ignorePrevious True to ignore if a previous interrupt has occurred, and only wait for a
+   *     new trigger. False will consider if an interrupt has occurred since the last time the
+   *     interrupt was read.
+   * @return The raw hardware interrupt result
+   */
+  int waitForInterruptRaw(double timeoutSeconds, boolean ignorePrevious) {
+    return InterruptJNI.waitForInterrupt(m_handle, timeoutSeconds, ignorePrevious);
+  }
+
+  /**
+   * Wait for an interrupt.
+   *
+   * @param timeoutSeconds The timeout in seconds. 0 or less will return immediately.
+   * @param ignorePrevious True to ignore if a previous interrupt has occurred, and only wait for a
+   *     new trigger. False will consider if an interrupt has occurred since the last time the
+   *     interrupt was read.
+   * @return Result of which edges were triggered, or if an timeout occurred.
+   */
+  public WaitResult waitForInterrupt(double timeoutSeconds, boolean ignorePrevious) {
+    int result = InterruptJNI.waitForInterrupt(m_handle, timeoutSeconds, ignorePrevious);
+
+    // Rising edge result is the interrupt bit set in the byte 0xFF
+    // Falling edge result is the interrupt bit set in the byte 0xFF00
+    // Set any bit set to be true for that edge, and AND the 2 results
+    // together to match the existing enum for all interrupts
+    boolean rising = (result & 0xFF) != 0;
+    boolean falling = (result & 0xFF00) != 0;
+    return WaitResult.getValue(rising, falling);
+  }
+
+  /**
+   * Wait for an interrupt, ignoring any previously occurring interrupts.
+   *
+   * @param timeoutSeconds The timeout in seconds. 0 or less will return immediately.
+   * @return Result of which edges were triggered, or if an timeout occurred.
+   */
+  public WaitResult waitForInterrupt(double timeoutSeconds) {
+    return waitForInterrupt(timeoutSeconds, true);
+  }
+
+  /**
+   * Set which edges to trigger the interrupt on.
+   *
+   * @param risingEdge Trigger on rising edge
+   * @param fallingEdge Trigger on falling edge
+   */
+  public void setInterruptEdges(boolean risingEdge, boolean fallingEdge) {
+    InterruptJNI.setInterruptUpSourceEdge(m_handle, risingEdge, fallingEdge);
+  }
+
+  /**
+   * Get the timestamp of the last rising edge.
+   *
+   * <p>This only works if rising edge was configured using setInterruptEdges.
+   *
+   * @return the timestamp in seconds relative to getFPGATime
+   */
+  public double getRisingTimestamp() {
+    return InterruptJNI.readInterruptRisingTimestamp(m_handle) * 1e-6;
+  }
+
+  /**
+   * Get the timestamp of the last falling edge.
+   *
+   * <p>This only works if falling edge was configured using setInterruptEdges.
+   *
+   * @return the timestamp in seconds relative to getFPGATime
+   */
+  public double getFallingTimestamp() {
+    return InterruptJNI.readInterruptFallingTimestamp(m_handle) * 1e-6;
+  }
+
+  /** Force triggering of any waiting interrupt, which will be seen as a timeout. */
+  public void wakeupWaitingInterrupt() {
+    InterruptJNI.releaseWaitingInterrupt(m_handle);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Talon.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Talon.java
deleted file mode 100644
index 6f20869..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Talon.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * Cross the Road Electronics (CTRE) Talon and Talon SR Speed Controller.
- *
- * <p>Note that the Talon 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 Talon User Manual
- * available from CTRE.
- *
- * <p><ul>
- * <li>2.037ms = full "forward"
- * <li>1.539ms = the "high end" of the deadband range
- * <li>1.513ms = center of the deadband range (off)
- * <li>1.487ms = the "low end" of the deadband range
- * <li>0.989ms = full "reverse"
- * </ul>
- */
-public class Talon extends PWMSpeedController {
-  /**
-   * Constructor for a Talon (original or Talon SR).
-   *
-   * @param channel The PWM channel that the Talon is attached to. 0-9 are on-board, 10-19 are on
-   *                the MXP port
-   */
-  public Talon(final int channel) {
-    super(channel);
-
-    setBounds(2.037, 1.539, 1.513, 1.487, 0.989);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_Talon, getChannel() + 1);
-    SendableRegistry.setName(this, "Talon", getChannel());
-  }
-}
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 7b49d4d..8b0d167 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Threads.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -11,35 +8,35 @@
 
 public final class Threads {
   /**
-  * Get the thread priority for the current thread.
-  * @return The current thread priority. Scaled 1-99.
-  */
+   * Get the thread priority for the current thread.
+   *
+   * @return The current thread priority. For real-time, this is 1-99 with 99 being highest. For
+   *     non-real-time, this is 0. See "man 7 sched" for details.
+   */
   public static int getCurrentThreadPriority() {
     return ThreadsJNI.getCurrentThreadPriority();
   }
 
   /**
-  * Get if the current thread is realtime.
-  * @return If the current thread is realtime
-  */
+   * Get if the current thread is real-time.
+   *
+   * @return If the current thread is real-time.
+   */
   public static boolean getCurrentThreadIsRealTime() {
     return ThreadsJNI.getCurrentThreadIsRealTime();
   }
 
   /**
-  * Sets the thread priority for the current thread.
-  *
-  * @param realTime Set to true to set a realtime priority, false for standard
-  *     priority
-  * @param priority Priority to set the thread to. Scaled 1-99, with 1 being
-  *     highest. On RoboRIO, priority is ignored for non realtime setting
-  *
-  * @return The success state of setting the priority
-  */
+   * Sets the thread priority for the current thread.
+   *
+   * @param realTime Set to true to set a real-time priority, false for standard priority.
+   * @param priority Priority to set the thread to. For real-time, this is 1-99 with 99 being
+   *     highest. For non-real-time, this is forced to 0. See "man 7 sched" for details.
+   * @return True on success.
+   */
   public static boolean setCurrentThreadPriority(boolean realTime, int priority) {
     return ThreadsJNI.setCurrentThreadPriority(realTime, priority);
   }
 
-  private Threads() {
-  }
+  private Threads() {}
 }
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 99bce3d..dda62d2 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimedRobot.java
@@ -1,18 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.PriorityQueue;
-
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.NotifierJNI;
+import java.util.PriorityQueue;
 
 /**
  * TimedRobot implements the IterativeRobotBase robot program framework.
@@ -23,7 +19,7 @@
  */
 public class TimedRobot extends IterativeRobotBase {
   @SuppressWarnings("MemberName")
-  class Callback implements Comparable<Callback> {
+  static class Callback implements Comparable<Callback> {
     public Runnable func;
     public double period;
     public double expirationTime;
@@ -31,20 +27,33 @@
     /**
      * Construct a callback container.
      *
-     * @param func             The callback to run.
-     * @param startTimeSeconds The common starting point for all callback
-     *                         scheduling in seconds.
-     * @param periodSeconds    The period at which to run the callback in
-     *                         seconds.
-     * @param offsetSeconds    The offset from the common starting time in
-     *                         seconds.
+     * @param func The callback to run.
+     * @param startTimeSeconds The common starting point for all callback scheduling in seconds.
+     * @param periodSeconds The period at which to run the callback in seconds.
+     * @param offsetSeconds The offset from the common starting time in seconds.
      */
     Callback(Runnable func, double startTimeSeconds, double periodSeconds, double offsetSeconds) {
       this.func = func;
       this.period = periodSeconds;
-      this.expirationTime = startTimeSeconds + offsetSeconds
-        + Math.floor((Timer.getFPGATimestamp() - startTimeSeconds)
-            / this.period) * this.period + this.period;
+      this.expirationTime =
+          startTimeSeconds
+              + offsetSeconds
+              + Math.floor((Timer.getFPGATimestamp() - startTimeSeconds) / this.period)
+                  * this.period
+              + this.period;
+    }
+
+    @Override
+    public boolean equals(Object rhs) {
+      if (rhs instanceof Callback) {
+        return Double.compare(expirationTime, ((Callback) rhs).expirationTime) == 0;
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Double.hashCode(expirationTime);
     }
 
     @Override
@@ -65,9 +74,7 @@
 
   private final PriorityQueue<Callback> m_callbacks = new PriorityQueue<>();
 
-  /**
-   * Constructor for TimedRobot.
-   */
+  /** Constructor for TimedRobot. */
   protected TimedRobot() {
     this(kDefaultPeriod);
   }
@@ -93,9 +100,7 @@
     NotifierJNI.cleanNotifier(m_notifier);
   }
 
-  /**
-   * Provide an alternate "main loop" via startCompetition().
-   */
+  /** Provide an alternate "main loop" via startCompetition(). */
   @Override
   @SuppressWarnings("UnsafeFinalization")
   public void startCompetition() {
@@ -106,6 +111,7 @@
     }
 
     // Tell the DS that the robot is ready to be enabled
+    System.out.println("********** Robot program startup complete **********");
     HAL.observeUserProgramStarting();
 
     // Loop forever, calling the appropriate mode-dependent function
@@ -139,28 +145,19 @@
     }
   }
 
-  /**
-   * Ends the main loop in startCompetition().
-   */
+  /** Ends the main loop in startCompetition(). */
   @Override
   public void endCompetition() {
     NotifierJNI.stopNotifier(m_notifier);
   }
 
   /**
-   * Get time period between calls to Periodic() functions.
-   */
-  public double getPeriod() {
-    return m_period;
-  }
-
-  /**
    * Add a callback to run at a specific period.
    *
    * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
    * synchronously. Interactions between them are thread-safe.
    *
-   * @param callback      The callback to run.
+   * @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) {
@@ -173,11 +170,10 @@
    * <p>This is scheduled on TimedRobot's Notifier, so TimedRobot and the callback run
    * synchronously. Interactions between them are thread-safe.
    *
-   * @param callback      The callback to run.
+   * @param callback The callback to run.
    * @param periodSeconds The period at which to run the callback in seconds.
-   * @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.
+   * @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) {
     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 5d71e72..939e255 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Timer.java
@@ -1,12 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
+/**
+ * A timer class.
+ *
+ * <p>Note that if the user calls SimHooks.restartTiming(), they should also reset the timer so
+ * get() won't return a negative duration.
+ */
 public class Timer {
   /**
    * Return the system clock time in seconds. Return the time from the FPGA hardware clock in
@@ -23,13 +26,13 @@
    * Return the approximate match time. The FMS does not send an official match time to the robots,
    * but does send an approximate match time. The value will count down the time remaining in the
    * current period (auto or teleop). Warning: This is not an official time (so it cannot be used to
-   * dispute ref calls or guarantee that a function will trigger before the match ends) The
-   * Practice Match function of the DS approximates the behavior seen on the field.
+   * dispute ref calls or guarantee that a function will trigger before the match ends) The Practice
+   * Match function of the DS approximates the behavior seen on the field.
    *
    * @return Time remaining in current match period (auto or teleop) in seconds
    */
   public static double getMatchTime() {
-    return DriverStation.getInstance().getMatchTime();
+    return DriverStation.getMatchTime();
   }
 
   /**
@@ -52,12 +55,7 @@
   private double m_accumulatedTime;
   private boolean m_running;
 
-  /**
-   * Lock for synchronization.
-   */
-  private final Object m_lock = new Object();
-
-  @SuppressWarnings("JavadocMethod")
+  @SuppressWarnings("MissingJavadocMethod")
   public Timer() {
     reset();
   }
@@ -74,24 +72,21 @@
    * @return Current time value for this timer in seconds
    */
   public double get() {
-    synchronized (m_lock) {
-      if (m_running) {
-        return m_accumulatedTime + (getMsClock() - m_startTime) / 1000.0;
-      } else {
-        return m_accumulatedTime;
-      }
+    if (m_running) {
+      return m_accumulatedTime + (getMsClock() - m_startTime) / 1000.0;
+    } else {
+      return m_accumulatedTime;
     }
   }
 
   /**
-   * Reset the timer by setting the time to 0. Make the timer startTime the current time so new
-   * requests will be relative now
+   * Reset the timer by setting the time to 0.
+   *
+   * <p>Make the timer startTime the current time so new requests will be relative now.
    */
   public void reset() {
-    synchronized (m_lock) {
-      m_accumulatedTime = 0;
-      m_startTime = getMsClock();
-    }
+    m_accumulatedTime = 0;
+    m_startTime = getMsClock();
   }
 
   /**
@@ -100,11 +95,9 @@
    * already running.
    */
   public void start() {
-    synchronized (m_lock) {
-      if (!m_running) {
-        m_startTime = getMsClock();
-        m_running = true;
-      }
+    if (!m_running) {
+      m_startTime = getMsClock();
+      m_running = true;
     }
   }
 
@@ -114,10 +107,8 @@
    * clock.
    */
   public void stop() {
-    synchronized (m_lock) {
-      m_accumulatedTime = get();
-      m_running = false;
-    }
+    m_accumulatedTime = get();
+    m_running = false;
   }
 
   /**
@@ -127,9 +118,7 @@
    * @return Whether the period has passed.
    */
   public boolean hasElapsed(double seconds) {
-    synchronized (m_lock) {
-      return get() > seconds;
-    }
+    return get() >= seconds;
   }
 
   /**
@@ -139,7 +128,9 @@
    *
    * @param period The period to check for (in seconds).
    * @return Whether the period has passed.
+   * @deprecated Use advanceIfElapsed() instead.
    */
+  @Deprecated(since = "2022", forRemoval = true)
   public boolean hasPeriodPassed(double period) {
     return advanceIfElapsed(period);
   }
@@ -153,15 +144,13 @@
    * @return Whether the period has passed.
    */
   public boolean advanceIfElapsed(double seconds) {
-    synchronized (m_lock) {
-      if (get() > seconds) {
-        // Advance the start time by the period.
-        // Don't set it to the current time... we want to avoid drift.
-        m_startTime += seconds * 1000;
-        return true;
-      } else {
-        return false;
-      }
+    if (get() >= seconds) {
+      // Advance the start time by the period.
+      // Don't set it to the current time... we want to avoid drift.
+      m_startTime += seconds * 1000;
+      return true;
+    } else {
+      return false;
     }
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimesliceRobot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimesliceRobot.java
new file mode 100644
index 0000000..5d10bb5
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/TimesliceRobot.java
@@ -0,0 +1,116 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+/**
+ * TimesliceRobot extends the TimedRobot robot program framework to provide timeslice scheduling of
+ * periodic functions.
+ *
+ * <p>The TimesliceRobot class is intended to be subclassed by a user creating a robot program.
+ *
+ * <p>This class schedules robot operations serially in a timeslice format. TimedRobot's periodic
+ * functions are the first in the timeslice table with 0 ms offset and 20 ms period. You can
+ * schedule additional controller periodic functions at a shorter period (5 ms by default). You give
+ * each one a timeslice duration, then they're run sequentially. The main benefit of this approach
+ * is consistent starting times for each controller periodic, which can make odometry and estimators
+ * more accurate and controller outputs change more consistently.
+ *
+ * <p>Here's an example of measured subsystem durations and their timeslice allocations:
+ *
+ * <table>
+ *   <tr>
+ *     <td><b>Subsystem</b></td>
+ *     <td><b>Duration (ms)</b></td>
+ *     <td><b>Allocation (ms)</b></td>
+ *   </tr>
+ *   <tr>
+ *     <td><b>Total</b></td>
+ *     <td>5.0</td>
+ *     <td>5.0</td>
+ *   </tr>
+ *   <tr>
+ *     <td>TimedRobot</td>
+ *     <td>?</td>
+ *     <td>2.0</td>
+ *   </tr>
+ *   <tr>
+ *     <td>Drivetrain</td>
+ *     <td>1.32</td>
+ *     <td>1.5</td>
+ *   </tr>
+ *   <tr>
+ *     <td>Flywheel</td>
+ *     <td>0.6</td>
+ *     <td>0.7</td>
+ *   </tr>
+ *   <tr>
+ *     <td>Turret</td>
+ *     <td>0.6</td>
+ *     <td>0.8</td>
+ *   </tr>
+ *   <tr>
+ *     <td><b>Free</b></td>
+ *     <td>0.0</td>
+ *     <td>N/A</td>
+ *   </tr>
+ * </table>
+ *
+ * <p>Since TimedRobot periodic functions only run every 20ms, that leaves a 2 ms empty spot in the
+ * allocation table for three of the four 5 ms cycles comprising 20 ms. That's OK because the OS
+ * needs time to do other things.
+ *
+ * <p>If the robot periodic functions and the controller periodic functions have a lot of scheduling
+ * jitter that cause them to occasionally overlap with later timeslices, consider giving the main
+ * robot thread a real-time priority using {@link Threads#setCurrentThreadPriority(boolean,int)}. An
+ * RT priority of 15 is a reasonable choice.
+ *
+ * <p>If you do enable RT though, <i>make sure your periodic functions do not block</i>. If they do,
+ * the operating system will lock up, and you'll have to boot the roboRIO into safe mode and delete
+ * the robot program to recover.
+ */
+public class TimesliceRobot extends TimedRobot {
+  /**
+   * Constructor for TimesliceRobot.
+   *
+   * @param robotPeriodicAllocation The allocation in seconds to give the TimesliceRobot periodic
+   *     functions.
+   * @param controllerPeriod The controller period in seconds. The sum of all scheduler allocations
+   *     should be less than or equal to this value.
+   */
+  public TimesliceRobot(double robotPeriodicAllocation, double controllerPeriod) {
+    m_nextOffset = robotPeriodicAllocation;
+    m_controllerPeriod = controllerPeriod;
+  }
+
+  /**
+   * Schedule a periodic function with the constructor's controller period and the given allocation.
+   * The function's runtime allocation will be placed after the end of the previous one's.
+   *
+   * <p>If a call to this function makes the allocations exceed the controller period, an exception
+   * will be thrown since that means the TimesliceRobot periodic functions and the given function
+   * will have conflicting timeslices.
+   *
+   * @param func Function to schedule.
+   * @param allocation The function's runtime allocation in seconds out of the controller period.
+   */
+  public void schedule(Runnable func, double allocation) {
+    if (m_nextOffset + allocation > m_controllerPeriod) {
+      throw new IllegalStateException(
+          "Function scheduled at offset "
+              + m_nextOffset
+              + " with allocation "
+              + allocation
+              + " exceeded controller period of "
+              + m_controllerPeriod
+              + "\n");
+    }
+
+    addPeriodic(func, m_controllerPeriod, m_nextOffset);
+    m_nextOffset += allocation;
+  }
+
+  private double m_nextOffset;
+  private final double m_controllerPeriod;
+}
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 3026e90..ea04192 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Tracer.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -12,12 +9,12 @@
 import java.util.function.Consumer;
 
 /**
- * A class for keeping track of how much time it takes for different parts of code to execute.
- * This is done with epochs, that are added by calls to {@link #addEpoch(String)},
- * and can be printed with a call to {@link #printEpochs()}.
+ * A class for keeping track of how much time it takes for different parts of code to execute. This
+ * is done with epochs, that are added by calls to {@link #addEpoch(String)}, and can be printed
+ * with a call to {@link #printEpochs()}.
  *
- * <p>Epochs are a way to partition the time elapsed so that when overruns occur, one can
- * determine which parts of an operation consumed the most time.
+ * <p>Epochs are a way to partition the time elapsed so that when overruns occur, one can determine
+ * which parts of an operation consumed the most time.
  */
 public class Tracer {
   private static final long kMinPrintPeriod = 1000000; // microseconds
@@ -28,24 +25,18 @@
   @SuppressWarnings("PMD.UseConcurrentHashMap")
   private final Map<String, Long> m_epochs = new HashMap<>(); // microseconds
 
-  /**
-   * Tracer constructor.
-   */
+  /** Tracer constructor. */
   public Tracer() {
     resetTimer();
   }
 
-  /**
-   * Clears all epochs.
-   */
+  /** Clears all epochs. */
   public void clearEpochs() {
     m_epochs.clear();
     resetTimer();
   }
 
-  /**
-   * Restarts the epoch timer.
-   */
+  /** Restarts the epoch timer. */
   public void resetTimer() {
     m_startTime = RobotController.getFPGATime();
   }
@@ -56,8 +47,8 @@
    * <p>Epochs are a way to partition the time elapsed so that when overruns occur, one can
    * determine which parts of an operation consumed the most time.
    *
-   * <p>This should be called immediately after execution has finished,
-   * with a call to this method or {@link #resetTimer()} before execution.
+   * <p>This should be called immediately after execution has finished, with a call to this method
+   * or {@link #resetTimer()} before execution.
    *
    * @param epochName The name to associate with the epoch.
    */
@@ -67,9 +58,7 @@
     m_startTime = currentTime;
   }
 
-  /**
-   * Prints list of epochs added so far and their times to the DriverStation.
-   */
+  /** Prints list of epochs added so far and their times to the DriverStation. */
   public void printEpochs() {
     printEpochs(out -> DriverStation.reportWarning(out, false));
   }
@@ -86,9 +75,10 @@
     if (now - m_lastEpochsPrintTime > kMinPrintPeriod) {
       StringBuilder sb = new StringBuilder();
       m_lastEpochsPrintTime = now;
-      m_epochs.forEach((key, value) -> {
-        sb.append(String.format("\t%s: %.6fs\n", key, value / 1.0e6));
-      });
+      m_epochs.forEach(
+          (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 0e14c96..2d3fb4e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Ultrasonic.java
@@ -1,24 +1,22 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.ArrayList;
-import java.util.List;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.SimBoolean;
 import edu.wpi.first.hal.SimDevice;
+import edu.wpi.first.hal.SimDevice.Direction;
 import edu.wpi.first.hal.SimDouble;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-import static java.util.Objects.requireNonNull;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Ultrasonic rangefinder class. The Ultrasonic rangefinder measures absolute distance based on the
@@ -29,21 +27,7 @@
  * echo is received. The time that the line is high determines the round trip distance (time of
  * flight).
  */
-public class Ultrasonic implements PIDSource, Sendable, AutoCloseable {
-  /**
-   * The units to return when PIDGet is called.
-   */
-  public enum Unit {
-    /**
-     * Use inches for PIDGet.
-     */
-    kInches,
-    /**
-     * Use millimeters for PIDGet.
-     */
-    kMillimeters
-  }
-
+public class Ultrasonic implements Sendable, AutoCloseable {
   // Time (sec) for the ping trigger pulse.
   private static final double kPingTime = 10 * 1e-6;
   private static final double kSpeedOfSoundInchesPerSec = 1130.0 * 12.0;
@@ -53,14 +37,12 @@
   private static volatile boolean m_automaticEnabled;
   private DigitalInput m_echoChannel;
   private DigitalOutput m_pingChannel;
-  private boolean m_allocatedChannels;
+  private final boolean m_allocatedChannels;
   private boolean m_enabled;
   private Counter m_counter;
   // task doing the round-robin automatic sensing
   private static Thread m_task;
-  private Unit m_units;
   private static int m_instances;
-  protected PIDSourceType m_pidSource = PIDSourceType.kDisplacement;
 
   private SimDevice m_simDevice;
   private SimBoolean m_simRangeValid;
@@ -79,16 +61,16 @@
     @Override
     public synchronized void run() {
       while (m_automaticEnabled) {
-        for (Ultrasonic sensor: m_sensors) {
+        for (Ultrasonic sensor : m_sensors) {
           if (!m_automaticEnabled) {
             break;
           }
 
           if (sensor.isEnabled()) {
-            sensor.m_pingChannel.pulse(kPingTime);  // do the ping
+            sensor.m_pingChannel.pulse(kPingTime); // do the ping
           }
 
-          Timer.delay(0.1);  // wait for ping to return
+          Timer.delay(0.1); // wait for ping to return
         }
       }
     }
@@ -103,8 +85,8 @@
   private synchronized void initialize() {
     m_simDevice = SimDevice.create("Ultrasonic", m_echoChannel.getChannel());
     if (m_simDevice != null) {
-      m_simRangeValid = m_simDevice.createBoolean("Range Valid", false, true);
-      m_simRange = m_simDevice.createDouble("Range (in)", false, 0.0);
+      m_simRangeValid = m_simDevice.createBoolean("Range Valid", Direction.kInput, true);
+      m_simRange = m_simDevice.createDouble("Range (in)", Direction.kInput, 0.0);
       m_pingChannel.setSimDevice(m_simDevice);
       m_echoChannel.setSimDevice(m_simDevice);
     }
@@ -129,76 +111,47 @@
     SendableRegistry.addLW(this, "Ultrasonic", m_echoChannel.getChannel());
   }
 
+  public int getEchoChannel() {
+    return m_echoChannel.getChannel();
+  }
+
   /**
    * Create an instance of the Ultrasonic Sensor. This is designed to supchannel the Daventech SRF04
    * and Vex ultrasonic sensors.
    *
    * @param pingChannel The digital output channel that sends the pulse to initiate the sensor
-   *                    sending the ping.
+   *     sending the ping.
    * @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.
-   * @param units       The units returned in either kInches or kMilliMeters
+   *     the echo is high represents the round trip time of the ping, and the distance.
    */
-  public Ultrasonic(final int pingChannel, final int echoChannel, Unit units) {
+  public Ultrasonic(final int pingChannel, final int echoChannel) {
     m_pingChannel = new DigitalOutput(pingChannel);
     m_echoChannel = new DigitalInput(echoChannel);
     SendableRegistry.addChild(this, m_pingChannel);
     SendableRegistry.addChild(this, m_echoChannel);
     m_allocatedChannels = true;
-    m_units = units;
     initialize();
   }
 
   /**
-   * Create an instance of the Ultrasonic Sensor. This is designed to supchannel the Daventech SRF04
-   * and Vex ultrasonic sensors. Default unit is inches.
-   *
-   * @param pingChannel The digital output channel that sends the pulse to initiate the sensor
-   *                    sending the ping.
-   * @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.
-   */
-  public Ultrasonic(final int pingChannel, final int echoChannel) {
-    this(pingChannel, echoChannel, Unit.kInches);
-  }
-
-  /**
    * Create an instance of an Ultrasonic Sensor from a DigitalInput for the echo channel and a
    * DigitalOutput for the ping channel.
    *
    * @param pingChannel The digital output object that starts the sensor doing a ping. Requires a
-   *                    10uS pulse to start.
-   * @param echoChannel The digital input object that times the return pulse to determine the
-   *                    range.
-   * @param units       The units returned in either kInches or kMilliMeters
+   *     10uS pulse to start.
+   * @param echoChannel The digital input object that times the return pulse to determine the range.
    */
-  public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel, Unit units) {
+  public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel) {
     requireNonNull(pingChannel, "Provided ping channel was null");
     requireNonNull(echoChannel, "Provided echo channel was null");
 
     m_allocatedChannels = false;
     m_pingChannel = pingChannel;
     m_echoChannel = echoChannel;
-    m_units = units;
     initialize();
   }
 
   /**
-   * Create an instance of an Ultrasonic Sensor from a DigitalInput for the echo channel and a
-   * DigitalOutput for the ping channel. Default unit is inches.
-   *
-   * @param pingChannel The digital output object that starts the sensor doing a ping. Requires a
-   *                    10uS pulse to start.
-   * @param echoChannel The digital input object that times the return pulse to determine the
-   *                    range.
-   */
-  public Ultrasonic(DigitalOutput pingChannel, DigitalInput echoChannel) {
-    this(pingChannel, echoChannel, Unit.kInches);
-  }
-
-  /**
    * Destructor for the ultrasonic sensor. Delete the instance of the ultrasonic sensor by freeing
    * the allocated digital channels. If the system was in automatic mode (round robin), then it is
    * stopped, then started again after this sensor is removed (provided this wasn't the last
@@ -245,10 +198,9 @@
    * each sensor.
    *
    * @param enabling Set to true if round robin scheduling should start for all the ultrasonic
-   *                 sensors. This scheduling method assures that the sensors are non-interfering
-   *                 because no two sensors fire at the same time. If another scheduling algorithm
-   *                 is preferred, it can be implemented by pinging the sensors manually and waiting
-   *                 for the results to come back.
+   *     sensors. This scheduling method assures that the sensors are non-interfering because no two
+   *     sensors fire at the same time. If another scheduling algorithm is preferred, it can be
+   *     implemented by pinging the sensors manually and waiting for the results to come back.
    */
   public static void setAutomaticMode(boolean enabling) {
     if (enabling == m_automaticEnabled) {
@@ -340,54 +292,6 @@
     return getRangeInches() * 25.4;
   }
 
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    if (!pidSource.equals(PIDSourceType.kDisplacement)) {
-      throw new IllegalArgumentException("Only displacement PID is allowed for ultrasonics.");
-    }
-    m_pidSource = pidSource;
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_pidSource;
-  }
-
-  /**
-   * Get the range in the current DistanceUnit for the PIDSource base object.
-   *
-   * @return The range in DistanceUnit
-   */
-  @Override
-  public double pidGet() {
-    switch (m_units) {
-      case kInches:
-        return getRangeInches();
-      case kMillimeters:
-        return getRangeMM();
-      default:
-        return 0.0;
-    }
-  }
-
-  /**
-   * Set the current DistanceUnit that should be used for the PIDSource base object.
-   *
-   * @param units The DistanceUnit that should be used.
-   */
-  public void setDistanceUnits(Unit units) {
-    m_units = units;
-  }
-
-  /**
-   * Get the current DistanceUnit that is used for the PIDSource base object.
-   *
-   * @return The type of DistanceUnit that is being used.
-   */
-  public Unit getDistanceUnits() {
-    return m_units;
-  }
-
   /**
    * Is the ultrasonic enabled.
    *
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Victor.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Victor.java
deleted file mode 100644
index 29c1b27..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Victor.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * VEX Robotics Victor 888 Speed Controller The Vex Robotics Victor 884 Speed Controller can also
- * be used with this class but may need to be calibrated per the Victor 884 user manual.
- *
- * <p>Note that the Victor uses the following bounds for PWM values. These values were determined
- * empirically and optimized for the Victor 888. These values should work reasonably well for
- * Victor 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
- *
- * <p><ul>
- * <li>2.027ms = full "forward"
- * <li>1.525ms = the "high end" of the deadband range
- * <li>1.507ms = center of the deadband range (off)
- * <li>1.490ms = the "low end" of the deadband range
- * <li>1.026ms = full "reverse"
- * </ul>
- */
-public class Victor extends PWMSpeedController {
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the Victor is attached to. 0-9 are
-   *        on-board, 10-19 are on the MXP port
-   */
-  public Victor(final int channel) {
-    super(channel);
-
-    setBounds(2.027, 1.525, 1.507, 1.49, 1.026);
-    setPeriodMultiplier(PeriodMultiplier.k2X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_Victor, getChannel() + 1);
-    SendableRegistry.setName(this, "Victor", getChannel());
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/VictorSP.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/VictorSP.java
deleted file mode 100644
index 22b3513..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/VictorSP.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * VEX Robotics Victor SP Speed Controller.
- *
- * <p>Note that the VictorSP 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 VictorSP User Manual
- * available from CTRE.
- *
- * <p><ul>
- * <li>2.004ms = full "forward"
- * <li>1.520ms = the "high end" of the deadband range
- * <li>1.500ms = center of the deadband range (off)
- * <li>1.480ms = the "low end" of the deadband range
- * <li>0.997ms = full "reverse"
- * </ul>
- */
-public class VictorSP extends PWMSpeedController {
-  /**
-   * Constructor.
-   *
-   * @param channel The PWM channel that the VictorSP is attached to. 0-9 are
-   *        on-board, 10-19 are on the MXP port
-   */
-  public VictorSP(final int channel) {
-    super(channel);
-
-    setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
-    setPeriodMultiplier(PeriodMultiplier.k1X);
-    setSpeed(0.0);
-    setZeroLatch();
-
-    HAL.report(tResourceType.kResourceType_VictorSP, getChannel() + 1);
-    SendableRegistry.setName(this, "VictorSP", getChannel());
-  }
-}
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 aaf5671..22581a5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Watchdog.java
@@ -1,18 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.NotifierJNI;
 import java.io.Closeable;
 import java.util.PriorityQueue;
 import java.util.concurrent.locks.ReentrantLock;
 
-import edu.wpi.first.hal.NotifierJNI;
-
 /**
  * A class that's a wrapper around a watchdog timer.
  *
@@ -23,13 +19,13 @@
  */
 public class Watchdog implements Closeable, Comparable<Watchdog> {
   // Used for timeout print rate-limiting
-  private static final long kMinPrintPeriod = 1000000; // microseconds
+  private static final long kMinPrintPeriodMicroS = (long) 1e6;
 
-  private double m_startTime; // seconds
-  private double m_timeout; // seconds
-  private double m_expirationTime; // seconds
+  private double m_startTimeSeconds;
+  private double m_timeoutSeconds;
+  private double m_expirationTimeSeconds;
   private final Runnable m_callback;
-  private double m_lastTimeoutPrintTime; // seconds
+  private double m_lastTimeoutPrintSeconds;
 
   boolean m_isExpired;
 
@@ -50,11 +46,11 @@
   /**
    * Watchdog constructor.
    *
-   * @param timeout  The watchdog's timeout in seconds with microsecond resolution.
+   * @param timeoutSeconds The watchdog's timeout in seconds with microsecond resolution.
    * @param callback This function is called when the timeout expires.
    */
-  public Watchdog(double timeout, Runnable callback) {
-    m_timeout = timeout;
+  public Watchdog(double timeoutSeconds, Runnable callback) {
+    m_timeoutSeconds = timeoutSeconds;
     m_callback = callback;
     m_tracer = new Tracer();
   }
@@ -65,50 +61,50 @@
   }
 
   @Override
-  public int compareTo(Watchdog rhs) {
-    // Elements with sooner expiration times are sorted as lesser. The head of
-    // Java's PriorityQueue is the least element.
-    return Double.compare(m_expirationTime, rhs.m_expirationTime);
-  }
-
-  @Override
   public boolean equals(Object obj) {
-    if (!(obj instanceof Watchdog)) {
-      return false;
+    if (obj instanceof Watchdog) {
+      return Double.compare(m_expirationTimeSeconds, ((Watchdog) obj).m_expirationTimeSeconds) == 0;
     }
-    Watchdog oth = (Watchdog) obj;
-    return oth.m_expirationTime == m_expirationTime;
+    return false;
   }
 
   @Override
   public int hashCode() {
-    return Double.hashCode(m_expirationTime);
+    return Double.hashCode(m_expirationTimeSeconds);
+  }
+
+  @Override
+  public int compareTo(Watchdog rhs) {
+    // Elements with sooner expiration times are sorted as lesser. The head of
+    // Java's PriorityQueue is the least element.
+    return Double.compare(m_expirationTimeSeconds, rhs.m_expirationTimeSeconds);
   }
 
   /**
    * Returns the time in seconds since the watchdog was last fed.
+   *
+   * @return The time in seconds since the watchdog was last fed.
    */
   public double getTime() {
-    return Timer.getFPGATimestamp() - m_startTime;
+    return Timer.getFPGATimestamp() - m_startTimeSeconds;
   }
 
   /**
    * Sets the watchdog's timeout.
    *
-   * @param timeout The watchdog's timeout in seconds with microsecond
-   *                resolution.
+   * @param timeoutSeconds The watchdog's timeout in seconds with microsecond resolution.
    */
-  public void setTimeout(double timeout) {
-    m_startTime = Timer.getFPGATimestamp();
+  public void setTimeout(double timeoutSeconds) {
+    m_startTimeSeconds = Timer.getFPGATimestamp();
     m_tracer.clearEpochs();
 
     m_queueMutex.lock();
     try {
-      m_timeout = timeout;
+      m_timeoutSeconds = timeoutSeconds;
       m_isExpired = false;
 
       m_watchdogs.remove(this);
-      m_expirationTime = m_startTime + m_timeout;
+      m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds;
       m_watchdogs.add(this);
       updateAlarm();
     } finally {
@@ -118,11 +114,13 @@
 
   /**
    * Returns the watchdog's timeout in seconds.
+   *
+   * @return The watchdog's timeout in seconds.
    */
   public double getTimeout() {
     m_queueMutex.lock();
     try {
-      return m_timeout;
+      return m_timeoutSeconds;
     } finally {
       m_queueMutex.unlock();
     }
@@ -130,6 +128,8 @@
 
   /**
    * Returns true if the watchdog timer has expired.
+   *
+   * @return True if the watchdog timer has expired.
    */
   public boolean isExpired() {
     m_queueMutex.lock();
@@ -144,7 +144,6 @@
    * Adds time since last epoch to the list printed by printEpochs().
    *
    * @see Tracer#addEpoch(String)
-   *
    * @param epochName The name to associate with the epoch.
    */
   public void addEpoch(String epochName) {
@@ -153,6 +152,7 @@
 
   /**
    * Prints list of epochs added so far and their times.
+   *
    * @see Tracer#printEpochs()
    */
   public void printEpochs() {
@@ -168,11 +168,9 @@
     enable();
   }
 
-  /**
-   * Enables the watchdog timer.
-   */
+  /** Enables the watchdog timer. */
   public void enable() {
-    m_startTime = Timer.getFPGATimestamp();
+    m_startTimeSeconds = Timer.getFPGATimestamp();
     m_tracer.clearEpochs();
 
     m_queueMutex.lock();
@@ -180,7 +178,7 @@
       m_isExpired = false;
 
       m_watchdogs.remove(this);
-      m_expirationTime = m_startTime + m_timeout;
+      m_expirationTimeSeconds = m_startTimeSeconds + m_timeoutSeconds;
       m_watchdogs.add(this);
       updateAlarm();
     } finally {
@@ -188,9 +186,7 @@
     }
   }
 
-  /**
-   * Disables the watchdog timer.
-   */
+  /** Disables the watchdog timer. */
   public void disable() {
     m_queueMutex.lock();
     try {
@@ -212,12 +208,13 @@
     m_suppressTimeoutMessage = suppress;
   }
 
+  @SuppressWarnings("resource")
   private static void updateAlarm() {
     if (m_watchdogs.size() == 0) {
       NotifierJNI.cancelNotifierAlarm(m_notifier);
     } else {
-      NotifierJNI.updateNotifierAlarm(m_notifier,
-          (long) (m_watchdogs.peek().m_expirationTime * 1e6));
+      NotifierJNI.updateNotifierAlarm(
+          m_notifier, (long) (m_watchdogs.peek().m_expirationTimeSeconds * 1e6));
     }
   }
 
@@ -247,12 +244,11 @@
         Watchdog watchdog = m_watchdogs.poll();
 
         double now = curTime * 1e-6;
-        if (now - watchdog.m_lastTimeoutPrintTime > kMinPrintPeriod) {
-          watchdog.m_lastTimeoutPrintTime = now;
+        if (now - watchdog.m_lastTimeoutPrintSeconds > kMinPrintPeriodMicroS) {
+          watchdog.m_lastTimeoutPrintSeconds = now;
           if (!watchdog.m_suppressTimeoutMessage) {
             DriverStation.reportWarning(
-                String.format("Watchdog not fed within %.6fs\n", watchdog.m_timeout),
-                false);
+                String.format("Watchdog not fed within %.6fs\n", watchdog.m_timeoutSeconds), false);
           }
         }
 
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 6248a76..5de3503 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/XboxController.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
@@ -18,14 +15,12 @@
  * and the mapping of ports to hardware buttons depends on the code in the Driver Station.
  */
 public class XboxController extends GenericHID {
-  /**
-   * Represents a digital button on an XboxController.
-   */
+  /** Represents a digital button on an XboxController. */
   public enum Button {
-    kBumperLeft(5),
-    kBumperRight(6),
-    kStickLeft(9),
-    kStickRight(10),
+    kLeftBumper(5),
+    kRightBumper(6),
+    kLeftStick(9),
+    kRightStick(10),
     kA(1),
     kB(2),
     kX(3),
@@ -33,17 +28,32 @@
     kBack(7),
     kStart(8);
 
-    @SuppressWarnings({"MemberName", "PMD.SingularField"})
+    @SuppressWarnings("MemberName")
     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 an XboxController.
-   */
+  /** Represents an axis on an XboxController. */
   public enum Axis {
     kLeftX(0),
     kRightX(4),
@@ -52,19 +62,35 @@
     kLeftTrigger(2),
     kRightTrigger(3);
 
-    @SuppressWarnings({"MemberName", "PMD.SingularField"})
+    @SuppressWarnings("MemberName")
     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 joystick. The joystick index is the USB port on the drivers
-   * station.
+   * Construct an instance of a controller.
    *
-   * @param port The port on the Driver Station that the joystick is plugged into.
+   * @param port The port index on the Driver Station that the controller is plugged into.
    */
   public XboxController(final int port) {
     super(port);
@@ -73,131 +99,167 @@
   }
 
   /**
-   * Get the X axis value of the controller.
+   * Get the X axis value of left side of the controller.
    *
-   * @param hand Side of controller whose value should be returned.
-   * @return The X axis value of the controller.
+   * @return The axis value.
    */
-  @Override
-  public double getX(Hand hand) {
-    if (hand.equals(Hand.kLeft)) {
-      return getRawAxis(Axis.kLeftX.value);
-    } else {
-      return getRawAxis(Axis.kRightX.value);
-    }
+  public double getLeftX() {
+    return getRawAxis(Axis.kLeftX.value);
   }
 
   /**
-   * Get the Y axis value of the controller.
+   * Get the X axis value of right side of the controller.
    *
-   * @param hand Side of controller whose value should be returned.
-   * @return The Y axis value of the controller.
+   * @return The axis value.
    */
-  @Override
-  public double getY(Hand hand) {
-    if (hand.equals(Hand.kLeft)) {
-      return getRawAxis(Axis.kLeftY.value);
-    } else {
-      return getRawAxis(Axis.kRightY.value);
-    }
+  public double getRightX() {
+    return getRawAxis(Axis.kRightX.value);
   }
 
   /**
-   * Get the trigger axis value of the controller.
+   * Get the Y axis value of left side of the controller.
    *
-   * @param hand Side of controller whose value should be returned.
-   * @return The trigger axis value of the controller.
+   * @return The axis value.
    */
-  public double getTriggerAxis(Hand hand) {
-    if (hand.equals(Hand.kLeft)) {
-      return getRawAxis(Axis.kLeftTrigger.value);
-    } else {
-      return getRawAxis(Axis.kRightTrigger.value);
-    }
+  public double getLeftY() {
+    return getRawAxis(Axis.kLeftY.value);
   }
 
   /**
-   * Read the value of the bumper button on the controller.
+   * Get the Y axis value of right side of the controller.
    *
-   * @param hand Side of controller whose value should be returned.
+   * @return The axis value.
+   */
+  public double getRightY() {
+    return getRawAxis(Axis.kRightY.value);
+  }
+
+  /**
+   * Get the left trigger (LT) axis value of the controller. Note that this axis is bound to the
+   * range of [0, 1] as opposed to the usual [-1, 1].
+   *
+   * @return The axis value.
+   */
+  public double getLeftTriggerAxis() {
+    return getRawAxis(Axis.kLeftTrigger.value);
+  }
+
+  /**
+   * Get the right trigger (RT) axis value of the controller. Note that this axis is bound to the
+   * range of [0, 1] as opposed to the usual [-1, 1].
+   *
+   * @return The axis value.
+   */
+  public double getRightTriggerAxis() {
+    return getRawAxis(Axis.kRightTrigger.value);
+  }
+
+  /**
+   * Read the value of the left bumper (LB) button on the controller.
+   *
    * @return The state of the button.
    */
-  public boolean getBumper(Hand hand) {
-    if (hand.equals(Hand.kLeft)) {
-      return getRawButton(Button.kBumperLeft.value);
-    } else {
-      return getRawButton(Button.kBumperRight.value);
-    }
+  public boolean getLeftBumper() {
+    return getRawButton(Button.kLeftBumper.value);
   }
 
   /**
-   * Whether the bumper was pressed since the last check.
+   * Read the value of the right bumper (RB) button on the controller.
    *
-   * @param hand Side of controller whose value should be returned.
-   * @return Whether the button was pressed since the last check.
-   */
-  public boolean getBumperPressed(Hand hand) {
-    if (hand == Hand.kLeft) {
-      return getRawButtonPressed(Button.kBumperLeft.value);
-    } else {
-      return getRawButtonPressed(Button.kBumperRight.value);
-    }
-  }
-
-  /**
-   * Whether the bumper was released since the last check.
-   *
-   * @param hand Side of controller whose value should be returned.
-   * @return Whether the button was released since the last check.
-   */
-  public boolean getBumperReleased(Hand hand) {
-    if (hand == Hand.kLeft) {
-      return getRawButtonReleased(Button.kBumperLeft.value);
-    } else {
-      return getRawButtonReleased(Button.kBumperRight.value);
-    }
-  }
-
-  /**
-   * Read the value of the stick button on the controller.
-   *
-   * @param hand Side of controller whose value should be returned.
    * @return The state of the button.
    */
-  public boolean getStickButton(Hand hand) {
-    if (hand.equals(Hand.kLeft)) {
-      return getRawButton(Button.kStickLeft.value);
-    } else {
-      return getRawButton(Button.kStickRight.value);
-    }
+  public boolean getRightBumper() {
+    return getRawButton(Button.kRightBumper.value);
   }
 
   /**
-   * Whether the stick button was pressed since the last check.
+   * Whether the left bumper (LB) was pressed since the last check.
    *
-   * @param hand Side of controller whose value should be returned.
    * @return Whether the button was pressed since the last check.
    */
-  public boolean getStickButtonPressed(Hand hand) {
-    if (hand == Hand.kLeft) {
-      return getRawButtonPressed(Button.kStickLeft.value);
-    } else {
-      return getRawButtonPressed(Button.kStickRight.value);
-    }
+  public boolean getLeftBumperPressed() {
+    return getRawButtonPressed(Button.kLeftBumper.value);
   }
 
   /**
-   * Whether the stick button was released since the last check.
+   * Whether the right bumper (RB) was pressed since the last check.
    *
-   * @param hand Side of controller whose value should be returned.
+   * @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 getStickButtonReleased(Hand hand) {
-    if (hand == Hand.kLeft) {
-      return getRawButtonReleased(Button.kStickLeft.value);
-    } else {
-      return getRawButtonReleased(Button.kStickRight.value);
-    }
+  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);
+  }
+
+  /**
+   * 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);
   }
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ControllerUtil.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ControllerUtil.java
deleted file mode 100644
index 0896ac6..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ControllerUtil.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-public final class ControllerUtil {
-  /**
-   * Returns modulus of error where error is the difference between the reference
-   * and a measurement.
-   *
-   * @param reference Reference input of a controller.
-   * @param measurement The current measurement.
-   * @param minimumInput The minimum value expected from the input.
-   * @param maximumInput The maximum value expected from the input.
-   */
-  public static double getModulusError(double reference, double measurement, double minimumInput,
-      double maximumInput) {
-    double error = reference - measurement;
-    double modulus = maximumInput - minimumInput;
-
-    // Wrap error above maximum input
-    int numMax = (int) ((error + maximumInput) / modulus);
-    error -= numMax * modulus;
-
-    // Wrap error below minimum input
-    int numMin = (int) ((error + minimumInput) / modulus);
-    error -= numMin * modulus;
-
-    return error;
-  }
-
-  private ControllerUtil() {
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/HolonomicDriveController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/HolonomicDriveController.java
deleted file mode 100644
index 4a98232..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/HolonomicDriveController.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
-import edu.wpi.first.wpilibj.trajectory.Trajectory;
-
-/**
- * This holonomic drive controller can be used to follow trajectories using a
- * holonomic drive train (i.e. swerve or mecanum). Holonomic trajectory following
- * is a much simpler problem to solve compared to skid-steer style drivetrains because
- * it is possible to individually control forward, sideways, and angular velocity.
- *
- * <p>The holonomic drive controller takes in one PID controller for each direction, forward
- * and sideways, and one profiled PID controller for the angular direction. Because the
- * heading dynamics are decoupled from translations, users can specify a custom heading
- * that the drivetrain should point toward. This heading reference is profiled for smoothness.
- */
-@SuppressWarnings("MemberName")
-public class HolonomicDriveController {
-  private Pose2d m_poseError = new Pose2d();
-  private Pose2d m_poseTolerance = new Pose2d();
-  private boolean m_enabled = true;
-
-  private final PIDController m_xController;
-  private final PIDController m_yController;
-  private final ProfiledPIDController m_thetaController;
-
-  /**
-   * Constructs a holonomic drive controller.
-   *
-   * @param xController     A PID Controller to respond to error in the field-relative x direction.
-   * @param yController     A PID Controller to respond to error in the field-relative y direction.
-   * @param thetaController A profiled PID controller to respond to error in angle.
-   */
-  @SuppressWarnings("ParameterName")
-  public HolonomicDriveController(PIDController xController,
-                                  PIDController yController,
-                                  ProfiledPIDController thetaController) {
-    m_xController = xController;
-    m_yController = yController;
-    m_thetaController = thetaController;
-  }
-
-  /**
-   * Returns true if the pose error is within tolerance of the reference.
-   *
-   * @return True if the pose error is within tolerance of the reference.
-   */
-  public boolean atReference() {
-    final var eTranslate = m_poseError.getTranslation();
-    final var eRotate = m_poseError.getRotation();
-    final var tolTranslate = m_poseTolerance.getTranslation();
-    final var tolRotate = m_poseTolerance.getRotation();
-    return Math.abs(eTranslate.getX()) < tolTranslate.getX()
-        && Math.abs(eTranslate.getY()) < tolTranslate.getY()
-        && Math.abs(eRotate.getRadians()) < tolRotate.getRadians();
-  }
-
-  /**
-   * Sets the pose error which is considered tolerance for use with atReference().
-   *
-   * @param tolerance The pose error which is tolerable.
-   */
-  public void setTolerance(Pose2d tolerance) {
-    m_poseTolerance = tolerance;
-  }
-
-  /**
-   * Returns the next output of the holonomic drive controller.
-   *
-   * @param currentPose             The current pose.
-   * @param poseRef                 The desired pose.
-   * @param linearVelocityRefMeters The linear velocity reference.
-   * @param angleRef                The angular reference.
-   * @return The next output of the holonomic drive controller.
-   */
-  @SuppressWarnings("LocalVariableName")
-  public ChassisSpeeds calculate(Pose2d currentPose,
-                                 Pose2d poseRef,
-                                 double linearVelocityRefMeters,
-                                 Rotation2d angleRef) {
-    // Calculate feedforward velocities (field-relative).
-    double xFF = linearVelocityRefMeters * poseRef.getRotation().getCos();
-    double yFF = linearVelocityRefMeters * poseRef.getRotation().getSin();
-    double thetaFF = m_thetaController.calculate(currentPose.getRotation().getRadians(),
-        angleRef.getRadians());
-
-    m_poseError = poseRef.relativeTo(currentPose);
-
-    if (!m_enabled) {
-      return ChassisSpeeds.fromFieldRelativeSpeeds(xFF, yFF, thetaFF, currentPose.getRotation());
-    }
-
-    // Calculate feedback velocities (based on position error).
-    double xFeedback = m_xController.calculate(currentPose.getX(), poseRef.getX());
-    double yFeedback = m_yController.calculate(currentPose.getY(), poseRef.getY());
-
-    // Return next output.
-    return ChassisSpeeds.fromFieldRelativeSpeeds(xFF + xFeedback,
-        yFF + yFeedback, thetaFF, currentPose.getRotation());
-  }
-
-  /**
-   * Returns the next output of the holonomic drive controller.
-   *
-   * @param currentPose  The current pose.
-   * @param desiredState The desired trajectory state.
-   * @param angleRef     The desired end-angle.
-   * @return The next output of the holonomic drive controller.
-   */
-  public ChassisSpeeds calculate(Pose2d currentPose, Trajectory.State desiredState,
-                                 Rotation2d angleRef) {
-    return calculate(currentPose, desiredState.poseMeters, desiredState.velocityMetersPerSecond,
-        angleRef);
-  }
-
-  /**
-   * Enables and disables the controller for troubleshooting problems. When calculate()
-   * is called on a disabled controller, only feedforward values are returned.
-   *
-   * @param enabled If the controller is enabled or not.
-   */
-  public void setEnabled(boolean enabled) {
-    m_enabled = enabled;
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/PIDController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/PIDController.java
deleted file mode 100644
index 526e91e..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/PIDController.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-import edu.wpi.first.wpiutil.math.MathUtil;
-
-/**
- * Implements a PID control loop.
- */
-@SuppressWarnings("PMD.TooManyFields")
-public class PIDController implements Sendable, AutoCloseable {
-  private static int instances;
-
-  // Factor for "proportional" control
-  @SuppressWarnings("MemberName")
-  private double m_Kp;
-
-  // Factor for "integral" control
-  @SuppressWarnings("MemberName")
-  private double m_Ki;
-
-  // Factor for "derivative" control
-  @SuppressWarnings("MemberName")
-  private double m_Kd;
-
-  // The period (in seconds) of the loop that calls the controller
-  private final double m_period;
-
-  private double m_maximumIntegral = 1.0;
-
-  private double m_minimumIntegral = -1.0;
-
-  private double m_maximumInput;
-
-  private double m_minimumInput;
-
-  // Do the endpoints wrap around? eg. Absolute encoder
-  private boolean m_continuous;
-
-  // The error at the time of the most recent call to calculate()
-  private double m_positionError;
-  private double m_velocityError;
-
-  // The error at the time of the second-most-recent call to calculate() (used to compute velocity)
-  private double m_prevError;
-
-  // The sum of the errors for use in the integral calc
-  private double m_totalError;
-
-  // The error that is considered at setpoint.
-  private double m_positionTolerance = 0.05;
-  private double m_velocityTolerance = Double.POSITIVE_INFINITY;
-
-  private double m_setpoint;
-
-  /**
-   * Allocates a PIDController with the given constants for Kp, Ki, and Kd and a default period of
-   * 0.02 seconds.
-   *
-   * @param Kp The proportional coefficient.
-   * @param Ki The integral coefficient.
-   * @param Kd The derivative coefficient.
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd) {
-    this(Kp, Ki, Kd, 0.02);
-  }
-
-  /**
-   * Allocates a PIDController with the given constants for Kp, Ki, and Kd.
-   *
-   * @param Kp     The proportional coefficient.
-   * @param Ki     The integral coefficient.
-   * @param Kd     The derivative coefficient.
-   * @param period The period between controller updates in seconds.
-   */
-  @SuppressWarnings("ParameterName")
-  public PIDController(double Kp, double Ki, double Kd, double period) {
-    m_Kp = Kp;
-    m_Ki = Ki;
-    m_Kd = Kd;
-
-    m_period = period;
-
-    instances++;
-    SendableRegistry.addLW(this, "PIDController", instances);
-
-    HAL.report(tResourceType.kResourceType_PIDController2, instances);
-  }
-
-  @Override
-  public void close() {
-    SendableRegistry.remove(this);
-  }
-
-  /**
-   * Sets the PID Controller gain parameters.
-   *
-   * <p>Set the proportional, integral, and differential coefficients.
-   *
-   * @param Kp The proportional coefficient.
-   * @param Ki The integral coefficient.
-   * @param Kd The derivative coefficient.
-   */
-  @SuppressWarnings("ParameterName")
-  public void setPID(double Kp, double Ki, double Kd) {
-    m_Kp = Kp;
-    m_Ki = Ki;
-    m_Kd = Kd;
-  }
-
-  /**
-   * Sets the Proportional coefficient of the PID controller gain.
-   *
-   * @param Kp proportional coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setP(double Kp) {
-    m_Kp = Kp;
-  }
-
-  /**
-   * Sets the Integral coefficient of the PID controller gain.
-   *
-   * @param Ki integral coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setI(double Ki) {
-    m_Ki = Ki;
-  }
-
-  /**
-   * Sets the Differential coefficient of the PID controller gain.
-   *
-   * @param Kd differential coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setD(double Kd) {
-    m_Kd = Kd;
-  }
-
-  /**
-   * Get the Proportional coefficient.
-   *
-   * @return proportional coefficient
-   */
-  public double getP() {
-    return m_Kp;
-  }
-
-  /**
-   * Get the Integral coefficient.
-   *
-   * @return integral coefficient
-   */
-  public double getI() {
-    return m_Ki;
-  }
-
-  /**
-   * Get the Differential coefficient.
-   *
-   * @return differential coefficient
-   */
-  public double getD() {
-    return m_Kd;
-  }
-
-  /**
-   * Returns the period of this controller.
-   *
-   * @return the period of the controller.
-   */
-  public double getPeriod() {
-    return m_period;
-  }
-
-  /**
-   * Sets the setpoint for the PIDController.
-   *
-   * @param setpoint The desired setpoint.
-   */
-  public void setSetpoint(double setpoint) {
-    m_setpoint = setpoint;
-  }
-
-  /**
-   * Returns the current setpoint of the PIDController.
-   *
-   * @return The current setpoint.
-   */
-  public double getSetpoint() {
-    return m_setpoint;
-  }
-
-  /**
-   * Returns true if the error is within the tolerance of the setpoint.
-   *
-   * <p>This will return false until at least one input value has been computed.
-   *
-   * @return Whether the error is within the acceptable bounds.
-   */
-  public boolean atSetpoint() {
-    return Math.abs(m_positionError) < m_positionTolerance
-        && Math.abs(m_velocityError) < m_velocityTolerance;
-  }
-
-  /**
-   * Enables continuous input.
-   *
-   * <p>Rather then using the max and min input range as constraints, it considers
-   * them to be the same point and automatically calculates the shortest route
-   * to the setpoint.
-   *
-   * @param minimumInput The minimum value expected from the input.
-   * @param maximumInput The maximum value expected from the input.
-   */
-  public void enableContinuousInput(double minimumInput, double maximumInput) {
-    m_continuous = true;
-    m_minimumInput = minimumInput;
-    m_maximumInput = maximumInput;
-  }
-
-  /**
-   * Disables continuous input.
-   */
-  public void disableContinuousInput() {
-    m_continuous = false;
-  }
-
-  /**
-   * Returns true if continuous input is enabled.
-   */
-  public boolean isContinuousInputEnabled() {
-    return m_continuous;
-  }
-
-  /**
-   * Sets the minimum and maximum values for the integrator.
-   *
-   * <p>When the cap is reached, the integrator value is added to the controller
-   * output rather than the integrator value times the integral gain.
-   *
-   * @param minimumIntegral The minimum value of the integrator.
-   * @param maximumIntegral The maximum value of the integrator.
-   */
-  public void setIntegratorRange(double minimumIntegral, double maximumIntegral) {
-    m_minimumIntegral = minimumIntegral;
-    m_maximumIntegral = maximumIntegral;
-  }
-
-  /**
-   * Sets the error which is considered tolerable for use with atSetpoint().
-   *
-   * @param positionTolerance Position error which is tolerable.
-   */
-  public void setTolerance(double positionTolerance) {
-    setTolerance(positionTolerance, Double.POSITIVE_INFINITY);
-  }
-
-  /**
-   * Sets the error which is considered tolerable for use with atSetpoint().
-   *
-   * @param positionTolerance Position error which is tolerable.
-   * @param velocityTolerance Velocity error which is tolerable.
-   */
-  public void setTolerance(double positionTolerance, double velocityTolerance) {
-    m_positionTolerance = positionTolerance;
-    m_velocityTolerance = velocityTolerance;
-  }
-
-  /**
-   * Returns the difference between the setpoint and the measurement.
-   *
-   * @return The error.
-   */
-  public double getPositionError() {
-    return m_positionError;
-  }
-
-  /**
-   * Returns the velocity error.
-   */
-  public double getVelocityError() {
-    return m_velocityError;
-  }
-
-  /**
-   * Returns the next output of the PID controller.
-   *
-   * @param measurement The current measurement of the process variable.
-   * @param setpoint    The new setpoint of the controller.
-   */
-  public double calculate(double measurement, double setpoint) {
-    // Set setpoint to provided value
-    setSetpoint(setpoint);
-    return calculate(measurement);
-  }
-
-  /**
-   * Returns the next output of the PID controller.
-   *
-   * @param measurement The current measurement of the process variable.
-   */
-  public double calculate(double measurement) {
-    m_prevError = m_positionError;
-
-    if (m_continuous) {
-      m_positionError = ControllerUtil.getModulusError(m_setpoint, measurement, m_minimumInput,
-          m_maximumInput);
-    } else {
-      m_positionError = m_setpoint - measurement;
-    }
-
-    m_velocityError = (m_positionError - m_prevError) / m_period;
-
-    if (m_Ki != 0) {
-      m_totalError = MathUtil.clamp(m_totalError + m_positionError * m_period,
-          m_minimumIntegral / m_Ki, m_maximumIntegral / m_Ki);
-    }
-
-    return m_Kp * m_positionError + m_Ki * m_totalError + m_Kd * m_velocityError;
-  }
-
-  /**
-   * Resets the previous error and the integral term.
-   */
-  public void reset() {
-    m_prevError = 0;
-    m_totalError = 0;
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("PIDController");
-    builder.addDoubleProperty("p", this::getP, this::setP);
-    builder.addDoubleProperty("i", this::getI, this::setI);
-    builder.addDoubleProperty("d", this::getD, this::setD);
-    builder.addDoubleProperty("setpoint", this::getSetpoint, this::setSetpoint);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ProfiledPIDController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ProfiledPIDController.java
deleted file mode 100644
index 3955c5b..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/ProfiledPIDController.java
+++ /dev/null
@@ -1,378 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.trajectory.TrapezoidProfile;
-
-/**
- * Implements a PID control loop whose setpoint is constrained by a trapezoid
- * profile. Users should call reset() when they first start running the controller
- * to avoid unwanted behavior.
- */
-public class ProfiledPIDController implements Sendable {
-  private static int instances;
-
-  private PIDController m_controller;
-  private double m_minimumInput;
-  private double m_maximumInput;
-  private TrapezoidProfile.State m_goal = new TrapezoidProfile.State();
-  private TrapezoidProfile.State m_setpoint = new TrapezoidProfile.State();
-  private TrapezoidProfile.Constraints m_constraints;
-
-  /**
-   * Allocates a ProfiledPIDController with the given constants for Kp, Ki, and
-   * Kd.
-   *
-   * @param Kp          The proportional coefficient.
-   * @param Ki          The integral coefficient.
-   * @param Kd          The derivative coefficient.
-   * @param constraints Velocity and acceleration constraints for goal.
-   */
-  @SuppressWarnings("ParameterName")
-  public ProfiledPIDController(double Kp, double Ki, double Kd,
-                        TrapezoidProfile.Constraints constraints) {
-    this(Kp, Ki, Kd, constraints, 0.02);
-  }
-
-  /**
-   * Allocates a ProfiledPIDController with the given constants for Kp, Ki, and
-   * Kd.
-   *
-   * @param Kp          The proportional coefficient.
-   * @param Ki          The integral coefficient.
-   * @param Kd          The derivative coefficient.
-   * @param constraints Velocity and acceleration constraints for goal.
-   * @param period      The period between controller updates in seconds. The
-   *                    default is 0.02 seconds.
-   */
-  @SuppressWarnings("ParameterName")
-  public ProfiledPIDController(double Kp, double Ki, double Kd,
-                        TrapezoidProfile.Constraints constraints,
-                        double period) {
-    m_controller = new PIDController(Kp, Ki, Kd, period);
-    m_constraints = constraints;
-    instances++;
-    HAL.report(tResourceType.kResourceType_ProfiledPIDController, instances);
-  }
-
-  /**
-   * Sets the PID Controller gain parameters.
-   *
-   * <p>Sets the proportional, integral, and differential coefficients.
-   *
-   * @param Kp Proportional coefficient
-   * @param Ki Integral coefficient
-   * @param Kd Differential coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setPID(double Kp, double Ki, double Kd) {
-    m_controller.setPID(Kp, Ki, Kd);
-  }
-
-  /**
-   * Sets the proportional coefficient of the PID controller gain.
-   *
-   * @param Kp proportional coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setP(double Kp) {
-    m_controller.setP(Kp);
-  }
-
-  /**
-   * Sets the integral coefficient of the PID controller gain.
-   *
-   * @param Ki integral coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setI(double Ki) {
-    m_controller.setI(Ki);
-  }
-
-  /**
-   * Sets the differential coefficient of the PID controller gain.
-   *
-   * @param Kd differential coefficient
-   */
-  @SuppressWarnings("ParameterName")
-  public void setD(double Kd) {
-    m_controller.setD(Kd);
-  }
-
-  /**
-   * Gets the proportional coefficient.
-   *
-   * @return proportional coefficient
-   */
-  public double getP() {
-    return m_controller.getP();
-  }
-
-  /**
-   * Gets the integral coefficient.
-   *
-   * @return integral coefficient
-   */
-  public double getI() {
-    return m_controller.getI();
-  }
-
-  /**
-   * Gets the differential coefficient.
-   *
-   * @return differential coefficient
-   */
-  public double getD() {
-    return m_controller.getD();
-  }
-
-  /**
-   * Gets the period of this controller.
-   *
-   * @return The period of the controller.
-   */
-  public double getPeriod() {
-    return m_controller.getPeriod();
-  }
-
-  /**
-   * Sets the goal for the ProfiledPIDController.
-   *
-   * @param goal The desired goal state.
-   */
-  public void setGoal(TrapezoidProfile.State goal) {
-    m_goal = goal;
-  }
-
-  /**
-   * Sets the goal for the ProfiledPIDController.
-   *
-   * @param goal The desired goal position.
-   */
-  public void setGoal(double goal) {
-    m_goal = new TrapezoidProfile.State(goal, 0);
-  }
-
-  /**
-   * Gets the goal for the ProfiledPIDController.
-   */
-  public TrapezoidProfile.State getGoal() {
-    return m_goal;
-  }
-
-  /**
-   * Returns true if the error is within the tolerance of the error.
-   *
-   * <p>This will return false until at least one input value has been computed.
-   */
-  public boolean atGoal() {
-    return atSetpoint() && m_goal.equals(m_setpoint);
-  }
-
-  /**
-   * Set velocity and acceleration constraints for goal.
-   *
-   * @param constraints Velocity and acceleration constraints for goal.
-   */
-  public void setConstraints(TrapezoidProfile.Constraints constraints) {
-    m_constraints = constraints;
-  }
-
-  /**
-   * Returns the current setpoint of the ProfiledPIDController.
-   *
-   * @return The current setpoint.
-   */
-  public TrapezoidProfile.State getSetpoint() {
-    return m_setpoint;
-  }
-
-  /**
-   * Returns true if the error is within the tolerance of the error.
-   *
-   * <p>This will return false until at least one input value has been computed.
-   */
-  public boolean atSetpoint() {
-    return m_controller.atSetpoint();
-  }
-
-  /**
-   * Enables continuous input.
-   *
-   * <p>Rather then using the max and min input range as constraints, it considers
-   * them to be the same point and automatically calculates the shortest route
-   * to the setpoint.
-   *
-   * @param minimumInput The minimum value expected from the input.
-   * @param maximumInput The maximum value expected from the input.
-   */
-  public void enableContinuousInput(double minimumInput, double maximumInput) {
-    m_controller.enableContinuousInput(minimumInput, maximumInput);
-    m_minimumInput = minimumInput;
-    m_maximumInput = maximumInput;
-  }
-
-  /**
-   * Disables continuous input.
-   */
-  public void disableContinuousInput() {
-    m_controller.disableContinuousInput();
-  }
-
-  /**
-   * Sets the minimum and maximum values for the integrator.
-   *
-   * <p>When the cap is reached, the integrator value is added to the controller
-   * output rather than the integrator value times the integral gain.
-   *
-   * @param minimumIntegral The minimum value of the integrator.
-   * @param maximumIntegral The maximum value of the integrator.
-   */
-  public void setIntegratorRange(double minimumIntegral, double maximumIntegral) {
-    m_controller.setIntegratorRange(minimumIntegral, maximumIntegral);
-  }
-
-  /**
-   * Sets the error which is considered tolerable for use with atSetpoint().
-   *
-   * @param positionTolerance Position error which is tolerable.
-   */
-  public void setTolerance(double positionTolerance) {
-    setTolerance(positionTolerance, Double.POSITIVE_INFINITY);
-  }
-
-  /**
-   * Sets the error which is considered tolerable for use with atSetpoint().
-   *
-   * @param positionTolerance Position error which is tolerable.
-   * @param velocityTolerance Velocity error which is tolerable.
-   */
-  public void setTolerance(double positionTolerance, double velocityTolerance) {
-    m_controller.setTolerance(positionTolerance, velocityTolerance);
-  }
-
-  /**
-   * Returns the difference between the setpoint and the measurement.
-   *
-   * @return The error.
-   */
-  public double getPositionError() {
-    return m_controller.getPositionError();
-  }
-
-  /**
-   * Returns the change in error per second.
-   */
-  public double getVelocityError() {
-    return m_controller.getVelocityError();
-  }
-
-  /**
-   * Returns the next output of the PID controller.
-   *
-   * @param measurement The current measurement of the process variable.
-   */
-  public double calculate(double measurement) {
-    if (m_controller.isContinuousInputEnabled()) {
-      // Get error which is smallest distance between goal and measurement
-      double goalMinDistance = ControllerUtil.getModulusError(m_goal.position, measurement,
-          m_minimumInput, m_maximumInput);
-      double setpointMinDistance = ControllerUtil.getModulusError(m_setpoint.position, measurement,
-          m_minimumInput, m_maximumInput);
-
-      // Recompute the profile goal with the smallest error, thus giving the shortest path. The goal
-      // may be outside the input range after this operation, but that's OK because the controller
-      // will still go there and report an error of zero. In other words, the setpoint only needs to
-      // be offset from the measurement by the input range modulus; they don't need to be equal.
-      m_goal.position = goalMinDistance + measurement;
-      m_setpoint.position = setpointMinDistance + measurement;
-    }
-
-    var profile = new TrapezoidProfile(m_constraints, m_goal, m_setpoint);
-    m_setpoint = profile.calculate(getPeriod());
-    return m_controller.calculate(measurement, m_setpoint.position);
-  }
-
-  /**
-   * Returns the next output of the PID controller.
-   *
-   * @param measurement The current measurement of the process variable.
-   * @param goal The new goal of the controller.
-   */
-  public double calculate(double measurement, TrapezoidProfile.State goal) {
-    setGoal(goal);
-    return calculate(measurement);
-  }
-
-  /**
-   * Returns the next output of the PIDController.
-   *
-   * @param measurement The current measurement of the process variable.
-   * @param goal The new goal of the controller.
-   */
-  public double calculate(double measurement, double goal) {
-    setGoal(goal);
-    return calculate(measurement);
-  }
-
-  /**
-   * Returns the next output of the PID controller.
-   *
-   * @param measurement The current measurement of the process variable.
-   * @param goal        The new goal of the controller.
-   * @param constraints Velocity and acceleration constraints for goal.
-   */
-  public double calculate(double measurement, TrapezoidProfile.State goal,
-                   TrapezoidProfile.Constraints constraints) {
-    setConstraints(constraints);
-    return calculate(measurement, goal);
-  }
-
-  /**
-   * Reset the previous error and the integral term.
-   *
-   * @param measurement The current measured State of the system.
-   */
-  public void reset(TrapezoidProfile.State measurement) {
-    m_controller.reset();
-    m_setpoint = measurement;
-  }
-
-  /**
-   * Reset the previous error and the integral term.
-   *
-   * @param measuredPosition The current measured position of the system.
-   * @param measuredVelocity The current measured velocity of the system.
-   */
-  public void reset(double measuredPosition, double measuredVelocity) {
-    reset(new TrapezoidProfile.State(measuredPosition, measuredVelocity));
-  }
-
-  /**
-   * Reset the previous error and the integral term.
-   *
-   * @param measuredPosition The current measured position of the system. The velocity is
-   *     assumed to be zero.
-   */
-  public void reset(double measuredPosition) {
-    reset(measuredPosition, 0.0);
-  }
-
-  @Override
-  public void initSendable(SendableBuilder builder) {
-    builder.setSmartDashboardType("ProfiledPIDController");
-    builder.addDoubleProperty("p", this::getP, this::setP);
-    builder.addDoubleProperty("i", this::getI, this::setI);
-    builder.addDoubleProperty("d", this::getD, this::setD);
-    builder.addDoubleProperty("goal", () -> getGoal().position, this::setGoal);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/RamseteController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/RamseteController.java
deleted file mode 100644
index b597352..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/controller/RamseteController.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
-import edu.wpi.first.wpilibj.trajectory.Trajectory;
-
-/**
- * Ramsete is a nonlinear time-varying feedback controller for unicycle models
- * that drives the model to a desired pose along a two-dimensional trajectory.
- * Why would we need a nonlinear control law in addition to the linear ones we
- * have used so far like PID? If we use the original approach with PID
- * controllers for left and right position and velocity states, the controllers
- * only deal with the local pose. If the robot deviates from the path, there is
- * no way for the controllers to correct and the robot may not reach the desired
- * global pose. This is due to multiple endpoints existing for the robot which
- * have the same encoder path arc lengths.
- *
- * <p>Instead of using wheel path arc lengths (which are in the robot's local
- * coordinate frame), nonlinear controllers like pure pursuit and Ramsete use
- * global pose. The controller uses this extra information to guide a linear
- * reference tracker like the PID controllers back in by adjusting the
- * references of the PID controllers.
- *
- * <p>The paper "Control of Wheeled Mobile Robots: An Experimental Overview"
- * describes a nonlinear controller for a wheeled vehicle with unicycle-like
- * kinematics; a global pose consisting of x, y, and theta; and a desired pose
- * consisting of x_d, y_d, and theta_d. We call it Ramsete because that's the
- * acronym for the title of the book it came from in Italian ("Robotica
- * Articolata e Mobile per i SErvizi e le TEcnologie").
- *
- * <p>See
- * <a href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">
- * Controls Engineering in the FIRST Robotics Competition</a> section on Ramsete
- * unicycle controller for a derivation and analysis.
- */
-public class RamseteController {
-  @SuppressWarnings("MemberName")
-  private final double m_b;
-  @SuppressWarnings("MemberName")
-  private final double m_zeta;
-
-  private Pose2d m_poseError = new Pose2d();
-  private Pose2d m_poseTolerance = new Pose2d();
-  private boolean m_enabled = true;
-
-  /**
-   * Construct a Ramsete unicycle controller.
-   *
-   * @param b    Tuning parameter (b &gt; 0) for which larger values make convergence more
-   *             aggressive like a proportional term.
-   * @param zeta Tuning parameter (0 &lt; zeta &lt; 1) for which larger values provide more damping
-   *             in response.
-   */
-  @SuppressWarnings("ParameterName")
-  public RamseteController(double b, double zeta) {
-    m_b = b;
-    m_zeta = zeta;
-  }
-
-  /**
-   * Construct a Ramsete unicycle controller. The default arguments for
-   * b and zeta of 2.0 and 0.7 have been well-tested to produce desirable
-   * results.
-   */
-  public RamseteController() {
-    this(2.0, 0.7);
-  }
-
-  /**
-   * Returns true if the pose error is within tolerance of the reference.
-   */
-  public boolean atReference() {
-    final var eTranslate = m_poseError.getTranslation();
-    final var eRotate = m_poseError.getRotation();
-    final var tolTranslate = m_poseTolerance.getTranslation();
-    final var tolRotate = m_poseTolerance.getRotation();
-    return Math.abs(eTranslate.getX()) < tolTranslate.getX()
-           && Math.abs(eTranslate.getY()) < tolTranslate.getY()
-           && Math.abs(eRotate.getRadians()) < tolRotate.getRadians();
-  }
-
-  /**
-   * Sets the pose error which is considered tolerable for use with
-   * atReference().
-   *
-   * @param poseTolerance Pose error which is tolerable.
-   */
-  public void setTolerance(Pose2d poseTolerance) {
-    m_poseTolerance = poseTolerance;
-  }
-
-  /**
-   * Returns the next output of the Ramsete controller.
-   *
-   * <p>The reference pose, linear velocity, and angular velocity should come
-   * from a drivetrain trajectory.
-   *
-   * @param currentPose                        The current pose.
-   * @param poseRef                            The desired pose.
-   * @param linearVelocityRefMeters            The desired linear velocity in meters per second.
-   * @param angularVelocityRefRadiansPerSecond The desired angular velocity in radians per second.
-   */
-  @SuppressWarnings("LocalVariableName")
-  public ChassisSpeeds calculate(Pose2d currentPose,
-                                 Pose2d poseRef,
-                                 double linearVelocityRefMeters,
-                                 double angularVelocityRefRadiansPerSecond) {
-    if (!m_enabled) {
-      return new ChassisSpeeds(linearVelocityRefMeters, 0.0, angularVelocityRefRadiansPerSecond);
-    }
-
-    m_poseError = poseRef.relativeTo(currentPose);
-
-    // Aliases for equation readability
-    final double eX = m_poseError.getX();
-    final double eY = m_poseError.getY();
-    final double eTheta = m_poseError.getRotation().getRadians();
-    final double vRef = linearVelocityRefMeters;
-    final double omegaRef = angularVelocityRefRadiansPerSecond;
-
-    double k = 2.0 * m_zeta * Math.sqrt(Math.pow(omegaRef, 2) + m_b * Math.pow(vRef, 2));
-
-    return new ChassisSpeeds(vRef * m_poseError.getRotation().getCos() + k * eX,
-                             0.0,
-                             omegaRef + k * eTheta + m_b * vRef * sinc(eTheta) * eY);
-  }
-
-  /**
-   * Returns the next output of the Ramsete controller.
-   *
-   * <p>The reference pose, linear velocity, and angular velocity should come
-   * from a drivetrain trajectory.
-   *
-   * @param currentPose  The current pose.
-   * @param desiredState The desired pose, linear velocity, and angular velocity
-   *                     from a trajectory.
-   */
-  @SuppressWarnings("LocalVariableName")
-  public ChassisSpeeds calculate(Pose2d currentPose, Trajectory.State desiredState) {
-    return calculate(currentPose, desiredState.poseMeters, desiredState.velocityMetersPerSecond,
-        desiredState.velocityMetersPerSecond * desiredState.curvatureRadPerMeter);
-  }
-
-  /**
-   * Enables and disables the controller for troubleshooting purposes.
-   *
-   * @param enabled If the controller is enabled or not.
-   */
-  public void setEnabled(boolean enabled) {
-    m_enabled = enabled;
-  }
-
-  /**
-   * Returns sin(x) / x.
-   *
-   * @param x Value of which to take sinc(x).
-   */
-  @SuppressWarnings("ParameterName")
-  private static double sinc(double x) {
-    if (Math.abs(x) < 1e-9) {
-      return 1.0 - 1.0 / 6.0 * x * x;
-    } else {
-      return Math.sin(x) / x;
-    }
-  }
-}
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 88a6ff9..57fb365 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
@@ -1,60 +1,58 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.drive;
 
-import java.util.StringJoiner;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.Sendable;
+import edu.wpi.first.math.MathUtil;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.SpeedController;
-import edu.wpi.first.wpilibj.SpeedControllerGroup;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-import edu.wpi.first.wpiutil.math.MathUtil;
 
 /**
  * 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 SpeedController per side. For four and
- * six motor drivetrains, construct and pass in {@link edu.wpi.first.wpilibj.SpeedControllerGroup}
- * instances as follows.
+ * (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 {
- *   SpeedController m_frontLeft = new PWMVictorSPX(1);
- *   SpeedController m_rearLeft = new PWMVictorSPX(2);
- *   SpeedControllerGroup m_left = new SpeedControllerGroup(m_frontLeft, m_rearLeft);
+ *   MotorController m_frontLeft = new PWMVictorSPX(1);
+ *   MotorController m_rearLeft = new PWMVictorSPX(2);
+ *   MotorControllerGroup m_left = new MotorControllerGroup(m_frontLeft, m_rearLeft);
  *
- *   SpeedController m_frontRight = new PWMVictorSPX(3);
- *   SpeedController m_rearRight = new PWMVictorSPX(4);
- *   SpeedControllerGroup m_right = new SpeedControllerGroup(m_frontRight, m_rearRight);
+ *   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 {
- *   SpeedController m_frontLeft = new PWMVictorSPX(1);
- *   SpeedController m_midLeft = new PWMVictorSPX(2);
- *   SpeedController m_rearLeft = new PWMVictorSPX(3);
- *   SpeedControllerGroup m_left = new SpeedControllerGroup(m_frontLeft, m_midLeft, m_rearLeft);
+ *   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);
  *
- *   SpeedController m_frontRight = new PWMVictorSPX(4);
- *   SpeedController m_midRight = new PWMVictorSPX(5);
- *   SpeedController m_rearRight = new PWMVictorSPX(6);
- *   SpeedControllerGroup m_right = new SpeedControllerGroup(m_frontRight, m_midRight, m_rearRight);
+ *   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);
  * }
@@ -63,6 +61,7 @@
  * <p>A differential drive robot has left and right wheels separated by an arbitrary width.
  *
  * <p>Drive base diagram:
+ *
  * <pre>
  * |_______|
  * | |   | |
@@ -83,42 +82,52 @@
  * positive.
  *
  * <p>Inputs smaller then {@value edu.wpi.first.wpilibj.drive.RobotDriveBase#kDefaultDeadband} will
- * be set to 0, and larger values will be scaled so that the full range is still used. This
- * deadband value can be changed with {@link #setDeadband}.
- *
- * <p>RobotDrive porting guide:
- * <br>{@link #tankDrive(double, double)} is equivalent to
- * {@link edu.wpi.first.wpilibj.RobotDrive#tankDrive(double, double)} if a deadband of 0 is used.
- * <br>{@link #arcadeDrive(double, double)} is equivalent to
- * {@link edu.wpi.first.wpilibj.RobotDrive#arcadeDrive(double, double)} if a deadband of 0 is used
- * and the the rotation input is inverted eg arcadeDrive(y, -rotation)
- * <br>{@link #curvatureDrive(double, double, boolean)} is similar in concept to
- * {@link edu.wpi.first.wpilibj.RobotDrive#drive(double, double)} with the addition of a quick turn
- * mode. However, it is not designed to give exactly the same response.
+ * be set to 0, and larger values will be scaled so that the full range is still used. This deadband
+ * value can be changed with {@link #setDeadband}.
  */
+@SuppressWarnings("removal")
 public class DifferentialDrive extends RobotDriveBase implements Sendable, AutoCloseable {
-  public static final double kDefaultQuickStopThreshold = 0.2;
-  public static final double kDefaultQuickStopAlpha = 0.1;
-
   private static int instances;
 
   private final SpeedController m_leftMotor;
   private final SpeedController m_rightMotor;
 
-  private double m_quickStopThreshold = kDefaultQuickStopThreshold;
-  private double m_quickStopAlpha = kDefaultQuickStopAlpha;
-  private double m_quickStopAccumulator;
-  private double m_rightSideInvertMultiplier = -1.0;
   private boolean m_reported;
 
+  @SuppressWarnings("MemberName")
+  public static class WheelSpeeds {
+    public double left;
+    public double right;
+
+    /** Constructs a WheelSpeeds with zeroes for left and right speeds. */
+    public WheelSpeeds() {}
+
+    /**
+     * Constructs a WheelSpeeds.
+     *
+     * @param left The left speed.
+     * @param right The right speed.
+     */
+    public WheelSpeeds(double left, double right) {
+      this.left = left;
+      this.right = right;
+    }
+  }
+
   /**
    * Construct a DifferentialDrive.
    *
-   * <p>To pass multiple motors per side, use a {@link SpeedControllerGroup}. If a motor needs to be
-   * inverted, do so before passing it in.
+   * <p>To pass multiple motors per side, use a {@link
+   * edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup}. If a motor needs to be inverted, do
+   * so before passing it in.
+   *
+   * @param leftMotor Left motor.
+   * @param rightMotor Right motor.
    */
   public DifferentialDrive(SpeedController leftMotor, SpeedController rightMotor) {
-    verify(leftMotor, rightMotor);
+    requireNonNull(leftMotor, "Left motor cannot be null");
+    requireNonNull(rightMotor, "Right motor cannot be null");
+
     m_leftMotor = leftMotor;
     m_rightMotor = rightMotor;
     SendableRegistry.addChild(this, m_leftMotor);
@@ -133,35 +142,12 @@
   }
 
   /**
-   * Verifies that all motors are nonnull, throwing a NullPointerException if any of them are.
-   * The exception's error message will specify all null motors, e.g. {@code
-   * NullPointerException("leftMotor, rightMotor")}, to give as much information as possible to
-   * the programmer.
+   * Arcade drive method for differential drive platform. The calculated values will be squared to
+   * decrease sensitivity at low speeds.
    *
-   * @throws NullPointerException if any of the given motors are null
-   */
-  @SuppressWarnings("PMD.AvoidThrowingNullPointerException")
-  private void verify(SpeedController leftMotor, SpeedController rightMotor) {
-    if (leftMotor != null && rightMotor != null) {
-      return;
-    }
-    StringJoiner joiner = new StringJoiner(", ");
-    if (leftMotor == null) {
-      joiner.add("leftMotor");
-    }
-    if (rightMotor == null) {
-      joiner.add("rightMotor");
-    }
-    throw new NullPointerException(joiner.toString());
-  }
-
-  /**
-   * Arcade drive method for differential drive platform.
-   * The calculated values will be squared to decrease sensitivity at low speeds.
-   *
-   * @param xSpeed    The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
+   *     positive.
    */
   @SuppressWarnings("ParameterName")
   public void arcadeDrive(double xSpeed, double zRotation) {
@@ -171,60 +157,26 @@
   /**
    * Arcade drive method for differential drive platform.
    *
-   * @param xSpeed        The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
-   * @param zRotation     The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                      positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
    * @param squareInputs If set, decreases the input sensitivity at low speeds.
    */
   @SuppressWarnings("ParameterName")
   public void arcadeDrive(double xSpeed, double zRotation, boolean squareInputs) {
     if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_DifferentialArcade, 2);
+      HAL.report(
+          tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_DifferentialArcade, 2);
       m_reported = true;
     }
 
-    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
-    xSpeed = applyDeadband(xSpeed, m_deadband);
+    xSpeed = MathUtil.applyDeadband(xSpeed, m_deadband);
+    zRotation = MathUtil.applyDeadband(zRotation, m_deadband);
 
-    zRotation = MathUtil.clamp(zRotation, -1.0, 1.0);
-    zRotation = applyDeadband(zRotation, m_deadband);
+    var speeds = arcadeDriveIK(xSpeed, zRotation, squareInputs);
 
-    // Square the inputs (while preserving the sign) to increase fine control
-    // while permitting full power.
-    if (squareInputs) {
-      xSpeed = Math.copySign(xSpeed * xSpeed, xSpeed);
-      zRotation = Math.copySign(zRotation * zRotation, zRotation);
-    }
-
-    double leftMotorOutput;
-    double rightMotorOutput;
-
-    double maxInput = Math.copySign(Math.max(Math.abs(xSpeed), Math.abs(zRotation)), xSpeed);
-
-    if (xSpeed >= 0.0) {
-      // First quadrant, else second quadrant
-      if (zRotation >= 0.0) {
-        leftMotorOutput = maxInput;
-        rightMotorOutput = xSpeed - zRotation;
-      } else {
-        leftMotorOutput = xSpeed + zRotation;
-        rightMotorOutput = maxInput;
-      }
-    } else {
-      // Third quadrant, else fourth quadrant
-      if (zRotation >= 0.0) {
-        leftMotorOutput = xSpeed + zRotation;
-        rightMotorOutput = maxInput;
-      } else {
-        leftMotorOutput = maxInput;
-        rightMotorOutput = xSpeed - zRotation;
-      }
-    }
-
-    m_leftMotor.set(MathUtil.clamp(leftMotorOutput, -1.0, 1.0) * m_maxOutput);
-    double maxOutput = m_maxOutput * m_rightSideInvertMultiplier;
-    m_rightMotor.set(MathUtil.clamp(rightMotorOutput, -1.0, 1.0) * maxOutput);
+    m_leftMotor.set(speeds.left * m_maxOutput);
+    m_rightMotor.set(speeds.right * m_maxOutput);
 
     feed();
   }
@@ -233,94 +185,40 @@
    * Curvature drive method for differential drive platform.
    *
    * <p>The rotation argument controls the curvature of the robot's path rather than its rate of
-   * heading change. This makes the robot more controllable at high speeds. Also handles the
-   * robot's quick turn functionality - "quick turn" overrides constant-curvature turning for
-   * turn-in-place maneuvers.
+   * heading change. This makes the robot more controllable at high speeds.
    *
-   * @param xSpeed      The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
-   * @param zRotation   The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                    positive.
-   * @param isQuickTurn If set, overrides constant-curvature turning for
-   *                    turn-in-place maneuvers.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   * @param allowTurnInPlace If set, overrides constant-curvature turning for turn-in-place
+   *     maneuvers.
    */
-  @SuppressWarnings({"ParameterName", "PMD.CyclomaticComplexity"})
-  public void curvatureDrive(double xSpeed, double zRotation, boolean isQuickTurn) {
+  @SuppressWarnings("ParameterName")
+  public void curvatureDrive(double xSpeed, double zRotation, boolean allowTurnInPlace) {
     if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_DifferentialCurvature, 2);
+      HAL.report(
+          tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_DifferentialCurvature, 2);
       m_reported = true;
     }
 
-    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
-    xSpeed = applyDeadband(xSpeed, m_deadband);
+    xSpeed = MathUtil.applyDeadband(xSpeed, m_deadband);
+    zRotation = MathUtil.applyDeadband(zRotation, m_deadband);
 
-    zRotation = MathUtil.clamp(zRotation, -1.0, 1.0);
-    zRotation = applyDeadband(zRotation, m_deadband);
+    var speeds = curvatureDriveIK(xSpeed, zRotation, allowTurnInPlace);
 
-    double angularPower;
-    boolean overPower;
-
-    if (isQuickTurn) {
-      if (Math.abs(xSpeed) < m_quickStopThreshold) {
-        m_quickStopAccumulator = (1 - m_quickStopAlpha) * m_quickStopAccumulator
-            + m_quickStopAlpha * MathUtil.clamp(zRotation, -1.0, 1.0) * 2;
-      }
-      overPower = true;
-      angularPower = zRotation;
-    } else {
-      overPower = false;
-      angularPower = Math.abs(xSpeed) * zRotation - m_quickStopAccumulator;
-
-      if (m_quickStopAccumulator > 1) {
-        m_quickStopAccumulator -= 1;
-      } else if (m_quickStopAccumulator < -1) {
-        m_quickStopAccumulator += 1;
-      } else {
-        m_quickStopAccumulator = 0.0;
-      }
-    }
-
-    double leftMotorOutput = xSpeed + angularPower;
-    double rightMotorOutput = xSpeed - angularPower;
-
-    // If rotation is overpowered, reduce both outputs to within acceptable range
-    if (overPower) {
-      if (leftMotorOutput > 1.0) {
-        rightMotorOutput -= leftMotorOutput - 1.0;
-        leftMotorOutput = 1.0;
-      } else if (rightMotorOutput > 1.0) {
-        leftMotorOutput -= rightMotorOutput - 1.0;
-        rightMotorOutput = 1.0;
-      } else if (leftMotorOutput < -1.0) {
-        rightMotorOutput -= leftMotorOutput + 1.0;
-        leftMotorOutput = -1.0;
-      } else if (rightMotorOutput < -1.0) {
-        leftMotorOutput -= rightMotorOutput + 1.0;
-        rightMotorOutput = -1.0;
-      }
-    }
-
-    // Normalize the wheel speeds
-    double maxMagnitude = Math.max(Math.abs(leftMotorOutput), Math.abs(rightMotorOutput));
-    if (maxMagnitude > 1.0) {
-      leftMotorOutput /= maxMagnitude;
-      rightMotorOutput /= maxMagnitude;
-    }
-
-    m_leftMotor.set(leftMotorOutput * m_maxOutput);
-    m_rightMotor.set(rightMotorOutput * m_maxOutput * m_rightSideInvertMultiplier);
+    m_leftMotor.set(speeds.left * m_maxOutput);
+    m_rightMotor.set(speeds.right * m_maxOutput);
 
     feed();
   }
 
   /**
-   * Tank drive method for differential drive platform.
-   * The calculated values will be squared to decrease sensitivity at low speeds.
+   * Tank drive method for differential drive platform. The calculated values will be squared to
+   * decrease sensitivity at low speeds.
    *
-   * @param leftSpeed  The robot's left side speed along the X axis [-1.0..1.0]. Forward is
-   *                   positive.
+   * @param leftSpeed The robot's left side speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param rightSpeed The robot's right side speed along the X axis [-1.0..1.0]. Forward is
-   *                   positive.
+   *     positive.
    */
   public void tankDrive(double leftSpeed, double rightSpeed) {
     tankDrive(leftSpeed, rightSpeed, true);
@@ -329,24 +227,137 @@
   /**
    * Tank drive method for differential drive platform.
    *
-   * @param leftSpeed     The robot left side's speed along the X axis [-1.0..1.0]. Forward is
-   *                      positive.
-   * @param rightSpeed    The robot right side's speed along the X axis [-1.0..1.0]. Forward is
-   *                      positive.
+   * @param leftSpeed The robot left side's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param rightSpeed The robot right side's speed along the X axis [-1.0..1.0]. Forward is
+   *     positive.
    * @param squareInputs If set, decreases the input sensitivity at low speeds.
    */
   public void tankDrive(double leftSpeed, double rightSpeed, boolean squareInputs) {
     if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_DifferentialTank, 2);
+      HAL.report(
+          tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_DifferentialTank, 2);
       m_reported = true;
     }
 
-    leftSpeed = MathUtil.clamp(leftSpeed, -1.0, 1.0);
-    leftSpeed = applyDeadband(leftSpeed, m_deadband);
+    leftSpeed = MathUtil.applyDeadband(leftSpeed, m_deadband);
+    rightSpeed = MathUtil.applyDeadband(rightSpeed, m_deadband);
 
+    var speeds = tankDriveIK(leftSpeed, rightSpeed, squareInputs);
+
+    m_leftMotor.set(speeds.left * m_maxOutput);
+    m_rightMotor.set(speeds.right * m_maxOutput);
+
+    feed();
+  }
+
+  /**
+   * Arcade drive inverse kinematics for differential drive platform.
+   *
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   * @param squareInputs If set, decreases the input sensitivity at low speeds.
+   * @return Wheel speeds.
+   */
+  @SuppressWarnings("ParameterName")
+  public static WheelSpeeds arcadeDriveIK(double xSpeed, double zRotation, boolean squareInputs) {
+    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
+    zRotation = MathUtil.clamp(zRotation, -1.0, 1.0);
+
+    // Square the inputs (while preserving the sign) to increase fine control
+    // while permitting full power.
+    if (squareInputs) {
+      xSpeed = Math.copySign(xSpeed * xSpeed, xSpeed);
+      zRotation = Math.copySign(zRotation * zRotation, zRotation);
+    }
+
+    double leftSpeed;
+    double rightSpeed;
+
+    double maxInput = Math.copySign(Math.max(Math.abs(xSpeed), Math.abs(zRotation)), xSpeed);
+
+    if (xSpeed >= 0.0) {
+      // First quadrant, else second quadrant
+      if (zRotation >= 0.0) {
+        leftSpeed = maxInput;
+        rightSpeed = xSpeed - zRotation;
+      } else {
+        leftSpeed = xSpeed + zRotation;
+        rightSpeed = maxInput;
+      }
+    } else {
+      // Third quadrant, else fourth quadrant
+      if (zRotation >= 0.0) {
+        leftSpeed = xSpeed + zRotation;
+        rightSpeed = maxInput;
+      } else {
+        leftSpeed = maxInput;
+        rightSpeed = xSpeed - zRotation;
+      }
+    }
+
+    // Normalize the wheel speeds
+    double maxMagnitude = Math.max(Math.abs(leftSpeed), Math.abs(rightSpeed));
+    if (maxMagnitude > 1.0) {
+      leftSpeed /= maxMagnitude;
+      rightSpeed /= maxMagnitude;
+    }
+
+    return new WheelSpeeds(leftSpeed, rightSpeed);
+  }
+
+  /**
+   * Curvature drive inverse kinematics for differential drive platform.
+   *
+   * <p>The rotation argument controls the curvature of the robot's path rather than its rate of
+   * heading change. This makes the robot more controllable at high speeds.
+   *
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   * @param allowTurnInPlace If set, overrides constant-curvature turning for turn-in-place
+   *     maneuvers.
+   * @return Wheel speeds.
+   */
+  @SuppressWarnings("ParameterName")
+  public static WheelSpeeds curvatureDriveIK(
+      double xSpeed, double zRotation, boolean allowTurnInPlace) {
+    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
+    zRotation = MathUtil.clamp(zRotation, -1.0, 1.0);
+
+    double leftSpeed = 0.0;
+    double rightSpeed = 0.0;
+
+    if (allowTurnInPlace) {
+      leftSpeed = xSpeed + zRotation;
+      rightSpeed = xSpeed - zRotation;
+    } else {
+      leftSpeed = xSpeed + Math.abs(xSpeed) * zRotation;
+      rightSpeed = xSpeed - Math.abs(xSpeed) * zRotation;
+    }
+
+    // Normalize wheel speeds
+    double maxMagnitude = Math.max(Math.abs(leftSpeed), Math.abs(rightSpeed));
+    if (maxMagnitude > 1.0) {
+      leftSpeed /= maxMagnitude;
+      rightSpeed /= maxMagnitude;
+    }
+
+    return new WheelSpeeds(leftSpeed, rightSpeed);
+  }
+
+  /**
+   * Tank drive inverse kinematics for differential drive platform.
+   *
+   * @param leftSpeed The robot left side's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param rightSpeed The robot right side's speed along the X axis [-1.0..1.0]. Forward is
+   *     positive.
+   * @param squareInputs If set, decreases the input sensitivity at low speeds.
+   * @return Wheel speeds.
+   */
+  public static WheelSpeeds tankDriveIK(double leftSpeed, double rightSpeed, boolean squareInputs) {
+    leftSpeed = MathUtil.clamp(leftSpeed, -1.0, 1.0);
     rightSpeed = MathUtil.clamp(rightSpeed, -1.0, 1.0);
-    rightSpeed = applyDeadband(rightSpeed, m_deadband);
 
     // Square the inputs (while preserving the sign) to increase fine control
     // while permitting full power.
@@ -355,59 +366,7 @@
       rightSpeed = Math.copySign(rightSpeed * rightSpeed, rightSpeed);
     }
 
-    m_leftMotor.set(leftSpeed * m_maxOutput);
-    m_rightMotor.set(rightSpeed * m_maxOutput * m_rightSideInvertMultiplier);
-
-    feed();
-  }
-
-  /**
-   * Sets the QuickStop speed threshold in curvature drive.
-   *
-   * <p>QuickStop compensates for the robot's moment of inertia when stopping after a QuickTurn.
-   *
-   * <p>While QuickTurn is enabled, the QuickStop accumulator takes on the rotation rate value
-   * outputted by the low-pass filter when the robot's speed along the X axis is below the
-   * threshold. When QuickTurn is disabled, the accumulator's value is applied against the computed
-   * angular power request to slow the robot's rotation.
-   *
-   * @param threshold X speed below which quick stop accumulator will receive rotation rate values
-   *                  [0..1.0].
-   */
-  public void setQuickStopThreshold(double threshold) {
-    m_quickStopThreshold = threshold;
-  }
-
-  /**
-   * Sets the low-pass filter gain for QuickStop in curvature drive.
-   *
-   * <p>The low-pass filter filters incoming rotation rate commands to smooth out high frequency
-   * changes.
-   *
-   * @param alpha Low-pass filter gain [0.0..2.0]. Smaller values result in slower output changes.
-   *              Values between 1.0 and 2.0 result in output oscillation. Values below 0.0 and
-   *              above 2.0 are unstable.
-   */
-  public void setQuickStopAlpha(double alpha) {
-    m_quickStopAlpha = alpha;
-  }
-
-  /**
-   * Gets if the power sent to the right side of the drivetrain is multiplied by -1.
-   *
-   * @return true if the right side is inverted
-   */
-  public boolean isRightSideInverted() {
-    return m_rightSideInvertMultiplier == -1.0;
-  }
-
-  /**
-   * Sets if the power sent to the right side of the drivetrain should be multiplied by -1.
-   *
-   * @param rightSideInverted true if right side power should be multiplied by -1
-   */
-  public void setRightSideInverted(boolean rightSideInverted) {
-    m_rightSideInvertMultiplier = rightSideInverted ? -1.0 : 1.0;
+    return new WheelSpeeds(leftSpeed, rightSpeed);
   }
 
   @Override
@@ -429,8 +388,6 @@
     builder.setSafeState(this::stopMotor);
     builder.addDoubleProperty("Left Motor Speed", m_leftMotor::get, m_leftMotor::set);
     builder.addDoubleProperty(
-        "Right Motor Speed",
-        () -> m_rightMotor.get() * m_rightSideInvertMultiplier,
-        x -> m_rightMotor.set(x * m_rightSideInvertMultiplier));
+        "Right Motor Speed", () -> m_rightMotor.get(), x -> m_rightMotor.set(x));
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/KilloughDrive.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/KilloughDrive.java
index 45da78a..3c49989 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/KilloughDrive.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/KilloughDrive.java
@@ -1,22 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.drive;
 
-import java.util.StringJoiner;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.Sendable;
+import edu.wpi.first.math.MathUtil;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.SpeedController;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-import edu.wpi.first.wpiutil.math.MathUtil;
 
 /**
  * A class for driving Killough drive platforms.
@@ -24,6 +21,7 @@
  * <p>Killough drives are triangular with one omni wheel on each corner.
  *
  * <p>Drive base diagram:
+ *
  * <pre>
  *  /_____\
  * / \   / \
@@ -42,6 +40,7 @@
  * points down. Rotations follow the right-hand rule, so clockwise rotation around the Z axis is
  * positive.
  */
+@SuppressWarnings("removal")
 public class KilloughDrive extends RobotDriveBase implements Sendable, AutoCloseable {
   public static final double kDefaultLeftMotorAngle = 60.0;
   public static final double kDefaultRightMotorAngle = 120.0;
@@ -59,6 +58,29 @@
 
   private boolean m_reported;
 
+  @SuppressWarnings("MemberName")
+  public static class WheelSpeeds {
+    public double left;
+    public double right;
+    public double back;
+
+    /** Constructs a WheelSpeeds with zeroes for left, right, and back speeds. */
+    public WheelSpeeds() {}
+
+    /**
+     * Constructs a WheelSpeeds.
+     *
+     * @param left The left speed.
+     * @param right The right speed.
+     * @param back The back speed.
+     */
+    public WheelSpeeds(double left, double right, double back) {
+      this.left = left;
+      this.right = right;
+      this.back = back;
+    }
+  }
+
   /**
    * Construct a Killough drive with the given motors and default motor angles.
    *
@@ -67,13 +89,18 @@
    *
    * <p>If a motor needs to be inverted, do so before passing it in.
    *
-   * @param leftMotor  The motor on the left corner.
+   * @param leftMotor The motor on the left corner.
    * @param rightMotor The motor on the right corner.
-   * @param backMotor  The motor on the back corner.
+   * @param backMotor The motor on the back corner.
    */
-  public KilloughDrive(SpeedController leftMotor, SpeedController rightMotor,
-                       SpeedController backMotor) {
-    this(leftMotor, rightMotor, backMotor, kDefaultLeftMotorAngle, kDefaultRightMotorAngle,
+  public KilloughDrive(
+      SpeedController leftMotor, SpeedController rightMotor, SpeedController backMotor) {
+    this(
+        leftMotor,
+        rightMotor,
+        backMotor,
+        kDefaultLeftMotorAngle,
+        kDefaultRightMotorAngle,
         kDefaultBackMotorAngle);
   }
 
@@ -82,26 +109,39 @@
    *
    * <p>Angles are measured in degrees clockwise from the positive X axis.
    *
-   * @param leftMotor       The motor on the left corner.
-   * @param rightMotor      The motor on the right corner.
-   * @param backMotor       The motor on the back corner.
-   * @param leftMotorAngle  The angle of the left wheel's forward direction of travel.
+   * @param leftMotor The motor on the left corner.
+   * @param rightMotor The motor on the right corner.
+   * @param backMotor The motor on the back corner.
+   * @param leftMotorAngle The angle of the left wheel's forward direction of travel.
    * @param rightMotorAngle The angle of the right wheel's forward direction of travel.
-   * @param backMotorAngle  The angle of the back wheel's forward direction of travel.
+   * @param backMotorAngle The angle of the back wheel's forward direction of travel.
    */
-  public KilloughDrive(SpeedController leftMotor, SpeedController rightMotor,
-                       SpeedController backMotor, double leftMotorAngle, double rightMotorAngle,
-                       double backMotorAngle) {
-    verify(leftMotor, rightMotor, backMotor);
+  public KilloughDrive(
+      SpeedController leftMotor,
+      SpeedController rightMotor,
+      SpeedController backMotor,
+      double leftMotorAngle,
+      double rightMotorAngle,
+      double backMotorAngle) {
+    requireNonNull(leftMotor, "Left motor cannot be null");
+    requireNonNull(rightMotor, "Right motor cannot be null");
+    requireNonNull(backMotor, "Back motor cannot be null");
+
     m_leftMotor = leftMotor;
     m_rightMotor = rightMotor;
     m_backMotor = backMotor;
-    m_leftVec = new Vector2d(Math.cos(leftMotorAngle * (Math.PI / 180.0)),
-                             Math.sin(leftMotorAngle * (Math.PI / 180.0)));
-    m_rightVec = new Vector2d(Math.cos(rightMotorAngle * (Math.PI / 180.0)),
-                              Math.sin(rightMotorAngle * (Math.PI / 180.0)));
-    m_backVec = new Vector2d(Math.cos(backMotorAngle * (Math.PI / 180.0)),
-                             Math.sin(backMotorAngle * (Math.PI / 180.0)));
+    m_leftVec =
+        new Vector2d(
+            Math.cos(leftMotorAngle * (Math.PI / 180.0)),
+            Math.sin(leftMotorAngle * (Math.PI / 180.0)));
+    m_rightVec =
+        new Vector2d(
+            Math.cos(rightMotorAngle * (Math.PI / 180.0)),
+            Math.sin(rightMotorAngle * (Math.PI / 180.0)));
+    m_backVec =
+        new Vector2d(
+            Math.cos(backMotorAngle * (Math.PI / 180.0)),
+            Math.sin(backMotorAngle * (Math.PI / 180.0)));
     SendableRegistry.addChild(this, m_leftMotor);
     SendableRegistry.addChild(this, m_rightMotor);
     SendableRegistry.addChild(this, m_backMotor);
@@ -115,42 +155,15 @@
   }
 
   /**
-   * Verifies that all motors are nonnull, throwing a NullPointerException if any of them are.
-   * The exception's error message will specify all null motors, e.g. {@code
-   * NullPointerException("leftMotor, rightMotor")}, to give as much information as possible to
-   * the programmer.
-   *
-   * @throws NullPointerException if any of the given motors are null
-   */
-  @SuppressWarnings("PMD.AvoidThrowingNullPointerException")
-  private void verify(SpeedController leftMotor, SpeedController rightMotor,
-                      SpeedController backMotor) {
-    if (leftMotor != null && rightMotor != null && backMotor != null) {
-      return;
-    }
-    StringJoiner joiner = new StringJoiner(", ");
-    if (leftMotor == null) {
-      joiner.add("leftMotor");
-    }
-    if (rightMotor == null) {
-      joiner.add("rightMotor");
-    }
-    if (backMotor == null) {
-      joiner.add("backMotor");
-    }
-    throw new NullPointerException(joiner.toString());
-  }
-
-  /**
    * Drive method for Killough platform.
    *
    * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
    * from its angle or rotation rate.
    *
-   * @param ySpeed    The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
-   * @param xSpeed    The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
+   *     positive.
    */
   @SuppressWarnings("ParameterName")
   public void driveCartesian(double ySpeed, double xSpeed, double zRotation) {
@@ -163,27 +176,77 @@
    * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
    * from its angle or rotation rate.
    *
-   * @param ySpeed    The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
-   * @param xSpeed    The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
-   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use
-   *                  this to implement field-oriented controls.
+   *     positive.
+   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use this
+   *     to implement field-oriented controls.
    */
   @SuppressWarnings("ParameterName")
-  public void driveCartesian(double ySpeed, double xSpeed, double zRotation,
-                             double gyroAngle) {
+  public void driveCartesian(double ySpeed, double xSpeed, double zRotation, double gyroAngle) {
     if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_KilloughCartesian, 3);
+      HAL.report(
+          tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_KilloughCartesian, 3);
       m_reported = true;
     }
 
-    ySpeed = MathUtil.clamp(ySpeed, -1.0, 1.0);
-    ySpeed = applyDeadband(ySpeed, m_deadband);
+    ySpeed = MathUtil.applyDeadband(ySpeed, m_deadband);
+    xSpeed = MathUtil.applyDeadband(xSpeed, m_deadband);
 
+    var speeds = driveCartesianIK(ySpeed, xSpeed, zRotation, gyroAngle);
+
+    m_leftMotor.set(speeds.left * m_maxOutput);
+    m_rightMotor.set(speeds.right * m_maxOutput);
+    m_backMotor.set(speeds.back * m_maxOutput);
+
+    feed();
+  }
+
+  /**
+   * Drive method for Killough platform.
+   *
+   * <p>Angles are measured counter-clockwise from straight ahead. The speed at which the robot
+   * drives (translation) is independent from its angle or rotation rate.
+   *
+   * @param magnitude The robot's speed at a given angle [-1.0..1.0]. Forward is positive.
+   * @param angle The angle around the Z axis at which the robot drives in degrees [-180..180].
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   */
+  @SuppressWarnings("ParameterName")
+  public void drivePolar(double magnitude, double angle, double zRotation) {
+    if (!m_reported) {
+      HAL.report(tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_KilloughPolar, 3);
+      m_reported = true;
+    }
+
+    driveCartesian(
+        magnitude * Math.sin(angle * (Math.PI / 180.0)),
+        magnitude * Math.cos(angle * (Math.PI / 180.0)),
+        zRotation,
+        0.0);
+  }
+
+  /**
+   * Cartesian inverse kinematics for Killough platform.
+   *
+   * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
+   * from its angle or rotation rate.
+   *
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use this
+   *     to implement field-oriented controls.
+   * @return Wheel speeds.
+   */
+  @SuppressWarnings("ParameterName")
+  public WheelSpeeds driveCartesianIK(
+      double ySpeed, double xSpeed, double zRotation, double gyroAngle) {
+    ySpeed = MathUtil.clamp(ySpeed, -1.0, 1.0);
     xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
-    xSpeed = applyDeadband(xSpeed, m_deadband);
 
     // Compensate for gyro angle.
     Vector2d input = new Vector2d(ySpeed, xSpeed);
@@ -196,34 +259,10 @@
 
     normalize(wheelSpeeds);
 
-    m_leftMotor.set(wheelSpeeds[MotorType.kLeft.value] * m_maxOutput);
-    m_rightMotor.set(wheelSpeeds[MotorType.kRight.value] * m_maxOutput);
-    m_backMotor.set(wheelSpeeds[MotorType.kBack.value] * m_maxOutput);
-
-    feed();
-  }
-
-  /**
-   * Drive method for Killough platform.
-   *
-   * <p>Angles are measured counter-clockwise from straight ahead. The speed at which the robot
-   * drives (translation) is independent from its angle or rotation rate.
-   *
-   * @param magnitude The robot's speed at a given angle [-1.0..1.0]. Forward is positive.
-   * @param angle     The angle around the Z axis at which the robot drives in degrees [-180..180].
-   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
-   */
-  @SuppressWarnings("ParameterName")
-  public void drivePolar(double magnitude, double angle, double zRotation) {
-    if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_KilloughPolar, 3);
-      m_reported = true;
-    }
-
-    driveCartesian(magnitude * Math.sin(angle * (Math.PI / 180.0)),
-                   magnitude * Math.cos(angle * (Math.PI / 180.0)), zRotation, 0.0);
+    return new WheelSpeeds(
+        wheelSpeeds[MotorType.kLeft.value],
+        wheelSpeeds[MotorType.kRight.value],
+        wheelSpeeds[MotorType.kBack.value]);
   }
 
   @Override
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 d709004..b672fa8 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
@@ -1,22 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.drive;
 
-import java.util.StringJoiner;
+import static java.util.Objects.requireNonNull;
 
 import edu.wpi.first.hal.FRCNetComm.tInstances;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.Sendable;
+import edu.wpi.first.math.MathUtil;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import edu.wpi.first.wpilibj.SpeedController;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-import edu.wpi.first.wpiutil.math.MathUtil;
 
 /**
  * A class for driving Mecanum drive platforms.
@@ -27,6 +24,7 @@
  * relations for a Mecanum drive robot.
  *
  * <p>Drive base diagram:
+ *
  * <pre>
  * \\_______/
  * \\ |   | /
@@ -47,20 +45,20 @@
  * positive.
  *
  * <p>Inputs smaller then {@value edu.wpi.first.wpilibj.drive.RobotDriveBase#kDefaultDeadband} will
- * be set to 0, and larger values will be scaled so that the full range is still used. This
- * deadband value can be changed with {@link #setDeadband}.
+ * be set to 0, and larger values will be scaled so that the full range is still used. This deadband
+ * value can be changed with {@link #setDeadband}.
  *
- * <p>RobotDrive porting guide:
- * <br>In MecanumDrive, the right side speed controllers are automatically inverted, while in
- * RobotDrive, no speed controllers are automatically inverted.
- * <br>{@link #driveCartesian(double, double, double, double)} is equivalent to
- * {@link edu.wpi.first.wpilibj.RobotDrive#mecanumDrive_Cartesian(double, double, double, double)}
- * if a deadband of 0 is used, and the ySpeed and gyroAngle values are inverted compared to
- * RobotDrive (eg driveCartesian(xSpeed, -ySpeed, zRotation, -gyroAngle).
- * <br>{@link #drivePolar(double, double, double)} is equivalent to
- * {@link edu.wpi.first.wpilibj.RobotDrive#mecanumDrive_Polar(double, double, double)} if a
- * deadband of 0 is used.
+ * <p>RobotDrive porting guide: <br>
+ * In MecanumDrive, the right side motor controllers are automatically inverted, while in
+ * RobotDrive, no motor controllers are automatically inverted. <br>
+ * {@link #driveCartesian(double, double, double, double)} is equivalent to RobotDrive's
+ * mecanumDrive_Cartesian(double, double, double, double) if a deadband of 0 is used, and the ySpeed
+ * and gyroAngle values are inverted compared to RobotDrive (eg driveCartesian(xSpeed, -ySpeed,
+ * zRotation, -gyroAngle). <br>
+ * {@link #drivePolar(double, double, double)} is equivalent to RobotDrive's
+ * mecanumDrive_Polar(double, double, double)} if a deadband of 0 is used.
  */
+@SuppressWarnings("removal")
 public class MecanumDrive extends RobotDriveBase implements Sendable, AutoCloseable {
   private static int instances;
 
@@ -69,17 +67,54 @@
   private final SpeedController m_frontRightMotor;
   private final SpeedController m_rearRightMotor;
 
-  private double m_rightSideInvertMultiplier = -1.0;
   private boolean m_reported;
 
+  @SuppressWarnings("MemberName")
+  public static class WheelSpeeds {
+    public double frontLeft;
+    public double frontRight;
+    public double rearLeft;
+    public double rearRight;
+
+    /** Constructs a WheelSpeeds with zeroes for all four speeds. */
+    public WheelSpeeds() {}
+
+    /**
+     * Constructs a WheelSpeeds.
+     *
+     * @param frontLeft The front left speed.
+     * @param frontRight The front right speed.
+     * @param rearLeft The rear left speed.
+     * @param rearRight The rear right speed.
+     */
+    public WheelSpeeds(double frontLeft, double frontRight, double rearLeft, double rearRight) {
+      this.frontLeft = frontLeft;
+      this.frontRight = frontRight;
+      this.rearLeft = rearLeft;
+      this.rearRight = rearRight;
+    }
+  }
+
   /**
    * Construct a MecanumDrive.
    *
    * <p>If a motor needs to be inverted, do so before passing it in.
+   *
+   * @param frontLeftMotor The motor on the front-left corner.
+   * @param rearLeftMotor The motor on the rear-left corner.
+   * @param frontRightMotor The motor on the front-right corner.
+   * @param rearRightMotor The motor on the rear-right corner.
    */
-  public MecanumDrive(SpeedController frontLeftMotor, SpeedController rearLeftMotor,
-                      SpeedController frontRightMotor, SpeedController rearRightMotor) {
-    verify(frontLeftMotor, rearLeftMotor, frontRightMotor, rearRightMotor);
+  public MecanumDrive(
+      SpeedController frontLeftMotor,
+      SpeedController rearLeftMotor,
+      SpeedController frontRightMotor,
+      SpeedController rearRightMotor) {
+    requireNonNull(frontLeftMotor, "Front-left motor cannot be null");
+    requireNonNull(rearLeftMotor, "Rear-left motor cannot be null");
+    requireNonNull(frontRightMotor, "Front-right motor cannot be null");
+    requireNonNull(rearRightMotor, "Rear-right motor cannot be null");
+
     m_frontLeftMotor = frontLeftMotor;
     m_rearLeftMotor = rearLeftMotor;
     m_frontRightMotor = frontRightMotor;
@@ -98,45 +133,15 @@
   }
 
   /**
-   * Verifies that all motors are nonnull, throwing a NullPointerException if any of them are.
-   * The exception's error message will specify all null motors, e.g. {@code
-   * NullPointerException("frontLeftMotor, rearRightMotor")}, to give as much information as
-   * possible to the programmer.
-   *
-   * @throws NullPointerException if any of the given motors are null
-   */
-  @SuppressWarnings({"PMD.AvoidThrowingNullPointerException", "PMD.CyclomaticComplexity"})
-  private void verify(SpeedController frontLeft, SpeedController rearLeft,
-                      SpeedController frontRight, SpeedController rearRightMotor) {
-    if (frontLeft != null && rearLeft != null && frontRight != null && rearRightMotor != null) {
-      return;
-    }
-    StringJoiner joiner = new StringJoiner(", ");
-    if (frontLeft == null) {
-      joiner.add("frontLeftMotor");
-    }
-    if (rearLeft == null) {
-      joiner.add("rearLeftMotor");
-    }
-    if (frontRight == null) {
-      joiner.add("frontRightMotor");
-    }
-    if (rearRightMotor == null) {
-      joiner.add("rearRightMotor");
-    }
-    throw new NullPointerException(joiner.toString());
-  }
-
-  /**
    * Drive method for Mecanum platform.
    *
    * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
    * from its angle or rotation rate.
    *
-   * @param ySpeed    The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
-   * @param xSpeed    The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
+   *     positive.
    */
   @SuppressWarnings("ParameterName")
   public void driveCartesian(double ySpeed, double xSpeed, double zRotation) {
@@ -149,45 +154,30 @@
    * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
    * from its angle or rotation rate.
    *
-   * @param ySpeed    The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
-   * @param xSpeed    The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
-   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use
-   *                  this to implement field-oriented controls.
+   *     positive.
+   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use this
+   *     to implement field-oriented controls.
    */
   @SuppressWarnings("ParameterName")
   public void driveCartesian(double ySpeed, double xSpeed, double zRotation, double gyroAngle) {
     if (!m_reported) {
-      HAL.report(tResourceType.kResourceType_RobotDrive,
-                 tInstances.kRobotDrive2_MecanumCartesian, 4);
+      HAL.report(
+          tResourceType.kResourceType_RobotDrive, tInstances.kRobotDrive2_MecanumCartesian, 4);
       m_reported = true;
     }
 
-    ySpeed = MathUtil.clamp(ySpeed, -1.0, 1.0);
-    ySpeed = applyDeadband(ySpeed, m_deadband);
+    ySpeed = MathUtil.applyDeadband(ySpeed, m_deadband);
+    xSpeed = MathUtil.applyDeadband(xSpeed, m_deadband);
 
-    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
-    xSpeed = applyDeadband(xSpeed, m_deadband);
+    var speeds = driveCartesianIK(ySpeed, xSpeed, zRotation, gyroAngle);
 
-    // Compensate for gyro angle.
-    Vector2d input = new Vector2d(ySpeed, xSpeed);
-    input.rotate(-gyroAngle);
-
-    double[] wheelSpeeds = new double[4];
-    wheelSpeeds[MotorType.kFrontLeft.value] = input.x + input.y + zRotation;
-    wheelSpeeds[MotorType.kFrontRight.value] = -input.x + input.y - zRotation;
-    wheelSpeeds[MotorType.kRearLeft.value] = -input.x + input.y + zRotation;
-    wheelSpeeds[MotorType.kRearRight.value] = input.x + input.y - zRotation;
-
-    normalize(wheelSpeeds);
-
-    m_frontLeftMotor.set(wheelSpeeds[MotorType.kFrontLeft.value] * m_maxOutput);
-    m_frontRightMotor.set(wheelSpeeds[MotorType.kFrontRight.value] * m_maxOutput
-        * m_rightSideInvertMultiplier);
-    m_rearLeftMotor.set(wheelSpeeds[MotorType.kRearLeft.value] * m_maxOutput);
-    m_rearRightMotor.set(wheelSpeeds[MotorType.kRearRight.value] * m_maxOutput
-        * m_rightSideInvertMultiplier);
+    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);
 
     feed();
   }
@@ -199,9 +189,9 @@
    * drives (translation) is independent from its angle or rotation rate.
    *
    * @param magnitude The robot's speed at a given angle [-1.0..1.0]. Forward is positive.
-   * @param angle     The angle around the Z axis at which the robot drives in degrees [-180..180].
+   * @param angle The angle around the Z axis at which the robot drives in degrees [-180..180].
    * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
-   *                  positive.
+   *     positive.
    */
   @SuppressWarnings("ParameterName")
   public void drivePolar(double magnitude, double angle, double zRotation) {
@@ -210,26 +200,50 @@
       m_reported = true;
     }
 
-    driveCartesian(magnitude * Math.sin(angle * (Math.PI / 180.0)),
-                   magnitude * Math.cos(angle * (Math.PI / 180.0)), zRotation, 0.0);
+    driveCartesian(
+        magnitude * Math.cos(angle * (Math.PI / 180.0)),
+        magnitude * Math.sin(angle * (Math.PI / 180.0)),
+        zRotation,
+        0.0);
   }
 
   /**
-   * Gets if the power sent to the right side of the drivetrain is multiplied by -1.
+   * Cartesian inverse kinematics for Mecanum platform.
    *
-   * @return true if the right side is inverted
+   * <p>Angles are measured clockwise from the positive X axis. The robot's speed is independent
+   * from its angle or rotation rate.
+   *
+   * @param ySpeed The robot's speed along the Y axis [-1.0..1.0]. Right is positive.
+   * @param xSpeed The robot's speed along the X axis [-1.0..1.0]. Forward is positive.
+   * @param zRotation The robot's rotation rate around the Z axis [-1.0..1.0]. Clockwise is
+   *     positive.
+   * @param gyroAngle The current angle reading from the gyro in degrees around the Z axis. Use this
+   *     to implement field-oriented controls.
+   * @return Wheel speeds.
    */
-  public boolean isRightSideInverted() {
-    return m_rightSideInvertMultiplier == -1.0;
-  }
+  @SuppressWarnings("ParameterName")
+  public static WheelSpeeds driveCartesianIK(
+      double ySpeed, double xSpeed, double zRotation, double gyroAngle) {
+    ySpeed = MathUtil.clamp(ySpeed, -1.0, 1.0);
+    xSpeed = MathUtil.clamp(xSpeed, -1.0, 1.0);
 
-  /**
-   * Sets if the power sent to the right side of the drivetrain should be multiplied by -1.
-   *
-   * @param rightSideInverted true if right side power should be multiplied by -1
-   */
-  public void setRightSideInverted(boolean rightSideInverted) {
-    m_rightSideInvertMultiplier = rightSideInverted ? -1.0 : 1.0;
+    // Compensate for gyro angle.
+    Vector2d input = new Vector2d(ySpeed, xSpeed);
+    input.rotate(-gyroAngle);
+
+    double[] wheelSpeeds = new double[4];
+    wheelSpeeds[MotorType.kFrontLeft.value] = input.x + input.y + zRotation;
+    wheelSpeeds[MotorType.kFrontRight.value] = input.x - input.y - zRotation;
+    wheelSpeeds[MotorType.kRearLeft.value] = input.x - input.y + zRotation;
+    wheelSpeeds[MotorType.kRearRight.value] = input.x + input.y - zRotation;
+
+    normalize(wheelSpeeds);
+
+    return new WheelSpeeds(
+        wheelSpeeds[MotorType.kFrontLeft.value],
+        wheelSpeeds[MotorType.kFrontRight.value],
+        wheelSpeeds[MotorType.kRearLeft.value],
+        wheelSpeeds[MotorType.kRearRight.value]);
   }
 
   @Override
@@ -251,17 +265,16 @@
     builder.setSmartDashboardType("MecanumDrive");
     builder.setActuator(true);
     builder.setSafeState(this::stopMotor);
-    builder.addDoubleProperty("Front Left Motor Speed",
-        m_frontLeftMotor::get,
-        m_frontLeftMotor::set);
-    builder.addDoubleProperty("Front Right Motor Speed",
-        () -> m_frontRightMotor.get() * m_rightSideInvertMultiplier,
-        value -> m_frontRightMotor.set(value * m_rightSideInvertMultiplier));
-    builder.addDoubleProperty("Rear Left Motor Speed",
-        m_rearLeftMotor::get,
-        m_rearLeftMotor::set);
-    builder.addDoubleProperty("Rear Right Motor Speed",
-        () -> m_rearRightMotor.get() * m_rightSideInvertMultiplier,
-        value -> m_rearRightMotor.set(value * m_rightSideInvertMultiplier));
+    builder.addDoubleProperty(
+        "Front Left Motor Speed", m_frontLeftMotor::get, m_frontLeftMotor::set);
+    builder.addDoubleProperty(
+        "Front Right Motor Speed",
+        () -> m_frontRightMotor.get(),
+        value -> m_frontRightMotor.set(value));
+    builder.addDoubleProperty("Rear Left Motor Speed", m_rearLeftMotor::get, m_rearLeftMotor::set);
+    builder.addDoubleProperty(
+        "Rear Right Motor Speed",
+        () -> m_rearRightMotor.get(),
+        value -> m_rearRightMotor.set(value));
   }
 }
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 785a0c5..569f94c 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
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.drive;
 
+import edu.wpi.first.math.MathUtil;
 import edu.wpi.first.wpilibj.MotorSafety;
 
-/**
- * Common base class for drive platforms.
- */
+/** Common base class for drive platforms. */
 public abstract class RobotDriveBase extends MotorSafety {
   public static final double kDefaultDeadband = 0.02;
   public static final double kDefaultMaxOutput = 1.0;
@@ -19,14 +15,16 @@
   protected double m_deadband = kDefaultDeadband;
   protected double m_maxOutput = kDefaultMaxOutput;
 
-  /**
-   * The location of a motor on the robot for the purpose of driving.
-   */
+  /** The location of a motor on the robot for the purpose of driving. */
   public enum MotorType {
-    kFrontLeft(0), kFrontRight(1), kRearLeft(2), kRearRight(3), kLeft(0),
-    kRight(1), kBack(2);
+    kFrontLeft(0),
+    kFrontRight(1),
+    kRearLeft(2),
+    kRearRight(3),
+    kLeft(0),
+    kRight(1),
+    kBack(2);
 
-    @SuppressWarnings("MemberName")
     public final int value;
 
     MotorType(int value) {
@@ -34,9 +32,7 @@
     }
   }
 
-  /**
-   * RobotDriveBase constructor.
-   */
+  /** RobotDriveBase constructor. */
   public RobotDriveBase() {
     setSafetyEnabled(true);
   }
@@ -45,8 +41,8 @@
    * Sets the deadband applied to the drive inputs (e.g., joystick values).
    *
    * <p>The default value is {@value #kDefaultDeadband}. Inputs smaller than the deadband are set to
-   * 0.0 while inputs larger than the deadband are scaled from 0.0 to 1.0. See
-   * {@link #applyDeadband}.
+   * 0.0 while inputs larger than the deadband are scaled from 0.0 to 1.0. See {@link
+   * edu.wpi.first.math.MathUtil#applyDeadband}.
    *
    * @param deadband The deadband to set.
    */
@@ -85,25 +81,22 @@
    * Returns 0.0 if the given value is within the specified range around zero. The remaining range
    * between the deadband and 1.0 is scaled from 0.0 to 1.0.
    *
-   * @param value    value to clip
+   * @param value value to clip
    * @param deadband range around zero
+   * @return The value after the deadband is applied.
+   * @deprecated Use MathUtil.applyDeadband(double,double).
    */
-  protected double applyDeadband(double value, double deadband) {
-    if (Math.abs(value) > deadband) {
-      if (value > 0.0) {
-        return (value - deadband) / (1.0 - deadband);
-      } else {
-        return (value + deadband) / (1.0 - deadband);
-      }
-    } else {
-      return 0.0;
-    }
+  @Deprecated(since = "2021", forRemoval = true)
+  protected static double applyDeadband(double value, double deadband) {
+    return MathUtil.applyDeadband(value, deadband);
   }
 
   /**
    * Normalize all wheel speeds if the magnitude of any wheel is greater than 1.0.
+   *
+   * @param wheelSpeeds List of wheel speeds to normalize.
    */
-  protected void normalize(double[] wheelSpeeds) {
+  protected static void normalize(double[] wheelSpeeds) {
     double maxMagnitude = Math.abs(wheelSpeeds[0]);
     for (int i = 1; i < wheelSpeeds.length; i++) {
       double temp = Math.abs(wheelSpeeds[i]);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/Vector2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/Vector2d.java
index 2b1f839..728cc83 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/Vector2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/drive/Vector2d.java
@@ -1,23 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.drive;
 
-/**
- * This is a 2D vector struct that supports basic vector operations.
- */
-@SuppressWarnings("MemberName")
+/** This is a 2D vector struct that supports basic vector operations. */
 public class Vector2d {
+  @SuppressWarnings("MemberName")
   public double x;
+
+  @SuppressWarnings("MemberName")
   public double y;
 
   public Vector2d() {}
 
-  @SuppressWarnings("ParameterName")
   public Vector2d(double x, double y) {
     this.x = x;
     this.y = y;
@@ -42,6 +38,7 @@
    * Returns dot product of this vector with argument.
    *
    * @param vec Vector with which to perform dot product.
+   * @return Dot product of this vector with argument.
    */
   public double dot(Vector2d vec) {
     return x * vec.x + y * vec.y;
@@ -49,6 +46,8 @@
 
   /**
    * Returns magnitude of vector.
+   *
+   * @return Magnitude of vector.
    */
   public double magnitude() {
     return Math.sqrt(x * x + y * y);
@@ -58,6 +57,7 @@
    * Returns scalar projection of this vector onto argument.
    *
    * @param vec Vector onto which to project this vector.
+   * @return scalar projection of this vector onto argument.
    */
   public double scalarProject(Vector2d vec) {
     return dot(vec) / vec.magnitude();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/Filter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/Filter.java
deleted file mode 100644
index 257d36a..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/Filter.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.filters;
-
-import edu.wpi.first.wpilibj.PIDSource;
-import edu.wpi.first.wpilibj.PIDSourceType;
-
-/**
- * Superclass for filters.
- *
- * @deprecated This class is no longer used.
- */
-@Deprecated(since = "2020", forRemoval = true)
-public abstract class Filter implements PIDSource {
-  private final PIDSource m_source;
-
-  public Filter(PIDSource source) {
-    m_source = source;
-  }
-
-  @Override
-  public void setPIDSourceType(PIDSourceType pidSource) {
-    m_source.setPIDSourceType(pidSource);
-  }
-
-  @Override
-  public PIDSourceType getPIDSourceType() {
-    return m_source.getPIDSourceType();
-  }
-
-  @Override
-  public abstract double pidGet();
-
-  /**
-   * Returns the current filter estimate without also inserting new data as pidGet() would do.
-   *
-   * @return The current filter estimate
-   */
-  public abstract double get();
-
-  /**
-   * Reset the filter state.
-   */
-  public abstract void reset();
-
-  /**
-   * Calls PIDGet() of source.
-   *
-   * @return Current value of source
-   */
-  protected double pidGetSource() {
-    return m_source.pidGet();
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/LinearDigitalFilter.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/LinearDigitalFilter.java
deleted file mode 100644
index cbeb4aa..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/filters/LinearDigitalFilter.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.filters;
-
-import java.util.Arrays;
-
-import edu.wpi.first.hal.FRCNetComm.tResourceType;
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.PIDSource;
-import edu.wpi.first.wpiutil.CircularBuffer;
-
-/**
- * This class implements a linear, digital filter. All types of FIR and IIR filters are supported.
- * Static factory methods are provided to create commonly used types of filters.
- *
- * <p>Filters are of the form: y[n] = (b0*x[n] + b1*x[n-1] + ... + bP*x[n-P]) - (a0*y[n-1] +
- * a2*y[n-2] + ... + aQ*y[n-Q])
- *
- * <p>Where: y[n] is the output at time "n" x[n] is the input at time "n" y[n-1] is the output from
- * the LAST time step ("n-1") x[n-1] is the input from the LAST time step ("n-1") b0...bP are the
- * "feedforward" (FIR) gains a0...aQ are the "feedback" (IIR) gains IMPORTANT! Note the "-" sign in
- * front of the feedback term! This is a common convention in signal processing.
- *
- * <p>What can linear filters do? Basically, they can filter, or diminish, the effects of
- * undesirable input frequencies. High frequencies, or rapid changes, can be indicative of sensor
- * noise or be otherwise undesirable. A "low pass" filter smooths out the signal, reducing the
- * impact of these high frequency components.  Likewise, a "high pass" filter gets rid of
- * slow-moving signal components, letting you detect large changes more easily.
- *
- * <p>Example FRC applications of filters: - Getting rid of noise from an analog sensor input (note:
- * the roboRIO's FPGA can do this faster in hardware) - Smoothing out joystick input to prevent the
- * wheels from slipping or the robot from tipping - Smoothing motor commands so that unnecessary
- * strain isn't put on electrical or mechanical components - If you use clever gains, you can make a
- * PID controller out of this class!
- *
- * <p>For more on filters, I highly recommend the following articles: http://en.wikipedia
- * .org/wiki/Linear_filter http://en.wikipedia.org/wiki/Iir_filter http://en.wikipedia
- * .org/wiki/Fir_filter
- *
- * <p>Note 1: PIDGet() should be called by the user on a known, regular period. You can set up a
- * Notifier to do this (look at the WPILib PIDController class), or do it "inline" with code in a
- * periodic function.
- *
- * <p>Note 2: For ALL filters, gains are necessarily a function of frequency. If you make a filter
- * that works well for you at, say, 100Hz, you will most definitely need to adjust the gains if you
- * then want to run it at 200Hz! Combining this with Note 1 - the impetus is on YOU as a developer
- * to make sure PIDGet() gets called at the desired, constant frequency!
- *
- * @deprecated Use LinearFilter class instead.
- */
-@Deprecated
-public class LinearDigitalFilter extends Filter {
-  private static int instances;
-
-  private final CircularBuffer m_inputs;
-  private final CircularBuffer m_outputs;
-  private final double[] m_inputGains;
-  private final double[] m_outputGains;
-
-  /**
-   * Create a linear FIR or IIR filter.
-   *
-   * @param source  The PIDSource object that is used to get values
-   * @param ffGains The "feed forward" or FIR gains
-   * @param fbGains The "feed back" or IIR gains
-   */
-  public LinearDigitalFilter(PIDSource source, double[] ffGains,
-                             double[] fbGains) {
-    super(source);
-    m_inputs = new CircularBuffer(ffGains.length);
-    m_outputs = new CircularBuffer(fbGains.length);
-    m_inputGains = Arrays.copyOf(ffGains, ffGains.length);
-    m_outputGains = Arrays.copyOf(fbGains, fbGains.length);
-
-    instances++;
-    HAL.report(tResourceType.kResourceType_LinearFilter, instances);
-  }
-
-  /**
-   * Creates a one-pole IIR low-pass filter of the form: y[n] = (1-gain)*x[n] + gain*y[n-1] where
-   * gain = e^(-dt / T), T is the time constant in seconds.
-   *
-   * <p>This filter is stable for time constants greater than zero.
-   *
-   * @param source       The PIDSource object that is used to get values
-   * @param timeConstant The discrete-time time constant in seconds
-   * @param period       The period in seconds between samples taken by the user
-   */
-  public static LinearDigitalFilter singlePoleIIR(PIDSource source,
-                                                  double timeConstant,
-                                                  double period) {
-    double gain = Math.exp(-period / timeConstant);
-    double[] ffGains = {1.0 - gain};
-    double[] fbGains = {-gain};
-
-    return new LinearDigitalFilter(source, ffGains, fbGains);
-  }
-
-  /**
-   * Creates a first-order high-pass filter of the form: y[n] = gain*x[n] + (-gain)*x[n-1] +
-   * gain*y[n-1] where gain = e^(-dt / T), T is the time constant in seconds.
-   *
-   * <p>This filter is stable for time constants greater than zero.
-   *
-   * @param source       The PIDSource object that is used to get values
-   * @param timeConstant The discrete-time time constant in seconds
-   * @param period       The period in seconds between samples taken by the user
-   */
-  public static LinearDigitalFilter highPass(PIDSource source,
-                                             double timeConstant,
-                                             double period) {
-    double gain = Math.exp(-period / timeConstant);
-    double[] ffGains = {gain, -gain};
-    double[] fbGains = {-gain};
-
-    return new LinearDigitalFilter(source, ffGains, fbGains);
-  }
-
-  /**
-   * Creates a K-tap FIR moving average filter of the form: y[n] = 1/k * (x[k] + x[k-1] + ... +
-   * x[0]).
-   *
-   * <p>This filter is always stable.
-   *
-   * @param source The PIDSource object that is used to get values
-   * @param taps   The number of samples to average over. Higher = smoother but slower
-   * @throws IllegalArgumentException if number of taps is less than 1
-   */
-  public static LinearDigitalFilter movingAverage(PIDSource source, int taps) {
-    if (taps <= 0) {
-      throw new IllegalArgumentException("Number of taps was not at least 1");
-    }
-
-    double[] ffGains = new double[taps];
-    for (int i = 0; i < ffGains.length; i++) {
-      ffGains[i] = 1.0 / taps;
-    }
-
-    double[] fbGains = new double[0];
-
-    return new LinearDigitalFilter(source, ffGains, fbGains);
-  }
-
-  @Override
-  public double get() {
-    double retVal = 0.0;
-
-    // Calculate the new value
-    for (int i = 0; i < m_inputGains.length; i++) {
-      retVal += m_inputs.get(i) * m_inputGains[i];
-    }
-    for (int i = 0; i < m_outputGains.length; i++) {
-      retVal -= m_outputs.get(i) * m_outputGains[i];
-    }
-
-    return retVal;
-  }
-
-  @Override
-  public void reset() {
-    m_inputs.clear();
-    m_outputs.clear();
-  }
-
-  /**
-   * Calculates the next value of the filter.
-   *
-   * @return The filtered value at this step
-   */
-  @Override
-  public double pidGet() {
-    double retVal = 0.0;
-
-    // Rotate the inputs
-    m_inputs.addFirst(pidGetSource());
-
-    // Calculate the new value
-    for (int i = 0; i < m_inputGains.length; i++) {
-      retVal += m_inputs.get(i) * m_inputGains[i];
-    }
-    for (int i = 0; i < m_outputGains.length; i++) {
-      retVal -= m_outputs.get(i) * m_outputGains[i];
-    }
-
-    // Rotate the outputs
-    m_outputs.addFirst(retVal);
-
-    return retVal;
-  }
-}
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 81086d4..de2780f 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
@@ -1,25 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.interfaces;
 
-/**
- * Interface for 3-axis accelerometers.
- */
+/** Interface for 3-axis accelerometers. */
 public interface Accelerometer {
   enum Range {
-    k2G, k4G, k8G, k16G
+    k2G,
+    k4G,
+    k8G,
+    k16G
   }
 
   /**
    * Common interface for setting the measuring range of an accelerometer.
    *
    * @param range The maximum acceleration, positive or negative, that the accelerometer will
-   *              measure. Not all accelerometers support all ranges.
+   *     measure. Not all accelerometers support all ranges.
    */
   void setRange(Range range);
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Gyro.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Gyro.java
index 3bac20f..27377be 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Gyro.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Gyro.java
@@ -1,22 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.interfaces;
 
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
+import edu.wpi.first.math.geometry.Rotation2d;
 
-/**
- * Interface for yaw rate gyros.
- */
+/** Interface for yaw rate gyros. */
 public interface Gyro extends AutoCloseable {
   /**
    * Calibrate the gyro. It's important to make sure that the robot is not moving while the
-   * calibration is in progress, this is typically done when the robot is first turned on while
-   * it's sitting at rest before the match starts.
+   * calibration is in progress, this is typically done when the robot is first turned on while it's
+   * sitting at rest before the match starts.
    */
   void calibrate();
 
@@ -30,11 +25,11 @@
    * Return the heading of the robot in degrees.
    *
    * <p>The angle is continuous, that is it will continue from 360 to 361 degrees. This allows
-   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past
-   * from 360 to 0 on the second time around.
+   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past from
+   * 360 to 0 on the second time around.
    *
-   * <p>The angle is expected to increase as the gyro turns clockwise when looked
-   * at from the top. It needs to follow the NED axis convention.
+   * <p>The angle is expected to increase as the gyro turns clockwise when looked at from the top.
+   * It needs to follow the NED axis convention.
    *
    * <p>This heading is based on integration of the returned rate from the gyro.
    *
@@ -47,27 +42,26 @@
    *
    * <p>The rate is based on the most recent reading of the gyro analog value
    *
-   * <p>The rate is expected to be positive as the gyro turns clockwise when looked
-   * at from the top. It needs to follow the NED axis convention.
+   * <p>The rate is expected to be positive as the gyro turns clockwise when looked at from the top.
+   * It needs to follow the NED axis convention.
    *
    * @return the current rate in degrees per second
    */
   double getRate();
 
   /**
-   * Return the heading of the robot as a {@link edu.wpi.first.wpilibj.geometry.Rotation2d}.
+   * Return the heading of the robot as a {@link edu.wpi.first.math.geometry.Rotation2d}.
    *
    * <p>The angle is continuous, that is it will continue from 360 to 361 degrees. This allows
-   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past
-   * from 360 to 0 on the second time around.
+   * algorithms that wouldn't want to see a discontinuity in the gyro output as it sweeps past from
+   * 360 to 0 on the second time around.
    *
-   * <p>The angle is expected to increase as the gyro turns counterclockwise
-   * when looked at from the top. It needs to follow the NWU axis convention.
+   * <p>The angle is expected to increase as the gyro turns counterclockwise when looked at from the
+   * top. It needs to follow the NWU axis convention.
    *
    * <p>This heading is based on integration of the returned rate from the gyro.
    *
-   * @return the current heading of the robot as a
-   *         {@link edu.wpi.first.wpilibj.geometry.Rotation2d}.
+   * @return the current heading of the robot as a {@link edu.wpi.first.math.geometry.Rotation2d}.
    */
   default Rotation2d getRotation2d() {
     return Rotation2d.fromDegrees(-getAngle());
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Potentiometer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Potentiometer.java
deleted file mode 100644
index ea5c1e6..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/interfaces/Potentiometer.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.interfaces;
-
-import edu.wpi.first.wpilibj.PIDSource;
-
-/**
- * Interface for a Potentiometer.
- */
-public interface Potentiometer extends PIDSource {
-  double get();
-}
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 6570780..ff3e8ef 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
@@ -1,22 +1,18 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.livewindow;
 
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl;
 
 /**
- * The LiveWindow class is the public interface for putting sensors and actuators on the
- * LiveWindow.
+ * The LiveWindow class is the public interface for putting sensors and actuators on the LiveWindow.
  */
 public class LiveWindow {
   private static class Component {
@@ -36,6 +32,10 @@
   private static Runnable enabledListener;
   private static Runnable disabledListener;
 
+  static {
+    SendableRegistry.setLiveWindowBuilderFactory(() -> new SendableBuilderImpl());
+  }
+
   private static Component getOrAdd(Sendable sendable) {
     Component data = (Component) SendableRegistry.getData(sendable, dataHandle);
     if (data == null) {
@@ -62,12 +62,17 @@
   }
 
   /**
-   * Set the enabled state of LiveWindow. If it's being enabled, turn off the scheduler and remove
-   * all the commands from the queue and enable all the components registered for LiveWindow. If
-   * it's being disabled, stop all the registered components and reenable the scheduler. TODO: add
-   * code to disable PID loops when enabling LiveWindow. The commands should reenable the PID loops
-   * themselves when they get rescheduled. This prevents arms from starting to move around, etc.
-   * after a period of adjusting them in LiveWindow mode.
+   * Set the enabled state of LiveWindow.
+   *
+   * <p>If it's being enabled, turn off the scheduler and remove all the commands from the queue and
+   * enable all the components registered for LiveWindow. If it's being disabled, stop all the
+   * registered components and reenable the scheduler.
+   *
+   * <p>TODO: add code to disable PID loops when enabling LiveWindow. The commands should reenable
+   * the PID loops themselves when they get rescheduled. This prevents arms from starting to move
+   * around, etc. after a period of adjusting them in LiveWindow mode.
+   *
+   * @param enabled True to enable LiveWindow.
    */
   public static synchronized void setEnabled(boolean enabled) {
     if (liveWindowEnabled != enabled) {
@@ -81,9 +86,11 @@
         }
       } else {
         System.out.println("stopping live window mode.");
-        SendableRegistry.foreachLiveWindow(dataHandle, cbdata -> {
-          cbdata.builder.stopLiveWindowMode();
-        });
+        SendableRegistry.foreachLiveWindow(
+            dataHandle,
+            cbdata -> {
+              ((SendableBuilderImpl) cbdata.builder).stopLiveWindowMode();
+            });
         if (disabledListener != null) {
           disabledListener.run();
         }
@@ -112,76 +119,77 @@
     getOrAdd(sendable).m_telemetryEnabled = false;
   }
 
-  /**
-   * Disable ALL telemetry.
-   */
+  /** Disable ALL telemetry. */
   public static synchronized void disableAllTelemetry() {
     telemetryEnabled = false;
-    SendableRegistry.foreachLiveWindow(dataHandle, cbdata -> {
-      if (cbdata.data == null) {
-        cbdata.data = new Component();
-      }
-      ((Component) cbdata.data).m_telemetryEnabled = false;
-    });
+    SendableRegistry.foreachLiveWindow(
+        dataHandle,
+        cbdata -> {
+          if (cbdata.data == null) {
+            cbdata.data = new Component();
+          }
+          ((Component) cbdata.data).m_telemetryEnabled = false;
+        });
   }
 
   /**
    * Tell all the sensors to update (send) their values.
    *
-   * <p>Actuators are handled through callbacks on their value changing from the
-   * SmartDashboard widgets.
+   * <p>Actuators are handled through callbacks on their value changing from the SmartDashboard
+   * widgets.
    */
-  @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
   public static synchronized void updateValues() {
     // Only do this if either LiveWindow mode or telemetry is enabled.
     if (!liveWindowEnabled && !telemetryEnabled) {
       return;
     }
 
-    SendableRegistry.foreachLiveWindow(dataHandle, cbdata -> {
-      if (cbdata.sendable == null || cbdata.parent != null) {
-        return;
-      }
+    SendableRegistry.foreachLiveWindow(
+        dataHandle,
+        cbdata -> {
+          if (cbdata.sendable == null || cbdata.parent != null) {
+            return;
+          }
 
-      if (cbdata.data == null) {
-        cbdata.data = new Component();
-      }
+          if (cbdata.data == null) {
+            cbdata.data = new Component();
+          }
 
-      Component component = (Component) cbdata.data;
+          Component component = (Component) cbdata.data;
 
-      if (!liveWindowEnabled && !component.m_telemetryEnabled) {
-        return;
-      }
+          if (!liveWindowEnabled && !component.m_telemetryEnabled) {
+            return;
+          }
 
-      if (component.m_firstTime) {
-        // By holding off creating the NetworkTable entries, it allows the
-        // components to be redefined. This allows default sensor and actuator
-        // values to be created that are replaced with the custom names from
-        // users calling setName.
-        if (cbdata.name.isEmpty()) {
-          return;
-        }
-        NetworkTable ssTable = liveWindowTable.getSubTable(cbdata.subsystem);
-        NetworkTable table;
-        // Treat name==subsystem as top level of subsystem
-        if (cbdata.name.equals(cbdata.subsystem)) {
-          table = ssTable;
-        } else {
-          table = ssTable.getSubTable(cbdata.name);
-        }
-        table.getEntry(".name").setString(cbdata.name);
-        cbdata.builder.setTable(table);
-        cbdata.sendable.initSendable(cbdata.builder);
-        ssTable.getEntry(".type").setString("LW Subsystem");
+          if (component.m_firstTime) {
+            // By holding off creating the NetworkTable entries, it allows the
+            // components to be redefined. This allows default sensor and actuator
+            // values to be created that are replaced with the custom names from
+            // users calling setName.
+            if (cbdata.name.isEmpty()) {
+              return;
+            }
+            NetworkTable ssTable = liveWindowTable.getSubTable(cbdata.subsystem);
+            NetworkTable table;
+            // Treat name==subsystem as top level of subsystem
+            if (cbdata.name.equals(cbdata.subsystem)) {
+              table = ssTable;
+            } else {
+              table = ssTable.getSubTable(cbdata.name);
+            }
+            table.getEntry(".name").setString(cbdata.name);
+            ((SendableBuilderImpl) cbdata.builder).setTable(table);
+            cbdata.sendable.initSendable(cbdata.builder);
+            ssTable.getEntry(".type").setString("LW Subsystem");
 
-        component.m_firstTime = false;
-      }
+            component.m_firstTime = false;
+          }
 
-      if (startLiveWindow) {
-        cbdata.builder.startLiveWindowMode();
-      }
-      cbdata.builder.updateTable();
-    });
+          if (startLiveWindow) {
+            ((SendableBuilderImpl) cbdata.builder).startLiveWindowMode();
+          }
+          cbdata.builder.update();
+        });
 
     startLiveWindow = false;
   }
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
new file mode 100644
index 0000000..c003b56
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/DMC60.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;
+
+/**
+ * Digilent DMC 60 Motor Controller.
+ *
+ * <p>Note that the DMC 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 DMC 60 User Manual available from
+ * Digilent.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class DMC60 extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the DMC60 is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public DMC60(final int channel) {
+    super("DMC60", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_DigilentDMC60, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..32e6417
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Jaguar.java
@@ -0,0 +1,44 @@
+// 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;
+
+/**
+ * Texas Instruments / Vex Robotics Jaguar Motor Controller as a PWM device.
+ *
+ * <p>Note that the Jaguar 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 Jaguar User Manual available from Vex.
+ *
+ * <ul>
+ *   <li>2.310ms = full "forward"
+ *   <li>1.550ms = the "high end" of the deadband range
+ *   <li>1.507ms = center of the deadband range (off)
+ *   <li>1.454ms = the "low end" of the deadband range
+ *   <li>0.697ms = full "reverse"
+ * </ul>
+ */
+public class Jaguar extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the Jaguar is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public Jaguar(final int channel) {
+    super("Jaguar", channel);
+
+    m_pwm.setBounds(2.31, 1.55, 1.507, 1.454, 0.697);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_Jaguar, getChannel() + 1);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java
new file mode 100644
index 0000000..ac82afa
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorController.java
@@ -0,0 +1,71 @@
+// 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.wpilibj.RobotController;
+import edu.wpi.first.wpilibj.SpeedController;
+
+/** Interface for motor controlling devices. */
+@SuppressWarnings("removal")
+public interface MotorController extends SpeedController {
+  /**
+   * Common interface for setting the speed of a motor controller.
+   *
+   * @param speed The speed to set. Value should be between -1.0 and 1.0.
+   */
+  @Override
+  void set(double speed);
+
+  /**
+   * Sets the voltage output of the MotorController. Compensates for the current bus voltage to
+   * ensure that the desired voltage is output even if the battery voltage is below 12V - highly
+   * useful when the voltage outputs are "meaningful" (e.g. they come from a feedforward
+   * calculation).
+   *
+   * <p>NOTE: This function *must* be called regularly in order for voltage compensation to work
+   * properly - unlike the ordinary set function, it is not "set it and forget it."
+   *
+   * @param outputVolts The voltage to output.
+   */
+  @Override
+  default void setVoltage(double outputVolts) {
+    set(outputVolts / RobotController.getBatteryVoltage());
+  }
+
+  /**
+   * Common interface for getting the current set speed of a motor controller.
+   *
+   * @return The current set speed. Value is between -1.0 and 1.0.
+   */
+  @Override
+  double get();
+
+  /**
+   * Common interface for inverting direction of a motor controller.
+   *
+   * @param isInverted The state of inversion true is inverted.
+   */
+  @Override
+  void setInverted(boolean isInverted);
+
+  /**
+   * Common interface for returning if a motor controller is in the inverted state or not.
+   *
+   * @return isInverted The state of the inversion true is inverted.
+   */
+  @Override
+  boolean getInverted();
+
+  /** Disable the motor controller. */
+  @Override
+  void disable();
+
+  /**
+   * Stops motor movement. Motor can be moved again by calling set without having to re-enable the
+   * motor.
+   */
+  @Override
+  void stopMotor();
+}
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
new file mode 100644
index 0000000..714aac5
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroup.java
@@ -0,0 +1,96 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.motorcontrol;
+
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.util.Arrays;
+
+/** Allows multiple {@link MotorController} objects to be linked together. */
+public class MotorControllerGroup implements MotorController, Sendable, AutoCloseable {
+  private boolean m_isInverted;
+  private final MotorController[] m_motorControllers;
+  private static int instances;
+
+  /**
+   * Create a new MotorControllerGroup with the provided MotorControllers.
+   *
+   * @param motorController The first MotorController to add
+   * @param motorControllers The MotorControllers to add
+   */
+  public MotorControllerGroup(
+      MotorController motorController, MotorController... motorControllers) {
+    m_motorControllers = new MotorController[motorControllers.length + 1];
+    m_motorControllers[0] = motorController;
+    System.arraycopy(motorControllers, 0, m_motorControllers, 1, motorControllers.length);
+    init();
+  }
+
+  public MotorControllerGroup(MotorController[] motorControllers) {
+    m_motorControllers = Arrays.copyOf(motorControllers, motorControllers.length);
+    init();
+  }
+
+  private void init() {
+    for (MotorController controller : m_motorControllers) {
+      SendableRegistry.addChild(this, controller);
+    }
+    instances++;
+    SendableRegistry.addLW(this, "MotorControllerGroup", instances);
+  }
+
+  @Override
+  public void close() {
+    SendableRegistry.remove(this);
+  }
+
+  @Override
+  public void set(double speed) {
+    for (MotorController motorController : m_motorControllers) {
+      motorController.set(m_isInverted ? -speed : speed);
+    }
+  }
+
+  @Override
+  public double get() {
+    if (m_motorControllers.length > 0) {
+      return m_motorControllers[0].get() * (m_isInverted ? -1 : 1);
+    }
+    return 0.0;
+  }
+
+  @Override
+  public void setInverted(boolean isInverted) {
+    m_isInverted = isInverted;
+  }
+
+  @Override
+  public boolean getInverted() {
+    return m_isInverted;
+  }
+
+  @Override
+  public void disable() {
+    for (MotorController motorController : m_motorControllers) {
+      motorController.disable();
+    }
+  }
+
+  @Override
+  public void stopMotor() {
+    for (MotorController motorController : m_motorControllers) {
+      motorController.stopMotor();
+    }
+  }
+
+  @Override
+  public void initSendable(SendableBuilder builder) {
+    builder.setSmartDashboardType("Motor Controller");
+    builder.setActuator(true);
+    builder.setSafeState(this::stopMotor);
+    builder.addDoubleProperty("Value", this::get, this::set);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/NidecBrushless.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
similarity index 66%
rename from wpilibj/src/main/java/edu/wpi/first/wpilibj/NidecBrushless.java
rename to wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
index 9d1011b..13daf68 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/NidecBrushless.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/NidecBrushless.java
@@ -1,22 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
+package edu.wpi.first.wpilibj.motorcontrol;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import edu.wpi.first.wpilibj.DigitalOutput;
+import edu.wpi.first.wpilibj.MotorSafety;
+import edu.wpi.first.wpilibj.PWM;
 
-/**
- * Nidec Brushless Motor.
- */
-public class NidecBrushless extends MotorSafety implements SpeedController, Sendable,
-    AutoCloseable {
+/** Nidec Brushless Motor. */
+public class NidecBrushless extends MotorSafety
+    implements MotorController, Sendable, AutoCloseable {
   private boolean m_isInverted;
   private final DigitalOutput m_dio;
   private final PWM m_pwm;
@@ -26,10 +25,10 @@
   /**
    * Constructor.
    *
-   * @param pwmChannel The PWM channel that the Nidec Brushless controller is attached to.
-   *                   0-9 are on-board, 10-19 are on the MXP port
-   * @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
+   * @param pwmChannel The PWM channel that the Nidec Brushless controller is attached to. 0-9 are
+   *     on-board, 10-19 are on the MXP port
+   * @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
    */
   public NidecBrushless(final int pwmChannel, final int dioChannel) {
     setSafetyEnabled(false);
@@ -95,16 +94,6 @@
   }
 
   /**
-   * Write out the PID value as seen in the PIDOutput base object.
-   *
-   * @param output Write out the PWM value as was found in the PIDController
-   */
-  @Override
-  public void pidWrite(double output) {
-    set(output);
-  }
-
-  /**
    * Stop the motor. This is called by the MotorSafety object when it has a timeout for this PWM and
    * needs to stop it from running. Calling set() will re-enable the motor.
    */
@@ -119,10 +108,7 @@
     return "Nidec " + getChannel();
   }
 
-  /**
-   * Disable the motor.  The enable() function must be called to re-enable
-   * the motor.
-   */
+  /** Disable the motor. The enable() function must be called to re-enable the motor. */
   @Override
   public void disable() {
     m_disabled = true;
@@ -131,8 +117,8 @@
   }
 
   /**
-   * Re-enable the motor after disable() has been called.  The set()
-   * function must be called to set a new motor speed.
+   * Re-enable the motor after disable() has been called. The set() function must be called to set a
+   * new motor speed.
    */
   public void enable() {
     m_disabled = 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
new file mode 100644
index 0000000..d33fb01
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMMotorController.java
@@ -0,0 +1,114 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.motorcontrol;
+
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import edu.wpi.first.wpilibj.MotorSafety;
+import edu.wpi.first.wpilibj.PWM;
+
+/** Common base class for all PWM Motor Controllers. */
+public abstract class PWMMotorController extends MotorSafety
+    implements MotorController, Sendable, AutoCloseable {
+  private boolean m_isInverted;
+  protected PWM m_pwm;
+
+  /**
+   * Constructor.
+   *
+   * @param name Name to use for SendableRegistry
+   * @param channel The PWM channel that the controller is attached to. 0-9 are on-board, 10-19 are
+   *     on the MXP port
+   */
+  protected PWMMotorController(final String name, final int channel) {
+    m_pwm = new PWM(channel, false);
+    SendableRegistry.addLW(this, name, channel);
+  }
+
+  /** Free the resource associated with the PWM channel and set the value to 0. */
+  @Override
+  public void close() {
+    SendableRegistry.remove(this);
+    m_pwm.close();
+  }
+
+  /**
+   * Set the PWM value.
+   *
+   * <p>The PWM value is set using a range of -1.0 to 1.0, appropriately scaling the value for the
+   * FPGA.
+   *
+   * @param speed The speed value between -1.0 and 1.0 to set.
+   */
+  @Override
+  public void set(double speed) {
+    m_pwm.setSpeed(m_isInverted ? -speed : speed);
+    feed();
+  }
+
+  /**
+   * Get the recently set value of the PWM. This value is affected by the inversion property. If you
+   * want the value that is sent directly to the MotorController, use {@link
+   * edu.wpi.first.wpilibj.PWM#getSpeed()} instead.
+   *
+   * @return The most recently set value for the PWM between -1.0 and 1.0.
+   */
+  @Override
+  public double get() {
+    return m_pwm.getSpeed() * (m_isInverted ? -1.0 : 1.0);
+  }
+
+  @Override
+  public void setInverted(boolean isInverted) {
+    m_isInverted = isInverted;
+  }
+
+  @Override
+  public boolean getInverted() {
+    return m_isInverted;
+  }
+
+  @Override
+  public void disable() {
+    m_pwm.setDisabled();
+  }
+
+  @Override
+  public void stopMotor() {
+    disable();
+  }
+
+  @Override
+  public String getDescription() {
+    return "PWM " + getChannel();
+  }
+
+  /**
+   * Gets the backing PWM handle.
+   *
+   * @return The pwm handle.
+   */
+  public int getPwmHandle() {
+    return m_pwm.getHandle();
+  }
+
+  /**
+   * Gets the PWM channel number.
+   *
+   * @return The channel number.
+   */
+  public int getChannel() {
+    return m_pwm.getChannel();
+  }
+
+  @Override
+  public void initSendable(SendableBuilder builder) {
+    builder.setSmartDashboardType("Motor Controller");
+    builder.setActuator(true);
+    builder.setSafeState(this::disable);
+    builder.addDoubleProperty("Value", this::get, this::set);
+  }
+}
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
new file mode 100644
index 0000000..e90f60d
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMSparkMax.java
@@ -0,0 +1,44 @@
+// 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 MAX Motor Controller with PWM control.
+ *
+ * <p>Note that the SPARK MAX 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 MAX 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 PWMSparkMax 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
+   */
+  public PWMSparkMax(final int channel) {
+    super("PWMSparkMax", channel);
+
+    m_pwm.setBounds(2.003, 1.55, 1.50, 1.46, 0.999);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_RevSparkMaxPWM, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..d521745
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonFX.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;
+
+/**
+ * Cross the Road Electronics (CTRE) Talon FX Motor Controller with PWM control.
+ *
+ * <p>Note that the TalonFX 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 TalonFX User Manual available from
+ * CTRE.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class PWMTalonFX extends PWMMotorController {
+  /**
+   * Constructor for a TalonFX connected via PWM.
+   *
+   * @param channel The PWM channel that the Talon FX is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public PWMTalonFX(final int channel) {
+    super("PWMTalonFX", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_TalonFX, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..81f1834
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMTalonSRX.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;
+
+/**
+ * Cross the Road Electronics (CTRE) Talon SRX Motor Controller with PWM control.
+ *
+ * <p>Note that the TalonSRX 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 TalonSRX User Manual available from
+ * CTRE.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class PWMTalonSRX extends PWMMotorController {
+  /**
+   * Constructor for a TalonSRX connected via PWM.
+   *
+   * @param channel The PWM channel that the Talon SRX is attached to. 0-9 are on-board, 10-19 are
+   *     on the MXP port
+   */
+  public PWMTalonSRX(final int channel) {
+    super("PWMTalonSRX", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_PWMTalonSRX, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..9f7a885
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVenom.java
@@ -0,0 +1,44 @@
+// 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;
+
+/**
+ * Playing with Fusion Venom Smart Motor with PWM control.
+ *
+ * <p>Note that the Venom 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.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class PWMVenom extends PWMMotorController {
+  /**
+   * Constructor for a Venom connected via PWM.
+   *
+   * @param channel The PWM channel that the Venom is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public PWMVenom(final int channel) {
+    super("PWMVenom", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_FusionVenom, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..9880464
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/PWMVictorSPX.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;
+
+/**
+ * Cross the Road Electronics (CTRE) Victor SPX Motor Controller with PWM control.
+ *
+ * <p>Note that the Victor SPX 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 Victor SPX User Manual available from
+ * CTRE.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class PWMVictorSPX extends PWMMotorController {
+  /**
+   * Constructor for a Victor SPX connected via PWM.
+   *
+   * @param channel The PWM channel that the PWMVictorSPX is attached to. 0-9 are on-board, 10-19
+   *     are on the MXP port
+   */
+  public PWMVictorSPX(final int channel) {
+    super("PWMVictorSPX", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_PWMVictorSPX, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..3876dfc
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/SD540.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;
+
+/**
+ * Mindsensors SD540 Motor Controller.
+ *
+ * <p>Note that the SD540 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 SD540 User Manual available from
+ * Mindsensors.
+ *
+ * <ul>
+ *   <li>2.05ms = full "forward"
+ *   <li>1.55ms = the "high end" of the deadband range
+ *   <li>1.50ms = center of the deadband range (off)
+ *   <li>1.44ms = the "low end" of the deadband range
+ *   <li>0.94ms = full "reverse"
+ * </ul>
+ */
+public class SD540 extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the SD540 is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public SD540(final int channel) {
+    super("SD540", channel);
+
+    m_pwm.setBounds(2.05, 1.55, 1.50, 1.44, 0.94);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_MindsensorsSD540, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..1b99228
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Spark.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 Motor Controller.
+ *
+ * <p>Note that the SPARK 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 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 Spark extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the SPARK is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public Spark(final int channel) {
+    super("Spark", channel);
+
+    m_pwm.setBounds(2.003, 1.55, 1.50, 1.46, 0.999);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_RevSPARK, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..576ba6a
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Talon.java
@@ -0,0 +1,44 @@
+// 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;
+
+/**
+ * Cross the Road Electronics (CTRE) Talon and Talon SR Motor Controller.
+ *
+ * <p>Note that the Talon 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 Talon User Manual available from CTRE.
+ *
+ * <ul>
+ *   <li>2.037ms = full "forward"
+ *   <li>1.539ms = the "high end" of the deadband range
+ *   <li>1.513ms = center of the deadband range (off)
+ *   <li>1.487ms = the "low end" of the deadband range
+ *   <li>0.989ms = full "reverse"
+ * </ul>
+ */
+public class Talon extends PWMMotorController {
+  /**
+   * Constructor for a Talon (original or Talon SR).
+   *
+   * @param channel The PWM channel that the Talon is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public Talon(final int channel) {
+    super("Talon", channel);
+
+    m_pwm.setBounds(2.037, 1.539, 1.513, 1.487, 0.989);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_Talon, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..fac6c4f
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/Victor.java
@@ -0,0 +1,47 @@
+// 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;
+
+/**
+ * VEX Robotics Victor 888 Motor Controller The Vex Robotics Victor 884 Motor Controller can also be
+ * used with this class but may need to be calibrated per the Victor 884 user manual.
+ *
+ * <p>Note that the Victor uses the following bounds for PWM values. These values were determined
+ * empirically and optimized for the Victor 888. These values should work reasonably well for Victor
+ * 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
+ *
+ * <ul>
+ *   <li>2.027ms = full "forward"
+ *   <li>1.525ms = the "high end" of the deadband range
+ *   <li>1.507ms = center of the deadband range (off)
+ *   <li>1.490ms = the "low end" of the deadband range
+ *   <li>1.026ms = full "reverse"
+ * </ul>
+ */
+public class Victor extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the Victor is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public Victor(final int channel) {
+    super("Victor", channel);
+
+    m_pwm.setBounds(2.027, 1.525, 1.507, 1.49, 1.026);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k2X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_Victor, getChannel() + 1);
+  }
+}
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
new file mode 100644
index 0000000..10dba67
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/motorcontrol/VictorSP.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;
+
+/**
+ * VEX Robotics Victor SP Motor Controller.
+ *
+ * <p>Note that the VictorSP 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 VictorSP User Manual available from
+ * CTRE.
+ *
+ * <ul>
+ *   <li>2.004ms = full "forward"
+ *   <li>1.520ms = the "high end" of the deadband range
+ *   <li>1.500ms = center of the deadband range (off)
+ *   <li>1.480ms = the "low end" of the deadband range
+ *   <li>0.997ms = full "reverse"
+ * </ul>
+ */
+public class VictorSP extends PWMMotorController {
+  /**
+   * Constructor.
+   *
+   * @param channel The PWM channel that the VictorSP is attached to. 0-9 are on-board, 10-19 are on
+   *     the MXP port
+   */
+  public VictorSP(final int channel) {
+    super("VictorSP", channel);
+
+    m_pwm.setBounds(2.004, 1.52, 1.50, 1.48, 0.997);
+    m_pwm.setPeriodMultiplier(PWM.PeriodMultiplier.k1X);
+    m_pwm.setSpeed(0.0);
+    m_pwm.setZeroLatch();
+
+    HAL.report(tResourceType.kResourceType_VictorSP, getChannel() + 1);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInLayouts.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInLayouts.java
index 27c3e8c..fb642ae 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInLayouts.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInLayouts.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -18,8 +15,9 @@
 public enum BuiltInLayouts implements LayoutType {
   /**
    * Groups components in a vertical list. New widgets added to the layout will be placed at the
-   * bottom of the list.
-   * <br>Custom properties:
+   * bottom of the list. <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Label position</td><td>String</td><td>"BOTTOM"</td>
@@ -30,8 +28,9 @@
   kList("List Layout"),
 
   /**
-   * Groups components in an <i>n</i> x <i>m</i> grid. Grid layouts default to 3x3.
-   * <br>Custom properties:
+   * Groups components in an <i>n</i> x <i>m</i> grid. Grid layouts default to 3x3. <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Number of columns</td><td>Number</td><td>3</td><td>Must be in the range [1,15]</td>
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java
index fa98a0e..82f2745 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -13,6 +10,7 @@
  * The types of the widgets bundled with Shuffleboard.
  *
  * <p>For example, setting a number to be displayed with a slider:
+ *
  * <pre>{@code
  * NetworkTableEntry example = Shuffleboard.getTab("My Tab")
  *   .add("My Number", 0)
@@ -26,23 +24,30 @@
  */
 public enum BuiltInWidgets implements WidgetType {
   /**
-   * Displays a value with a simple text field.
-   * <br>Supported types:
+   * Displays a value with a simple text field. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>String</li>
-   * <li>Number</li>
-   * <li>Boolean</li>
+   *   <li>String
+   *   <li>Number
+   *   <li>Boolean
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kTextView("Text View"),
   /**
-   * Displays a number with a controllable slider.
-   * <br>Supported types:
+   * Displays a number with a controllable slider. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Number</li>
+   *   <li>Number
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Min</td><td>Number</td><td>-1.0</td><td>The minimum value of the slider</td></tr>
@@ -53,12 +58,16 @@
    */
   kNumberSlider("Number Slider"),
   /**
-   * Displays a number with a view-only bar.
-   * <br>Supported types:
+   * Displays a number with a view-only bar. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Number</li>
+   *   <li>Number
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Min</td><td>Number</td><td>-1.0</td><td>The minimum value of the bar</td></tr>
@@ -69,11 +78,16 @@
   kNumberBar("Number Bar"),
   /**
    * Displays a number with a view-only dial. Displayed values are rounded to the nearest integer.
-   * <br>Supported types:
+   * <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Number</li>
+   *   <li>Number
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Min</td><td>Number</td><td>0</td><td>The minimum value of the dial</td></tr>
@@ -86,13 +100,17 @@
   /**
    * Displays a number with a graph. <strong>NOTE:</strong> graphs can be taxing on the computer
    * running the dashboard. Keep the number of visible data points to a minimum. Making the widget
-   * smaller also helps with performance, but may cause the graph to become difficult to read.
-   * <br>Supported types:
+   * smaller also helps with performance, but may cause the graph to become difficult to read. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Number</li>
-   * <li>Number array</li>
+   *   <li>Number
+   *   <li>Number array
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Visible time</td><td>Number</td><td>30</td>
@@ -101,12 +119,16 @@
    */
   kGraph("Graph"),
   /**
-   * Displays a boolean value as a large colored box.
-   * <br>Supported types:
+   * Displays a boolean value as a large colored box. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Boolean</li>
+   *   <li>Boolean
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Color when true</td><td>Color</td><td>"green"</td>
@@ -118,31 +140,41 @@
    */
   kBooleanBox("Boolean Box"),
   /**
-   * Displays a boolean with a large interactive toggle button.
-   * <br>Supported types:
+   * Displays a boolean with a large interactive toggle button. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Boolean</li>
+   *   <li>Boolean
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kToggleButton("Toggle Button"),
   /**
-   * Displays a boolean with a fixed-size toggle switch.
-   * <br>Supported types:
+   * Displays a boolean with a fixed-size toggle switch. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Boolean</li>
+   *   <li>Boolean
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kToggleSwitch("Toggle Switch"),
   /**
-   * Displays an analog input or a raw number with a number bar.
-   * <br>Supported types:
+   * Displays an analog input or a raw number with a number bar. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>Number</li>
-   * <li>{@link edu.wpi.first.wpilibj.AnalogInput}</li>
+   *   <li>Number
+   *   <li>{@link edu.wpi.first.wpilibj.AnalogInput}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Min</td><td>Number</td><td>0</td><td>The minimum value of the bar</td></tr>
@@ -156,121 +188,151 @@
    */
   kVoltageView("Voltage View"),
   /**
-   * Displays a {@link edu.wpi.first.wpilibj.PowerDistributionPanel PowerDistributionPanel}.
-   * <br>Supported types:
+   * Displays a {@link edu.wpi.first.wpilibj.PowerDistribution PowerDistribution}. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.PowerDistributionPanel}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.PowerDistribution}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Show voltage and current values</td><td>Boolean</td><td>true</td>
    * <td>Whether or not to display the voltage and current draw</td></tr>
    * </table>
    */
-  kPowerDistributionPanel("PDP"),
+  kPowerDistribution("PDP"),
   /**
-   * Displays a {@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser SendableChooser} with
-   * a dropdown combo box with a list of options.
-   * <br>Supported types:
+   * Displays a {@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser SendableChooser} with a
+   * dropdown combo box with a list of options. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kComboBoxChooser("ComboBox Chooser"),
   /**
-   * Displays a {@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser SendableChooser} with
-   * a toggle button for each available option.
-   * <br>Supported types:
+   * Displays a {@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser SendableChooser} with a
+   * toggle button for each available option. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.smartdashboard.SendableChooser}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kSplitButtonChooser("Split Button Chooser"),
   /**
    * Displays an {@link edu.wpi.first.wpilibj.Encoder} displaying its speed, total traveled
-   * distance, and its distance per tick.
-   * <br>Supported types:
+   * distance, and its distance per tick. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.Encoder}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.Encoder}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kEncoder("Encoder"),
   /**
-   * Displays a {@link edu.wpi.first.wpilibj.SpeedController SpeedController}. The speed controller
-   * will be controllable from the dashboard when test mode is enabled, but will otherwise be
-   * view-only.
-   * <br>Supported types:
+   * Displays a {@link edu.wpi.first.wpilibj.motorcontrol.MotorController MotorController}. The
+   * motor controller will be controllable from the dashboard when test mode is enabled, but will
+   * otherwise be view-only. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.PWMSpeedController}</li>
-   * <li>{@link edu.wpi.first.wpilibj.DMC60}</li>
-   * <li>{@link edu.wpi.first.wpilibj.Jaguar}</li>
-   * <li>{@link edu.wpi.first.wpilibj.PWMSparkMax}</li>
-   * <li>{@link edu.wpi.first.wpilibj.PWMTalonFX}</li>
-   * <li>{@link edu.wpi.first.wpilibj.PWMTalonSRX}</li>
-   * <li>{@link edu.wpi.first.wpilibj.PWMVenom}</li>
-   * <li>{@link edu.wpi.first.wpilibj.PWMVictorSPX}</li>
-   * <li>{@link edu.wpi.first.wpilibj.SD540}</li>
-   * <li>{@link edu.wpi.first.wpilibj.Spark}</li>
-   * <li>{@link edu.wpi.first.wpilibj.Talon}</li>
-   * <li>{@link edu.wpi.first.wpilibj.Victor}</li>
-   * <li>{@link edu.wpi.first.wpilibj.VictorSP}</li>
-   * <li>{@link edu.wpi.first.wpilibj.SpeedControllerGroup}</li>
-   * <li>Any custom subclass of {@code SpeedController}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMMotorController}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.DMC60}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.Jaguar}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMTalonFX}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMTalonSRX}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMVenom}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.SD540}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.Spark}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.Talon}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.Victor}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.VictorSP}
+   *   <li>{@link edu.wpi.first.wpilibj.motorcontrol.MotorControllerGroup}
+   *   <li>Any custom subclass of {@code MotorController}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Orientation</td><td>String</td><td>"HORIZONTAL"</td>
    * <td>One of {@code ["HORIZONTAL", "VERTICAL"]}</td></tr>
    * </table>
    */
-  kSpeedController("Speed Controller"),
+  kMotorController("Motor Controller"),
   /**
    * Displays a command with a toggle button. Pressing the button will start the command, and the
-   * button will automatically release when the command completes.
-   * <br>Supported types:
+   * button will automatically release when the command completes. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.command.Command}</li>
-   * <li>{@link edu.wpi.first.wpilibj.command.CommandGroup}</li>
-   * <li>Any custom subclass of {@code Command} or {@code CommandGroup}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.command.Command}
+   *   <li>{@link edu.wpi.first.wpilibj.command.CommandGroup}
+   *   <li>Any custom subclass of {@code Command} or {@code CommandGroup}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kCommand("Command"),
   /**
    * Displays a PID command with a checkbox and an editor for the PIDF constants. Selecting the
    * checkbox will start the command, and the checkbox will automatically deselect when the command
-   * completes.
-   * <br>Supported types:
+   * completes. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.command.PIDCommand}</li>
-   * <li>Any custom subclass of {@code PIDCommand}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.command.PIDCommand}
+   *   <li>Any custom subclass of {@code PIDCommand}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kPIDCommand("PID Command"),
   /**
    * Displays a PID controller with an editor for the PIDF constants and a toggle switch for
-   * enabling and disabling the controller.
-   * <br>Supported types:
+   * enabling and disabling the controller. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.PIDController}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.PIDController}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kPIDController("PID Controller"),
   /**
    * Displays an accelerometer with a number bar displaying the magnitude of the acceleration and
-   * text displaying the exact value.
-   * <br>Supported types:
+   * text displaying the exact value. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.AnalogAccelerometer}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.AnalogAccelerometer}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Min</td><td>Number</td><td>-1</td>
@@ -287,14 +349,18 @@
    */
   kAccelerometer("Accelerometer"),
   /**
-   * Displays a 3-axis accelerometer with a number bar for each axis' acceleration.
-   * <br>Supported types:
+   * Displays a 3-axis accelerometer with a number bar for each axis' acceleration. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.ADXL345_I2C}</li>
-   * <li>{@link edu.wpi.first.wpilibj.ADXL345_SPI}</li>
-   * <li>{@link edu.wpi.first.wpilibj.ADXL362}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.ADXL345_I2C}
+   *   <li>{@link edu.wpi.first.wpilibj.ADXL345_SPI}
+   *   <li>{@link edu.wpi.first.wpilibj.ADXL362}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Range</td><td>{@link Range}</td><td>k16G</td><td>The accelerometer range</td></tr>
@@ -308,14 +374,18 @@
    */
   k3AxisAccelerometer("3-Axis Accelerometer"),
   /**
-   * Displays a gyro with a dial from 0 to 360 degrees.
-   * <br>Supported types:
+   * Displays a gyro with a dial from 0 to 360 degrees. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.ADXRS450_Gyro}</li>
-   * <li>{@link edu.wpi.first.wpilibj.AnalogGyro}</li>
-   * <li>Any custom subclass of {@code GyroBase} (such as a MXP gyro)</li>
+   *   <li>{@link edu.wpi.first.wpilibj.ADXRS450_Gyro}
+   *   <li>{@link edu.wpi.first.wpilibj.AnalogGyro}
+   *   <li>Any custom subclass of {@code GyroBase} (such as a MXP gyro)
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Major tick spacing</td><td>Number</td><td>45</td><td>Degrees</td></tr>
@@ -326,23 +396,30 @@
    */
   kGyro("Gyro"),
   /**
-   * Displays a relay with toggle buttons for each supported mode (off, on, forward, reverse).
-   * <br>Supported types:
+   * Displays a relay with toggle buttons for each supported mode (off, on, forward, reverse). <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.Relay}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.Relay}
    * </ul>
-   * <br>This widget has no custom properties.
+   *
+   * <br>
+   * This widget has no custom properties.
    */
   kRelay("Relay"),
   /**
    * Displays a differential drive with a widget that displays the speed of each side of the
    * drivebase and a vector for the direction and rotation of the drivebase. The widget will be
-   * controllable if the robot is in test mode.
-   * <br>Supported types:
+   * controllable if the robot is in test mode. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.drive.DifferentialDrive}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.drive.DifferentialDrive}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Number of wheels</td><td>Number</td><td>4</td><td>Must be a positive even integer
@@ -354,13 +431,17 @@
   kDifferentialDrive("Differential Drivebase"),
   /**
    * Displays a mecanum drive with a widget that displays the speed of each wheel, and vectors for
-   * the direction and rotation of the drivebase. The widget will be controllable if the robot is
-   * in test mode.
-   * <br>Supported types:
+   * the direction and rotation of the drivebase. The widget will be controllable if the robot is in
+   * test mode. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.first.wpilibj.drive.MecanumDrive}</li>
+   *   <li>{@link edu.wpi.first.wpilibj.drive.MecanumDrive}
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Show velocity vectors</td><td>Boolean</td><td>true</td></tr>
@@ -368,12 +449,16 @@
    */
   kMecanumDrive("Mecanum Drivebase"),
   /**
-   * Displays a camera stream.
-   * <br>Supported types:
+   * Displays a camera stream. <br>
+   * Supported types:
+   *
    * <ul>
-   * <li>{@link edu.wpi.cscore.VideoSource} (as long as it is streaming on an MJPEG server)</li>
+   *   <li>{@link edu.wpi.first.cscore.VideoSource} (as long as it is streaming on an MJPEG server)
    * </ul>
-   * <br>Custom properties:
+   *
+   * <br>
+   * Custom properties:
+   *
    * <table>
    * <tr><th>Name</th><th>Type</th><th>Default Value</th><th>Notes</th></tr>
    * <tr><td>Show crosshair</td><td>Boolean</td><td>true</td>
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java
index d56fde8..cef9c07 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ComplexWidget.java
@@ -1,19 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
+import edu.wpi.first.util.sendable.Sendable;
 import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl;
 
 /**
- * A Shuffleboard widget that handles a {@link Sendable} object such as a speed controller or
+ * A Shuffleboard widget that handles a {@link Sendable} object such as a motor controller or
  * sensor.
  */
 public final class ComplexWidget extends ShuffleboardWidget<ComplexWidget> {
@@ -34,7 +30,7 @@
       m_sendable.initSendable(m_builder);
       m_builder.startListeners();
     }
-    m_builder.updateTable();
+    m_builder.update();
   }
 
   /**
@@ -58,5 +54,4 @@
       m_builder.stopLiveWindowMode();
     }
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
index 26649d0..58ac678 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
@@ -1,12 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -20,13 +20,7 @@
 import java.util.function.DoubleSupplier;
 import java.util.function.Supplier;
 
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * A helper class for Shuffleboard containers to handle common child operations.
- */
+/** A helper class for Shuffleboard containers to handle common child operations. */
 final class ContainerHelper {
   private final ShuffleboardContainer m_container;
   private final Set<String> m_usedTitles = new HashSet<>();
@@ -126,9 +120,8 @@
     checkTitle(title);
   }
 
-  private <T> SuppliedValueWidget<T> addSupplied(String title,
-                                                 Supplier<T> supplier,
-                                                 BiConsumer<NetworkTableEntry, T> setter) {
+  private <T> SuppliedValueWidget<T> addSupplied(
+      String title, Supplier<T> supplier, BiConsumer<NetworkTableEntry, T> setter) {
     SuppliedValueWidget<T> widget = new SuppliedValueWidget<>(m_container, title, supplier, setter);
     m_components.add(widget);
     return widget;
@@ -147,5 +140,4 @@
     }
     m_usedTitles.add(title);
   }
-
 }
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 08f9712..4d64efe 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
@@ -1,16 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
 /**
- * The importance of an event marker in Shuffleboard.  The exact meaning of each importance level is
- * up for interpretation on a team-to-team basis, but users should follow the general guidelines
- * of the various importance levels.  The examples given are for reference and may be ignored or
+ * The importance of an event marker in Shuffleboard. The exact meaning of each importance level is
+ * up for interpretation on a team-to-team basis, but users should follow the general guidelines of
+ * the various importance levels. The examples given are for reference and may be ignored or
  * considered to be more or less important from team to team.
  */
 public enum EventImportance {
@@ -18,14 +15,10 @@
   // Modifying the enum or enum strings requires a corresponding change to the C++ enum
   // and the enum in Shuffleboard
 
-  /**
-   * A trivial event such as a change in command state.
-   */
+  /** A trivial event such as a change in command state. */
   kTrivial("TRIVIAL"),
 
-  /**
-   * A low importance event such as acquisition of a game piece.
-   */
+  /** A low importance event such as acquisition of a game piece. */
   kLow("LOW"),
 
   /**
@@ -33,14 +26,10 @@
    */
   kNormal("NORMAL"),
 
-  /**
-   * A high-importance event such as scoring a game piece.
-   */
+  /** A high-importance event such as scoring a game piece. */
   kHigh("HIGH"),
 
-  /**
-   * A critically important event such as a brownout, component failure, or software deadlock.
-   */
+  /** A critically important event such as a brownout, component failure, or software deadlock. */
   kCritical("CRITICAL");
 
   private final String m_simpleName;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/LayoutType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/LayoutType.java
index 2d7b565..82ffcbe 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/LayoutType.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/LayoutType.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -16,6 +13,8 @@
 public interface LayoutType {
   /**
    * Gets the string type of the layout as defined by that layout in Shuffleboard.
+   *
+   * @return The string type of the layout.
    */
   String getLayoutName();
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
index 9f04274..a7be3c9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -12,9 +9,7 @@
 import edu.wpi.first.networktables.NetworkTableInstance;
 import edu.wpi.first.wpilibj.DriverStation;
 
-/**
- * Controls Shuffleboard recordings via NetworkTables.
- */
+/** Controls Shuffleboard recordings via NetworkTables. */
 final class RecordingController {
   private static final String kRecordingTableName = "/Shuffleboard/.recording/";
   private static final String kRecordingControlKey = kRecordingTableName + "RecordData";
@@ -49,21 +44,20 @@
 
   public void addEventMarker(String name, String description, EventImportance importance) {
     if (name == null || name.isEmpty()) {
-      DriverStation.reportError(
-          "Shuffleboard event name was not specified", true);
+      DriverStation.reportError("Shuffleboard event name was not specified", true);
       return;
     }
 
     if (importance == null) {
-      DriverStation.reportError(
-          "Shuffleboard event importance was null", true);
+      DriverStation.reportError("Shuffleboard event importance was null", true);
       return;
     }
 
     String eventDescription = description == null ? "" : description;
 
-    m_eventsTable.getSubTable(name)
+    m_eventsTable
+        .getSubTable(name)
         .getEntry("Info")
-        .setStringArray(new String[]{eventDescription, importance.getSimpleName()});
+        .setStringArray(new String[] {eventDescription, importance.getSimpleName()});
   }
 }
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 bb7738f..ee72303 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
@@ -1,23 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
+import edu.wpi.first.cscore.VideoSource;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import java.util.Map;
 import java.util.WeakHashMap;
 
-import edu.wpi.cscore.VideoSource;
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
-
-/**
- * A wrapper to make video sources sendable and usable from Shuffleboard.
- */
+/** A wrapper to make video sources sendable and usable from Shuffleboard. */
 public final class SendableCameraWrapper implements Sendable, AutoCloseable {
   private static final String kProtocol = "camera_server://";
 
@@ -26,8 +20,8 @@
   private final String m_uri;
 
   /**
-   * Creates a new sendable wrapper. Private constructor to avoid direct instantiation with
-   * multiple wrappers floating around for the same camera.
+   * Creates a new sendable wrapper. Private constructor to avoid direct instantiation with multiple
+   * wrappers floating around for the same camera.
    *
    * @param source the source to wrap
    */
@@ -43,12 +37,12 @@
   }
 
   /**
-   * Gets a sendable wrapper object for the given video source, creating the wrapper if one does
-   * not already exist for the source.
+   * Gets a sendable wrapper object for the given video source, creating the wrapper if one does not
+   * already exist for the source.
    *
    * @param source the video source to wrap
-   * @return a sendable wrapper object for the video source, usable in Shuffleboard via
-   * {@link ShuffleboardTab#add(Sendable)} and {@link ShuffleboardLayout#add(Sendable)}
+   * @return a sendable wrapper object for the video source, usable in Shuffleboard via {@link
+   *     ShuffleboardTab#add(Sendable)} and {@link ShuffleboardLayout#add(Sendable)}
    */
   public static SendableCameraWrapper wrap(VideoSource source) {
     return m_wrappers.computeIfAbsent(source, SendableCameraWrapper::new);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
index c88256a..1bea28e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -16,7 +13,8 @@
  * example, programmers can specify a specific {@code boolean} value to be displayed with a toggle
  * button instead of the default colored box, or set custom colors for that box.
  *
- * <p>For example, displaying a boolean entry with a toggle button:</p>
+ * <p>For example, displaying a boolean entry with a toggle button:
+ *
  * <pre>{@code
  * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .add("My Boolean", false)
@@ -24,7 +22,8 @@
  *   .getEntry();
  * }</pre>
  *
- * <p>Changing the colors of the boolean box:</p>
+ * <p>Changing the colors of the boolean box:
+ *
  * <pre>{@code
  * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .add("My Boolean", false)
@@ -34,7 +33,8 @@
  * }</pre>
  *
  * <p>Specifying a parent layout. Note that the layout type must <i>always</i> be specified, even if
- * the layout has already been generated by a previously defined entry.</p>
+ * the layout has already been generated by a previously defined entry.
+ *
  * <pre>{@code
  * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .getLayout("List", "Example List")
@@ -43,12 +43,10 @@
  *   .getEntry();
  * }</pre>
  *
- * <p>Teams are encouraged to set up shuffleboard layouts at the start of the robot program.</p>
+ * <p>Teams are encouraged to set up shuffleboard layouts at the start of the robot program.
  */
 public final class Shuffleboard {
-  /**
-   * The name of the base NetworkTable into which all Shuffleboard data will be added.
-   */
+  /** The name of the base NetworkTable into which all Shuffleboard data will be added. */
   public static final String kBaseTableName = "/Shuffleboard";
 
   private static final ShuffleboardRoot root =
@@ -80,8 +78,8 @@
   }
 
   /**
-   * Selects the tab in the dashboard with the given index in the range [0..n-1], where <i>n</i>
-   * is the number of tabs in the dashboard at the time this method is called.
+   * Selects the tab in the dashboard with the given index in the range [0..n-1], where <i>n</i> is
+   * the number of tabs in the dashboard at the time this method is called.
    *
    * @param index the index of the tab to select
    */
@@ -99,10 +97,10 @@
   }
 
   /**
-   * Enables user control of widgets containing actuators: speed controllers, relays, etc. This
-   * should only be used when the robot is in test mode. IterativeRobotBase and SampleRobot are
-   * both configured to call this method when entering test mode; most users should not need to use
-   * this method directly.
+   * Enables user control of widgets containing actuators: motor controllers, relays, etc. This
+   * should only be used when the robot is in test mode. IterativeRobotBase and SampleRobot are both
+   * configured to call this method when entering test mode; most users should not need to use this
+   * method directly.
    */
   public static void enableActuatorWidgets() {
     root.enableActuatorWidgets();
@@ -111,8 +109,8 @@
   /**
    * Disables user control of widgets containing actuators. For safety reasons, actuators should
    * only be controlled while in test mode. IterativeRobotBase and SampleRobot are both configured
-   * to call this method when exiting in test mode; most users should not need to use
-   * this method directly.
+   * to call this method when exiting in test mode; most users should not need to use this method
+   * directly.
    */
   public static void disableActuatorWidgets() {
     update(); // Need to update to make sure the sendable builders are initialized
@@ -143,11 +141,10 @@
    *
    * <p>To avoid recording files overwriting each other, make sure to use unique recording file
    * names. File name formats accept templates for inserting the date and time when the recording
-   * started with the {@code ${date}} and {@code ${time}} templates, respectively. For example,
-   * the default format is {@code "recording-${time}"} and recording files created with it will have
+   * started with the {@code ${date}} and {@code ${time}} templates, respectively. For example, the
+   * default format is {@code "recording-${time}"} and recording files created with it will have
    * names like {@code "recording-2018.01.15.sbr"}. Users are <strong>strongly</strong> recommended
    * to use the {@code ${time}} template to ensure unique file names.
-   * </p>
    *
    * @param format the format for the
    * @see #clearRecordingFileNameFormat()
@@ -170,12 +167,12 @@
    * state to as critical as a total power loss or component failure. If Shuffleboard is recording,
    * the event will also be recorded.
    *
-   * <p>If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then
-   * no event will be sent and an error will be printed to the driver station.
+   * <p>If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then no
+   * event will be sent and an error will be printed to the driver station.
    *
-   * @param name        the name of the event
+   * @param name the name of the event
    * @param description a description of the event
-   * @param importance  the importance of the event
+   * @param importance the importance of the event
    */
   public static void addEventMarker(String name, String description, EventImportance importance) {
     recordingController.addEventMarker(name, description, importance);
@@ -186,11 +183,11 @@
    * state to as critical as a total power loss or component failure. If Shuffleboard is recording,
    * the event will also be recorded.
    *
-   * <p>If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then
-   * no event will be sent and an error will be printed to the driver station.
+   * <p>If {@code name} is {@code null} or empty, or {@code importance} is {@code null}, then no
+   * event will be sent and an error will be printed to the driver station.
    *
-   * @param name        the name of the event
-   * @param importance  the importance of the event
+   * @param name the name of the event
+   * @param importance the importance of the event
    */
   public static void addEventMarker(String name, EventImportance importance) {
     addEventMarker(name, "", importance);
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 5d90396..fd7d541 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
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import java.util.Map;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.networktables.NetworkTable;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import java.util.Map;
 
 /**
  * A generic component in Shuffleboard.
@@ -58,9 +54,7 @@
     return m_title;
   }
 
-  /**
-   * Gets the custom properties for this component. May be null.
-   */
+  /** Gets the custom properties for this component. May be null. */
   final Map<String, Object> getProperties() {
     return m_properties;
   }
@@ -72,6 +66,7 @@
    * @param properties the properties for this component
    * @return this component
    */
+  @SuppressWarnings("unchecked")
   public final C withProperties(Map<String, Object> properties) {
     m_properties = properties;
     m_metadataDirty = true;
@@ -87,9 +82,10 @@
    * component there before the one with the specific position is sent.
    *
    * @param columnIndex the column in the tab to place this component
-   * @param rowIndex    the row in the tab to place this component
+   * @param rowIndex the row in the tab to place this component
    * @return this component
    */
+  @SuppressWarnings("unchecked")
   public final C withPosition(int columnIndex, int rowIndex) {
     m_column = columnIndex;
     m_row = rowIndex;
@@ -101,10 +97,11 @@
    * Sets the size of this component in the tab. This has no effect if this component is inside a
    * layout.
    *
-   * @param width  how many columns wide the component should be
+   * @param width how many columns wide the component should be
    * @param height how many rows high the component should be
    * @return this component
    */
+  @SuppressWarnings("unchecked")
   public final C withSize(int width, int height) {
     m_width = width;
     m_height = height;
@@ -127,14 +124,14 @@
     if (m_width <= 0 || m_height <= 0) {
       metaTable.getEntry("Size").delete();
     } else {
-      metaTable.getEntry("Size").setDoubleArray(new double[]{m_width, m_height});
+      metaTable.getEntry("Size").setDoubleArray(new double[] {m_width, m_height});
     }
 
     // Tile position
     if (m_column < 0 || m_row < 0) {
       metaTable.getEntry("Position").delete();
     } else {
-      metaTable.getEntry("Position").setDoubleArray(new double[]{m_column, m_row});
+      metaTable.getEntry("Position").setDoubleArray(new double[] {m_column, m_row});
     }
 
     // Custom properties
@@ -144,5 +141,4 @@
     }
     m_metadataDirty = false;
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
index 9a89404..8b4e009 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
@@ -1,39 +1,34 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
+import edu.wpi.first.cscore.VideoSource;
+import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
 import java.util.function.Supplier;
 
-import edu.wpi.cscore.VideoSource;
-import edu.wpi.first.wpilibj.Sendable;
-
-/**
- * Common interface for objects that can contain shuffleboard components.
- */
+/** Common interface for objects that can contain shuffleboard components. */
 public interface ShuffleboardContainer extends ShuffleboardValue {
-
   /**
    * Gets the components that are direct children of this container.
+   *
+   * @return The components that are direct children of this container.
    */
   List<ShuffleboardComponent<?>> getComponents();
 
   /**
    * Gets the layout with the given type and title, creating it if it does not already exist at the
-   * time this method is called. Note: this method should only be used to use a layout type that
-   * is not already built into Shuffleboard. To use a layout built into Shuffleboard, use
-   * {@link #getLayout(String, LayoutType)} and the layouts in {@link BuiltInLayouts}.
+   * time this method is called. Note: this method should only be used to use a layout type that is
+   * not already built into Shuffleboard. To use a layout built into Shuffleboard, use {@link
+   * #getLayout(String, LayoutType)} and the layouts in {@link BuiltInLayouts}.
    *
    * @param title the title of the layout
-   * @param type  the type of the layout, eg "List Layout" or "Grid Layout"
+   * @param type the type of the layout, eg "List Layout" or "Grid Layout"
    * @return the layout
    * @see #getLayout(String, LayoutType)
    */
@@ -43,7 +38,7 @@
    * Gets the layout with the given type and title, creating it if it does not already exist at the
    * time this method is called.
    *
-   * @param title      the title of the layout
+   * @param title the title of the layout
    * @param layoutType the type of the layout, eg "List" or "Grid"
    * @return the layout
    */
@@ -72,11 +67,11 @@
   /**
    * Adds a widget to this container to display the given sendable.
    *
-   * @param title    the title of the widget
+   * @param title the title of the widget
    * @param sendable the sendable to display
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   ComplexWidget add(String title, Sendable sendable) throws IllegalArgumentException;
 
@@ -87,7 +82,7 @@
    * @param video the video stream to display
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   default ComplexWidget add(String title, VideoSource video) throws IllegalArgumentException {
     return add(title, SendableCameraWrapper.wrap(video));
@@ -99,7 +94,7 @@
    * @param sendable the sendable to display
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title, or if the sendable's name has not been specified
+   *     title, or if the sendable's name has not been specified
    */
   ComplexWidget add(Sendable sendable);
 
@@ -109,7 +104,7 @@
    * @param video the video to display
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the same
-   *                                  title as the video source
+   *     title as the video source
    */
   default ComplexWidget add(VideoSource video) {
     return add(SendableCameraWrapper.wrap(video));
@@ -118,11 +113,11 @@
   /**
    * Adds a widget to this container to display the given data.
    *
-   * @param title        the title of the widget
+   * @param title the title of the widget
    * @param defaultValue the default value of the widget
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    * @see #addPersistent(String, Object) add(String title, Object defaultValue)
    */
   SimpleWidget add(String title, Object defaultValue) throws IllegalArgumentException;
@@ -136,7 +131,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<String> addString(String title, Supplier<String> valueSupplier)
       throws IllegalArgumentException;
@@ -150,7 +145,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<Double> addNumber(String title, DoubleSupplier valueSupplier)
       throws IllegalArgumentException;
@@ -164,7 +159,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<Boolean> addBoolean(String title, BooleanSupplier valueSupplier)
       throws IllegalArgumentException;
@@ -178,7 +173,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<String[]> addStringArray(String title, Supplier<String[]> valueSupplier)
       throws IllegalArgumentException;
@@ -192,7 +187,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<double[]> addDoubleArray(String title, Supplier<double[]> valueSupplier)
       throws IllegalArgumentException;
@@ -206,7 +201,7 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<boolean[]> addBooleanArray(String title, Supplier<boolean[]> valueSupplier)
       throws IllegalArgumentException;
@@ -220,21 +215,21 @@
    * @param valueSupplier the supplier for values
    * @return a widget to display data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    */
   SuppliedValueWidget<byte[]> addRaw(String title, Supplier<byte[]> valueSupplier)
       throws IllegalArgumentException;
 
   /**
-   * Adds a widget to this container to display a simple piece of data. Unlike
-   * {@link #add(String, Object)}, the value in the widget will be saved on the robot and will be
-   * used when the robot program next starts rather than {@code defaultValue}.
+   * Adds a widget to this container to display a simple piece of data. Unlike {@link #add(String,
+   * Object)}, the value in the widget will be saved on the robot and will be used when the robot
+   * program next starts rather than {@code defaultValue}.
    *
-   * @param title        the title of the widget
+   * @param title the title of the widget
    * @param defaultValue the default value of the widget
    * @return a widget to display the sendable data
    * @throws IllegalArgumentException if a widget already exists in this container with the given
-   *                                  title
+   *     title
    * @see #add(String, Object) add(String title, Object defaultValue)
    */
   default SimpleWidget addPersistent(String title, Object defaultValue)
@@ -243,5 +238,4 @@
     widget.getEntry().setPersistent();
     return widget;
   }
-
 }
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 8af1d78..a864433 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
@@ -1,23 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.Consumer;
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
 
 final class ShuffleboardInstance implements ShuffleboardRoot {
   private final Map<String, ShuffleboardTab> m_tabs = new LinkedHashMap<>();
@@ -53,10 +49,8 @@
   @Override
   public void update() {
     if (m_tabsChanged) {
-      String[] tabTitles = m_tabs.values()
-          .stream()
-          .map(ShuffleboardTab::getTitle)
-          .toArray(String[]::new);
+      String[] tabTitles =
+          m_tabs.values().stream().map(ShuffleboardTab::getTitle).toArray(String[]::new);
       m_rootMetaTable.getEntry("Tabs").forceSetStringArray(tabTitles);
       m_tabsChanged = false;
     }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
index b6ed776..726f9cc 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
@@ -1,26 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
 import java.util.function.Supplier;
 
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.wpilibj.Sendable;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-
-/**
- * A layout in a Shuffleboard tab. Layouts can contain widgets and other layouts.
- */
+/** A layout in a Shuffleboard tab. Layouts can contain widgets and other layouts. */
 public class ShuffleboardLayout extends ShuffleboardComponent<ShuffleboardLayout>
     implements ShuffleboardContainer {
   private final ContainerHelper m_helper = new ContainerHelper(this);
@@ -60,50 +54,43 @@
   }
 
   @Override
-  public SuppliedValueWidget<String> addString(String title,
-                                               Supplier<String> valueSupplier)
+  public SuppliedValueWidget<String> addString(String title, Supplier<String> valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addString(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<Double> addNumber(String title,
-                                               DoubleSupplier valueSupplier)
+  public SuppliedValueWidget<Double> addNumber(String title, DoubleSupplier valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addNumber(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<Boolean> addBoolean(String title,
-                                                 BooleanSupplier valueSupplier)
+  public SuppliedValueWidget<Boolean> addBoolean(String title, BooleanSupplier valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addBoolean(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<String[]> addStringArray(String title,
-                                                      Supplier<String[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<String[]> addStringArray(
+      String title, Supplier<String[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addStringArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<double[]> addDoubleArray(String title,
-                                                      Supplier<double[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<double[]> addDoubleArray(
+      String title, Supplier<double[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addDoubleArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<boolean[]> addBooleanArray(String title,
-                                                        Supplier<boolean[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<boolean[]> addBooleanArray(
+      String title, Supplier<boolean[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addBooleanArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<byte[]> addRaw(String title,
-                                            Supplier<byte[]> valueSupplier)
+  public SuppliedValueWidget<byte[]> addRaw(String title, Supplier<byte[]> valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addRaw(title, valueSupplier);
   }
@@ -117,5 +104,4 @@
       component.buildInto(table, metaTable.getSubTable(component.getTitle()));
     }
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java
index a32af69..fa6cd08 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardRoot.java
@@ -1,20 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
 /**
- * The root of the data placed in Shuffleboard. It contains the tabs, but no data is placed
- * directly in the root.
+ * The root of the data placed in Shuffleboard. It contains the tabs, but no data is placed directly
+ * in the root.
  *
  * <p>This class is package-private to minimize API surface area.
  */
 interface ShuffleboardRoot {
-
   /**
    * Gets the tab with the given title, creating it if it does not already exist.
    *
@@ -23,24 +19,18 @@
    */
   ShuffleboardTab getTab(String title);
 
-  /**
-   * Updates all tabs.
-   */
+  /** Updates all tabs. */
   void update();
 
-  /**
-   * Enables all widgets in Shuffleboard that offer user control over actuators.
-   */
+  /** Enables all widgets in Shuffleboard that offer user control over actuators. */
   void enableActuatorWidgets();
 
-  /**
-   * Disables all widgets in Shuffleboard that offer user control over actuators.
-   */
+  /** Disables all widgets in Shuffleboard that offer user control over actuators. */
   void disableActuatorWidgets();
 
   /**
-   * Selects the tab in the dashboard with the given index in the range [0..n-1], where <i>n</i>
-   * is the number of tabs in the dashboard at the time this method is called.
+   * Selects the tab in the dashboard with the given index in the range [0..n-1], where <i>n</i> is
+   * the number of tabs in the dashboard at the time this method is called.
    *
    * @param index the index of the tab to select
    */
@@ -52,5 +42,4 @@
    * @param title the title of the tab to select
    */
   void selectTab(String title);
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
index 5d575df..172d86f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
@@ -1,25 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
 import java.util.function.Supplier;
 
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.wpilibj.Sendable;
-
 /**
- * Represents a tab in the Shuffleboard dashboard. Widgets can be added to the tab with
- * {@link #add(Sendable)}, {@link #add(String, Object)}, and {@link #add(String, Sendable)}. Widgets
- * can also be added to layouts with {@link #getLayout(String, String)}; layouts can be nested
+ * Represents a tab in the Shuffleboard dashboard. Widgets can be added to the tab with {@link
+ * #add(Sendable)}, {@link #add(String, Object)}, and {@link #add(String, Sendable)}. Widgets can
+ * also be added to layouts with {@link #getLayout(String, String)}; layouts can be nested
  * arbitrarily deep (note that too many levels may make deeper components unusable).
  */
 public final class ShuffleboardTab implements ShuffleboardContainer {
@@ -72,50 +68,43 @@
   }
 
   @Override
-  public SuppliedValueWidget<String> addString(String title,
-                                               Supplier<String> valueSupplier)
+  public SuppliedValueWidget<String> addString(String title, Supplier<String> valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addString(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<Double> addNumber(String title,
-                                               DoubleSupplier valueSupplier)
+  public SuppliedValueWidget<Double> addNumber(String title, DoubleSupplier valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addNumber(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<Boolean> addBoolean(String title,
-                                                 BooleanSupplier valueSupplier)
+  public SuppliedValueWidget<Boolean> addBoolean(String title, BooleanSupplier valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addBoolean(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<String[]> addStringArray(String title,
-                                                      Supplier<String[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<String[]> addStringArray(
+      String title, Supplier<String[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addStringArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<double[]> addDoubleArray(String title,
-                                                      Supplier<double[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<double[]> addDoubleArray(
+      String title, Supplier<double[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addDoubleArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<boolean[]> addBooleanArray(String title,
-                                                        Supplier<boolean[]> valueSupplier)
-      throws IllegalArgumentException {
+  public SuppliedValueWidget<boolean[]> addBooleanArray(
+      String title, Supplier<boolean[]> valueSupplier) throws IllegalArgumentException {
     return m_helper.addBooleanArray(title, valueSupplier);
   }
 
   @Override
-  public SuppliedValueWidget<byte[]> addRaw(String title,
-                                            Supplier<byte[]> valueSupplier)
+  public SuppliedValueWidget<byte[]> addRaw(String title, Supplier<byte[]> valueSupplier)
       throws IllegalArgumentException {
     return m_helper.addRaw(title, valueSupplier);
   }
@@ -128,5 +117,4 @@
       component.buildInto(tabTable, metaTable.getSubTable(component.getTitle()));
     }
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java
index 8bfa6c3..88f4389 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardValue.java
@@ -1,18 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
 import edu.wpi.first.networktables.NetworkTable;
 
 interface ShuffleboardValue {
-
   /**
    * Gets the title of this Shuffleboard value.
+   *
+   * @return The title of this Shuffleboard value.
    */
   String getTitle();
 
@@ -20,12 +18,10 @@
    * Builds the entries for this value.
    *
    * @param parentTable the table containing all the data for the parent. Values that require a
-   *                    complex entry or table structure should call {@code
-   *                    parentTable.getSubTable(getTitle())} to get the table to put data into.
-   *                    Values that only use a single entry should call {@code
-   *                    parentTable.getEntry(getTitle())} to get that entry.
-   * @param metaTable   the table containing all the metadata for this value and its sub-values
+   *     complex entry or table structure should call {@code parentTable.getSubTable(getTitle())} to
+   *     get the table to put data into. Values that only use a single entry should call {@code
+   *     parentTable.getEntry(getTitle())} to get that entry.
+   * @param metaTable the table containing all the metadata for this value and its sub-values
    */
   void buildInto(NetworkTable parentTable, NetworkTable metaTable);
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java
index c929f4a..39ce036 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardWidget.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -16,7 +13,6 @@
  */
 abstract class ShuffleboardWidget<W extends ShuffleboardWidget<W>>
     extends ShuffleboardComponent<W> {
-
   ShuffleboardWidget(ShuffleboardContainer parent, String title) {
     super(parent, title);
   }
@@ -36,15 +32,15 @@
   /**
    * Sets the type of widget used to display the data. If not set, the default widget type will be
    * used. This method should only be used to use a widget that does not come built into
-   * Shuffleboard (i.e. one that comes with a custom or third-party plugin). To use a widget that
-   * is built into Shuffleboard, use {@link #withWidget(WidgetType)} and {@link BuiltInWidgets}.
+   * Shuffleboard (i.e. one that comes with a custom or third-party plugin). To use a widget that is
+   * built into Shuffleboard, use {@link #withWidget(WidgetType)} and {@link BuiltInWidgets}.
    *
    * @param widgetType the type of the widget used to display the data
    * @return this widget object
    */
+  @SuppressWarnings("unchecked")
   public final W withWidget(String widgetType) {
     setType(widgetType);
     return (W) this;
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
index c645baa..2043432 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
@@ -1,18 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 
-/**
- * A Shuffleboard widget that handles a single data point such as a number or string.
- */
+/** A Shuffleboard widget that handles a single data point such as a number or string. */
 public final class SimpleWidget extends ShuffleboardWidget<SimpleWidget> {
   private NetworkTableEntry m_entry;
 
@@ -22,6 +17,8 @@
 
   /**
    * Gets the NetworkTable entry that contains the data for this widget.
+   *
+   * @return The NetworkTable entry that contains the data for this widget.
    */
   public NetworkTableEntry getEntry() {
     if (m_entry == null) {
@@ -46,5 +43,4 @@
     ShuffleboardTab tab = (ShuffleboardTab) parent;
     tab.getRoot().update();
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
index c7c9e8b..497b30f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import java.util.function.BiConsumer;
-import java.util.function.Supplier;
-
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
 
 /**
  * A Shuffleboard widget whose value is provided by user code.
@@ -25,15 +21,16 @@
   /**
    * Package-private constructor for use by the Shuffleboard API.
    *
-   * @param parent   the parent container for the widget
-   * @param title    the title of the widget
+   * @param parent the parent container for the widget
+   * @param title the title of the widget
    * @param supplier the supplier for values to place in the NetworkTable entry
-   * @param setter   the function for placing values in the NetworkTable entry
+   * @param setter the function for placing values in the NetworkTable entry
    */
-  SuppliedValueWidget(ShuffleboardContainer parent,
-                      String title,
-                      Supplier<T> supplier,
-                      BiConsumer<NetworkTableEntry, T> setter) {
+  SuppliedValueWidget(
+      ShuffleboardContainer parent,
+      String title,
+      Supplier<T> supplier,
+      BiConsumer<NetworkTableEntry, T> setter) {
     super(parent, title);
     this.m_supplier = supplier;
     this.m_setter = setter;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/WidgetType.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/WidgetType.java
index 1e8242f..3033f8e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/WidgetType.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/WidgetType.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
@@ -16,6 +13,8 @@
 public interface WidgetType {
   /**
    * Gets the string type of the widget as defined by that widget in Shuffleboard.
+   *
+   * @return The string type of the widget.
    */
   String getWidgetName();
 }
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
new file mode 100644
index 0000000..c50c2f8
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL345Sim.java
@@ -0,0 +1,62 @@
+// 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.simulation;
+
+import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.wpilibj.ADXL345_I2C;
+import edu.wpi.first.wpilibj.ADXL345_SPI;
+import java.util.Objects;
+
+public class ADXL345Sim {
+  protected SimDouble m_simX;
+  protected SimDouble m_simY;
+  protected SimDouble m_simZ;
+
+  /**
+   * Constructor.
+   *
+   * @param device The device to simulate
+   */
+  public ADXL345Sim(ADXL345_SPI device) {
+    SimDeviceSim simDevice = new SimDeviceSim("Accel:ADXL345_SPI" + "[" + device.getPort() + "]");
+    initSim(simDevice);
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param device The device to simulate
+   */
+  public ADXL345Sim(ADXL345_I2C device) {
+    SimDeviceSim simDevice =
+        new SimDeviceSim(
+            "Accel:ADXL345_I2C" + "[" + device.getPort() + "," + device.getDeviceAddress() + "]");
+    initSim(simDevice);
+  }
+
+  private void initSim(SimDeviceSim simDevice) {
+    Objects.requireNonNull(simDevice);
+
+    m_simX = simDevice.getDouble("x");
+    m_simY = simDevice.getDouble("y");
+    m_simZ = simDevice.getDouble("z");
+
+    Objects.requireNonNull(m_simX);
+    Objects.requireNonNull(m_simY);
+    Objects.requireNonNull(m_simZ);
+  }
+
+  public void setX(double x) {
+    m_simX.set(x);
+  }
+
+  public void setY(double y) {
+    m_simY.set(y);
+  }
+
+  public void setZ(double z) {
+    m_simZ.set(z);
+  }
+}
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
new file mode 100644
index 0000000..76df8bd
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXL362Sim.java
@@ -0,0 +1,48 @@
+// 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.simulation;
+
+import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.wpilibj.ADXL362;
+import java.util.Objects;
+
+public class ADXL362Sim {
+  protected SimDouble m_simX;
+  protected SimDouble m_simY;
+  protected SimDouble m_simZ;
+
+  /**
+   * Constructor.
+   *
+   * @param device The device to simulate
+   */
+  public ADXL362Sim(ADXL362 device) {
+    SimDeviceSim wrappedSimDevice =
+        new SimDeviceSim("Accel:ADXL362" + "[" + device.getPort() + "]");
+    initSim(wrappedSimDevice);
+  }
+
+  private void initSim(SimDeviceSim wrappedSimDevice) {
+    m_simX = wrappedSimDevice.getDouble("x");
+    m_simY = wrappedSimDevice.getDouble("y");
+    m_simZ = wrappedSimDevice.getDouble("z");
+
+    Objects.requireNonNull(m_simX);
+    Objects.requireNonNull(m_simY);
+    Objects.requireNonNull(m_simZ);
+  }
+
+  public void setX(double x) {
+    m_simX.set(x);
+  }
+
+  public void setY(double y) {
+    m_simY.set(y);
+  }
+
+  public void setZ(double z) {
+    m_simZ.set(z);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSim.java
index 950efef..27e06a3 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSim.java
@@ -1,19 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.wpilibj.ADXRS450_Gyro;
 
-/**
- * Class to control a simulated ADXRS450 gyroscope.
- */
-@SuppressWarnings("TypeName")
+/** Class to control a simulated ADXRS450 gyroscope. */
+@SuppressWarnings({"TypeName", "AbbreviationAsWordInName"})
 public class ADXRS450_GyroSim {
   private final SimDouble m_simAngle;
   private final SimDouble m_simRate;
@@ -24,9 +19,9 @@
    * @param gyro ADXRS450_Gyro to simulate
    */
   public ADXRS450_GyroSim(ADXRS450_Gyro gyro) {
-    SimDeviceSim wrappedSimDevice = new SimDeviceSim("ADXRS450_Gyro" + "[" + gyro.getPort() + "]");
-    m_simAngle = wrappedSimDevice.getDouble("Angle");
-    m_simRate = wrappedSimDevice.getDouble("Rate");
+    SimDeviceSim wrappedSimDevice = new SimDeviceSim("Gyro:ADXRS450" + "[" + gyro.getPort() + "]");
+    m_simAngle = wrappedSimDevice.getDouble("angle_x");
+    m_simRate = wrappedSimDevice.getDouble("rate_x");
   }
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSim.java
index 81c9a7b..bf543c0 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -13,15 +10,11 @@
 import edu.wpi.first.wpilibj.AddressableLED;
 import java.util.NoSuchElementException;
 
-/**
- * Class to control a simulated addressable LED.
- */
+/** Class to control a simulated addressable LED. */
 public class AddressableLEDSim {
   private final int m_index;
 
-  /**
-   * Constructs for the first addressable LED.
-   */
+  /** Constructs for the first addressable LED. */
   public AddressableLEDSim() {
     m_index = 0;
   }
@@ -57,8 +50,8 @@
   }
 
   /**
-   * Creates an AddressableLEDSim for a simulated index.
-   * The index is incremented for each simulated AddressableLED.
+   * Creates an AddressableLEDSim for a simulated index. The index is incremented for each simulated
+   * AddressableLED.
    *
    * @param index simulator index
    * @return Simulated object
@@ -67,61 +60,161 @@
     return new AddressableLEDSim(index);
   }
 
+  /**
+   * Register a callback on the Initialized property.
+   *
+   * @param callback the callback that will be called whenever the Initialized property is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AddressableLEDDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AddressableLEDDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check if initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return AddressableLEDDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Change the Initialized value of the LED strip.
+   *
+   * @param initialized the new value
+   */
   public void setInitialized(boolean initialized) {
     AddressableLEDDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback on the output port.
+   *
+   * @param callback the callback that will be called whenever the output port is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerOutputPortCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AddressableLEDDataJNI.registerOutputPortCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AddressableLEDDataJNI::cancelOutputPortCallback);
   }
+
+  /**
+   * Get the output port.
+   *
+   * @return the output port
+   */
   public int getOutputPort() {
     return AddressableLEDDataJNI.getOutputPort(m_index);
   }
+
+  /**
+   * Change the output port.
+   *
+   * @param outputPort the new output port
+   */
   public void setOutputPort(int outputPort) {
     AddressableLEDDataJNI.setOutputPort(m_index, outputPort);
   }
 
+  /**
+   * Register a callback on the length.
+   *
+   * @param callback the callback that will be called whenever the length is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerLengthCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AddressableLEDDataJNI.registerLengthCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AddressableLEDDataJNI::cancelLengthCallback);
   }
+
+  /**
+   * Get the length of the LED strip.
+   *
+   * @return the length
+   */
   public int getLength() {
     return AddressableLEDDataJNI.getLength(m_index);
   }
+
+  /**
+   * Change the length of the LED strip.
+   *
+   * @param length the new value
+   */
   public void setLength(int length) {
     AddressableLEDDataJNI.setLength(m_index, length);
   }
 
+  /**
+   * Register a callback on whether the LEDs are running.
+   *
+   * @param callback the callback that will be called whenever the LED state is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerRunningCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AddressableLEDDataJNI.registerRunningCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AddressableLEDDataJNI::cancelRunningCallback);
   }
+
+  /**
+   * Check if the LEDs are running.
+   *
+   * @return true if they are
+   */
   public boolean getRunning() {
     return AddressableLEDDataJNI.getRunning(m_index);
   }
+
+  /**
+   * Change whether the LEDs are active.
+   *
+   * @param running the new value
+   */
   public void setRunning(boolean running) {
     AddressableLEDDataJNI.setRunning(m_index, running);
   }
 
+  /**
+   * Register a callback on the LED data.
+   *
+   * @param callback the callback that will be called whenever the LED data is changed
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerDataCallback(ConstBufferCallback callback) {
     int uid = AddressableLEDDataJNI.registerDataCallback(m_index, callback);
     return new CallbackStore(m_index, uid, AddressableLEDDataJNI::cancelDataCallback);
   }
+
+  /**
+   * Get the LED data.
+   *
+   * @return the LED data
+   */
   public byte[] getData() {
     return AddressableLEDDataJNI.getData(m_index);
   }
+
+  /**
+   * Change the LED data.
+   *
+   * @param data the new data
+   */
   public void setData(byte[] data) {
     AddressableLEDDataJNI.setData(m_index, data);
   }
 
+  /** Reset all simulation data for this LED object. */
   public void resetData() {
     AddressableLEDDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSim.java
index e1b541f..6b11673 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSim.java
@@ -1,19 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.math.geometry.Rotation2d;
 import edu.wpi.first.wpilibj.AnalogEncoder;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
 
-/**
- * Class to control a simulated analog encoder.
- */
+/** Class to control a simulated analog encoder. */
 public class AnalogEncoderSim {
   private final SimDouble m_simPosition;
 
@@ -23,7 +18,8 @@
    * @param encoder AnalogEncoder to simulate
    */
   public AnalogEncoderSim(AnalogEncoder encoder) {
-    SimDeviceSim wrappedSimDevice = new SimDeviceSim("AnalogEncoder" + "[" + encoder.getChannel() + "]");
+    SimDeviceSim wrappedSimDevice =
+        new SimDeviceSim("AnalogEncoder" + "[" + encoder.getChannel() + "]");
     m_simPosition = wrappedSimDevice.getDouble("Position");
   }
 
@@ -47,6 +43,8 @@
 
   /**
    * Get the simulated position.
+   *
+   * @return The simulated position.
    */
   public double getTurns() {
     return m_simPosition.get();
@@ -54,6 +52,8 @@
 
   /**
    * Get the position as a {@link Rotation2d}.
+   *
+   * @return The position as a {@link Rotation2d}.
    */
   public Rotation2d getPosition() {
     return Rotation2d.fromDegrees(getTurns() * 360.0);
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSim.java
index b204fae..87217aa 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,9 +8,7 @@
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.wpilibj.AnalogGyro;
 
-/**
- * Class to control a simulated analog gyro.
- */
+/** Class to control a simulated analog gyro. */
 public class AnalogGyroSim {
   private final int m_index;
 
@@ -35,39 +30,100 @@
     m_index = channel;
   }
 
+  /**
+   * Register a callback on the angle.
+   *
+   * @param callback the callback that will be called whenever the angle changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerAngleCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogGyroDataJNI.registerAngleCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogGyroDataJNI::cancelAngleCallback);
   }
+
+  /**
+   * Get the current angle of the gyro.
+   *
+   * @return the angle measured by the gyro
+   */
   public double getAngle() {
     return AnalogGyroDataJNI.getAngle(m_index);
   }
+
+  /**
+   * Change the angle measured by the gyro.
+   *
+   * @param angle the new value
+   */
   public void setAngle(double angle) {
     AnalogGyroDataJNI.setAngle(m_index, angle);
   }
 
+  /**
+   * Register a callback on the rate.
+   *
+   * @param callback the callback that will be called whenever the rate changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerRateCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogGyroDataJNI.registerRateCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogGyroDataJNI::cancelRateCallback);
   }
+
+  /**
+   * Get the rate of angle change on this gyro.
+   *
+   * @return the rate
+   */
   public double getRate() {
     return AnalogGyroDataJNI.getRate(m_index);
   }
+
+  /**
+   * Change the rate of the gyro.
+   *
+   * @param rate the new rate
+   */
   public void setRate(double rate) {
     AnalogGyroDataJNI.setRate(m_index, rate);
   }
 
+  /**
+   * Register a callback on whether the gyro is initialized.
+   *
+   * @param callback the callback that will be called whenever the gyro is initialized
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogGyroDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogGyroDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check if the gyro is initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return AnalogGyroDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Set whether this gyro is initialized.
+   *
+   * @param initialized the new value
+   */
   public void setInitialized(boolean initialized) {
     AnalogGyroDataJNI.setInitialized(m_index, initialized);
   }
 
+  /** Reset all simulation data for this object. */
   public void resetData() {
     AnalogGyroDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogInputSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogInputSim.java
index 1709d6a..87dce71 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogInputSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogInputSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,9 +8,7 @@
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.wpilibj.AnalogInput;
 
-/**
- * Class to control a simulated analog input.
- */
+/** Class to control a simulated analog input. */
 public class AnalogInputSim {
   private final int m_index;
 
@@ -35,105 +30,293 @@
     m_index = channel;
   }
 
+  /**
+   * Register a callback on whether the analog input is initialized.
+   *
+   * @param callback the callback that will be called whenever the analog input is initialized
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check if this analog input has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return AnalogInDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Change whether this analog input has been initialized.
+   *
+   * @param initialized the new value
+   */
   public void setInitialized(boolean initialized) {
     AnalogInDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback on the number of average bits.
+   *
+   * @param callback the callback that will be called whenever the number of average bits is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerAverageBitsCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerAverageBitsCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAverageBitsCallback);
   }
+
+  /**
+   * Get the number of average bits.
+   *
+   * @return the number of average bits
+   */
   public int getAverageBits() {
     return AnalogInDataJNI.getAverageBits(m_index);
   }
+
+  /**
+   * Change the number of average bits.
+   *
+   * @param averageBits the new value
+   */
   public void setAverageBits(int averageBits) {
     AnalogInDataJNI.setAverageBits(m_index, averageBits);
   }
 
-  public CallbackStore registerOversampleBitsCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the amount of oversampling bits.
+   *
+   * @param callback the callback that will be called whenever the oversampling bits are changed.
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerOversampleBitsCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerOversampleBitsCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelOversampleBitsCallback);
   }
+
+  /**
+   * Get the amount of oversampling bits.
+   *
+   * @return the amount of oversampling bits
+   */
   public int getOversampleBits() {
     return AnalogInDataJNI.getOversampleBits(m_index);
   }
+
+  /**
+   * Change the amount of oversampling bits.
+   *
+   * @param oversampleBits the new value
+   */
   public void setOversampleBits(int oversampleBits) {
     AnalogInDataJNI.setOversampleBits(m_index, oversampleBits);
   }
 
+  /**
+   * Register a callback on the voltage.
+   *
+   * @param callback the callback that will be called whenever the voltage is changed.
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerVoltageCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerVoltageCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelVoltageCallback);
   }
+
+  /**
+   * Get the voltage.
+   *
+   * @return the voltage
+   */
   public double getVoltage() {
     return AnalogInDataJNI.getVoltage(m_index);
   }
+
+  /**
+   * Change the voltage.
+   *
+   * @param voltage the new value
+   */
   public void setVoltage(double voltage) {
     AnalogInDataJNI.setVoltage(m_index, voltage);
   }
 
-  public CallbackStore registerAccumulatorInitializedCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = AnalogInDataJNI.registerAccumulatorInitializedCallback(m_index, callback, initialNotify);
+  /**
+   * Register a callback on whether the accumulator is initialized.
+   *
+   * @param callback the callback that will be called whenever the accumulator is initialized
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerAccumulatorInitializedCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        AnalogInDataJNI.registerAccumulatorInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAccumulatorInitializedCallback);
   }
+
+  /**
+   * Check if the accumulator has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getAccumulatorInitialized() {
     return AnalogInDataJNI.getAccumulatorInitialized(m_index);
   }
+
+  /**
+   * Change whether the accumulator has been initialized.
+   *
+   * @param accumulatorInitialized the new value
+   */
   public void setAccumulatorInitialized(boolean accumulatorInitialized) {
     AnalogInDataJNI.setAccumulatorInitialized(m_index, accumulatorInitialized);
   }
 
-  public CallbackStore registerAccumulatorValueCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the accumulator value.
+   *
+   * @param callback the callback that will be called whenever the accumulator value is changed.
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerAccumulatorValueCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerAccumulatorValueCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAccumulatorValueCallback);
   }
+
+  /**
+   * Get the accumulator value.
+   *
+   * @return the accumulator value
+   */
   public long getAccumulatorValue() {
     return AnalogInDataJNI.getAccumulatorValue(m_index);
   }
+
+  /**
+   * Change the accumulator value.
+   *
+   * @param accumulatorValue the new value
+   */
   public void setAccumulatorValue(long accumulatorValue) {
     AnalogInDataJNI.setAccumulatorValue(m_index, accumulatorValue);
   }
 
-  public CallbackStore registerAccumulatorCountCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the accumulator count.
+   *
+   * @param callback the callback that will be called whenever the accumulator count is changed.
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerAccumulatorCountCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerAccumulatorCountCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAccumulatorCountCallback);
   }
+
+  /**
+   * Get the accumulator count.
+   *
+   * @return the accumulator count.
+   */
   public long getAccumulatorCount() {
     return AnalogInDataJNI.getAccumulatorCount(m_index);
   }
+
+  /**
+   * Change the accumulator count.
+   *
+   * @param accumulatorCount the new count.
+   */
   public void setAccumulatorCount(long accumulatorCount) {
     AnalogInDataJNI.setAccumulatorCount(m_index, accumulatorCount);
   }
 
-  public CallbackStore registerAccumulatorCenterCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the accumulator center.
+   *
+   * @param callback the callback that will be called whenever the accumulator center is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerAccumulatorCenterCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerAccumulatorCenterCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAccumulatorCenterCallback);
   }
+
+  /**
+   * Get the accumulator center.
+   *
+   * @return the accumulator center
+   */
   public int getAccumulatorCenter() {
     return AnalogInDataJNI.getAccumulatorCenter(m_index);
   }
+
+  /**
+   * Change the accumulator center.
+   *
+   * @param accumulatorCenter the new center
+   */
   public void setAccumulatorCenter(int accumulatorCenter) {
     AnalogInDataJNI.setAccumulatorCenter(m_index, accumulatorCenter);
   }
 
-  public CallbackStore registerAccumulatorDeadbandCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the accumulator deadband.
+   *
+   * @param callback the callback that will be called whenever the accumulator deadband is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerAccumulatorDeadbandCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogInDataJNI.registerAccumulatorDeadbandCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogInDataJNI::cancelAccumulatorDeadbandCallback);
   }
+
+  /**
+   * Get the accumulator deadband.
+   *
+   * @return the accumulator deadband
+   */
   public int getAccumulatorDeadband() {
     return AnalogInDataJNI.getAccumulatorDeadband(m_index);
   }
+
+  /**
+   * Change the accumulator deadband.
+   *
+   * @param accumulatorDeadband the new deadband
+   */
   public void setAccumulatorDeadband(int accumulatorDeadband) {
     AnalogInDataJNI.setAccumulatorDeadband(m_index, accumulatorDeadband);
   }
 
+  /** Reset all simulation data for this object. */
   public void resetData() {
     AnalogInDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSim.java
index 077a2eb..985fcd9 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,9 +8,7 @@
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.wpilibj.AnalogOutput;
 
-/**
- * Class to control a simulated analog output.
- */
+/** Class to control a simulated analog output. */
 public class AnalogOutputSim {
   private final int m_index;
 
@@ -35,28 +30,69 @@
     m_index = channel;
   }
 
+  /**
+   * Register a callback to be run whenever the voltage 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 CallbackStore registerVoltageCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogOutDataJNI.registerVoltageCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogOutDataJNI::cancelVoltageCallback);
   }
+
+  /**
+   * Read the analog output voltage.
+   *
+   * @return the voltage on this analog output
+   */
   public double getVoltage() {
     return AnalogOutDataJNI.getVoltage(m_index);
   }
+
+  /**
+   * Set the analog output voltage.
+   *
+   * @param voltage the new voltage on this analog output
+   */
   public void setVoltage(double voltage) {
     AnalogOutDataJNI.setVoltage(m_index, voltage);
   }
 
+  /**
+   * Register a callback to be run when this analog output is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogOutDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogOutDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this analog output has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return AnalogOutDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this analog output has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     AnalogOutDataJNI.setInitialized(m_index, initialized);
   }
 
+  /** Reset all simulation data on this object. */
   public void resetData() {
     AnalogOutDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSim.java
index 6916fdc..58d51b1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,9 +9,7 @@
 import edu.wpi.first.wpilibj.AnalogTrigger;
 import java.util.NoSuchElementException;
 
-/**
- * Class to control a simulated analog trigger.
- */
+/** Class to control a simulated analog trigger. */
 public class AnalogTriggerSim {
   private final int m_index;
 
@@ -47,8 +42,8 @@
   }
 
   /**
-   * Creates an AnalogTriggerSim for a simulated index.
-   * The index is incremented for each simulated AnalogTrigger.
+   * Creates an AnalogTriggerSim for a simulated index. The index is incremented for each simulated
+   * AnalogTrigger.
    *
    * @param index simulator index
    * @return Simulated object
@@ -57,39 +52,104 @@
     return new AnalogTriggerSim(index);
   }
 
+  /**
+   * Register a callback on whether the analog trigger is initialized.
+   *
+   * @param callback the callback that will be called whenever the analog trigger is initialized
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AnalogTriggerDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogTriggerDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check if this analog trigger has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return AnalogTriggerDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Change whether this analog trigger has been initialized.
+   *
+   * @param initialized the new value
+   */
   public void setInitialized(boolean initialized) {
     AnalogTriggerDataJNI.setInitialized(m_index, initialized);
   }
 
-  public CallbackStore registerTriggerLowerBoundCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = AnalogTriggerDataJNI.registerTriggerLowerBoundCallback(m_index, callback, initialNotify);
+  /**
+   * Register a callback on the lower bound.
+   *
+   * @param callback the callback that will be called whenever the lower bound is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerTriggerLowerBoundCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        AnalogTriggerDataJNI.registerTriggerLowerBoundCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogTriggerDataJNI::cancelTriggerLowerBoundCallback);
   }
+
+  /**
+   * Get the lower bound.
+   *
+   * @return the lower bound
+   */
   public double getTriggerLowerBound() {
     return AnalogTriggerDataJNI.getTriggerLowerBound(m_index);
   }
+
+  /**
+   * Change the lower bound.
+   *
+   * @param triggerLowerBound the new lower bound
+   */
   public void setTriggerLowerBound(double triggerLowerBound) {
     AnalogTriggerDataJNI.setTriggerLowerBound(m_index, triggerLowerBound);
   }
 
-  public CallbackStore registerTriggerUpperBoundCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = AnalogTriggerDataJNI.registerTriggerUpperBoundCallback(m_index, callback, initialNotify);
+  /**
+   * Register a callback on the upper bound.
+   *
+   * @param callback the callback that will be called whenever the upper bound is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerTriggerUpperBoundCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        AnalogTriggerDataJNI.registerTriggerUpperBoundCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AnalogTriggerDataJNI::cancelTriggerUpperBoundCallback);
   }
+
+  /**
+   * Get the upper bound.
+   *
+   * @return the upper bound
+   */
   public double getTriggerUpperBound() {
     return AnalogTriggerDataJNI.getTriggerUpperBound(m_index);
   }
+
+  /**
+   * Change the upper bound.
+   *
+   * @param triggerUpperBound the new upper bound
+   */
   public void setTriggerUpperBound(double triggerUpperBound) {
     AnalogTriggerDataJNI.setTriggerUpperBound(m_index, triggerUpperBound);
   }
 
+  /** Reset all simulation data for this object. */
   public void resetData() {
     AnalogTriggerDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BatterySim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BatterySim.java
index 0150ed4..78c9888 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BatterySim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BatterySim.java
@@ -1,34 +1,28 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.RobotController;
-
+/** A utility class to simulate the robot battery. */
 public final class BatterySim {
   private BatterySim() {
     // Utility class
   }
 
   /**
-   * Calculate the loaded battery voltage. Use this with
-   * {@link RoboRioSim#setVInVoltage(double)} to set the simulated battery
-   * voltage, which can then be retrieved with the {@link RobotController#getBatteryVoltage()}
-   * method.
+   * Calculate the loaded battery voltage. Use this with {@link RoboRioSim#setVInVoltage(double)} to
+   * set the simulated battery voltage, which can then be retrieved with the {@link
+   * edu.wpi.first.wpilibj.RobotController#getBatteryVoltage()} method.
    *
    * @param nominalVoltage The nominal battery voltage. Usually 12v.
-   * @param resistanceOhms The forward resistance of the battery. Most batteries are at or
-   *                       below 20 milliohms.
-   * @param currents       The currents drawn from the battery.
+   * @param resistanceOhms The forward resistance of the battery. Most batteries are at or below 20
+   *     milliohms.
+   * @param currents The currents drawn from the battery.
    * @return The battery's voltage under load.
    */
-  public static double calculateLoadedBatteryVoltage(double nominalVoltage,
-                                                     double resistanceOhms,
-                                                     double... currents) {
+  public static double calculateLoadedBatteryVoltage(
+      double nominalVoltage, double resistanceOhms, double... currents) {
     double retval = nominalVoltage;
     for (var current : currents) {
       retval -= current * resistanceOhms;
@@ -37,11 +31,10 @@
   }
 
   /**
-   * Calculate the loaded battery voltage. Use this with
-   * {@link RoboRioSim#setVInVoltage(double)} to set the simulated battery
-   * voltage, which can then be retrieved with the {@link RobotController#getBatteryVoltage()}
-   * method. This function assumes a nominal voltage of 12v and a resistance of 20 milliohms
-   * (0.020 ohms)
+   * Calculate the loaded battery voltage. Use this with {@link RoboRioSim#setVInVoltage(double)} to
+   * set the simulated battery voltage, which can then be retrieved with the {@link
+   * edu.wpi.first.wpilibj.RobotController#getBatteryVoltage()} method. This function assumes a
+   * nominal voltage of 12v and a resistance of 20 milliohms (0.020 ohms)
    *
    * @param currents The currents drawn from the battery.
    * @return The battery's voltage under load.
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BuiltInAccelerometerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BuiltInAccelerometerSim.java
index 8905542..5714a2d 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BuiltInAccelerometerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/BuiltInAccelerometerSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,15 +8,11 @@
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.wpilibj.BuiltInAccelerometer;
 
-/**
- * Class to control a simulated built-in accelerometer.
- */
+/** Class to control a simulated built-in accelerometer. */
 public class BuiltInAccelerometerSim {
   private final int m_index;
 
-  /**
-   * Constructs for the first built-in accelerometer.
-   */
+  /** Constructs for the first built-in accelerometer. */
   public BuiltInAccelerometerSim() {
     m_index = 0;
   }
@@ -34,61 +27,165 @@
     m_index = 0;
   }
 
+  /**
+   * Register a callback to be run when this accelerometer activates.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerActiveCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AccelerometerDataJNI.registerActiveCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AccelerometerDataJNI::cancelActiveCallback);
   }
+
+  /**
+   * Check whether the accelerometer is active.
+   *
+   * @return true if active
+   */
   public boolean getActive() {
     return AccelerometerDataJNI.getActive(m_index);
   }
+
+  /**
+   * Define whether this accelerometer is active.
+   *
+   * @param active the new state
+   */
   public void setActive(boolean active) {
     AccelerometerDataJNI.setActive(m_index, active);
   }
 
+  /**
+   * Register a callback to be run whenever the range 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 CallbackStore registerRangeCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AccelerometerDataJNI.registerRangeCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AccelerometerDataJNI::cancelRangeCallback);
   }
+
+  /**
+   * Check the range of this accelerometer.
+   *
+   * @return the accelerometer range
+   */
   public int getRange() {
     return AccelerometerDataJNI.getRange(m_index);
   }
+
+  /**
+   * Change the range of this accelerometer.
+   *
+   * @param range the new accelerometer range
+   */
   public void setRange(int range) {
     AccelerometerDataJNI.setRange(m_index, range);
   }
 
+  /**
+   * Register a callback to be run whenever the X axis value 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 CallbackStore registerXCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AccelerometerDataJNI.registerXCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AccelerometerDataJNI::cancelXCallback);
   }
+
+  /**
+   * Measure the X axis value.
+   *
+   * @return the X axis measurement
+   */
   public double getX() {
     return AccelerometerDataJNI.getX(m_index);
   }
+
+  /**
+   * Change the X axis value of the accelerometer.
+   *
+   * @param x the new reading of the X axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setX(double x) {
     AccelerometerDataJNI.setX(m_index, x);
   }
 
+  /**
+   * Register a callback to be run whenever the Y axis value 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 CallbackStore registerYCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AccelerometerDataJNI.registerYCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AccelerometerDataJNI::cancelYCallback);
   }
+
+  /**
+   * Measure the Y axis value.
+   *
+   * @return the Y axis measurement
+   */
   public double getY() {
     return AccelerometerDataJNI.getY(m_index);
   }
+
+  /**
+   * Change the Y axis value of the accelerometer.
+   *
+   * @param y the new reading of the Y axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setY(double y) {
     AccelerometerDataJNI.setY(m_index, y);
   }
 
+  /**
+   * Register a callback to be run whenever the Z axis value 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 CallbackStore registerZCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = AccelerometerDataJNI.registerZCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, AccelerometerDataJNI::cancelZCallback);
   }
+
+  /**
+   * Measure the Z axis value.
+   *
+   * @return the Z axis measurement
+   */
   public double getZ() {
     return AccelerometerDataJNI.getZ(m_index);
   }
+
+  /**
+   * Change the Z axis value of the accelerometer.
+   *
+   * @param z the new reading of the Z axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setZ(double z) {
     AccelerometerDataJNI.setZ(m_index, z);
   }
 
+  /** Reset all simulation data of this object. */
   public void resetData() {
     AccelerometerDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CTREPCMSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CTREPCMSim.java
new file mode 100644
index 0000000..12ae153
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CTREPCMSim.java
@@ -0,0 +1,239 @@
+// 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.simulation;
+
+import edu.wpi.first.hal.simulation.CTREPCMDataJNI;
+import edu.wpi.first.hal.simulation.NotifyCallback;
+import edu.wpi.first.wpilibj.PneumaticsBase;
+import edu.wpi.first.wpilibj.SensorUtil;
+
+/** Class to control a simulated Pneumatic Control Module (PCM). */
+@SuppressWarnings("AbbreviationAsWordInName")
+public class CTREPCMSim {
+  private final int m_index;
+
+  /** Constructs for the default PCM. */
+  public CTREPCMSim() {
+    m_index = SensorUtil.getDefaultCTREPCMModule();
+  }
+
+  /**
+   * Constructs from a PCM module number (CAN ID).
+   *
+   * @param module module number
+   */
+  public CTREPCMSim(int module) {
+    m_index = module;
+  }
+
+  /**
+   * Constructs from a Compressor object.
+   *
+   * @param module PCM module to simulate
+   */
+  public CTREPCMSim(PneumaticsBase module) {
+    m_index = module.getModuleNumber();
+  }
+
+  /**
+   * Register a callback to be run when the solenoid output on a channel changes.
+   *
+   * @param channel the channel to monitor
+   * @param callback the callback
+   * @param initialNotify should the callback be run with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerSolenoidOutputCallback(
+      int channel, NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        CTREPCMDataJNI.registerSolenoidOutputCallback(m_index, channel, callback, initialNotify);
+    return new CallbackStore(m_index, channel, uid, CTREPCMDataJNI::cancelSolenoidOutputCallback);
+  }
+
+  /**
+   * Check the solenoid output on a specific channel.
+   *
+   * @param channel the channel to check
+   * @return the solenoid output
+   */
+  public boolean getSolenoidOutput(int channel) {
+    return CTREPCMDataJNI.getSolenoidOutput(m_index, channel);
+  }
+
+  /**
+   * Change the solenoid output on a specific channel.
+   *
+   * @param channel the channel to check
+   * @param solenoidOutput the new solenoid output
+   */
+  public void setSolenoidOutput(int channel, boolean solenoidOutput) {
+    CTREPCMDataJNI.setSolenoidOutput(m_index, channel, solenoidOutput);
+  }
+
+  /**
+   * Register a callback to be run when the compressor is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
+    int uid = CTREPCMDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, CTREPCMDataJNI::cancelInitializedCallback);
+  }
+
+  /**
+   * Check whether the compressor has been initialized.
+   *
+   * @return true if initialized
+   */
+  public boolean getInitialized() {
+    return CTREPCMDataJNI.getInitialized(m_index);
+  }
+
+  /**
+   * Define whether the compressor has been initialized.
+   *
+   * @param initialized whether the compressor is initialized
+   */
+  public void setInitialized(boolean initialized) {
+    CTREPCMDataJNI.setInitialized(m_index, initialized);
+  }
+
+  /**
+   * Register a callback to be run when the compressor activates.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerCompressorOnCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = CTREPCMDataJNI.registerCompressorOnCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, CTREPCMDataJNI::cancelCompressorOnCallback);
+  }
+
+  /**
+   * Check if the compressor is on.
+   *
+   * @return true if the compressor is active
+   */
+  public boolean getCompressorOn() {
+    return CTREPCMDataJNI.getCompressorOn(m_index);
+  }
+
+  /**
+   * Set whether the compressor is active.
+   *
+   * @param compressorOn the new value
+   */
+  public void setCompressorOn(boolean compressorOn) {
+    CTREPCMDataJNI.setCompressorOn(m_index, compressorOn);
+  }
+
+  /**
+   * Register a callback to be run whenever the closed loop state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerClosedLoopEnabledCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = CTREPCMDataJNI.registerClosedLoopEnabledCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, CTREPCMDataJNI::cancelClosedLoopEnabledCallback);
+  }
+
+  /**
+   * Check whether the closed loop compressor control is active.
+   *
+   * @return true if active
+   */
+  public boolean getClosedLoopEnabled() {
+    return CTREPCMDataJNI.getClosedLoopEnabled(m_index);
+  }
+
+  /**
+   * Turn on/off the closed loop control of the compressor.
+   *
+   * @param closedLoopEnabled whether the control loop is active
+   */
+  public void setClosedLoopEnabled(boolean closedLoopEnabled) {
+    CTREPCMDataJNI.setClosedLoopEnabled(m_index, closedLoopEnabled);
+  }
+
+  /**
+   * Register a callback to be run whenever the pressure switch value changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerPressureSwitchCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = CTREPCMDataJNI.registerPressureSwitchCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, CTREPCMDataJNI::cancelPressureSwitchCallback);
+  }
+
+  /**
+   * Check the value of the pressure switch.
+   *
+   * @return the pressure switch value
+   */
+  public boolean getPressureSwitch() {
+    return CTREPCMDataJNI.getPressureSwitch(m_index);
+  }
+
+  /**
+   * Set the value of the pressure switch.
+   *
+   * @param pressureSwitch the new value
+   */
+  public void setPressureSwitch(boolean pressureSwitch) {
+    CTREPCMDataJNI.setPressureSwitch(m_index, pressureSwitch);
+  }
+
+  /**
+   * Register a callback to be run whenever the compressor current 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 CallbackStore registerCompressorCurrentCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = CTREPCMDataJNI.registerCompressorCurrentCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, CTREPCMDataJNI::cancelCompressorCurrentCallback);
+  }
+
+  /**
+   * Read the compressor current.
+   *
+   * @return the current of the compressor connected to this module
+   */
+  public double getCompressorCurrent() {
+    return CTREPCMDataJNI.getCompressorCurrent(m_index);
+  }
+
+  /**
+   * Set the compressor current.
+   *
+   * @param compressorCurrent the new compressor current
+   */
+  public void setCompressorCurrent(double compressorCurrent) {
+    CTREPCMDataJNI.setCompressorCurrent(m_index, compressorCurrent);
+  }
+
+  /** Reset all simulation data for this object. */
+  public void resetData() {
+    CTREPCMDataJNI.resetData(m_index);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CallbackStore.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CallbackStore.java
index 3df142f..048d3c1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CallbackStore.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/CallbackStore.java
@@ -1,25 +1,33 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
+/** Manages simulation callbacks; each object is associated with a callback. */
 public class CallbackStore implements AutoCloseable {
+  /** <b>Note: This interface is for simulation classes only. It should not be used by teams!</b> */
   interface CancelCallbackFunc {
     void cancel(int index, int uid);
   }
 
+  /** <b>Note: This interface is for simulation classes only. It should not be used by teams!</b> */
   interface CancelCallbackChannelFunc {
     void cancel(int index, int channel, int uid);
   }
 
+  /** <b>Note: This interface is for simulation classes only. It should not be used by teams!</b> */
   interface CancelCallbackNoIndexFunc {
     void cancel(int uid);
   }
 
+  /**
+   * <b>Note: This constructor is for simulation classes only. It should not be called by teams!</b>
+   *
+   * @param index TODO
+   * @param uid TODO
+   * @param ccf TODO
+   */
   public CallbackStore(int index, int uid, CancelCallbackFunc ccf) {
     this.m_cancelType = kNormalCancel;
     this.m_index = index;
@@ -27,6 +35,14 @@
     this.m_cancelCallback = ccf;
   }
 
+  /**
+   * <b>Note: This constructor is for simulation classes only. It should not be called by teams!</b>
+   *
+   * @param index TODO
+   * @param channel TODO
+   * @param uid TODO
+   * @param ccf TODO
+   */
   public CallbackStore(int index, int channel, int uid, CancelCallbackChannelFunc ccf) {
     this.m_cancelType = kChannelCancel;
     this.m_index = index;
@@ -35,6 +51,12 @@
     this.m_cancelCallbackChannel = ccf;
   }
 
+  /**
+   * <b>Note: This constructor is for simulation classes only. It should not be called by teams!</b>
+   *
+   * @param uid TODO
+   * @param ccf TODO
+   */
   public CallbackStore(int uid, CancelCallbackNoIndexFunc ccf) {
     this.m_cancelType = kNoIndexCancel;
     this.m_uid = uid;
@@ -52,6 +74,7 @@
   private static final int kNoIndexCancel = 2;
   private int m_cancelType;
 
+  /** Cancel the callback associated with this object. */
   @Override
   public void close() {
     switch (m_cancelType) {
@@ -71,11 +94,12 @@
     m_cancelType = -1;
   }
 
+  @SuppressWarnings({"NoFinalizer", "deprecation"})
   @Override
   protected void finalize() throws Throwable {
     try {
       if (m_cancelType >= 0) {
-        close();        // close open files
+        close(); // close open files
       }
     } finally {
       super.finalize();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DIOSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DIOSim.java
index 758d7e4..83afb74 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DIOSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DIOSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,9 +9,7 @@
 import edu.wpi.first.wpilibj.DigitalInput;
 import edu.wpi.first.wpilibj.DigitalOutput;
 
-/**
- * Class to control a simulated digital input or output.
- */
+/** Class to control a simulated digital input or output. */
 public class DIOSim {
   private final int m_index;
 
@@ -45,61 +40,162 @@
     m_index = channel;
   }
 
+  /**
+   * Register a callback to be run when this DIO is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DIODataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DIODataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this DIO has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return DIODataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this DIO has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     DIODataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever the DIO value changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerValueCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DIODataJNI.registerValueCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DIODataJNI::cancelValueCallback);
   }
+
+  /**
+   * Read the value of the DIO port.
+   *
+   * @return the DIO value
+   */
   public boolean getValue() {
     return DIODataJNI.getValue(m_index);
   }
+
+  /**
+   * Change the DIO value.
+   *
+   * @param value the new value
+   */
   public void setValue(boolean value) {
     DIODataJNI.setValue(m_index, value);
   }
 
+  /**
+   * Register a callback to be run whenever the pulse length 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 CallbackStore registerPulseLengthCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DIODataJNI.registerPulseLengthCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DIODataJNI::cancelPulseLengthCallback);
   }
+
+  /**
+   * Read the pulse length.
+   *
+   * @return the pulse length of this DIO port
+   */
   public double getPulseLength() {
     return DIODataJNI.getPulseLength(m_index);
   }
+
+  /**
+   * Change the pulse length of this DIO port.
+   *
+   * @param pulseLength the new pulse length
+   */
   public void setPulseLength(double pulseLength) {
     DIODataJNI.setPulseLength(m_index, pulseLength);
   }
 
+  /**
+   * Register a callback to be run whenever this DIO changes to be an input.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called 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 CallbackStore registerIsInputCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DIODataJNI.registerIsInputCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DIODataJNI::cancelIsInputCallback);
   }
+
+  /**
+   * Check whether this DIO port is currently an Input.
+   *
+   * @return true if Input
+   */
   public boolean getIsInput() {
     return DIODataJNI.getIsInput(m_index);
   }
+
+  /**
+   * Define whether this DIO port is an Input.
+   *
+   * @param isInput whether this DIO should be an Input
+   */
   public void setIsInput(boolean isInput) {
     DIODataJNI.setIsInput(m_index, isInput);
   }
 
+  /**
+   * Register a callback to be run whenever the filter index changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerFilterIndexCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DIODataJNI.registerFilterIndexCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DIODataJNI::cancelFilterIndexCallback);
   }
+
+  /**
+   * Read the filter index.
+   *
+   * @return the filter index of this DIO port
+   */
   public int getFilterIndex() {
     return DIODataJNI.getFilterIndex(m_index);
   }
+
+  /**
+   * Change the filter index of this DIO port.
+   *
+   * @param filterIndex the new filter index
+   */
   public void setFilterIndex(int filterIndex) {
     DIODataJNI.setFilterIndex(m_index, filterIndex);
   }
 
+  /** Reset all simulation data of this object. */
   public void resetData() {
     DIODataJNI.resetData(m_index);
   }
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 ab3d003..b123eac 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
@@ -1,173 +1,292 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.system.LinearSystem;
-import edu.wpi.first.wpilibj.system.RungeKutta;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpilibj.util.Units;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.Nat;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-import edu.wpi.first.wpiutil.math.numbers.N1;
-import edu.wpi.first.wpiutil.math.numbers.N2;
-import edu.wpi.first.wpiutil.math.numbers.N7;
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Nat;
+import edu.wpi.first.math.StateSpaceUtil;
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.geometry.Pose2d;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.numbers.N2;
+import edu.wpi.first.math.numbers.N7;
+import edu.wpi.first.math.system.LinearSystem;
+import edu.wpi.first.math.system.NumericalIntegration;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
+import edu.wpi.first.math.util.Units;
+import edu.wpi.first.wpilibj.RobotController;
 
 /**
- * This class simulates the state of the drivetrain. In simulationPeriodic, users should first set inputs from motors with
- * {@link #setInputs(double, double)}, call {@link #update(double)} to update the simulation,
- * and set estimated encoder and gyro positions, as well as estimated odometry pose. Teams can use {@link edu.wpi.first.wpilibj.simulation.Field2d} to
- * visualize their robot on the Sim GUI's field.
+ * This class simulates the state of the drivetrain. In simulationPeriodic, users should first set
+ * inputs from motors with {@link #setInputs(double, double)}, call {@link #update(double)} to
+ * update the simulation, and set estimated encoder and gyro positions, as well as estimated
+ * odometry pose. Teams can use {@link edu.wpi.first.wpilibj.smartdashboard.Field2d} to visualize
+ * their robot on the Sim GUI's field.
  *
- *  <p>Our state-space system is:
+ * <p>Our state-space system is:
  *
- *  <p>x = [[x, y, theta, vel_l, vel_r, dist_l, dist_r, voltError_l, voltError_r, headingError]]^T
- *  in the field coordinate system (dist_* are wheel distances.)
+ * <p>x = [[x, y, theta, vel_l, vel_r, dist_l, dist_r]]áµ€ in the field coordinate system (dist_* are
+ * wheel distances.)
  *
- *  <p>u = [[voltage_l, voltage_r]]^T This is typically the control input of the last timestep
- *  from a LTVDiffDriveController.
+ * <p>u = [[voltage_l, voltage_r]]áµ€ This is typically the control input of the last timestep from a
+ * LTVDiffDriveController.
  *
- *  <p>y = [[x, y, theta]]^T from a global measurement source(ex. vision),
- *  or y = [[dist_l, dist_r, theta]] from encoders and gyro.
- *
+ * <p>y = x
  */
 public class DifferentialDrivetrainSim {
   private final DCMotor m_motor;
   private final double m_originalGearing;
+  private final Matrix<N7, N1> m_measurementStdDevs;
   private double m_currentGearing;
   private final double m_wheelRadiusMeters;
+
   @SuppressWarnings("MemberName")
   private Matrix<N2, N1> m_u;
+
   @SuppressWarnings("MemberName")
   private Matrix<N7, N1> m_x;
 
+  @SuppressWarnings("MemberName")
+  private Matrix<N7, N1> m_y;
+
   private final double m_rb;
   private final LinearSystem<N2, N2, N2> m_plant;
 
   /**
    * Create a SimDrivetrain.
    *
-   * @param driveMotor        A {@link DCMotor} representing the left side of the drivetrain.
-   * @param gearing           The gearing on the drive between motor and wheel, as output over input.
-   *                          This must be the same ratio as the ratio used to identify or
-   *                          create the drivetrainPlant.
-   * @param jKgMetersSquared  The moment of inertia of the drivetrain about its center.
-   * @param massKg            The mass of the drivebase.
+   * @param driveMotor A {@link DCMotor} representing the left side of the drivetrain.
+   * @param gearing The gearing ratio between motor and wheel, as output over input. This must be
+   *     the same ratio as the ratio used to identify or create the drivetrainPlant.
+   * @param jKgMetersSquared The moment of inertia of the drivetrain about its center.
+   * @param massKg The mass of the drivebase.
    * @param wheelRadiusMeters The radius of the wheels on the drivetrain.
-   * @param trackWidthMeters  The robot's track width, or distance between left and right wheels.
+   * @param trackWidthMeters The robot's track width, or distance between left and right wheels.
+   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
+   *     left velocity, right velocity, left distance, right distance]áµ€. Can be null if no noise is
+   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
+   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
+   *     point.
    */
-  public DifferentialDrivetrainSim(DCMotor driveMotor, double gearing,
-                                   double jKgMetersSquared, double massKg,
-                                   double wheelRadiusMeters, double trackWidthMeters) {
-    this(LinearSystemId.createDrivetrainVelocitySystem(driveMotor, massKg, wheelRadiusMeters,
-        trackWidthMeters / 2.0, jKgMetersSquared, gearing),
-        driveMotor, gearing, trackWidthMeters, wheelRadiusMeters);
+  @SuppressWarnings("ParameterName")
+  public DifferentialDrivetrainSim(
+      DCMotor driveMotor,
+      double gearing,
+      double jKgMetersSquared,
+      double massKg,
+      double wheelRadiusMeters,
+      double trackWidthMeters,
+      Matrix<N7, N1> measurementStdDevs) {
+    this(
+        LinearSystemId.createDrivetrainVelocitySystem(
+            driveMotor,
+            massKg,
+            wheelRadiusMeters,
+            trackWidthMeters / 2.0,
+            jKgMetersSquared,
+            gearing),
+        driveMotor,
+        gearing,
+        trackWidthMeters,
+        wheelRadiusMeters,
+        measurementStdDevs);
   }
 
   /**
-   * Create a SimDrivetrain
-   * .
-   * @param drivetrainPlant   The {@link LinearSystem} representing the robot's drivetrain. This
-   *                          system can be created with {@link edu.wpi.first.wpilibj.system.plant.LinearSystemId#createDrivetrainVelocitySystem(DCMotor, double, double, double, double, double)}
-   *                          or {@link edu.wpi.first.wpilibj.system.plant.LinearSystemId#identifyDrivetrainSystem(double, double, double, double)}.
-   * @param driveMotor        A {@link DCMotor} representing the drivetrain.
-   * @param gearing           The gearingRatio ratio of the robot, as output over input.
-   *                          This must be the same ratio as the ratio used to identify or
-   *                          create the drivetrainPlant.
-   * @param trackWidthMeters  The distance between the two sides of the drivetrian. Can be
-   *                          found with frc-characterization.
+   * Create a SimDrivetrain .
+   *
+   * @param drivetrainPlant The {@link LinearSystem} representing the robot's drivetrain. This
+   *     system can be created with {@link
+   *     edu.wpi.first.math.system.plant.LinearSystemId#createDrivetrainVelocitySystem(DCMotor,
+   *     double, double, double, double, double)} or {@link
+   *     edu.wpi.first.math.system.plant.LinearSystemId#identifyDrivetrainSystem(double, double,
+   *     double, double)}.
+   * @param driveMotor A {@link DCMotor} representing the drivetrain.
+   * @param gearing The gearingRatio ratio of the robot, as output over input. This must be the same
+   *     ratio as the ratio used to identify or create the drivetrainPlant.
+   * @param trackWidthMeters The distance between the two sides of the drivetrian. Can be found with
+   *     SysId.
    * @param wheelRadiusMeters The radius of the wheels on the drivetrain, in meters.
+   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
+   *     left velocity, right velocity, left distance, right distance]áµ€. Can be null if no noise is
+   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
+   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
+   *     point.
    */
-  public DifferentialDrivetrainSim(LinearSystem<N2, N2, N2> drivetrainPlant,
-                                   DCMotor driveMotor, double gearing,
-                                   double trackWidthMeters,
-                                   double wheelRadiusMeters) {
+  public DifferentialDrivetrainSim(
+      LinearSystem<N2, N2, N2> drivetrainPlant,
+      DCMotor driveMotor,
+      double gearing,
+      double trackWidthMeters,
+      double wheelRadiusMeters,
+      Matrix<N7, N1> measurementStdDevs) {
     this.m_plant = drivetrainPlant;
     this.m_rb = trackWidthMeters / 2.0;
     this.m_motor = driveMotor;
     this.m_originalGearing = gearing;
+    this.m_measurementStdDevs = measurementStdDevs;
     m_wheelRadiusMeters = wheelRadiusMeters;
     m_currentGearing = m_originalGearing;
 
     m_x = new Matrix<>(Nat.N7(), Nat.N1());
     m_u = VecBuilder.fill(0, 0);
+    m_y = new Matrix<>(Nat.N7(), Nat.N1());
   }
 
   /**
-   * Sets the applied voltage to the drivetrain. Note that positive voltage must make that
-   * side of the drivetrain travel forward (+X).
+   * Sets the applied voltage to the drivetrain. Note that positive voltage must make that side of
+   * the drivetrain travel forward (+X).
    *
-   * @param leftVoltageVolts  The left voltage.
+   * @param leftVoltageVolts The left voltage.
    * @param rightVoltageVolts The right voltage.
    */
   public void setInputs(double leftVoltageVolts, double rightVoltageVolts) {
-    m_u = VecBuilder.fill(leftVoltageVolts, rightVoltageVolts);
-  }
-
-  @SuppressWarnings("LocalVariableName")
-  public void update(double dtSeconds) {
-
-    // Update state estimate with RK4
-    m_x = RungeKutta.rungeKutta(this::getDynamics, m_x, m_u, dtSeconds);
-  }
-
-  public double getState(State state) {
-    return m_x.get(state.value, 0);
+    m_u = clampInput(VecBuilder.fill(leftVoltageVolts, rightVoltageVolts));
   }
 
   /**
-   * Returns the full simulated state of the drivetrain.
+   * Update the drivetrain states with the current time difference.
+   *
+   * @param dtSeconds the time difference
    */
-  public Matrix<N7, N1> getState() {
+  @SuppressWarnings("LocalVariableName")
+  public void update(double dtSeconds) {
+    // Update state estimate with RK4
+    m_x = NumericalIntegration.rk4(this::getDynamics, m_x, m_u, dtSeconds);
+    m_y = m_x;
+    if (m_measurementStdDevs != null) {
+      m_y = m_y.plus(StateSpaceUtil.makeWhiteNoiseVector(m_measurementStdDevs));
+    }
+  }
+
+  /** Returns the full simulated state of the drivetrain. */
+  Matrix<N7, N1> getState() {
     return m_x;
   }
 
   /**
+   * Get one of the drivetrain states.
+   *
+   * @param state the state to get
+   * @return the state
+   */
+  double getState(State state) {
+    return m_x.get(state.value, 0);
+  }
+
+  private double getOutput(State output) {
+    return m_y.get(output.value, 0);
+  }
+
+  /**
    * Returns the direction the robot is pointing.
    *
-   * <p>Note that this angle is counterclockwise-positive, while most gyros are
-   * clockwise positive.
+   * <p>Note that this angle is counterclockwise-positive, while most gyros are clockwise positive.
+   *
+   * @return The direction the robot is pointing.
    */
   public Rotation2d getHeading() {
-    return new Rotation2d(getState(State.kHeading));
+    return new Rotation2d(getOutput(State.kHeading));
   }
 
   /**
    * Returns the current pose.
+   *
+   * @return The current pose.
    */
   public Pose2d getPose() {
-    return new Pose2d(m_x.get(0, 0),
-      m_x.get(1, 0),
-      new Rotation2d(m_x.get(2, 0)));
+    return new Pose2d(getOutput(State.kX), getOutput(State.kY), getHeading());
   }
 
+  /**
+   * Get the right encoder position in meters.
+   *
+   * @return The encoder position.
+   */
+  public double getRightPositionMeters() {
+    return getOutput(State.kRightPosition);
+  }
+
+  /**
+   * Get the right encoder velocity in meters per second.
+   *
+   * @return The encoder velocity.
+   */
+  public double getRightVelocityMetersPerSecond() {
+    return getOutput(State.kRightVelocity);
+  }
+
+  /**
+   * Get the left encoder position in meters.
+   *
+   * @return The encoder position.
+   */
+  public double getLeftPositionMeters() {
+    return getOutput(State.kLeftPosition);
+  }
+
+  /**
+   * Get the left encoder velocity in meters per second.
+   *
+   * @return The encoder velocity.
+   */
+  public double getLeftVelocityMetersPerSecond() {
+    return getOutput(State.kLeftVelocity);
+  }
+
+  /**
+   * Get the current draw of the left side of the drivetrain.
+   *
+   * @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;
+  }
+
+  /**
+   * Get the current draw of the right side of the drivetrain.
+   *
+   * @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;
+  }
+
+  /**
+   * Get the current draw of the drivetrain.
+   *
+   * @return the current draw, in amps
+   */
   public double getCurrentDrawAmps() {
-    var loadIleft = m_motor.getCurrent(
-        getState(State.kLeftVelocity) * m_currentGearing / m_wheelRadiusMeters,
-        m_u.get(0, 0)) * Math.signum(m_u.get(0, 0));
-
-    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 loadIleft + loadIright;
+    return getLeftCurrentDrawAmps() + getRightCurrentDrawAmps();
   }
 
+  /**
+   * Get the drivetrain gearing.
+   *
+   * @return the gearing ration
+   */
   public double getCurrentGearing() {
     return m_currentGearing;
   }
 
   /**
-   * Sets the gearing reduction on the drivetrain. This is commonly used for
-   * shifting drivetrains.
+   * Sets the gearing reduction on the drivetrain. This is commonly used for shifting drivetrains.
    *
    * @param newGearRatio The new gear ratio, as output over input.
    */
@@ -197,9 +316,8 @@
     m_x.set(State.kRightPosition.value, 0, 0);
   }
 
-  @SuppressWarnings({"DuplicatedCode", "LocalVariableName"})
+  @SuppressWarnings({"DuplicatedCode", "LocalVariableName", "ParameterName"})
   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.
     var B = new Matrix<>(Nat.N4(), Nat.N2());
@@ -208,8 +326,14 @@
     // Because G^2 can be factored out of A, we can divide by the old ratio squared and multiply
     // by the new ratio squared to get a new drivetrain model.
     var A = new Matrix<>(Nat.N4(), Nat.N4());
-    A.assignBlock(0, 0, m_plant.getA().times((this.m_currentGearing * this.m_currentGearing)
-        / (this.m_originalGearing * this.m_originalGearing)));
+    A.assignBlock(
+        0,
+        0,
+        m_plant
+            .getA()
+            .times(
+                (this.m_currentGearing * this.m_currentGearing)
+                    / (this.m_originalGearing * this.m_originalGearing)));
 
     A.assignBlock(2, 0, Matrix.eye(Nat.N2()));
 
@@ -218,16 +342,29 @@
     var xdot = new Matrix<>(Nat.N7(), Nat.N1());
     xdot.set(0, 0, v * Math.cos(x.get(State.kHeading.value, 0)));
     xdot.set(1, 0, v * Math.sin(x.get(State.kHeading.value, 0)));
-    xdot.set(2, 0, (x.get(State.kRightVelocity.value, 0)
-        - x.get(State.kLeftVelocity.value, 0)) / (2.0 * m_rb));
-    xdot.assignBlock(3, 0,
-        A.times(x.block(Nat.N4(), Nat.N1(), 3, 0))
-        .plus(B.times(u)));
+    xdot.set(
+        2,
+        0,
+        (x.get(State.kRightVelocity.value, 0) - x.get(State.kLeftVelocity.value, 0))
+            / (2.0 * m_rb));
+    xdot.assignBlock(3, 0, A.times(x.block(Nat.N4(), Nat.N1(), 3, 0)).plus(B.times(u)));
 
     return xdot;
   }
 
-  public enum State {
+  /**
+   * Clamp the input vector such that no element exceeds the battery voltage. If any does, the
+   * relative magnitudes of the input will be maintained.
+   *
+   * @param u The input vector.
+   * @return The normalized input.
+   */
+  protected Matrix<N2, N1> clampInput(Matrix<N2, N1> u) {
+    return StateSpaceUtil.normalizeInputVector(u, RobotController.getBatteryVoltage());
+  }
+
+  /** Represents the different states of the drivetrain. */
+  enum State {
     kX(0),
     kY(1),
     kHeading(2),
@@ -239,18 +376,15 @@
     @SuppressWarnings("MemberName")
     public final int value;
 
+    @SuppressWarnings("ParameterName")
     State(int i) {
       this.value = i;
     }
   }
 
   /**
-   * Represents a gearing option of the Toughbox mini.
-   * 12.75:1 -- 14:50 and 14:50
-   * 10.71:1 -- 14:50 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
+   * Represents a gearing option of the Toughbox mini. 12.75:1 -- 14:50 and 14:50 10.71:1 -- 14:50
+   * 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 {
     k12p75(12.75),
@@ -262,11 +396,13 @@
     @SuppressWarnings("MemberName")
     public final double value;
 
+    @SuppressWarnings("ParameterName")
     KitbotGearing(double i) {
       this.value = i;
     }
   }
 
+  /** Represents common motor layouts of the kit drivetrain. */
   public enum KitbotMotor {
     kSingleCIMPerSide(DCMotor.getCIM(1)),
     kDualCIMPerSide(DCMotor.getCIM(2)),
@@ -276,19 +412,29 @@
     @SuppressWarnings("MemberName")
     public final DCMotor value;
 
+    @SuppressWarnings("ParameterName")
     KitbotMotor(DCMotor i) {
       this.value = i;
     }
   }
 
+  /** Represents common wheel sizes of the kit drivetrain. */
   public enum KitbotWheelSize {
+    kSixInch(Units.inchesToMeters(6)),
+    kEightInch(Units.inchesToMeters(8)),
+    kTenInch(Units.inchesToMeters(10)),
+
+    @Deprecated
     SixInch(Units.inchesToMeters(6)),
+    @Deprecated
     EightInch(Units.inchesToMeters(8)),
+    @Deprecated
     TenInch(Units.inchesToMeters(10));
 
     @SuppressWarnings("MemberName")
     public final double value;
 
+    @SuppressWarnings("ParameterName")
     KitbotWheelSize(double i) {
       this.value = i;
     }
@@ -297,32 +443,59 @@
   /**
    * Create a sim for the standard FRC kitbot.
    *
-   * @param motor     The motors installed in the bot.
-   * @param gearing   The gearing reduction used.
+   * @param motor The motors installed in the bot.
+   * @param gearing The gearing reduction used.
    * @param wheelSize The wheel size.
+   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
+   *     left velocity, right velocity, left distance, right distance]áµ€. Can be null if no noise is
+   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
+   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
+   *     point.
+   * @return A sim for the standard FRC kitbot.
    */
-  public static DifferentialDrivetrainSim createKitbotSim(KitbotMotor motor, KitbotGearing gearing,
-                                                          KitbotWheelSize wheelSize) {
+  public static DifferentialDrivetrainSim createKitbotSim(
+      KitbotMotor motor,
+      KitbotGearing gearing,
+      KitbotWheelSize wheelSize,
+      Matrix<N7, N1> measurementStdDevs) {
     // MOI estimation -- note that I = m r^2 for point masses
     var batteryMoi = 12.5 / 2.2 * Math.pow(Units.inchesToMeters(10), 2);
-    var gearboxMoi = (2.8 /* CIM motor */ * 2 / 2.2 + 2.0 /* Toughbox Mini- ish */)
-        * Math.pow(Units.inchesToMeters(26.0 / 2.0), 2);
+    var gearboxMoi =
+        (2.8 /* CIM motor */ * 2 / 2.2 + 2.0 /* Toughbox Mini- ish */)
+            * Math.pow(Units.inchesToMeters(26.0 / 2.0), 2);
 
-    return createKitbotSim(motor, gearing, wheelSize, batteryMoi + gearboxMoi);
+    return createKitbotSim(motor, gearing, wheelSize, batteryMoi + gearboxMoi, measurementStdDevs);
   }
 
   /**
    * Create a sim for the standard FRC kitbot.
    *
-   * @param motor            The motors installed in the bot.
-   * @param gearing          The gearing reduction used.
-   * @param wheelSize        The wheel size.
+   * @param motor The motors installed in the bot.
+   * @param gearing The gearing reduction used.
+   * @param wheelSize The wheel size.
    * @param jKgMetersSquared The moment of inertia of the drivebase. This can be calculated using
-   *                         frc-characterization.
+   *     SysId.
+   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
+   *     left velocity, right velocity, left distance, right distance]áµ€. Can be null if no noise is
+   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
+   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
+   *     point.
+   * @return A sim for the standard FRC kitbot.
    */
-  public static DifferentialDrivetrainSim createKitbotSim(KitbotMotor motor, KitbotGearing gearing,
-                                                          KitbotWheelSize wheelSize, double jKgMetersSquared) {
-    return new DifferentialDrivetrainSim(motor.value, gearing.value, jKgMetersSquared, 25 / 2.2,
-        wheelSize.value / 2.0, Units.inchesToMeters(26));
+  @SuppressWarnings("ParameterName")
+  public static DifferentialDrivetrainSim createKitbotSim(
+      KitbotMotor motor,
+      KitbotGearing gearing,
+      KitbotWheelSize wheelSize,
+      double jKgMetersSquared,
+      Matrix<N7, N1> measurementStdDevs) {
+    return new DifferentialDrivetrainSim(
+        motor.value,
+        gearing.value,
+        jKgMetersSquared,
+        Units.lbsToKilograms(60),
+        wheelSize.value / 2.0,
+        Units.inchesToMeters(26),
+        measurementStdDevs);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSim.java
index db942eb..f18f092 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -15,8 +12,8 @@
 /**
  * Class to control a simulated digital PWM output.
  *
- * <p>This is for duty cycle PWM outputs on a DigitalOutput, not for the servo
- * style PWM outputs on a PWM channel.
+ * <p>This is for duty cycle PWM outputs on a DigitalOutput, not for the servo style PWM outputs on
+ * a PWM channel.
  */
 public class DigitalPWMSim {
   private final int m_index;
@@ -50,8 +47,8 @@
   }
 
   /**
-   * Creates an DigitalPWMSim for a simulated index.
-   * The index is incremented for each simulated DigitalPWM.
+   * Creates an DigitalPWMSim for a simulated index. The index is incremented for each simulated
+   * DigitalPWM.
    *
    * @param index simulator index
    * @return Simulated object
@@ -60,39 +57,100 @@
     return new DigitalPWMSim(index);
   }
 
+  /**
+   * Register a callback to be run when this PWM output is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DigitalPWMDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DigitalPWMDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this PWM output has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return DigitalPWMDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this PWM output has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     DigitalPWMDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever the duty cycle value 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 CallbackStore registerDutyCycleCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DigitalPWMDataJNI.registerDutyCycleCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DigitalPWMDataJNI::cancelDutyCycleCallback);
   }
+
+  /**
+   * Read the duty cycle value.
+   *
+   * @return the duty cycle value of this PWM output
+   */
   public double getDutyCycle() {
     return DigitalPWMDataJNI.getDutyCycle(m_index);
   }
+
+  /**
+   * Set the duty cycle value of this PWM output.
+   *
+   * @param dutyCycle the new value
+   */
   public void setDutyCycle(double dutyCycle) {
     DigitalPWMDataJNI.setDutyCycle(m_index, dutyCycle);
   }
 
+  /**
+   * Register a callback to be run whenever the pin 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 CallbackStore registerPinCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DigitalPWMDataJNI.registerPinCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DigitalPWMDataJNI::cancelPinCallback);
   }
+
+  /**
+   * Check the pin number.
+   *
+   * @return the pin number
+   */
   public int getPin() {
     return DigitalPWMDataJNI.getPin(m_index);
   }
+
+  /**
+   * Change the pin number.
+   *
+   * @param pin the new pin number
+   */
   public void setPin(int pin) {
     DigitalPWMDataJNI.setPin(m_index, pin);
   }
 
+  /** Reset all simulation data. */
   public void resetData() {
     DigitalPWMDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DriverStationSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DriverStationSim.java
index 40b1017..f6da659 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DriverStationSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DriverStationSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,81 +9,223 @@
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.wpilibj.DriverStation;
 
-/**
- * Class to control a simulated driver station.
- */
-@SuppressWarnings({"PMD.UseUtilityClass", "PMD.GodClass", "PMD.ExcessivePublicCount"})
-public class DriverStationSim {
-  public static CallbackStore registerEnabledCallback(NotifyCallback callback, boolean initialNotify) {
+/** Class to control a simulated driver station. */
+public final class DriverStationSim {
+  private DriverStationSim() {
+    throw new UnsupportedOperationException("This is a utility class!");
+  }
+
+  /**
+   * Register a callback on whether the DS is enabled.
+   *
+   * @param callback the callback that will be called whenever the enabled state is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerEnabledCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerEnabledCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelEnabledCallback);
   }
+
+  /**
+   * Check if the DS is enabled.
+   *
+   * @return true if enabled
+   */
   public static boolean getEnabled() {
     return DriverStationDataJNI.getEnabled();
   }
+
+  /**
+   * Change whether the DS is enabled.
+   *
+   * @param enabled the new value
+   */
   public static void setEnabled(boolean enabled) {
     DriverStationDataJNI.setEnabled(enabled);
   }
 
-  public static CallbackStore registerAutonomousCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on whether the DS is in autonomous mode.
+   *
+   * @param callback the callback that will be called on autonomous mode entrance/exit
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerAutonomousCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerAutonomousCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelAutonomousCallback);
   }
+
+  /**
+   * Check if the DS is in autonomous.
+   *
+   * @return true if autonomous
+   */
   public static boolean getAutonomous() {
     return DriverStationDataJNI.getAutonomous();
   }
+
+  /**
+   * Change whether the DS is in autonomous.
+   *
+   * @param autonomous the new value
+   */
   public static void setAutonomous(boolean autonomous) {
     DriverStationDataJNI.setAutonomous(autonomous);
   }
 
+  /**
+   * Register a callback on whether the DS is in test mode.
+   *
+   * @param callback the callback that will be called whenever the test mode is entered or left
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerTestCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerTestCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelTestCallback);
   }
+
+  /**
+   * Check if the DS is in test.
+   *
+   * @return true if test
+   */
   public static boolean getTest() {
     return DriverStationDataJNI.getTest();
   }
+
+  /**
+   * Change whether the DS is in test.
+   *
+   * @param test the new value
+   */
   public static void setTest(boolean test) {
     DriverStationDataJNI.setTest(test);
   }
 
-  public static CallbackStore registerEStopCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the eStop state.
+   *
+   * @param callback the callback that will be called whenever the eStop state changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerEStopCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerEStopCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelEStopCallback);
   }
+
+  /**
+   * Check if eStop has been activated.
+   *
+   * @return true if eStopped
+   */
   public static boolean getEStop() {
     return DriverStationDataJNI.getEStop();
   }
+
+  /**
+   * Set whether eStop is active.
+   *
+   * @param eStop true to activate
+   */
+  @SuppressWarnings("ParameterName")
   public static void setEStop(boolean eStop) {
     DriverStationDataJNI.setEStop(eStop);
   }
 
-  public static CallbackStore registerFmsAttachedCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on whether the FMS is connected.
+   *
+   * @param callback the callback that will be called whenever the FMS connection changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerFmsAttachedCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerFmsAttachedCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelFmsAttachedCallback);
   }
+
+  /**
+   * Check if the FMS is connected.
+   *
+   * @return true if FMS is connected
+   */
   public static boolean getFmsAttached() {
     return DriverStationDataJNI.getFmsAttached();
   }
+
+  /**
+   * Change whether the FMS is connected.
+   *
+   * @param fmsAttached the new value
+   */
   public static void setFmsAttached(boolean fmsAttached) {
     DriverStationDataJNI.setFmsAttached(fmsAttached);
   }
 
-  public static CallbackStore registerDsAttachedCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on whether the DS is connected.
+   *
+   * @param callback the callback that will be called whenever the DS connection changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerDsAttachedCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerDsAttachedCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelDsAttachedCallback);
   }
+
+  /**
+   * Check if the DS is attached.
+   *
+   * @return true if attached
+   */
   public static boolean getDsAttached() {
     return DriverStationDataJNI.getDsAttached();
   }
+
+  /**
+   * Change whether the DS is attached.
+   *
+   * @param dsAttached the new value
+   */
   public static void setDsAttached(boolean dsAttached) {
     DriverStationDataJNI.setDsAttached(dsAttached);
   }
 
-  public static CallbackStore registerAllianceStationIdCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the alliance station ID.
+   *
+   * @param callback the callback that will be called whenever the alliance station changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerAllianceStationIdCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerAllianceStationIdCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelAllianceStationIdCallback);
   }
+
+  /**
+   * Get the alliance station ID (color + number).
+   *
+   * @return the alliance station color and number
+   */
   public static AllianceStationID getAllianceStationId() {
     switch (DriverStationDataJNI.getAllianceStationId()) {
       case 0:
@@ -105,6 +244,12 @@
         return null;
     }
   }
+
+  /**
+   * Change the alliance station.
+   *
+   * @param allianceStationId the new alliance station
+   */
   public static void setAllianceStationId(AllianceStationID allianceStationId) {
     int allianceStation;
     switch (allianceStationId) {
@@ -132,24 +277,42 @@
     DriverStationDataJNI.setAllianceStationId(allianceStation);
   }
 
-  public static CallbackStore registerMatchTimeCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on match time.
+   *
+   * @param callback the callback that will be called whenever match time changes
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @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 registerMatchTimeCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = DriverStationDataJNI.registerMatchTimeCallback(callback, initialNotify);
     return new CallbackStore(uid, DriverStationDataJNI::cancelMatchTimeCallback);
   }
+
+  /**
+   * Get the current value of the match timer.
+   *
+   * @return the current match time
+   */
   public static double getMatchTime() {
     return DriverStationDataJNI.getMatchTime();
   }
+
+  /**
+   * Sets the match timer.
+   *
+   * @param matchTime the new match time
+   */
   public static void setMatchTime(double matchTime) {
     DriverStationDataJNI.setMatchTime(matchTime);
   }
 
-  /**
-   * Updates DriverStation data so that new values are visible to the user
-   * program.
-   */
+  /** Updates DriverStation data so that new values are visible to the user program. */
   public static void notifyNewData() {
     DriverStationDataJNI.notifyNewData();
-    DriverStation.getInstance().waitForData();
+    DriverStation.waitForData();
   }
 
   /**
@@ -194,7 +357,7 @@
   /**
    * Sets the state of one joystick button. Button indexes begin at 1.
    *
-   * @param stick  The joystick number
+   * @param stick The joystick number
    * @param button The button index, beginning at 1
    * @param state The state of the joystick button
    */
@@ -206,7 +369,7 @@
    * Gets the value of the axis on a joystick.
    *
    * @param stick The joystick number
-   * @param axis  The analog axis number
+   * @param axis The analog axis number
    * @param value The value of the axis on the joystick
    */
   public static void setJoystickAxis(int stick, int axis, double value) {
@@ -367,6 +530,7 @@
     DriverStationDataJNI.setReplayNumber(replayNumber);
   }
 
+  /** Reset all simulation data for the Driver Station. */
   public static void resetData() {
     DriverStationDataJNI.resetData();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSim.java
index 92b0883..f5c900c 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSim.java
@@ -1,18 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.wpilibj.DutyCycleEncoder;
 
-/**
- * Class to control a simulated duty cycle encoder.
- */
+/** Class to control a simulated duty cycle encoder. */
 public class DutyCycleEncoderSim {
   private final SimDouble m_simPosition;
   private final SimDouble m_simDistancePerRotation;
@@ -23,9 +18,10 @@
    * @param encoder DutyCycleEncoder to simulate
    */
   public DutyCycleEncoderSim(DutyCycleEncoder encoder) {
-    SimDeviceSim wrappedSimDevice = new SimDeviceSim("DutyCycleEncoder" + "[" + encoder.getFPGAIndex() + "]");
-    m_simPosition = wrappedSimDevice.getDouble("Position");
-    m_simDistancePerRotation = wrappedSimDevice.getDouble("DistancePerRotation");
+    SimDeviceSim wrappedSimDevice =
+        new SimDeviceSim("DutyCycle:DutyCycleEncoder" + "[" + encoder.getSourceChannel() + "]");
+    m_simPosition = wrappedSimDevice.getDouble("position");
+    m_simDistancePerRotation = wrappedSimDevice.getDouble("distance_per_rot");
   }
 
   /**
@@ -39,6 +35,8 @@
 
   /**
    * Set the position.
+   *
+   * @param distance The position.
    */
   public void setDistance(double distance) {
     m_simPosition.set(distance / m_simDistancePerRotation.get());
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleSim.java
index 726e08c..e893954 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/DutyCycleSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,9 +9,7 @@
 import edu.wpi.first.wpilibj.DutyCycle;
 import java.util.NoSuchElementException;
 
-/**
- * Class to control a simulated duty cycle digital input.
- */
+/** Class to control a simulated duty cycle digital input. */
 public class DutyCycleSim {
   private final int m_index;
 
@@ -47,8 +42,8 @@
   }
 
   /**
-   * Creates a DutyCycleSim for a simulated index.
-   * The index is incremented for each simulated DutyCycle.
+   * Creates a DutyCycleSim for a simulated index. The index is incremented for each simulated
+   * DutyCycle.
    *
    * @param index simulator index
    * @return Simulated object
@@ -57,36 +52,101 @@
     return new DutyCycleSim(index);
   }
 
+  /**
+   * Register a callback to be run when this duty cycle input is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DutyCycleDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DutyCycleDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this duty cycle input has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return DutyCycleDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this duty cycle input has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     DutyCycleDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever the frequency 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 CallbackStore registerFrequencyCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DutyCycleDataJNI.registerFrequencyCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DutyCycleDataJNI::cancelFrequencyCallback);
   }
+
+  /**
+   * Measure the frequency.
+   *
+   * @return the duty cycle frequency
+   */
   public int getFrequency() {
     return DutyCycleDataJNI.getFrequency(m_index);
   }
+
+  /**
+   * Change the duty cycle frequency.
+   *
+   * @param frequency the new frequency
+   */
   public void setFrequency(int frequency) {
     DutyCycleDataJNI.setFrequency(m_index, frequency);
   }
 
+  /**
+   * Register a callback to be run whenever the output 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 CallbackStore registerOutputCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = DutyCycleDataJNI.registerOutputCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, DutyCycleDataJNI::cancelOutputCallback);
   }
+
+  /**
+   * Measure the output from this duty cycle port.
+   *
+   * @return the output value
+   */
   public double getOutput() {
     return DutyCycleDataJNI.getOutput(m_index);
   }
+
+  /**
+   * Change the duty cycle output.
+   *
+   * @param output the new output value
+   */
   public void setOutput(double output) {
     DutyCycleDataJNI.setOutput(m_index, output);
   }
+
+  /** Reset all simulation data for the duty cycle output. */
+  public void resetData() {
+    DutyCycleDataJNI.resetData(m_index);
+  }
 }
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 1610089..ecd2379 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
@@ -1,24 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.system.LinearSystem;
-import edu.wpi.first.wpilibj.system.RungeKutta;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-import edu.wpi.first.wpiutil.math.numbers.N1;
-import edu.wpi.first.wpiutil.math.numbers.N2;
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.numbers.N2;
+import edu.wpi.first.math.system.LinearSystem;
+import edu.wpi.first.math.system.NumericalIntegration;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
 
-/**
- * Represents a simulated elevator mechanism.
- */
+/** Represents a simulated elevator mechanism. */
 public class ElevatorSim extends LinearSystemSim<N2, N1, N1> {
   // Gearbox for the elevator.
   private final DCMotor m_gearbox;
@@ -38,32 +33,42 @@
   /**
    * Creates a simulated elevator mechanism.
    *
-   * @param plant            The linear system that represents the elevator.
-   * @param gearbox          The type of and number of motors in the elevator gearbox.
-   * @param gearing          The gearing of the elevator (numbers greater than 1 represent reductions).
+   * @param plant The linear system that represents the elevator.
+   * @param gearbox The type of and number of motors in the elevator gearbox.
+   * @param gearing The gearing of the elevator (numbers greater than 1 represent reductions).
    * @param drumRadiusMeters The radius of the drum that the elevator spool is wrapped around.
-   * @param minHeightMeters  The min allowable height of the elevator.
-   * @param maxHeightMeters  The max allowable height of the elevator.
+   * @param minHeightMeters The min allowable height of the elevator.
+   * @param maxHeightMeters The max allowable height of the elevator.
    */
-  public ElevatorSim(LinearSystem<N2, N1, N1> plant, DCMotor gearbox, double gearing,
-                     double drumRadiusMeters, double minHeightMeters, double maxHeightMeters) {
+  public ElevatorSim(
+      LinearSystem<N2, N1, N1> plant,
+      DCMotor gearbox,
+      double gearing,
+      double drumRadiusMeters,
+      double minHeightMeters,
+      double maxHeightMeters) {
     this(plant, gearbox, gearing, drumRadiusMeters, minHeightMeters, maxHeightMeters, null);
   }
 
   /**
    * Creates a simulated elevator mechanism.
    *
-   * @param plant              The linear system that represents the elevator.
-   * @param gearbox            The type of and number of motors in the elevator gearbox.
-   * @param gearing            The gearing of the elevator (numbers greater than 1 represent reductions).
-   * @param drumRadiusMeters   The radius of the drum that the elevator spool is wrapped around.
-   * @param minHeightMeters    The min allowable height of the elevator.
-   * @param maxHeightMeters    The max allowable height of the elevator.
+   * @param plant The linear system that represents the elevator.
+   * @param gearbox The type of and number of motors in the elevator gearbox.
+   * @param gearing The gearing of the elevator (numbers greater than 1 represent reductions).
+   * @param drumRadiusMeters The radius of the drum that the elevator spool is wrapped around.
+   * @param minHeightMeters The min allowable height of the elevator.
+   * @param maxHeightMeters The max allowable height of the elevator.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public ElevatorSim(LinearSystem<N2, N1, N1> plant, DCMotor gearbox, double gearing,
-                     double drumRadiusMeters, double minHeightMeters, double maxHeightMeters,
-                     Matrix<N1, N1> measurementStdDevs) {
+  public ElevatorSim(
+      LinearSystem<N2, N1, N1> plant,
+      DCMotor gearbox,
+      double gearing,
+      double drumRadiusMeters,
+      double minHeightMeters,
+      double maxHeightMeters,
+      Matrix<N1, N1> measurementStdDevs) {
     super(plant, measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
@@ -75,34 +80,45 @@
   /**
    * Creates a simulated elevator mechanism.
    *
-   * @param gearbox          The type of and number of motors in the elevator gearbox.
-   * @param gearing          The gearing of the elevator (numbers greater than 1 represent reductions).
-   * @param carriageMassKg   The mass of the elevator carriage.
+   * @param gearbox The type of and number of motors in the elevator gearbox.
+   * @param gearing The gearing of the elevator (numbers greater than 1 represent reductions).
+   * @param carriageMassKg The mass of the elevator carriage.
    * @param drumRadiusMeters The radius of the drum that the elevator spool is wrapped around.
-   * @param minHeightMeters  The min allowable height of the elevator.
-   * @param maxHeightMeters  The max allowable height of the elevator.
+   * @param minHeightMeters The min allowable height of the elevator.
+   * @param maxHeightMeters The max allowable height of the elevator.
    */
-  public ElevatorSim(DCMotor gearbox, double gearing, double carriageMassKg,
-                     double drumRadiusMeters, double minHeightMeters, double maxHeightMeters) {
-    this(gearbox, gearing, carriageMassKg, drumRadiusMeters, minHeightMeters, maxHeightMeters,
-        null);
+  public ElevatorSim(
+      DCMotor gearbox,
+      double gearing,
+      double carriageMassKg,
+      double drumRadiusMeters,
+      double minHeightMeters,
+      double maxHeightMeters) {
+    this(
+        gearbox, gearing, carriageMassKg, drumRadiusMeters, minHeightMeters, maxHeightMeters, null);
   }
 
   /**
    * Creates a simulated elevator mechanism.
    *
-   * @param gearbox            The type of and number of motors in the elevator gearbox.
-   * @param gearing            The gearing of the elevator (numbers greater than 1 represent reductions).
-   * @param carriageMassKg     The mass of the elevator carriage.
-   * @param drumRadiusMeters   The radius of the drum that the elevator spool is wrapped around.
-   * @param minHeightMeters    The min allowable height of the elevator.
-   * @param maxHeightMeters    The max allowable height of the elevator.
+   * @param gearbox The type of and number of motors in the elevator gearbox.
+   * @param gearing The gearing of the elevator (numbers greater than 1 represent reductions).
+   * @param carriageMassKg The mass of the elevator carriage.
+   * @param drumRadiusMeters The radius of the drum that the elevator spool is wrapped around.
+   * @param minHeightMeters The min allowable height of the elevator.
+   * @param maxHeightMeters The max allowable height of the elevator.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public ElevatorSim(DCMotor gearbox, double gearing, double carriageMassKg,
-                     double drumRadiusMeters, double minHeightMeters, double maxHeightMeters,
-                     Matrix<N1, N1> measurementStdDevs) {
-    super(LinearSystemId.createElevatorSystem(gearbox, carriageMassKg, drumRadiusMeters, gearing),
+  public ElevatorSim(
+      DCMotor gearbox,
+      double gearing,
+      double carriageMassKg,
+      double drumRadiusMeters,
+      double minHeightMeters,
+      double maxHeightMeters,
+      Matrix<N1, N1> measurementStdDevs) {
+    super(
+        LinearSystemId.createElevatorSystem(gearbox, carriageMassKg, drumRadiusMeters, gearing),
         measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
@@ -112,23 +128,41 @@
   }
 
   /**
+   * Returns whether the elevator would hit the lower limit.
+   *
+   * @param elevatorHeightMeters The elevator height.
+   * @return Whether the elevator would hit the lower limit.
+   */
+  public boolean wouldHitLowerLimit(double elevatorHeightMeters) {
+    return elevatorHeightMeters < this.m_minHeight;
+  }
+
+  /**
+   * Returns whether the elevator would hit the upper limit.
+   *
+   * @param elevatorHeightMeters The elevator height.
+   * @return Whether the elevator would hit the upper limit.
+   */
+  public boolean wouldHitUpperLimit(double elevatorHeightMeters) {
+    return elevatorHeightMeters > this.m_maxHeight;
+  }
+
+  /**
    * Returns whether the elevator has hit the lower limit.
    *
-   * @param x The current elevator state.
    * @return Whether the elevator has hit the lower limit.
    */
-  public boolean hasHitLowerLimit(Matrix<N2, N1> x) {
-    return x.get(0, 0) < this.m_minHeight;
+  public boolean hasHitLowerLimit() {
+    return wouldHitLowerLimit(getPositionMeters());
   }
 
   /**
    * Returns whether the elevator has hit the upper limit.
    *
-   * @param x The current elevator state.
    * @return Whether the elevator has hit the upper limit.
    */
-  public boolean hasHitUpperLimit(Matrix<N2, N1> x) {
-    return x.get(0, 0) > this.m_maxHeight;
+  public boolean hasHitUpperLimit() {
+    return wouldHitUpperLimit(getPositionMeters());
   }
 
   /**
@@ -146,7 +180,7 @@
    * @return The velocity of the elevator.
    */
   public double getVelocityMetersPerSecond() {
-    return m_x.get(0, 1);
+    return m_x.get(1, 0);
   }
 
   /**
@@ -162,7 +196,8 @@
     // v = r w, so w = v/r
     double motorVelocityRadPerSec = getVelocityMetersPerSecond() / m_drumRadius * m_gearing;
     var appliedVoltage = m_u.get(0, 0);
-    return m_gearbox.getCurrent(motorVelocityRadPerSec, appliedVoltage) * Math.signum(appliedVoltage);
+    return m_gearbox.getCurrent(motorVelocityRadPerSec, appliedVoltage)
+        * Math.signum(appliedVoltage);
   }
 
   /**
@@ -178,22 +213,28 @@
    * Updates the state of the elevator.
    *
    * @param currentXhat The current state estimate.
-   * @param u           The system inputs (voltage).
-   * @param dtSeconds   The time difference between controller updates.
+   * @param u The system inputs (voltage).
+   * @param dtSeconds The time difference between controller updates.
    */
+  @SuppressWarnings({"ParameterName", "LambdaParameterName"})
   @Override
-  protected Matrix<N2, N1> updateX(Matrix<N2, N1> currentXhat,
-                                   Matrix<N1, N1> u, double dtSeconds) {
+  protected Matrix<N2, N1> updateX(Matrix<N2, N1> currentXhat, Matrix<N1, N1> u, double dtSeconds) {
     // Calculate updated x-hat from Runge-Kutta.
-    var updatedXhat = RungeKutta.rungeKutta(
-        (x, u_) -> (m_plant.getA().times(x)).plus(m_plant.getB().times(u_))
-            .plus(VecBuilder.fill(0, -9.8)), currentXhat, u, dtSeconds);
+    var updatedXhat =
+        NumericalIntegration.rkdp(
+            (x, u_) ->
+                (m_plant.getA().times(x))
+                    .plus(m_plant.getB().times(u_))
+                    .plus(VecBuilder.fill(0, -9.8)),
+            currentXhat,
+            u,
+            dtSeconds);
 
     // We check for collisions after updating x-hat.
-    if (hasHitLowerLimit(updatedXhat)) {
+    if (wouldHitLowerLimit(updatedXhat.get(0, 0))) {
       return VecBuilder.fill(m_minHeight, 0);
     }
-    if (hasHitUpperLimit(updatedXhat)) {
+    if (wouldHitUpperLimit(updatedXhat.get(0, 0))) {
       return VecBuilder.fill(m_maxHeight, 0);
     }
     return updatedXhat;
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/EncoderSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/EncoderSim.java
index 9df5187..924ba99 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/EncoderSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/EncoderSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,9 +9,7 @@
 import edu.wpi.first.wpilibj.Encoder;
 import java.util.NoSuchElementException;
 
-/**
- * Class to control a simulated encoder.
- */
+/** Class to control a simulated encoder. */
 public class EncoderSim {
   private final int m_index;
 
@@ -32,8 +27,8 @@
   }
 
   /**
-   * Creates an EncoderSim for a digital input channel.  Encoders take two
-   * channels, so either one may be specified.
+   * Creates an EncoderSim for a digital input channel. Encoders take two channels, so either one
+   * may be specified.
    *
    * @param channel digital input channel
    * @return Simulated object
@@ -48,8 +43,8 @@
   }
 
   /**
-   * Creates an EncoderSim for a simulated index.
-   * The index is incremented for each simulated Encoder.
+   * Creates an EncoderSim for a simulated index. The index is incremented for each simulated
+   * Encoder.
    *
    * @param index simulator index
    * @return Simulated object
@@ -58,110 +53,293 @@
     return new EncoderSim(index);
   }
 
+  /**
+   * Register a callback on the Initialized property of the encoder.
+   *
+   * @param callback the callback that will be called whenever the Initialized property is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Read the Initialized value of the encoder.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return EncoderDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Change the Initialized value of the encoder.
+   *
+   * @param initialized the new value
+   */
   public void setInitialized(boolean initialized) {
     EncoderDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback on the count property of the encoder.
+   *
+   * @param callback the callback that will be called whenever the count property is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerCountCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerCountCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelCountCallback);
   }
+
+  /**
+   * Read the count of the encoder.
+   *
+   * @return the count
+   */
   public int getCount() {
     return EncoderDataJNI.getCount(m_index);
   }
+
+  /**
+   * Change the count of the encoder.
+   *
+   * @param count the new count
+   */
   public void setCount(int count) {
     EncoderDataJNI.setCount(m_index, count);
   }
 
+  /**
+   * Register a callback on the period of the encoder.
+   *
+   * @param callback the callback that will be called whenever the period is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerPeriodCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerPeriodCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelPeriodCallback);
   }
+
+  /**
+   * Read the period of the encoder.
+   *
+   * @return the encoder period
+   */
   public double getPeriod() {
     return EncoderDataJNI.getPeriod(m_index);
   }
+
+  /**
+   * Change the encoder period.
+   *
+   * @param period the new period
+   */
   public void setPeriod(double period) {
     EncoderDataJNI.setPeriod(m_index, period);
   }
 
+  /**
+   * Register a callback to be called whenever the encoder is reset.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerResetCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerResetCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelResetCallback);
   }
+
+  /**
+   * Check if the encoder has been reset.
+   *
+   * @return true if reset
+   */
   public boolean getReset() {
     return EncoderDataJNI.getReset(m_index);
   }
+
+  /**
+   * Change the reset property of the encoder.
+   *
+   * @param reset the new value
+   */
   public void setReset(boolean reset) {
     EncoderDataJNI.setReset(m_index, reset);
   }
 
+  /**
+   * Register a callback to be run whenever the max period of the encoder is changed.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerMaxPeriodCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerMaxPeriodCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelMaxPeriodCallback);
   }
+
+  /**
+   * Get the max period of the encoder.
+   *
+   * @return the max period of the encoder
+   */
   public double getMaxPeriod() {
     return EncoderDataJNI.getMaxPeriod(m_index);
   }
+
+  /**
+   * Change the max period of the encoder.
+   *
+   * @param maxPeriod the new value
+   */
   public void setMaxPeriod(double maxPeriod) {
     EncoderDataJNI.setMaxPeriod(m_index, maxPeriod);
   }
 
+  /**
+   * Register a callback on the direction of the encoder.
+   *
+   * @param callback the callback that will be called whenever the direction is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerDirectionCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerDirectionCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelDirectionCallback);
   }
+
+  /**
+   * Get the direction of the encoder.
+   *
+   * @return the direction of the encoder
+   */
   public boolean getDirection() {
     return EncoderDataJNI.getDirection(m_index);
   }
+
+  /**
+   * Set the direction of the encoder.
+   *
+   * @param direction the new direction
+   */
   public void setDirection(boolean direction) {
     EncoderDataJNI.setDirection(m_index, direction);
   }
 
-  public CallbackStore registerReverseDirectionCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the reverse direction.
+   *
+   * @param callback the callback that will be called whenever the reverse direction is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerReverseDirectionCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerReverseDirectionCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelReverseDirectionCallback);
   }
+
+  /**
+   * Get the reverse direction of the encoder.
+   *
+   * @return the reverse direction of the encoder
+   */
   public boolean getReverseDirection() {
     return EncoderDataJNI.getReverseDirection(m_index);
   }
+
+  /**
+   * Set the reverse direction.
+   *
+   * @param reverseDirection the new value
+   */
   public void setReverseDirection(boolean reverseDirection) {
     EncoderDataJNI.setReverseDirection(m_index, reverseDirection);
   }
 
-  public CallbackStore registerSamplesToAverageCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback on the samples-to-average value of this encoder.
+   *
+   * @param callback the callback that will be called whenever the samples-to-average is changed
+   * @param initialNotify if true, the callback will be run on the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerSamplesToAverageCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = EncoderDataJNI.registerSamplesToAverageCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, EncoderDataJNI::cancelSamplesToAverageCallback);
   }
+
+  /**
+   * Get the samples-to-average value.
+   *
+   * @return the samples-to-average value
+   */
   public int getSamplesToAverage() {
     return EncoderDataJNI.getSamplesToAverage(m_index);
   }
+
+  /**
+   * Set the samples-to-average value.
+   *
+   * @param samplesToAverage the new value
+   */
   public void setSamplesToAverage(int samplesToAverage) {
     EncoderDataJNI.setSamplesToAverage(m_index, samplesToAverage);
   }
 
+  /**
+   * Change the encoder distance.
+   *
+   * @param distance the new distance
+   */
   public void setDistance(double distance) {
     EncoderDataJNI.setDistance(m_index, distance);
   }
 
+  /**
+   * Read the distance of the encoder.
+   *
+   * @return the encoder distance
+   */
   public double getDistance() {
     return EncoderDataJNI.getDistance(m_index);
   }
 
+  /**
+   * Change the rate of the encoder.
+   *
+   * @param rate the new rate
+   */
   public void setRate(double rate) {
     EncoderDataJNI.setRate(m_index, rate);
   }
 
+  /**
+   * Get the rate of the encoder.
+   *
+   * @return the rate of change
+   */
   public double getRate() {
     return EncoderDataJNI.getRate(m_index);
   }
 
+  /** Resets all simulation data for this encoder. */
   public void resetData() {
     EncoderDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Field2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Field2d.java
deleted file mode 100644
index bbda95a..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Field2d.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.simulation;
-
-import edu.wpi.first.hal.SimDevice;
-import edu.wpi.first.hal.SimDouble;
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.geometry.Translation2d;
-
-/**
- * 2D representation of game field (for simulation).
- *
- * <p>In non-simulation mode this simply stores and returns the robot pose.
- *
- * <p>The robot pose is the actual location shown on the simulation view.
- * This may or may not match the robot's internal odometry.  For example, if
- * the robot is shown at a particular starting location, the pose in this
- * class would represent the actual location on the field, but the robot's
- * internal state might have a 0,0,0 pose (unless it's initialized to
- * something different).
- *
- * <p>As the user is able to edit the pose, code performing updates should get
- * the robot pose, transform it as appropriate (e.g. based on simulated wheel
- * velocity), and set the new pose.
- */
-public class Field2d {
-  public Field2d() {
-    m_device = SimDevice.create("Field2D");
-    if (m_device != null) {
-      m_x = m_device.createDouble("x", false, 0.0);
-      m_y = m_device.createDouble("y", false, 0.0);
-      m_rot = m_device.createDouble("rot", false, 0.0);
-    }
-  }
-
-  /**
-   * Set the robot pose from a Pose object.
-   *
-   * @param pose 2D pose
-   */
-  public void setRobotPose(Pose2d pose) {
-    if (m_device != null) {
-      Translation2d translation = pose.getTranslation();
-      m_x.set(translation.getX());
-      m_y.set(translation.getY());
-      m_rot.set(pose.getRotation().getDegrees());
-    } else {
-      m_pose = pose;
-    }
-  }
-
-  /**
-   * Set the robot pose from x, y, and rotation.
-   *
-   * @param xMeters X location, in meters
-   * @param yMeters Y location, in meters
-   * @param rotation rotation
-   */
-  public void setRobotPose(double xMeters, double yMeters, Rotation2d rotation) {
-    if (m_device != null) {
-      m_x.set(xMeters);
-      m_y.set(yMeters);
-      m_rot.set(rotation.getDegrees());
-    } else {
-      m_pose = new Pose2d(xMeters, yMeters, rotation);
-    }
-  }
-
-  /**
-   * Get the robot pose.
-   *
-   * @return 2D pose
-   */
-  public Pose2d getRobotPose() {
-    if (m_device != null) {
-      return new Pose2d(m_x.get(), m_y.get(), Rotation2d.fromDegrees(m_rot.get()));
-    } else {
-      return m_pose;
-    }
-  }
-
-  private Pose2d m_pose;
-
-  @SuppressWarnings("MemberName")
-  private final SimDevice m_device;
-
-  @SuppressWarnings("MemberName")
-  private SimDouble m_x;
-
-  @SuppressWarnings("MemberName")
-  private SimDouble m_y;
-
-  private SimDouble m_rot;
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/FlywheelSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/FlywheelSim.java
index 785facc..aaf13de 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/FlywheelSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/FlywheelSim.java
@@ -1,22 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.system.LinearSystem;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpilibj.util.Units;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.numbers.N1;
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.system.LinearSystem;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
+import edu.wpi.first.math.util.Units;
 
-/**
- * Represents a simulated flywheel mechanism.
- */
+/** Represents a simulated flywheel mechanism. */
 public class FlywheelSim extends LinearSystemSim<N1, N1, N1> {
   // Gearbox for the flywheel.
   private final DCMotor m_gearbox;
@@ -27,10 +22,9 @@
   /**
    * Creates a simulated flywheel mechanism.
    *
-   * @param plant   The linear system that represents the flywheel.
+   * @param plant The linear system that represents the flywheel.
    * @param gearbox The type of and number of motors in the flywheel gearbox.
-   * @param gearing The gearing of the flywheel (numbers greater than 1
-   *                represent reductions).
+   * @param gearing The gearing of the flywheel (numbers greater than 1 represent reductions).
    */
   public FlywheelSim(LinearSystem<N1, N1, N1> plant, DCMotor gearbox, double gearing) {
     super(plant);
@@ -41,15 +35,16 @@
   /**
    * Creates a simulated flywheel mechanism.
    *
-   * @param plant              The linear system that represents the flywheel.
-   * @param gearbox            The type of and number of motors in the flywheel
-   *                           gearbox.
-   * @param gearing            The gearing of the flywheel (numbers greater
-   *                           than 1 represent reductions).
+   * @param plant The linear system that represents the flywheel.
+   * @param gearbox The type of and number of motors in the flywheel gearbox.
+   * @param gearing The gearing of the flywheel (numbers greater than 1 represent reductions).
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public FlywheelSim(LinearSystem<N1, N1, N1> plant, DCMotor gearbox, double gearing,
-                     Matrix<N1, N1> measurementStdDevs) {
+  public FlywheelSim(
+      LinearSystem<N1, N1, N1> plant,
+      DCMotor gearbox,
+      double gearing,
+      Matrix<N1, N1> measurementStdDevs) {
     super(plant, measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
@@ -58,15 +53,14 @@
   /**
    * Creates a simulated flywheel mechanism.
    *
-   * @param gearbox          The type of and number of motors in the flywheel gearbox.
-   * @param gearing          The gearing of the flywheel (numbers greater than 1
-   *                         represent reductions).
-   * @param jKgMetersSquared The moment of inertia of the flywheel. If this is unknown,
-   *                         use the {@link #FlywheelSim(LinearSystem, DCMotor, double, Matrix)}
-   *                         constructor.
+   * @param gearbox The type of and number of motors in the flywheel gearbox.
+   * @param gearing The gearing of the flywheel (numbers greater than 1 represent reductions).
+   * @param jKgMetersSquared The moment of inertia of the flywheel. If this is unknown, use the
+   *     {@link #FlywheelSim(LinearSystem, DCMotor, double, Matrix)} constructor.
    */
+  @SuppressWarnings("ParameterName")
   public FlywheelSim(DCMotor gearbox, double gearing, double jKgMetersSquared) {
-    super(LinearSystemId.createFlywheelSystem(gearbox, gearing, jKgMetersSquared));
+    super(LinearSystemId.createFlywheelSystem(gearbox, jKgMetersSquared, gearing));
     m_gearbox = gearbox;
     m_gearing = gearing;
   }
@@ -74,18 +68,17 @@
   /**
    * Creates a simulated flywheel mechanism.
    *
-   * @param gearbox            The type of and number of motors in the flywheel
-   *                           gearbox.
-   * @param gearing            The gearing of the flywheel (numbers greater
-   *                           than 1 represent reductions).
-   * @param jKgMetersSquared   The moment of inertia of the flywheel. If this is unknown,
-   *                           use the {@link #FlywheelSim(LinearSystem, DCMotor, double, Matrix)}
-   *                           constructor.
+   * @param gearbox The type of and number of motors in the flywheel gearbox.
+   * @param gearing The gearing of the flywheel (numbers greater than 1 represent reductions).
+   * @param jKgMetersSquared The moment of inertia of the flywheel. If this is unknown, use the
+   *     {@link #FlywheelSim(LinearSystem, DCMotor, double, Matrix)} constructor.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public FlywheelSim(DCMotor gearbox, double gearing, double jKgMetersSquared,
-                     Matrix<N1, N1> measurementStdDevs) {
-    super(LinearSystemId.createFlywheelSystem(gearbox, gearing, jKgMetersSquared),
+  @SuppressWarnings("ParameterName")
+  public FlywheelSim(
+      DCMotor gearbox, double gearing, double jKgMetersSquared, Matrix<N1, N1> measurementStdDevs) {
+    super(
+        LinearSystemId.createFlywheelSystem(gearbox, jKgMetersSquared, gearing),
         measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
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 506be52..c5878ca 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
@@ -1,17 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.wpilibj.GenericHID;
 
-/**
- * Class to control a simulated generic joystick.
- */
+/** Class to control a simulated generic joystick. */
 public class GenericHIDSim {
   protected final int m_port;
 
@@ -33,65 +28,135 @@
     m_port = port;
   }
 
-  /**
-   * Updates joystick data so that new values are visible to the user program.
-   */
+  /** Updates joystick data so that new values are visible to the user program. */
   public void notifyNewData() {
     DriverStationSim.notifyNewData();
   }
 
+  /**
+   * Set the value of a given button.
+   *
+   * @param button the button to set
+   * @param value the new value
+   */
   public void setRawButton(int button, boolean value) {
     DriverStationSim.setJoystickButton(m_port, button, value);
   }
 
+  /**
+   * Set the value of a given axis.
+   *
+   * @param axis the axis to set
+   * @param value the new value
+   */
   public void setRawAxis(int axis, double value) {
     DriverStationSim.setJoystickAxis(m_port, axis, value);
   }
 
+  /**
+   * Set the value of a given POV.
+   *
+   * @param pov the POV to set
+   * @param value the new value
+   */
   public void setPOV(int pov, int value) {
     DriverStationSim.setJoystickPOV(m_port, pov, value);
   }
 
+  /**
+   * Set the value of the default POV (port 0).
+   *
+   * @param value the new value
+   */
   public void setPOV(int value) {
     setPOV(0, value);
   }
 
+  /**
+   * Set the axis count of this device.
+   *
+   * @param count the new axis count
+   */
   public void setAxisCount(int count) {
     DriverStationSim.setJoystickAxisCount(m_port, count);
   }
 
+  /**
+   * Set the POV count of this device.
+   *
+   * @param count the new POV count
+   */
   public void setPOVCount(int count) {
     DriverStationSim.setJoystickPOVCount(m_port, count);
   }
 
+  /**
+   * Set the button count of this device.
+   *
+   * @param count the new button count
+   */
   public void setButtonCount(int count) {
     DriverStationSim.setJoystickButtonCount(m_port, count);
   }
 
+  /**
+   * Set the type of this device.
+   *
+   * @param type the new device type
+   */
   public void setType(GenericHID.HIDType type) {
     DriverStationSim.setJoystickType(m_port, type.value);
   }
 
+  /**
+   * Set the name of this device.
+   *
+   * @param name the new device name
+   */
   public void setName(String name) {
     DriverStationSim.setJoystickName(m_port, name);
   }
 
+  /**
+   * Set the type of an axis.
+   *
+   * @param axis the axis
+   * @param type the type
+   */
   public void setAxisType(int axis, int type) {
     DriverStationSim.setJoystickAxisType(m_port, axis, type);
   }
 
+  /**
+   * Read the output of a button.
+   *
+   * @param outputNumber the button number
+   * @return the value of the button (true = pressed)
+   */
   public boolean getOutput(int outputNumber) {
     long outputs = getOutputs();
     return (outputs & (1 << (outputNumber - 1))) != 0;
   }
 
+  /**
+   * Get the encoded 16-bit integer that passes button values.
+   *
+   * @return the button values
+   */
   public long getOutputs() {
     return DriverStationSim.getJoystickOutputs(m_port);
   }
 
+  /**
+   * Get the joystick rumble.
+   *
+   * @param type the rumble to read
+   * @return the rumble value
+   */
   public double getRumble(GenericHID.RumbleType type) {
-    int value = DriverStationSim.getJoystickRumble(
-        m_port, type == GenericHID.RumbleType.kLeftRumble ? 0 : 1);
+    int value =
+        DriverStationSim.getJoystickRumble(
+            m_port, type == GenericHID.RumbleType.kLeftRumble ? 0 : 1);
     return value / 65535.0;
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/I2CSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/I2CSim.java
index 1252eeb..986e310 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/I2CSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/I2CSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -12,34 +9,75 @@
 import edu.wpi.first.hal.simulation.I2CDataJNI;
 import edu.wpi.first.hal.simulation.NotifyCallback;
 
+/** A class to control a simulated I2C device. */
 public class I2CSim {
   private final int m_index;
 
+  /**
+   * Construct a new I2C simulation object.
+   *
+   * @param index the HAL index of the I2C object
+   */
   public I2CSim(int index) {
     m_index = index;
   }
 
+  /**
+   * Register a callback to be run when this I2C device is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = I2CDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, I2CDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this I2C device has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return I2CDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this I2C device has been initialized.
+   *
+   * @param initialized whether this device is initialized
+   */
   public void setInitialized(boolean initialized) {
     I2CDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever a `read` operation is done.
+   *
+   * @param callback the callback that is run on `read` operations
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerReadCallback(BufferCallback callback) {
     int uid = I2CDataJNI.registerReadCallback(m_index, callback);
     return new CallbackStore(m_index, uid, I2CDataJNI::cancelReadCallback);
   }
 
+  /**
+   * Register a callback to be run whenever a `write` operation is done.
+   *
+   * @param callback the callback that is run on `write` operations
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerWriteCallback(ConstBufferCallback callback) {
     int uid = I2CDataJNI.registerWriteCallback(m_index, callback);
     return new CallbackStore(m_index, uid, I2CDataJNI::cancelWriteCallback);
   }
 
+  /** Reset all I2C simulation data. */
   public void resetData() {
     I2CDataJNI.resetData(m_index);
   }
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 d1d7100..f55b038 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
@@ -1,17 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.wpilibj.Joystick;
 
-/**
- * Class to control a simulated joystick.
- */
+/** Class to control a simulated joystick. */
 public class JoystickSim extends GenericHIDSim {
   private Joystick m_joystick;
 
@@ -42,34 +37,68 @@
     setPOVCount(1);
   }
 
+  /**
+   * Set the X value of the joystick.
+   *
+   * @param value the new X value
+   */
   public void setX(double value) {
     setRawAxis(m_joystick != null ? m_joystick.getXChannel() : Joystick.kDefaultXChannel, value);
   }
 
+  /**
+   * Set the Y value of the joystick.
+   *
+   * @param value the new Y value
+   */
   public void setY(double value) {
     setRawAxis(m_joystick != null ? m_joystick.getYChannel() : Joystick.kDefaultYChannel, value);
   }
 
+  /**
+   * Set the Z value of the joystick.
+   *
+   * @param value the new Z value
+   */
   public void setZ(double value) {
     setRawAxis(m_joystick != null ? m_joystick.getZChannel() : Joystick.kDefaultZChannel, value);
   }
 
+  /**
+   * Set the twist value of the joystick.
+   *
+   * @param value the new twist value
+   */
   public void setTwist(double value) {
     setRawAxis(
-        m_joystick != null ? m_joystick.getTwistChannel() : Joystick.kDefaultTwistChannel,
-        value);
+        m_joystick != null ? m_joystick.getTwistChannel() : Joystick.kDefaultTwistChannel, value);
   }
 
+  /**
+   * Set the throttle value of the joystick.
+   *
+   * @param value the new throttle value
+   */
   public void setThrottle(double value) {
     setRawAxis(
         m_joystick != null ? m_joystick.getThrottleChannel() : Joystick.kDefaultThrottleChannel,
         value);
   }
 
+  /**
+   * Set the trigger value of the joystick.
+   *
+   * @param state the new value
+   */
   public void setTrigger(boolean state) {
     setRawButton(1, state);
   }
 
+  /**
+   * Set the top state of the joystick.
+   *
+   * @param state the new state
+   */
   public void setTop(boolean state) {
     setRawButton(2, state);
   }
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 dc69ffa..2b4e9a9 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
@@ -1,24 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Num;
+import edu.wpi.first.math.StateSpaceUtil;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.system.LinearSystem;
+import edu.wpi.first.wpilibj.RobotController;
 import org.ejml.MatrixDimensionException;
 import org.ejml.simple.SimpleMatrix;
 
-import edu.wpi.first.wpilibj.math.StateSpaceUtil;
-import edu.wpi.first.wpilibj.system.LinearSystem;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.Num;
-import edu.wpi.first.wpiutil.math.numbers.N1;
-
 /**
- * This class helps simulate linear systems. To use this class, do the following in the
- * {@link edu.wpi.first.wpilibj.IterativeRobotBase#simulationPeriodic} method.
+ * This class helps simulate linear systems. To use this class, do the following in the {@link
+ * edu.wpi.first.wpilibj.IterativeRobotBase#simulationPeriodic} method.
  *
  * <p>Call {@link #setInput(double...)} with the inputs to the system (usually voltage).
  *
@@ -26,8 +23,8 @@
  *
  * <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 <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.
  */
 @SuppressWarnings("ClassTypeParameterName")
@@ -38,8 +35,10 @@
   // Variables for state, output, and input.
   @SuppressWarnings("MemberName")
   protected Matrix<States, N1> m_x;
+
   @SuppressWarnings("MemberName")
   protected Matrix<Outputs, N1> m_y;
+
   @SuppressWarnings("MemberName")
   protected Matrix<Inputs, N1> m_u;
 
@@ -59,11 +58,12 @@
   /**
    * Creates a simulated generic linear system with measurement noise.
    *
-   * @param system             The system being controlled.
-   * @param measurementStdDevs Standard deviations of measurements.
+   * @param system The system being controlled.
+   * @param measurementStdDevs Standard deviations of measurements. Can be null if no noise is
+   *     desired.
    */
-  public LinearSystemSim(LinearSystem<States, Inputs, Outputs> system,
-                         Matrix<Outputs, N1> measurementStdDevs) {
+  public LinearSystemSim(
+      LinearSystem<States, Inputs, Outputs> system, Matrix<Outputs, N1> measurementStdDevs) {
     this.m_plant = system;
     this.m_measurementStdDevs = measurementStdDevs;
 
@@ -115,18 +115,20 @@
    *
    * @param u The system inputs.
    */
+  @SuppressWarnings("ParameterName")
   public void setInput(Matrix<Inputs, N1> u) {
-    this.m_u = u;
+    this.m_u = clampInput(u);
   }
 
   /**
    * Sets the system inputs.
    *
-   * @param row   The row in the input matrix to set.
+   * @param row The row in the input matrix to set.
    * @param value The value to set the row to.
    */
   public void setInput(int row, double value) {
     m_u.set(row, 0, value);
+    m_u = clampInput(m_u);
   }
 
   /**
@@ -134,10 +136,11 @@
    *
    * @param u An array of doubles that represent the inputs of the system.
    */
+  @SuppressWarnings("ParameterName")
   public void setInput(double... u) {
     if (u.length != m_u.getNumRows()) {
-      throw new MatrixDimensionException("Malformed input! Got " + u.length
-          + " elements instead of " + m_u.getNumRows());
+      throw new MatrixDimensionException(
+          "Malformed input! Got " + u.length + " elements instead of " + m_u.getNumRows());
     }
     m_u = new Matrix<>(new SimpleMatrix(m_u.getNumRows(), 1, true, u));
   }
@@ -165,12 +168,24 @@
    * Updates the state estimate of the system.
    *
    * @param currentXhat The current state estimate.
-   * @param u           The system inputs (usually voltage).
-   * @param dtSeconds   The time difference between controller updates.
+   * @param u The system inputs (usually voltage).
+   * @param dtSeconds The time difference between controller updates.
    * @return The new state.
    */
-  protected Matrix<States, N1> updateX(Matrix<States, N1> currentXhat,
-                                       Matrix<Inputs, N1> u, double dtSeconds) {
+  @SuppressWarnings("ParameterName")
+  protected Matrix<States, N1> updateX(
+      Matrix<States, N1> currentXhat, Matrix<Inputs, N1> u, double dtSeconds) {
     return m_plant.calculateX(currentXhat, u, dtSeconds);
   }
+
+  /**
+   * Clamp the input vector such that no element exceeds the given voltage. If any does, the
+   * relative magnitudes of the input will be maintained.
+   *
+   * @param u The input vector.
+   * @return The normalized input.
+   */
+  protected Matrix<Inputs, N1> clampInput(Matrix<Inputs, N1> u) {
+    return StateSpaceUtil.normalizeInputVector(u, RobotController.getBatteryVoltage());
+  }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Mechanism2D.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Mechanism2D.java
deleted file mode 100644
index 7006c91..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/Mechanism2D.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.simulation;
-
-import edu.wpi.first.hal.SimDevice;
-import edu.wpi.first.hal.SimDouble;
-import java.util.HashMap;
-import java.util.Map;
-
-public class Mechanism2D {
-  private static final SimDevice m_device = SimDevice.create("Mechanism2D");
-  private static final Map<String, SimDouble> m_createdItems = new HashMap<String, SimDouble>();
-
-  /**
-     * Set/Create the angle of a ligament.
-     *
-     * @param ligamentPath json path to the ligament
-     * @param angle        to set the ligament
-     */
-  public void setLigamentAngle(String ligamentPath, float angle) {
-    ligamentPath = ligamentPath + "/angle";
-    if (m_device != null) {
-      if (!m_createdItems.containsKey(ligamentPath)) {
-        m_createdItems.put(ligamentPath, m_device.createDouble(ligamentPath, false, angle));
-      }
-      m_createdItems.get(ligamentPath).set(angle);
-    }
-  }
-
-  /**
-   * Set/Create the length of a ligament.
-   *
-   * @param ligamentPath json path to the ligament
-   * @param length       to set the ligament
-   */
-  public void setLigamentLength(String ligamentPath, float length) {
-    ligamentPath = ligamentPath + "/length";
-    if (m_device != null) {
-      if (!m_createdItems.containsKey(ligamentPath)) {
-        m_createdItems.put(ligamentPath, m_device.createDouble(ligamentPath, false, length));
-      }
-      m_createdItems.get(ligamentPath).set(length);
-    }
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/NotifierSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/NotifierSim.java
index 3778e36..a5be97f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/NotifierSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/NotifierSim.java
@@ -1,20 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.simulation.NotifierDataJNI;
 
-/**
- * Class to control simulated notifiers.
- */
+/** Class to control simulated notifiers. */
 public final class NotifierSim {
-  private NotifierSim() {
-  }
+  private NotifierSim() {}
 
   /**
    * Gets the timeout of the next notifier.
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PCMSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PCMSim.java
deleted file mode 100644
index 7881f0c..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PCMSim.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.simulation;
-
-import edu.wpi.first.hal.simulation.NotifyCallback;
-import edu.wpi.first.hal.simulation.PCMDataJNI;
-import edu.wpi.first.wpilibj.Compressor;
-import edu.wpi.first.wpilibj.SensorUtil;
-
-/**
- * Class to control a simulated Pneumatic Control Module (PCM).
- */
-public class PCMSim {
-  private final int m_index;
-
-  /**
-   * Constructs for the default PCM.
-   */
-  public PCMSim() {
-    m_index = SensorUtil.getDefaultSolenoidModule();
-  }
-
-  /**
-   * Constructs from a PCM module number (CAN ID).
-   *
-   * @param module module number
-   */
-  public PCMSim(int module) {
-    m_index = module;
-  }
-
-  /**
-   * Constructs from a Compressor object.
-   *
-   * @param compressor Compressor connected to PCM to simulate
-   */
-  public PCMSim(Compressor compressor) {
-    m_index = compressor.getModule();
-  }
-
-  public CallbackStore registerSolenoidInitializedCallback(int channel, NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerSolenoidInitializedCallback(m_index, channel, callback, initialNotify);
-    return new CallbackStore(m_index, channel, uid, PCMDataJNI::cancelSolenoidInitializedCallback);
-  }
-  public boolean getSolenoidInitialized(int channel) {
-    return PCMDataJNI.getSolenoidInitialized(m_index, channel);
-  }
-  public void setSolenoidInitialized(int channel, boolean solenoidInitialized) {
-    PCMDataJNI.setSolenoidInitialized(m_index, channel, solenoidInitialized);
-  }
-
-  public CallbackStore registerSolenoidOutputCallback(int channel, NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerSolenoidOutputCallback(m_index, channel, callback, initialNotify);
-    return new CallbackStore(m_index, channel, uid, PCMDataJNI::cancelSolenoidOutputCallback);
-  }
-  public boolean getSolenoidOutput(int channel) {
-    return PCMDataJNI.getSolenoidOutput(m_index, channel);
-  }
-  public void setSolenoidOutput(int channel, boolean solenoidOutput) {
-    PCMDataJNI.setSolenoidOutput(m_index, channel, solenoidOutput);
-  }
-
-  public CallbackStore registerCompressorInitializedCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerCompressorInitializedCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PCMDataJNI::cancelCompressorInitializedCallback);
-  }
-  public boolean getCompressorInitialized() {
-    return PCMDataJNI.getCompressorInitialized(m_index);
-  }
-  public void setCompressorInitialized(boolean compressorInitialized) {
-    PCMDataJNI.setCompressorInitialized(m_index, compressorInitialized);
-  }
-
-  public CallbackStore registerCompressorOnCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerCompressorOnCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PCMDataJNI::cancelCompressorOnCallback);
-  }
-  public boolean getCompressorOn() {
-    return PCMDataJNI.getCompressorOn(m_index);
-  }
-  public void setCompressorOn(boolean compressorOn) {
-    PCMDataJNI.setCompressorOn(m_index, compressorOn);
-  }
-
-  public CallbackStore registerClosedLoopEnabledCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerClosedLoopEnabledCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PCMDataJNI::cancelClosedLoopEnabledCallback);
-  }
-  public boolean getClosedLoopEnabled() {
-    return PCMDataJNI.getClosedLoopEnabled(m_index);
-  }
-  public void setClosedLoopEnabled(boolean closedLoopEnabled) {
-    PCMDataJNI.setClosedLoopEnabled(m_index, closedLoopEnabled);
-  }
-
-  public CallbackStore registerPressureSwitchCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerPressureSwitchCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PCMDataJNI::cancelPressureSwitchCallback);
-  }
-  public boolean getPressureSwitch() {
-    return PCMDataJNI.getPressureSwitch(m_index);
-  }
-  public void setPressureSwitch(boolean pressureSwitch) {
-    PCMDataJNI.setPressureSwitch(m_index, pressureSwitch);
-  }
-
-  public CallbackStore registerCompressorCurrentCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PCMDataJNI.registerCompressorCurrentCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PCMDataJNI::cancelCompressorCurrentCallback);
-  }
-  public double getCompressorCurrent() {
-    return PCMDataJNI.getCompressorCurrent(m_index);
-  }
-  public void setCompressorCurrent(double compressorCurrent) {
-    PCMDataJNI.setCompressorCurrent(m_index, compressorCurrent);
-  }
-
-  public void resetData() {
-    PCMDataJNI.resetData(m_index);
-  }
-}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PDPSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PDPSim.java
index ad57973..eef8ccf 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PDPSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PDPSim.java
@@ -1,25 +1,18 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.simulation.NotifyCallback;
-import edu.wpi.first.hal.simulation.PDPDataJNI;
-import edu.wpi.first.wpilibj.PowerDistributionPanel;
+import edu.wpi.first.hal.simulation.PowerDistributionDataJNI;
+import edu.wpi.first.wpilibj.PowerDistribution;
 
-/**
- * Class to control a simulated Power Distribution Panel (PDP).
- */
+/** Class to control a simulated Power Distribution Panel (PDP). */
 public class PDPSim {
   private final int m_index;
 
-  /**
-   * Constructs for the default PDP.
-   */
+  /** Constructs for the default PDP. */
   public PDPSim() {
     m_index = 0;
   }
@@ -34,59 +27,148 @@
   }
 
   /**
-   * Constructs from a PowerDistributionPanel object.
+   * Constructs from a PowerDistribution object.
    *
-   * @param pdp PowerDistributionPanel to simulate
+   * @param pdp PowerDistribution to simulate
    */
-  public PDPSim(PowerDistributionPanel pdp) {
+  public PDPSim(PowerDistribution pdp) {
     m_index = pdp.getModule();
   }
 
+  /**
+   * Register a callback to be run when the PDP is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PDPDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PDPDataJNI::cancelInitializedCallback);
+    int uid =
+        PowerDistributionDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, PowerDistributionDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether the PDP has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
-    return PDPDataJNI.getInitialized(m_index);
+    return PowerDistributionDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether the PDP has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
-    PDPDataJNI.setInitialized(m_index, initialized);
+    PowerDistributionDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever the PDP temperature 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 CallbackStore registerTemperatureCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PDPDataJNI.registerTemperatureCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PDPDataJNI::cancelTemperatureCallback);
+    int uid =
+        PowerDistributionDataJNI.registerTemperatureCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, PowerDistributionDataJNI::cancelTemperatureCallback);
   }
+
+  /**
+   * Check the temperature of the PDP.
+   *
+   * @return the PDP temperature
+   */
   public double getTemperature() {
-    return PDPDataJNI.getTemperature(m_index);
+    return PowerDistributionDataJNI.getTemperature(m_index);
   }
+
+  /**
+   * Define the PDP temperature.
+   *
+   * @param temperature the new PDP temperature
+   */
   public void setTemperature(double temperature) {
-    PDPDataJNI.setTemperature(m_index, temperature);
+    PowerDistributionDataJNI.setTemperature(m_index, temperature);
   }
 
+  /**
+   * Register a callback to be run whenever the PDP voltage 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 CallbackStore registerVoltageCallback(NotifyCallback callback, boolean initialNotify) {
-    int uid = PDPDataJNI.registerVoltageCallback(m_index, callback, initialNotify);
-    return new CallbackStore(m_index, uid, PDPDataJNI::cancelVoltageCallback);
+    int uid = PowerDistributionDataJNI.registerVoltageCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, PowerDistributionDataJNI::cancelVoltageCallback);
   }
+
+  /**
+   * Check the PDP voltage.
+   *
+   * @return the PDP voltage.
+   */
   public double getVoltage() {
-    return PDPDataJNI.getVoltage(m_index);
+    return PowerDistributionDataJNI.getVoltage(m_index);
   }
+
+  /**
+   * Set the PDP voltage.
+   *
+   * @param voltage the new PDP voltage
+   */
   public void setVoltage(double voltage) {
-    PDPDataJNI.setVoltage(m_index, voltage);
+    PowerDistributionDataJNI.setVoltage(m_index, voltage);
   }
 
-  public CallbackStore registerCurrentCallback(int channel, NotifyCallback callback, boolean initialNotify) {
-    int uid = PDPDataJNI.registerCurrentCallback(m_index, channel, callback, initialNotify);
-    return new CallbackStore(m_index, channel, uid, PDPDataJNI::cancelCurrentCallback);
+  /**
+   * Register a callback to be run whenever the current of a specific channel changes.
+   *
+   * @param channel the channel
+   * @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 CallbackStore registerCurrentCallback(
+      int channel, NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        PowerDistributionDataJNI.registerCurrentCallback(m_index, channel, callback, initialNotify);
+    return new CallbackStore(
+        m_index, channel, uid, PowerDistributionDataJNI::cancelCurrentCallback);
   }
+
+  /**
+   * Read the current in one of the PDP channels.
+   *
+   * @param channel the channel to check
+   * @return the current in the given channel
+   */
   public double getCurrent(int channel) {
-    return PDPDataJNI.getCurrent(m_index, channel);
-  }
-  public void setCurrent(int channel, double current) {
-    PDPDataJNI.setCurrent(m_index, channel, current);
+    return PowerDistributionDataJNI.getCurrent(m_index, channel);
   }
 
+  /**
+   * Change the current in the given channel.
+   *
+   * @param channel the channel to edit
+   * @param current the new current for the channel
+   */
+  public void setCurrent(int channel, double current) {
+    PowerDistributionDataJNI.setCurrent(m_index, channel, current);
+  }
+
+  /** Reset all PDP simulation data. */
   public void resetData() {
-    PDPDataJNI.resetData(m_index);
+    PowerDistributionDataJNI.resetData(m_index);
   }
 }
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
new file mode 100644
index 0000000..69109b8
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PS4ControllerSim.java
@@ -0,0 +1,212 @@
+// 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.simulation;
+
+import edu.wpi.first.wpilibj.PS4Controller;
+
+/** Class to control a simulated PS4 controller. */
+public class PS4ControllerSim extends GenericHIDSim {
+  /**
+   * Constructs from a PS4Controller object.
+   *
+   * @param joystick controller to simulate
+   */
+  public PS4ControllerSim(PS4Controller joystick) {
+    super(joystick);
+    setAxisCount(6);
+    setButtonCount(14);
+  }
+
+  /**
+   * Constructs from a joystick port number.
+   *
+   * @param port port number
+   */
+  public PS4ControllerSim(int port) {
+    super(port);
+    setAxisCount(6);
+    setButtonCount(14);
+  }
+
+  /**
+   * Change the X axis value of the controller's left stick.
+   *
+   * @param value the new value
+   */
+  public void setLeftX(double value) {
+    setRawAxis(PS4Controller.Axis.kLeftX.value, value);
+  }
+
+  /**
+   * Change the X axis value of the controller's right stick.
+   *
+   * @param value the new value
+   */
+  public void setRightX(double value) {
+    setRawAxis(PS4Controller.Axis.kRightX.value, value);
+  }
+
+  /**
+   * Change the Y axis value of the controller's left stick.
+   *
+   * @param value the new value
+   */
+  public void setLeftY(double value) {
+    setRawAxis(PS4Controller.Axis.kLeftY.value, value);
+  }
+
+  /**
+   * Change the Y axis value of the controller's right stick.
+   *
+   * @param value the new value
+   */
+  public void setRightY(double value) {
+    setRawAxis(PS4Controller.Axis.kRightY.value, value);
+  }
+
+  /**
+   * Change the L2 axis axis value of the controller.
+   *
+   * @param value the new value
+   */
+  public void setL2Axis(double value) {
+    setRawAxis(PS4Controller.Axis.kL2.value, value);
+  }
+
+  /**
+   * Change the R2 axis value of the controller.
+   *
+   * @param value the new value
+   */
+  public void setR2Axis(double value) {
+    setRawAxis(PS4Controller.Axis.kR2.value, value);
+  }
+
+  /**
+   * Change the value of the Square button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setSquareButton(boolean value) {
+    setRawButton(PS4Controller.Button.kSquare.value, value);
+  }
+
+  /**
+   * Change the value of the Cross button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setCrossButton(boolean value) {
+    setRawButton(PS4Controller.Button.kCross.value, value);
+  }
+
+  /**
+   * Change the value of the Circle button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setCircleButton(boolean value) {
+    setRawButton(PS4Controller.Button.kCircle.value, value);
+  }
+
+  /**
+   * Change the value of the Triangle button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setTriangleButton(boolean value) {
+    setRawButton(PS4Controller.Button.kTriangle.value, value);
+  }
+
+  /**
+   * Change the value of the L1 button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setL1Button(boolean value) {
+    setRawButton(PS4Controller.Button.kL1.value, value);
+  }
+
+  /**
+   * Change the value of the R1 button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setR1Button(boolean value) {
+    setRawButton(PS4Controller.Button.kR1.value, value);
+  }
+
+  /**
+   * Change the value of the L2 button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setL2Button(boolean value) {
+    setRawButton(PS4Controller.Button.kL2.value, value);
+  }
+
+  /**
+   * Change the value of the R2 button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setR2Button(boolean value) {
+    setRawButton(PS4Controller.Button.kR2.value, value);
+  }
+
+  /**
+   * Change the value of the Share button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setShareButton(boolean value) {
+    setRawButton(PS4Controller.Button.kShare.value, value);
+  }
+
+  /**
+   * Change the value of the Options button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setOptionsButton(boolean value) {
+    setRawButton(PS4Controller.Button.kOptions.value, value);
+  }
+
+  /**
+   * Change the value of the L3 (left stick) button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setL3Button(boolean value) {
+    setRawButton(PS4Controller.Button.kL3.value, value);
+  }
+
+  /**
+   * Change the value of the R3 (right stick) button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setR3Button(boolean value) {
+    setRawButton(PS4Controller.Button.kR3.value, value);
+  }
+
+  /**
+   * Change the value of the PS button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setPSButton(boolean value) {
+    setRawButton(PS4Controller.Button.kPS.value, value);
+  }
+
+  /**
+   * Change the value of the touchpad button on the controller.
+   *
+   * @param value the new value
+   */
+  public void setTouchpad(boolean value) {
+    setRawButton(PS4Controller.Button.kTouchpad.value, value);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PWMSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PWMSim.java
index d0cca92..8a60e52 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PWMSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/PWMSim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,9 +8,7 @@
 import edu.wpi.first.hal.simulation.PWMDataJNI;
 import edu.wpi.first.wpilibj.PWM;
 
-/**
- * Class to control a simulated PWM output.
- */
+/** Class to control a simulated PWM output. */
 public class PWMSim {
   private final int m_index;
 
@@ -35,72 +30,193 @@
     m_index = channel;
   }
 
+  /**
+   * Register a callback to be run when the PWM is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether the PWM has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return PWMDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether the PWM has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     PWMDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run when the PWM raw value changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerRawValueCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerRawValueCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelRawValueCallback);
   }
+
+  /**
+   * Get the PWM raw value.
+   *
+   * @return the PWM raw value
+   */
   public int getRawValue() {
     return PWMDataJNI.getRawValue(m_index);
   }
+
+  /**
+   * Set the PWM raw value.
+   *
+   * @param rawValue the PWM raw value
+   */
   public void setRawValue(int rawValue) {
     PWMDataJNI.setRawValue(m_index, rawValue);
   }
 
+  /**
+   * Register a callback to be run when the PWM speed changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerSpeedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerSpeedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelSpeedCallback);
   }
+
+  /**
+   * Get the PWM speed.
+   *
+   * @return the PWM speed (-1.0 to 1.0)
+   */
   public double getSpeed() {
     return PWMDataJNI.getSpeed(m_index);
   }
+
+  /**
+   * Set the PWM speed.
+   *
+   * @param speed the PWM speed (-1.0 to 1.0)
+   */
   public void setSpeed(double speed) {
     PWMDataJNI.setSpeed(m_index, speed);
   }
 
+  /**
+   * Register a callback to be run when the PWM position changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerPositionCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerPositionCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelPositionCallback);
   }
+
+  /**
+   * Get the PWM position.
+   *
+   * @return the PWM position (0.0 to 1.0)
+   */
   public double getPosition() {
     return PWMDataJNI.getPosition(m_index);
   }
+
+  /**
+   * Set the PWM position.
+   *
+   * @param position the PWM position (0.0 to 1.0)
+   */
   public void setPosition(double position) {
     PWMDataJNI.setPosition(m_index, position);
   }
 
+  /**
+   * Register a callback to be run when the PWM period scale changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run the callback with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
   public CallbackStore registerPeriodScaleCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerPeriodScaleCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelPeriodScaleCallback);
   }
+
+  /**
+   * Get the PWM period scale.
+   *
+   * @return the PWM period scale
+   */
   public int getPeriodScale() {
     return PWMDataJNI.getPeriodScale(m_index);
   }
+
+  /**
+   * Set the PWM period scale.
+   *
+   * @param periodScale the PWM period scale
+   */
   public void setPeriodScale(int periodScale) {
     PWMDataJNI.setPeriodScale(m_index, periodScale);
   }
 
+  /**
+   * Register a callback to be run when the PWM zero latch state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerZeroLatchCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = PWMDataJNI.registerZeroLatchCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, PWMDataJNI::cancelZeroLatchCallback);
   }
+
+  /**
+   * Check whether the PWM is zero latched.
+   *
+   * @return true if zero latched
+   */
   public boolean getZeroLatch() {
     return PWMDataJNI.getZeroLatch(m_index);
   }
+
+  /**
+   * Define whether the PWM has been zero latched.
+   *
+   * @param zeroLatch true to indicate zero latched
+   */
   public void setZeroLatch(boolean zeroLatch) {
     PWMDataJNI.setZeroLatch(m_index, zeroLatch);
   }
 
+  /** Reset all simulation data. */
   public void resetData() {
     PWMDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/REVPHSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/REVPHSim.java
new file mode 100644
index 0000000..8a5f0f8
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/REVPHSim.java
@@ -0,0 +1,239 @@
+// 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.simulation;
+
+import edu.wpi.first.hal.simulation.NotifyCallback;
+import edu.wpi.first.hal.simulation.REVPHDataJNI;
+import edu.wpi.first.wpilibj.PneumaticsBase;
+import edu.wpi.first.wpilibj.SensorUtil;
+
+/** Class to control a simulated PneumaticHub (PH). */
+@SuppressWarnings("AbbreviationAsWordInName")
+public class REVPHSim {
+  private final int m_index;
+
+  /** Constructs for the default PH. */
+  public REVPHSim() {
+    m_index = SensorUtil.getDefaultREVPHModule();
+  }
+
+  /**
+   * Constructs from a PH module number (CAN ID).
+   *
+   * @param module module number
+   */
+  public REVPHSim(int module) {
+    m_index = module;
+  }
+
+  /**
+   * Constructs from a Compressor object.
+   *
+   * @param module PCM module to simulate
+   */
+  public REVPHSim(PneumaticsBase module) {
+    m_index = module.getModuleNumber();
+  }
+
+  /**
+   * Register a callback to be run when the solenoid output on a channel changes.
+   *
+   * @param channel the channel to monitor
+   * @param callback the callback
+   * @param initialNotify should the callback be run with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerSolenoidOutputCallback(
+      int channel, NotifyCallback callback, boolean initialNotify) {
+    int uid =
+        REVPHDataJNI.registerSolenoidOutputCallback(m_index, channel, callback, initialNotify);
+    return new CallbackStore(m_index, channel, uid, REVPHDataJNI::cancelSolenoidOutputCallback);
+  }
+
+  /**
+   * Check the solenoid output on a specific channel.
+   *
+   * @param channel the channel to check
+   * @return the solenoid output
+   */
+  public boolean getSolenoidOutput(int channel) {
+    return REVPHDataJNI.getSolenoidOutput(m_index, channel);
+  }
+
+  /**
+   * Change the solenoid output on a specific channel.
+   *
+   * @param channel the channel to check
+   * @param solenoidOutput the new solenoid output
+   */
+  public void setSolenoidOutput(int channel, boolean solenoidOutput) {
+    REVPHDataJNI.setSolenoidOutput(m_index, channel, solenoidOutput);
+  }
+
+  /**
+   * Register a callback to be run when the compressor is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
+    int uid = REVPHDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, REVPHDataJNI::cancelInitializedCallback);
+  }
+
+  /**
+   * Check whether the compressor has been initialized.
+   *
+   * @return true if initialized
+   */
+  public boolean getInitialized() {
+    return REVPHDataJNI.getInitialized(m_index);
+  }
+
+  /**
+   * Define whether the compressor has been initialized.
+   *
+   * @param initialized whether the compressor is initialized
+   */
+  public void setInitialized(boolean initialized) {
+    REVPHDataJNI.setInitialized(m_index, initialized);
+  }
+
+  /**
+   * Register a callback to be run when the compressor activates.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerCompressorOnCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = REVPHDataJNI.registerCompressorOnCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, REVPHDataJNI::cancelCompressorOnCallback);
+  }
+
+  /**
+   * Check if the compressor is on.
+   *
+   * @return true if the compressor is active
+   */
+  public boolean getCompressorOn() {
+    return REVPHDataJNI.getCompressorOn(m_index);
+  }
+
+  /**
+   * Set whether the compressor is active.
+   *
+   * @param compressorOn the new value
+   */
+  public void setCompressorOn(boolean compressorOn) {
+    REVPHDataJNI.setCompressorOn(m_index, compressorOn);
+  }
+
+  /**
+   * Register a callback to be run whenever the closed loop state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerClosedLoopEnabledCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = REVPHDataJNI.registerClosedLoopEnabledCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, REVPHDataJNI::cancelClosedLoopEnabledCallback);
+  }
+
+  /**
+   * Check whether the closed loop compressor control is active.
+   *
+   * @return true if active
+   */
+  public boolean getClosedLoopEnabled() {
+    return REVPHDataJNI.getClosedLoopEnabled(m_index);
+  }
+
+  /**
+   * Turn on/off the closed loop control of the compressor.
+   *
+   * @param closedLoopEnabled whether the control loop is active
+   */
+  public void setClosedLoopEnabled(boolean closedLoopEnabled) {
+    REVPHDataJNI.setClosedLoopEnabled(m_index, closedLoopEnabled);
+  }
+
+  /**
+   * Register a callback to be run whenever the pressure switch value changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerPressureSwitchCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = REVPHDataJNI.registerPressureSwitchCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, REVPHDataJNI::cancelPressureSwitchCallback);
+  }
+
+  /**
+   * Check the value of the pressure switch.
+   *
+   * @return the pressure switch value
+   */
+  public boolean getPressureSwitch() {
+    return REVPHDataJNI.getPressureSwitch(m_index);
+  }
+
+  /**
+   * Set the value of the pressure switch.
+   *
+   * @param pressureSwitch the new value
+   */
+  public void setPressureSwitch(boolean pressureSwitch) {
+    REVPHDataJNI.setPressureSwitch(m_index, pressureSwitch);
+  }
+
+  /**
+   * Register a callback to be run whenever the compressor current 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 CallbackStore registerCompressorCurrentCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = REVPHDataJNI.registerCompressorCurrentCallback(m_index, callback, initialNotify);
+    return new CallbackStore(m_index, uid, REVPHDataJNI::cancelCompressorCurrentCallback);
+  }
+
+  /**
+   * Read the compressor current.
+   *
+   * @return the current of the compressor connected to this module
+   */
+  public double getCompressorCurrent() {
+    return REVPHDataJNI.getCompressorCurrent(m_index);
+  }
+
+  /**
+   * Set the compressor current.
+   *
+   * @param compressorCurrent the new compressor current
+   */
+  public void setCompressorCurrent(double compressorCurrent) {
+    REVPHDataJNI.setCompressorCurrent(m_index, compressorCurrent);
+  }
+
+  /** Reset all simulation data for this object. */
+  public void resetData() {
+    REVPHDataJNI.resetData(m_index);
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RelaySim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RelaySim.java
index dc7441e..b6f2542 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RelaySim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/RelaySim.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -11,9 +8,7 @@
 import edu.wpi.first.hal.simulation.RelayDataJNI;
 import edu.wpi.first.wpilibj.Relay;
 
-/**
- * Class to control a simulated relay.
- */
+/** Class to control a simulated relay. */
 public class RelaySim {
   private final int m_index;
 
@@ -35,50 +30,133 @@
     m_index = channel;
   }
 
-  public CallbackStore registerInitializedForwardCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback to be run when the forward direction is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedForwardCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RelayDataJNI.registerInitializedForwardCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, RelayDataJNI::cancelInitializedForwardCallback);
   }
+
+  /**
+   * Check whether the forward direction has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitializedForward() {
     return RelayDataJNI.getInitializedForward(m_index);
   }
+
+  /**
+   * Define whether the forward direction has been initialized.
+   *
+   * @param initializedForward whether this object is initialized
+   */
   public void setInitializedForward(boolean initializedForward) {
     RelayDataJNI.setInitializedForward(m_index, initializedForward);
   }
 
-  public CallbackStore registerInitializedReverseCallback(NotifyCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback to be run when the reverse direction is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedReverseCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RelayDataJNI.registerInitializedReverseCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, RelayDataJNI::cancelInitializedReverseCallback);
   }
+
+  /**
+   * Check whether the reverse direction has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitializedReverse() {
     return RelayDataJNI.getInitializedReverse(m_index);
   }
+
+  /**
+   * Define whether the reverse direction has been initialized.
+   *
+   * @param initializedReverse whether this object is initialized
+   */
   public void setInitializedReverse(boolean initializedReverse) {
     RelayDataJNI.setInitializedReverse(m_index, initializedReverse);
   }
 
+  /**
+   * Register a callback to be run when the forward direction changes state.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerForwardCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = RelayDataJNI.registerForwardCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, RelayDataJNI::cancelForwardCallback);
   }
+
+  /**
+   * Check whether the forward direction is active.
+   *
+   * @return true if active
+   */
   public boolean getForward() {
     return RelayDataJNI.getForward(m_index);
   }
+
+  /**
+   * Set whether the forward direction is active.
+   *
+   * @param forward true to make active
+   */
   public void setForward(boolean forward) {
     RelayDataJNI.setForward(m_index, forward);
   }
 
+  /**
+   * Register a callback to be run when the reverse direction changes state.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerReverseCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = RelayDataJNI.registerReverseCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, RelayDataJNI::cancelReverseCallback);
   }
+
+  /**
+   * Check whether the reverse direction is active.
+   *
+   * @return true if active
+   */
   public boolean getReverse() {
     return RelayDataJNI.getReverse(m_index);
   }
+
+  /**
+   * Set whether the reverse direction is active.
+   *
+   * @param reverse true to make active
+   */
   public void setReverse(boolean reverse) {
     RelayDataJNI.setReverse(m_index, reverse);
   }
 
+  /** Reset all simulation data. */
   public void resetData() {
     RelayDataJNI.resetData(m_index);
   }
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 f90ce0f..976ba4a 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
@@ -1,200 +1,537 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.hal.simulation.RoboRioDataJNI;
 
-/**
- * Class to control a simulated RoboRIO.
- */
-@SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.UseUtilityClass"})
-public class RoboRioSim {
-  public static CallbackStore registerFPGAButtonCallback(NotifyCallback callback,
-                                                         boolean initialNotify) {
+/** A utility class to control a simulated RoboRIO. */
+public final class RoboRioSim {
+  private RoboRioSim() {
+    // Utility class
+  }
+
+  /**
+   * Register a callback to be run when the FPGA button state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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.
+   */
+  @SuppressWarnings("AbbreviationAsWordInName")
+  public static CallbackStore registerFPGAButtonCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerFPGAButtonCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelFPGAButtonCallback);
   }
+
+  /**
+   * Query the state of the FPGA button.
+   *
+   * @return the FPGA button state
+   */
+  @SuppressWarnings("AbbreviationAsWordInName")
   public static boolean getFPGAButton() {
     return RoboRioDataJNI.getFPGAButton();
   }
-  public static void setFPGAButton(boolean fPGAButton) {
-    RoboRioDataJNI.setFPGAButton(fPGAButton);
+
+  /**
+   * Define the state of the FPGA button.
+   *
+   * @param fpgaButton the new state
+   */
+  @SuppressWarnings("AbbreviationAsWordInName")
+  public static void setFPGAButton(boolean fpgaButton) {
+    RoboRioDataJNI.setFPGAButton(fpgaButton);
   }
 
-  public static CallbackStore registerVInVoltageCallback(NotifyCallback callback,
-                                                         boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the Vin voltage 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 registerVInVoltageCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerVInVoltageCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelVInVoltageCallback);
   }
+
+  /**
+   * Measure the Vin voltage.
+   *
+   * @return the Vin voltage
+   */
   public static double getVInVoltage() {
     return RoboRioDataJNI.getVInVoltage();
   }
+
+  /**
+   * Define the Vin voltage.
+   *
+   * @param vInVoltage the new voltage
+   */
+  @SuppressWarnings("ParameterName")
   public static void setVInVoltage(double vInVoltage) {
     RoboRioDataJNI.setVInVoltage(vInVoltage);
   }
 
-  public static CallbackStore registerVInCurrentCallback(NotifyCallback callback,
-                                                         boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the Vin current changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerVInCurrentCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerVInCurrentCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelVInCurrentCallback);
   }
+
+  /**
+   * Measure the Vin current.
+   *
+   * @return the Vin current
+   */
   public static double getVInCurrent() {
     return RoboRioDataJNI.getVInCurrent();
   }
+
+  /**
+   * Define the Vin current.
+   *
+   * @param vInCurrent the new current
+   */
+  @SuppressWarnings("ParameterName")
   public static void setVInCurrent(double vInCurrent) {
     RoboRioDataJNI.setVInCurrent(vInCurrent);
   }
 
-  public static CallbackStore registerUserVoltage6VCallback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 6V rail voltage changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserVoltage6VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserVoltage6VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserVoltage6VCallback);
   }
+
+  /**
+   * Measure the 6V rail voltage.
+   *
+   * @return the 6V rail voltage
+   */
   public static double getUserVoltage6V() {
     return RoboRioDataJNI.getUserVoltage6V();
   }
+
+  /**
+   * Define the 6V rail voltage.
+   *
+   * @param userVoltage6V the new voltage
+   */
   public static void setUserVoltage6V(double userVoltage6V) {
     RoboRioDataJNI.setUserVoltage6V(userVoltage6V);
   }
 
-  public static CallbackStore registerUserCurrent6VCallback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 6V rail current changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserCurrent6VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserCurrent6VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserCurrent6VCallback);
   }
+
+  /**
+   * Measure the 6V rail current.
+   *
+   * @return the 6V rail current
+   */
   public static double getUserCurrent6V() {
     return RoboRioDataJNI.getUserCurrent6V();
   }
+
+  /**
+   * Define the 6V rail current.
+   *
+   * @param userCurrent6V the new current
+   */
   public static void setUserCurrent6V(double userCurrent6V) {
     RoboRioDataJNI.setUserCurrent6V(userCurrent6V);
   }
 
-  public static CallbackStore registerUserActive6VCallback(NotifyCallback callback,
-                                                           boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 6V rail active state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called 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 registerUserActive6VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserActive6VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserActive6VCallback);
   }
+
+  /**
+   * Get the 6V rail active state.
+   *
+   * @return true if the 6V rail is active
+   */
   public static boolean getUserActive6V() {
     return RoboRioDataJNI.getUserActive6V();
   }
+
+  /**
+   * Set the 6V rail active state.
+   *
+   * @param userActive6V true to make rail active
+   */
   public static void setUserActive6V(boolean userActive6V) {
     RoboRioDataJNI.setUserActive6V(userActive6V);
   }
 
-  public static CallbackStore registerUserVoltage5VCallback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 5V rail voltage changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserVoltage5VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserVoltage5VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserVoltage5VCallback);
   }
+
+  /**
+   * Measure the 5V rail voltage.
+   *
+   * @return the 5V rail voltage
+   */
   public static double getUserVoltage5V() {
     return RoboRioDataJNI.getUserVoltage5V();
   }
+
+  /**
+   * Define the 5V rail voltage.
+   *
+   * @param userVoltage5V the new voltage
+   */
   public static void setUserVoltage5V(double userVoltage5V) {
     RoboRioDataJNI.setUserVoltage5V(userVoltage5V);
   }
 
-  public static CallbackStore registerUserCurrent5VCallback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 5V rail current changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserCurrent5VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserCurrent5VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserCurrent5VCallback);
   }
+
+  /**
+   * Measure the 5V rail current.
+   *
+   * @return the 5V rail current
+   */
   public static double getUserCurrent5V() {
     return RoboRioDataJNI.getUserCurrent5V();
   }
+
+  /**
+   * Define the 5V rail current.
+   *
+   * @param userCurrent5V the new current
+   */
   public static void setUserCurrent5V(double userCurrent5V) {
     RoboRioDataJNI.setUserCurrent5V(userCurrent5V);
   }
 
-  public static CallbackStore registerUserActive5VCallback(NotifyCallback callback,
-                                                           boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 5V rail active state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called 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 registerUserActive5VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserActive5VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserActive5VCallback);
   }
+
+  /**
+   * Get the 5V rail active state.
+   *
+   * @return true if the 5V rail is active
+   */
   public static boolean getUserActive5V() {
     return RoboRioDataJNI.getUserActive5V();
   }
+
+  /**
+   * Set the 5V rail active state.
+   *
+   * @param userActive5V true to make rail active
+   */
   public static void setUserActive5V(boolean userActive5V) {
     RoboRioDataJNI.setUserActive5V(userActive5V);
   }
 
-  public static CallbackStore registerUserVoltage3V3Callback(NotifyCallback callback,
-                                                             boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 3.3V rail voltage changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserVoltage3V3Callback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserVoltage3V3Callback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserVoltage3V3Callback);
   }
+
+  /**
+   * Measure the 3.3V rail voltage.
+   *
+   * @return the 3.3V rail voltage
+   */
   public static double getUserVoltage3V3() {
     return RoboRioDataJNI.getUserVoltage3V3();
   }
+
+  /**
+   * Define the 3.3V rail voltage.
+   *
+   * @param userVoltage3V3 the new voltage
+   */
   public static void setUserVoltage3V3(double userVoltage3V3) {
     RoboRioDataJNI.setUserVoltage3V3(userVoltage3V3);
   }
 
-  public static CallbackStore registerUserCurrent3V3Callback(NotifyCallback callback,
-                                                             boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 3.3V rail current changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserCurrent3V3Callback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserCurrent3V3Callback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserCurrent3V3Callback);
   }
+
+  /**
+   * Measure the 3.3V rail current.
+   *
+   * @return the 3.3V rail current
+   */
   public static double getUserCurrent3V3() {
     return RoboRioDataJNI.getUserCurrent3V3();
   }
+
+  /**
+   * Define the 3.3V rail current.
+   *
+   * @param userCurrent3V3 the new current
+   */
   public static void setUserCurrent3V3(double userCurrent3V3) {
     RoboRioDataJNI.setUserCurrent3V3(userCurrent3V3);
   }
 
-  public static CallbackStore registerUserActive3V3Callback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 3.3V rail active state changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called 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 registerUserActive3V3Callback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserActive3V3Callback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserActive3V3Callback);
   }
+
+  /**
+   * Get the 3.3V rail active state.
+   *
+   * @return true if the 3.3V rail is active
+   */
   public static boolean getUserActive3V3() {
     return RoboRioDataJNI.getUserActive3V3();
   }
+
+  /**
+   * Set the 3.3V rail active state.
+   *
+   * @param userActive3V3 true to make rail active
+   */
   public static void setUserActive3V3(boolean userActive3V3) {
     RoboRioDataJNI.setUserActive3V3(userActive3V3);
   }
 
-  public static CallbackStore registerUserFaults6VCallback(NotifyCallback callback,
-                                                           boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 6V rail number of faults changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserFaults6VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserFaults6VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserFaults6VCallback);
   }
+
+  /**
+   * Get the 6V rail number of faults.
+   *
+   * @return number of faults
+   */
   public static int getUserFaults6V() {
     return RoboRioDataJNI.getUserFaults6V();
   }
+
+  /**
+   * Set the 6V rail number of faults.
+   *
+   * @param userFaults6V number of faults
+   */
   public static void setUserFaults6V(int userFaults6V) {
     RoboRioDataJNI.setUserFaults6V(userFaults6V);
   }
 
-  public static CallbackStore registerUserFaults5VCallback(NotifyCallback callback,
-                                                           boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 5V rail number of faults changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserFaults5VCallback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserFaults5VCallback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserFaults5VCallback);
   }
+
+  /**
+   * Get the 5V rail number of faults.
+   *
+   * @return number of faults
+   */
   public static int getUserFaults5V() {
     return RoboRioDataJNI.getUserFaults5V();
   }
+
+  /**
+   * Set the 5V rail number of faults.
+   *
+   * @param userFaults5V number of faults
+   */
   public static void setUserFaults5V(int userFaults5V) {
     RoboRioDataJNI.setUserFaults5V(userFaults5V);
   }
 
-  public static CallbackStore registerUserFaults3V3Callback(NotifyCallback callback,
-                                                            boolean initialNotify) {
+  /**
+   * Register a callback to be run whenever the 3.3V rail number of faults changes.
+   *
+   * @param callback the callback
+   * @param initialNotify whether the callback should be called with the initial value
+   * @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 registerUserFaults3V3Callback(
+      NotifyCallback callback, boolean initialNotify) {
     int uid = RoboRioDataJNI.registerUserFaults3V3Callback(callback, initialNotify);
     return new CallbackStore(uid, RoboRioDataJNI::cancelUserFaults3V3Callback);
   }
+
+  /**
+   * Get the 3.3V rail number of faults.
+   *
+   * @return number of faults
+   */
   public static int getUserFaults3V3() {
     return RoboRioDataJNI.getUserFaults3V3();
   }
+
+  /**
+   * Set the 3.3V rail number of faults.
+   *
+   * @param userFaults3V3 number of faults
+   */
   public static void setUserFaults3V3(int userFaults3V3) {
     RoboRioDataJNI.setUserFaults3V3(userFaults3V3);
   }
 
+  /**
+   * Register a callback to be run whenever the Brownout voltage 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 registerBrownoutVoltageCallback(
+      NotifyCallback callback, boolean initialNotify) {
+    int uid = RoboRioDataJNI.registerBrownoutVoltageCallback(callback, initialNotify);
+    return new CallbackStore(uid, RoboRioDataJNI::cancelBrownoutVoltageCallback);
+  }
+
+  /**
+   * Measure the Brownout voltage.
+   *
+   * @return the Brownout voltage
+   */
+  public static double getBrownoutVoltage() {
+    return RoboRioDataJNI.getBrownoutVoltage();
+  }
+
+  /**
+   * Define the Brownout voltage.
+   *
+   * @param vInVoltage the new voltage
+   */
+  @SuppressWarnings("ParameterName")
+  public static void setBrownoutVoltage(double vInVoltage) {
+    RoboRioDataJNI.setBrownoutVoltage(vInVoltage);
+  }
+
+  /** Reset all simulation data. */
   public static void resetData() {
     RoboRioDataJNI.resetData();
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPIAccelerometerSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPIAccelerometerSim.java
index ce6b5c1..da1213a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPIAccelerometerSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SPIAccelerometerSim.java
@@ -1,77 +1,184 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.simulation.NotifyCallback;
 import edu.wpi.first.hal.simulation.SPIAccelerometerDataJNI;
 
+/** A class to control a simulated accelerometer over SPI. */
 public class SPIAccelerometerSim {
   private final int m_index;
 
+  /**
+   * Construct a new simulation object.
+   *
+   * @param index the HAL index of the accelerometer
+   */
   public SPIAccelerometerSim(int index) {
     m_index = index;
   }
 
+  /**
+   * Register a callback to be run when this accelerometer activates.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerActiveCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIAccelerometerDataJNI.registerActiveCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIAccelerometerDataJNI::cancelActiveCallback);
   }
+
+  /**
+   * Check whether the accelerometer is active.
+   *
+   * @return true if active
+   */
   public boolean getActive() {
     return SPIAccelerometerDataJNI.getActive(m_index);
   }
+
+  /**
+   * Define whether this accelerometer is active.
+   *
+   * @param active the new state
+   */
   public void setActive(boolean active) {
     SPIAccelerometerDataJNI.setActive(m_index, active);
   }
 
+  /**
+   * Register a callback to be run whenever the range 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 CallbackStore registerRangeCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIAccelerometerDataJNI.registerRangeCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIAccelerometerDataJNI::cancelRangeCallback);
   }
+
+  /**
+   * Check the range of this accelerometer.
+   *
+   * @return the accelerometer range
+   */
   public int getRange() {
     return SPIAccelerometerDataJNI.getRange(m_index);
   }
+
+  /**
+   * Change the range of this accelerometer.
+   *
+   * @param range the new accelerometer range
+   */
   public void setRange(int range) {
     SPIAccelerometerDataJNI.setRange(m_index, range);
   }
 
+  /**
+   * Register a callback to be run whenever the X axis value 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 CallbackStore registerXCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIAccelerometerDataJNI.registerXCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIAccelerometerDataJNI::cancelXCallback);
   }
+
+  /**
+   * Measure the X axis value.
+   *
+   * @return the X axis measurement
+   */
   public double getX() {
     return SPIAccelerometerDataJNI.getX(m_index);
   }
+
+  /**
+   * Change the X axis value of the accelerometer.
+   *
+   * @param x the new reading of the X axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setX(double x) {
     SPIAccelerometerDataJNI.setX(m_index, x);
   }
 
+  /**
+   * Register a callback to be run whenever the Y axis value 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 CallbackStore registerYCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIAccelerometerDataJNI.registerYCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIAccelerometerDataJNI::cancelYCallback);
   }
+
+  /**
+   * Measure the Y axis value.
+   *
+   * @return the Y axis measurement
+   */
   public double getY() {
     return SPIAccelerometerDataJNI.getY(m_index);
   }
+
+  /**
+   * Change the Y axis value of the accelerometer.
+   *
+   * @param y the new reading of the Y axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setY(double y) {
     SPIAccelerometerDataJNI.setY(m_index, y);
   }
 
+  /**
+   * Register a callback to be run whenever the Z axis value 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 CallbackStore registerZCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIAccelerometerDataJNI.registerZCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIAccelerometerDataJNI::cancelZCallback);
   }
+
+  /**
+   * Measure the Z axis value.
+   *
+   * @return the Z axis measurement
+   */
   public double getZ() {
     return SPIAccelerometerDataJNI.getZ(m_index);
   }
+
+  /**
+   * Change the Z axis value of the accelerometer.
+   *
+   * @param z the new reading of the Z axis
+   */
+  @SuppressWarnings("ParameterName")
   public void setZ(double z) {
     SPIAccelerometerDataJNI.setZ(m_index, z);
   }
 
+  /** Reset all simulation data of this object. */
   public void resetData() {
     SPIAccelerometerDataJNI.resetData(m_index);
   }
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 71c197a..6c0b9e9 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
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
@@ -13,39 +10,77 @@
 import edu.wpi.first.hal.simulation.SPIDataJNI;
 import edu.wpi.first.hal.simulation.SpiReadAutoReceiveBufferCallback;
 
+/** A class for controlling a simulated SPI device. */
 public class SPISim {
   private final int m_index;
 
+  /** Create a new simulated SPI device. */
   public SPISim() {
     m_index = 0;
   }
 
+  /**
+   * Register a callback to be run when this device is initialized.
+   *
+   * @param callback the callback
+   * @param initialNotify whether to run 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 CallbackStore registerInitializedCallback(NotifyCallback callback, boolean initialNotify) {
     int uid = SPIDataJNI.registerInitializedCallback(m_index, callback, initialNotify);
     return new CallbackStore(m_index, uid, SPIDataJNI::cancelInitializedCallback);
   }
+
+  /**
+   * Check whether this device has been initialized.
+   *
+   * @return true if initialized
+   */
   public boolean getInitialized() {
     return SPIDataJNI.getInitialized(m_index);
   }
+
+  /**
+   * Define whether this device has been initialized.
+   *
+   * @param initialized whether this object is initialized
+   */
   public void setInitialized(boolean initialized) {
     SPIDataJNI.setInitialized(m_index, initialized);
   }
 
+  /**
+   * Register a callback to be run whenever a `read` operation is executed.
+   *
+   * @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 registerReadCallback(BufferCallback callback) {
     int uid = SPIDataJNI.registerReadCallback(m_index, callback);
     return new CallbackStore(m_index, uid, SPIDataJNI::cancelReadCallback);
   }
 
+  /**
+   * Register a callback to be run whenever a `write` operation is executed.
+   *
+   * @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 registerWriteCallback(ConstBufferCallback callback) {
     int uid = SPIDataJNI.registerWriteCallback(m_index, callback);
     return new CallbackStore(m_index, uid, SPIDataJNI::cancelWriteCallback);
   }
 
-  public CallbackStore registerReadAutoReceiveBufferCallback(SpiReadAutoReceiveBufferCallback callback) {
+  public CallbackStore registerReadAutoReceiveBufferCallback(
+      SpiReadAutoReceiveBufferCallback callback) {
     int uid = SPIDataJNI.registerReadAutoReceiveBufferCallback(m_index, callback);
     return new CallbackStore(m_index, uid, SPIDataJNI::cancelReadAutoReceiveBufferCallback);
   }
 
+  /** Reset all simulation data. */
   public void resetData() {
     SPIDataJNI.resetData(m_index);
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimDeviceSim.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimDeviceSim.java
index 713c112..bca1f6f 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimDeviceSim.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/SimDeviceSim.java
@@ -1,23 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.SimBoolean;
 import edu.wpi.first.hal.SimDouble;
 import edu.wpi.first.hal.SimEnum;
+import edu.wpi.first.hal.SimInt;
+import edu.wpi.first.hal.SimLong;
 import edu.wpi.first.hal.SimValue;
 import edu.wpi.first.hal.simulation.SimDeviceCallback;
 import edu.wpi.first.hal.simulation.SimDeviceDataJNI;
 import edu.wpi.first.hal.simulation.SimValueCallback;
 
-/**
- * Class to control the simulation side of a SimDevice.
- */
+/** Class to control the simulation side of a SimDevice. */
 public class SimDeviceSim {
   private final int m_handle;
 
@@ -30,6 +27,33 @@
     m_handle = SimDeviceDataJNI.getSimDeviceHandle(name);
   }
 
+  /**
+   * Constructs a SimDeviceSim.
+   *
+   * @param name name of the SimDevice
+   * @param index device index number to append to name
+   */
+  public SimDeviceSim(String name, int index) {
+    this(name + "[" + index + "]");
+  }
+
+  /**
+   * Constructs a SimDeviceSim.
+   *
+   * @param name name of the SimDevice
+   * @param index device index number to append to name
+   * @param channel device channel number to append to name
+   */
+  public SimDeviceSim(String name, int index, int channel) {
+    this(name + "[" + index + "," + channel + "]");
+  }
+
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
   public SimValue getValue(String name) {
     int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
     if (handle <= 0) {
@@ -38,6 +62,40 @@
     return new SimValue(handle);
   }
 
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
+  public SimInt getInt(String name) {
+    int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
+    if (handle <= 0) {
+      return null;
+    }
+    return new SimInt(handle);
+  }
+
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
+  public SimLong getLong(String name) {
+    int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
+    if (handle <= 0) {
+      return null;
+    }
+    return new SimLong(handle);
+  }
+
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
   public SimDouble getDouble(String name) {
     int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
     if (handle <= 0) {
@@ -46,6 +104,12 @@
     return new SimDouble(handle);
   }
 
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
   public SimEnum getEnum(String name) {
     int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
     if (handle <= 0) {
@@ -54,6 +118,12 @@
     return new SimEnum(handle);
   }
 
+  /**
+   * Get the property object with the given name.
+   *
+   * @param name the property name
+   * @return the property object
+   */
   public SimBoolean getBoolean(String name) {
     int handle = SimDeviceDataJNI.getSimValueHandle(m_handle, name);
     if (handle <= 0) {
@@ -62,42 +132,125 @@
     return new SimBoolean(handle);
   }
 
+  /**
+   * Get all options for the given enum.
+   *
+   * @param val the enum
+   * @return names of the different values for that enum
+   */
   public static String[] getEnumOptions(SimEnum val) {
     return SimDeviceDataJNI.getSimValueEnumOptions(val.getNativeHandle());
   }
 
+  /**
+   * Get all data of this object.
+   *
+   * @return all data and fields of this object
+   */
   public SimDeviceDataJNI.SimValueInfo[] enumerateValues() {
     return SimDeviceDataJNI.enumerateSimValues(m_handle);
   }
 
+  /**
+   * Get the native handle of this object.
+   *
+   * @return the handle used to refer to this object through JNI
+   */
   public int getNativeHandle() {
     return m_handle;
   }
 
-  public CallbackStore registerValueCreatedCallback(SimValueCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback to be run every time a new value is added to this device.
+   *
+   * @param callback the callback
+   * @param initialNotify should the callback be run 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 CallbackStore registerValueCreatedCallback(
+      SimValueCallback callback, boolean initialNotify) {
     int uid = SimDeviceDataJNI.registerSimValueCreatedCallback(m_handle, callback, initialNotify);
     return new CallbackStore(uid, SimDeviceDataJNI::cancelSimValueCreatedCallback);
   }
 
-  public CallbackStore registerValueChangedCallback(SimValueCallback callback, boolean initialNotify) {
-    int uid = SimDeviceDataJNI.registerSimValueChangedCallback(m_handle, callback, initialNotify);
+  /**
+   * Register a callback to be run every time a value is changed on this device.
+   *
+   * @param value simulated value
+   * @param callback the callback
+   * @param initialNotify should the callback be run 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 CallbackStore registerValueChangedCallback(
+      SimValue value, SimValueCallback callback, boolean initialNotify) {
+    int uid =
+        SimDeviceDataJNI.registerSimValueChangedCallback(
+            value.getNativeHandle(), callback, initialNotify);
     return new CallbackStore(uid, SimDeviceDataJNI::cancelSimValueChangedCallback);
   }
 
+  /**
+   * Register a callback for SimDouble.reset() and similar functions. The callback is called with
+   * the old value.
+   *
+   * @param value simulated value
+   * @param callback callback
+   * @param initialNotify ignored (present for consistency)
+   * @return the {@link CallbackStore} object associated with this callback. Save a reference to
+   *     this object so GC doesn't cancel the callback.
+   */
+  public CallbackStore registerValueResetCallback(
+      SimValue value, SimValueCallback callback, boolean initialNotify) {
+    int uid =
+        SimDeviceDataJNI.registerSimValueResetCallback(
+            value.getNativeHandle(), callback, initialNotify);
+    return new CallbackStore(uid, SimDeviceDataJNI::cancelSimValueResetCallback);
+  }
+
+  /**
+   * Get all sim devices with the given prefix.
+   *
+   * @param prefix the prefix to filter sim devices
+   * @return all sim devices
+   */
   public static SimDeviceDataJNI.SimDeviceInfo[] enumerateDevices(String prefix) {
     return SimDeviceDataJNI.enumerateSimDevices(prefix);
   }
 
-  public static CallbackStore registerDeviceCreatedCallback(String prefix, SimDeviceCallback callback, boolean initialNotify) {
+  /**
+   * Register a callback to be run every time a new {@link edu.wpi.first.hal.SimDevice} is created.
+   *
+   * @param prefix the prefix to filter sim devices
+   * @param callback the callback
+   * @param initialNotify should the callback be run 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 registerDeviceCreatedCallback(
+      String prefix, SimDeviceCallback callback, boolean initialNotify) {
     int uid = SimDeviceDataJNI.registerSimDeviceCreatedCallback(prefix, callback, initialNotify);
     return new CallbackStore(uid, SimDeviceDataJNI::cancelSimDeviceCreatedCallback);
   }
 
-  public static CallbackStore registerDeviceFreedCallback(String prefix, SimDeviceCallback callback) {
-    int uid = SimDeviceDataJNI.registerSimDeviceFreedCallback(prefix, callback);
+  /**
+   * Register a callback to be run every time a {@link edu.wpi.first.hal.SimDevice} is
+   * freed/destroyed.
+   *
+   * @param prefix the prefix to filter sim devices
+   * @param callback the callback
+   * @param initialNotify should the callback be run 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 registerDeviceFreedCallback(
+      String prefix, SimDeviceCallback callback, boolean initialNotify) {
+    int uid = SimDeviceDataJNI.registerSimDeviceFreedCallback(prefix, callback, initialNotify);
     return new CallbackStore(uid, SimDeviceDataJNI::cancelSimDeviceFreedCallback);
   }
 
+  /** Reset all SimDevice data. */
   public static void resetData() {
     SimDeviceDataJNI.resetSimDeviceData();
   }
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 9c17e1c..1664081 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
@@ -1,18 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
 import edu.wpi.first.hal.simulation.SimulatorJNI;
 
 public final class SimHooks {
-  private SimHooks() {
-  }
+  private SimHooks() {}
 
+  /**
+   * Override the HAL runtime type (simulated/real).
+   *
+   * @param type runtime type
+   */
   public static void setHALRuntimeType(int type) {
     SimulatorJNI.setRuntimeType(type);
   }
@@ -29,26 +30,44 @@
     return SimulatorJNI.getProgramStarted();
   }
 
+  /** Restart the simulator time. */
   public static void restartTiming() {
     SimulatorJNI.restartTiming();
   }
 
+  /** Pause the simulator time. */
   public static void pauseTiming() {
     SimulatorJNI.pauseTiming();
   }
 
+  /** Resume the simulator time. */
   public static void resumeTiming() {
     SimulatorJNI.resumeTiming();
   }
 
+  /**
+   * Check if the simulator time is paused.
+   *
+   * @return true if paused
+   */
   public static boolean isTimingPaused() {
     return SimulatorJNI.isTimingPaused();
   }
 
+  /**
+   * Advance the simulator time and wait for all notifiers to run.
+   *
+   * @param deltaSeconds the amount to advance (in seconds)
+   */
   public static void stepTiming(double deltaSeconds) {
     SimulatorJNI.stepTiming((long) (deltaSeconds * 1e6));
   }
 
+  /**
+   * Advance the simulator time and return immediately.
+   *
+   * @param deltaSeconds the amount to advance (in seconds)
+   */
   public static void stepTimingAsync(double deltaSeconds) {
     SimulatorJNI.stepTimingAsync((long) (deltaSeconds * 1e6));
   }
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 f3188a0..87981f7 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
@@ -1,24 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.system.LinearSystem;
-import edu.wpi.first.wpilibj.system.RungeKutta;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-import edu.wpi.first.wpiutil.math.numbers.N1;
-import edu.wpi.first.wpiutil.math.numbers.N2;
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.numbers.N2;
+import edu.wpi.first.math.system.LinearSystem;
+import edu.wpi.first.math.system.NumericalIntegration;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
 
-/**
- * Represents a simulated single jointed arm mechanism.
- */
+/** Represents a simulated single jointed arm mechanism. */
 public class SingleJointedArmSim extends LinearSystemSim<N2, N1, N1> {
   // The gearbox for the arm.
   private final DCMotor m_gearbox;
@@ -45,39 +40,59 @@
   /**
    * Creates a simulated arm mechanism.
    *
-   * @param plant           The linear system that represents the arm.
-   * @param gearbox         The type of and number of motors in the arm gearbox.
-   * @param gearing         The gearing of the arm (numbers greater than 1 represent reductions).
+   * @param plant The linear system that represents the arm.
+   * @param gearbox The type of and number of motors in the arm gearbox.
+   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
    * @param armLengthMeters The length of the arm.
-   * @param minAngleRads    The minimum angle that the arm is capable of.
-   * @param maxAngleRads    The maximum angle that the arm is capable of.
-   * @param armMassKg       The mass of the arm.
+   * @param minAngleRads The minimum angle that the arm is capable of.
+   * @param maxAngleRads The maximum angle that the arm is capable of.
+   * @param armMassKg The mass of the arm.
    * @param simulateGravity Whether gravity should be simulated or not.
    */
-  public SingleJointedArmSim(LinearSystem<N2, N1, N1> plant, DCMotor gearbox, double gearing,
-                             double armLengthMeters, double minAngleRads, double maxAngleRads,
-                             double armMassKg, boolean simulateGravity) {
-    this(plant, gearbox, gearing, armLengthMeters, minAngleRads, maxAngleRads, armMassKg,
-        simulateGravity, null);
+  public SingleJointedArmSim(
+      LinearSystem<N2, N1, N1> plant,
+      DCMotor gearbox,
+      double gearing,
+      double armLengthMeters,
+      double minAngleRads,
+      double maxAngleRads,
+      double armMassKg,
+      boolean simulateGravity) {
+    this(
+        plant,
+        gearbox,
+        gearing,
+        armLengthMeters,
+        minAngleRads,
+        maxAngleRads,
+        armMassKg,
+        simulateGravity,
+        null);
   }
 
   /**
    * Creates a simulated arm mechanism.
    *
-   * @param plant              The linear system that represents the arm.
-   * @param gearbox            The type of and number of motors in the arm gearbox.
-   * @param gearing            The gearing of the arm (numbers greater than 1 represent reductions).
-   * @param armLengthMeters    The length of the arm.
-   * @param minAngleRads       The minimum angle that the arm is capable of.
-   * @param maxAngleRads       The maximum angle that the arm is capable of.
-   * @param armMassKg          The mass of the arm.
-   * @param simulateGravity    Whether gravity should be simulated or not.
+   * @param plant The linear system that represents the arm.
+   * @param gearbox The type of and number of motors in the arm gearbox.
+   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
+   * @param armLengthMeters The length of the arm.
+   * @param minAngleRads The minimum angle that the arm is capable of.
+   * @param maxAngleRads The maximum angle that the arm is capable of.
+   * @param armMassKg The mass of the arm.
+   * @param simulateGravity Whether gravity should be simulated or not.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public SingleJointedArmSim(LinearSystem<N2, N1, N1> plant, DCMotor gearbox, double gearing,
-                             double armLengthMeters, double minAngleRads, double maxAngleRads,
-                             double armMassKg, boolean simulateGravity,
-                             Matrix<N1, N1> measurementStdDevs) {
+  public SingleJointedArmSim(
+      LinearSystem<N2, N1, N1> plant,
+      DCMotor gearbox,
+      double gearing,
+      double armLengthMeters,
+      double minAngleRads,
+      double maxAngleRads,
+      double armMassKg,
+      boolean simulateGravity,
+      Matrix<N1, N1> measurementStdDevs) {
     super(plant, measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
@@ -91,40 +106,63 @@
   /**
    * Creates a simulated arm mechanism.
    *
-   * @param gearbox          The type of and number of motors in the arm gearbox.
-   * @param gearing          The gearing of the arm (numbers greater than 1 represent reductions).
-   * @param jKgMetersSquared The moment of inertia of the arm. This can be calculated from CAD software.
-   * @param armLengthMeters  The length of the arm.
-   * @param minAngleRads     The minimum angle that the arm is capable of.
-   * @param maxAngleRads     The maximum angle that the arm is capable of.
-   * @param armMassKg        The mass of the arm.
-   * @param simulateGravity  Whether gravity should be simulated or not.
+   * @param gearbox The type of and number of motors in the arm gearbox.
+   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
+   * @param jKgMetersSquared The moment of inertia of the arm, can be calculated from CAD software.
+   * @param armLengthMeters The length of the arm.
+   * @param minAngleRads The minimum angle that the arm is capable of.
+   * @param maxAngleRads The maximum angle that the arm is capable of.
+   * @param armMassKg The mass of the arm.
+   * @param simulateGravity Whether gravity should be simulated or not.
    */
-  public SingleJointedArmSim(DCMotor gearbox, double gearing, double jKgMetersSquared,
-                             double armLengthMeters, double minAngleRads, double maxAngleRads,
-                             double armMassKg, boolean simulateGravity) {
-    this(gearbox, gearing, jKgMetersSquared, armLengthMeters, minAngleRads, maxAngleRads,
-        armMassKg, simulateGravity, null);
+  @SuppressWarnings("ParameterName")
+  public SingleJointedArmSim(
+      DCMotor gearbox,
+      double gearing,
+      double jKgMetersSquared,
+      double armLengthMeters,
+      double minAngleRads,
+      double maxAngleRads,
+      double armMassKg,
+      boolean simulateGravity) {
+    this(
+        gearbox,
+        gearing,
+        jKgMetersSquared,
+        armLengthMeters,
+        minAngleRads,
+        maxAngleRads,
+        armMassKg,
+        simulateGravity,
+        null);
   }
 
   /**
    * Creates a simulated arm mechanism.
    *
-   * @param gearbox            The type of and number of motors in the arm gearbox.
-   * @param gearing            The gearing of the arm (numbers greater than 1 represent reductions).
-   * @param jKgMetersSquared   The moment of inertia of the arm. This can be calculated from CAD software.
-   * @param armLengthMeters    The length of the arm.
-   * @param minAngleRads       The minimum angle that the arm is capable of.
-   * @param maxAngleRads       The maximum angle that the arm is capable of.
-   * @param armMassKg          The mass of the arm.
-   * @param simulateGravity    Whether gravity should be simulated or not.
+   * @param gearbox The type of and number of motors in the arm gearbox.
+   * @param gearing The gearing of the arm (numbers greater than 1 represent reductions).
+   * @param jKgMetersSquared The moment of inertia of the arm; can be calculated from CAD software.
+   * @param armLengthMeters The length of the arm.
+   * @param minAngleRads The minimum angle that the arm is capable of.
+   * @param maxAngleRads The maximum angle that the arm is capable of.
+   * @param armMassKg The mass of the arm.
+   * @param simulateGravity Whether gravity should be simulated or not.
    * @param measurementStdDevs The standard deviations of the measurements.
    */
-  public SingleJointedArmSim(DCMotor gearbox, double gearing, double jKgMetersSquared,
-                             double armLengthMeters, double minAngleRads, double maxAngleRads,
-                             double armMassKg, boolean simulateGravity,
-                             Matrix<N1, N1> measurementStdDevs) {
-    super(LinearSystemId.createSingleJointedArmSystem(gearbox, jKgMetersSquared, gearing),
+  @SuppressWarnings("ParameterName")
+  public SingleJointedArmSim(
+      DCMotor gearbox,
+      double gearing,
+      double jKgMetersSquared,
+      double armLengthMeters,
+      double minAngleRads,
+      double maxAngleRads,
+      double armMassKg,
+      boolean simulateGravity,
+      Matrix<N1, N1> measurementStdDevs) {
+    super(
+        LinearSystemId.createSingleJointedArmSystem(gearbox, jKgMetersSquared, gearing),
         measurementStdDevs);
     m_gearbox = gearbox;
     m_gearing = gearing;
@@ -136,23 +174,41 @@
   }
 
   /**
+   * Returns whether the arm would hit the lower limit.
+   *
+   * @param currentAngleRads The current arm height.
+   * @return Whether the arm would hit the lower limit.
+   */
+  public boolean wouldHitLowerLimit(double currentAngleRads) {
+    return currentAngleRads < this.m_minAngle;
+  }
+
+  /**
+   * Returns whether the arm would hit the upper limit.
+   *
+   * @param currentAngleRads The current arm height.
+   * @return Whether the arm would hit the upper limit.
+   */
+  public boolean wouldHitUpperLimit(double currentAngleRads) {
+    return currentAngleRads > this.m_maxAngle;
+  }
+
+  /**
    * Returns whether the arm has hit the lower limit.
    *
-   * @param x The current arm state.
    * @return Whether the arm has hit the lower limit.
    */
-  public boolean hasHitLowerLimit(Matrix<N2, N1> x) {
-    return x.get(0, 0) < this.m_minAngle;
+  public boolean hasHitLowerLimit() {
+    return wouldHitLowerLimit(getAngleRads());
   }
 
   /**
    * Returns whether the arm has hit the upper limit.
    *
-   * @param x The current arm state.
    * @return Whether the arm has hit the upper limit.
    */
-  public boolean hasHitUpperLimit(Matrix<N2, N1> x) {
-    return x.get(0, 0) > this.m_maxAngle;
+  public boolean hasHitUpperLimit() {
+    return wouldHitUpperLimit(getAngleRads());
   }
 
   /**
@@ -161,7 +217,7 @@
    * @return The current arm angle.
    */
   public double getAngleRads() {
-    return m_x.get(0, 0);
+    return m_y.get(0, 0);
   }
 
   /**
@@ -187,7 +243,7 @@
   }
 
   /**
-   * Sets the input voltage for the elevator.
+   * Sets the input voltage for the arm.
    *
    * @param volts The input voltage.
    */
@@ -196,12 +252,10 @@
   }
 
   /**
-   * Calculates a rough estimate of the moment of inertia of an arm given its
-   * length and mass.
+   * Calculates a rough estimate of the moment of inertia of an arm given its length and mass.
    *
    * @param lengthMeters The length of the arm.
-   * @param massKg       The mass of the arm.
-   *
+   * @param massKg The mass of the arm.
    * @return The calculated moment of inertia.
    */
   public static double estimateMOI(double lengthMeters, double massKg) {
@@ -212,12 +266,12 @@
    * Updates the state of the arm.
    *
    * @param currentXhat The current state estimate.
-   * @param u           The system inputs (voltage).
-   * @param dtSeconds   The time difference between controller updates.
+   * @param u The system inputs (voltage).
+   * @param dtSeconds The time difference between controller updates.
    */
   @Override
-  protected Matrix<N2, N1> updateX(Matrix<N2, N1> currentXhat,
-                                   Matrix<N1, N1> u, double dtSeconds) {
+  @SuppressWarnings({"ParameterName", "LambdaParameterName"})
+  protected Matrix<N2, N1> updateX(Matrix<N2, N1> currentXhat, Matrix<N1, N1> u, double dtSeconds) {
     // Horizontal case:
     // Torque = F * r = I * alpha
     // alpha = F * r / I
@@ -227,22 +281,33 @@
     // This acceleration is added to the linear system dynamics x-dot = Ax + Bu
     // We therefore find that f(x, u) = Ax + Bu + [[0] [m * g * r / I *
     // cos(theta)]]
-    Matrix<N2, N1> updatedXhat = RungeKutta.rungeKutta(
-        (Matrix<N2, N1> x, Matrix<N1, N1> u_) -> {
-          Matrix<N2, N1> xdot = m_plant.getA().times(x).plus(m_plant.getB().times(u_));
-          if (m_simulateGravity) {
-            xdot = xdot.plus(VecBuilder.fill(0,
-                m_armMass * m_r * -9.8 * 3.0 / (m_armMass * m_r * m_r) * Math.cos(x.get(0, 0))));
-          }
-          return xdot;
-        },
-        currentXhat, u, dtSeconds);
+    Matrix<N2, N1> updatedXhat =
+        NumericalIntegration.rkdp(
+            (Matrix<N2, N1> x, Matrix<N1, N1> u_) -> {
+              Matrix<N2, N1> xdot = m_plant.getA().times(x).plus(m_plant.getB().times(u_));
+              if (m_simulateGravity) {
+                xdot =
+                    xdot.plus(
+                        VecBuilder.fill(
+                            0,
+                            m_armMass
+                                * m_r
+                                * -9.8
+                                * 3.0
+                                / (m_armMass * m_r * m_r)
+                                * Math.cos(x.get(0, 0))));
+              }
+              return xdot;
+            },
+            currentXhat,
+            u,
+            dtSeconds);
 
     // We check for collision after updating xhat
-    if (hasHitLowerLimit(updatedXhat)) {
+    if (wouldHitLowerLimit(updatedXhat.get(0, 0))) {
       return VecBuilder.fill(m_minAngle, 0);
     }
-    if (hasHitUpperLimit(updatedXhat)) {
+    if (wouldHitUpperLimit(updatedXhat.get(0, 0))) {
       return VecBuilder.fill(m_maxAngle, 0);
     }
     return updatedXhat;
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
new file mode 100644
index 0000000..3781281
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/simulation/UltrasonicSim.java
@@ -0,0 +1,38 @@
+// 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.simulation;
+
+import edu.wpi.first.hal.SimBoolean;
+import edu.wpi.first.hal.SimDouble;
+import edu.wpi.first.math.util.Units;
+import edu.wpi.first.wpilibj.Ultrasonic;
+
+public class UltrasonicSim {
+  private final SimBoolean m_simRangeValid;
+  private final SimDouble m_simRange;
+
+  /**
+   * Constructor.
+   *
+   * @param ultrasonic The real ultrasonic to simulate
+   */
+  public UltrasonicSim(Ultrasonic ultrasonic) {
+    SimDeviceSim simDevice = new SimDeviceSim("Ultrasonic", ultrasonic.getEchoChannel());
+    m_simRangeValid = simDevice.getBoolean("Range Valid");
+    m_simRange = simDevice.getDouble("Range (in)");
+  }
+
+  public void setRangeValid(boolean valid) {
+    m_simRangeValid.set(valid);
+  }
+
+  public void setRangeInches(double inches) {
+    m_simRange.set(inches);
+  }
+
+  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 40a02bf..625ba81 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
@@ -1,18 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.GenericHID;
 import edu.wpi.first.wpilibj.XboxController;
 
-/**
- * Class to control a simulated Xbox 360 or Xbox One controller.
- */
+/** Class to control a simulated Xbox 360 or Xbox One controller. */
 public class XboxControllerSim extends GenericHIDSim {
   /**
    * Constructs from a XboxController object.
@@ -36,66 +30,146 @@
     setButtonCount(10);
   }
 
-  public void setX(GenericHID.Hand hand, double value) {
-    if (hand.equals(GenericHID.Hand.kLeft)) {
-      setRawAxis(XboxController.Axis.kLeftX.value, value);
-    } else {
-      setRawAxis(XboxController.Axis.kRightX.value, value);
-    }
+  /**
+   * Change the left X value of the joystick.
+   *
+   * @param value the new value
+   */
+  public void setLeftX(double value) {
+    setRawAxis(XboxController.Axis.kLeftX.value, value);
   }
 
-  public void setY(GenericHID.Hand hand, double value) {
-    if (hand.equals(GenericHID.Hand.kLeft)) {
-      setRawAxis(XboxController.Axis.kLeftY.value, value);
-    } else {
-      setRawAxis(XboxController.Axis.kRightY.value, value);
-    }
+  /**
+   * Change the right X value of the joystick.
+   *
+   * @param value the new value
+   */
+  public void setRightX(double value) {
+    setRawAxis(XboxController.Axis.kRightX.value, value);
   }
 
-  public void setTriggerAxis(GenericHID.Hand hand, double value) {
-    if (hand.equals(GenericHID.Hand.kLeft)) {
-      setRawAxis(XboxController.Axis.kLeftTrigger.value, value);
-    } else {
-      setRawAxis(XboxController.Axis.kRightTrigger.value, value);
-    }
+  /**
+   * Change the left Y value of the joystick.
+   *
+   * @param value the new value
+   */
+  public void setLeftY(double value) {
+    setRawAxis(XboxController.Axis.kLeftY.value, value);
   }
 
-  public void setBumper(GenericHID.Hand hand, boolean state) {
-    if (hand.equals(GenericHID.Hand.kLeft)) {
-      setRawButton(XboxController.Button.kBumperLeft.value, state);
-    } else {
-      setRawButton(XboxController.Button.kBumperRight.value, state);
-    }
+  /**
+   * Change the right Y value of the joystick.
+   *
+   * @param value the new value
+   */
+  public void setRightY(double value) {
+    setRawAxis(XboxController.Axis.kRightY.value, value);
   }
 
-  public void setStickButton(GenericHID.Hand hand, boolean state) {
-    if (hand.equals(GenericHID.Hand.kLeft)) {
-      setRawButton(XboxController.Button.kStickLeft.value, state);
-    } else {
-      setRawButton(XboxController.Button.kStickRight.value, state);
-    }
+  /**
+   * Change the value of the left trigger axis on the joystick.
+   *
+   * @param value the new value
+   */
+  public void setLeftTriggerAxis(double value) {
+    setRawAxis(XboxController.Axis.kLeftTrigger.value, value);
   }
 
+  /**
+   * Change the value of the right trigger axis on the joystick.
+   *
+   * @param value the new value
+   */
+  public void setRightTriggerAxis(double value) {
+    setRawAxis(XboxController.Axis.kRightTrigger.value, value);
+  }
+
+  /**
+   * Change the value of the left bumper on the joystick.
+   *
+   * @param state the new value
+   */
+  public void setLeftBumper(boolean state) {
+    setRawButton(XboxController.Button.kLeftBumper.value, state);
+  }
+
+  /**
+   * Change the value of the right bumper on the joystick.
+   *
+   * @param state the new value
+   */
+  public void setRightBumper(boolean state) {
+    setRawButton(XboxController.Button.kRightBumper.value, state);
+  }
+
+  /**
+   * Change the value of the left stick button on the joystick.
+   *
+   * @param state the new value
+   */
+  public void setLeftStickButton(boolean state) {
+    setRawButton(XboxController.Button.kLeftStick.value, state);
+  }
+
+  /**
+   * Change the value of the right stick button on the joystick.
+   *
+   * @param state the new value
+   */
+  public void setRightStickButton(boolean state) {
+    setRawButton(XboxController.Button.kRightStick.value, state);
+  }
+
+  /**
+   * Change the value of the A button.
+   *
+   * @param state the new value
+   */
   public void setAButton(boolean state) {
     setRawButton(XboxController.Button.kA.value, state);
   }
 
+  /**
+   * Change the value of the B button.
+   *
+   * @param state the new value
+   */
   public void setBButton(boolean state) {
     setRawButton(XboxController.Button.kB.value, state);
   }
 
+  /**
+   * Change the value of the X button.
+   *
+   * @param state the new value
+   */
   public void setXButton(boolean state) {
     setRawButton(XboxController.Button.kX.value, state);
   }
 
+  /**
+   * Change the value of the Y button.
+   *
+   * @param state the new value
+   */
   public void setYButton(boolean state) {
     setRawButton(XboxController.Button.kY.value, state);
   }
 
+  /**
+   * Change the value of the Back button.
+   *
+   * @param state the new value
+   */
   public void setBackButton(boolean state) {
     setRawButton(XboxController.Button.kBack.value, state);
   }
 
+  /**
+   * Change the value of the Start button.
+   *
+   * @param state the new value
+   */
   public void setStartButton(boolean state) {
     setRawButton(XboxController.Button.kStart.value, state);
   }
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
new file mode 100644
index 0000000..b76f056
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
@@ -0,0 +1,119 @@
+// 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.smartdashboard;
+
+import edu.wpi.first.math.geometry.Pose2d;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 2D representation of game field for dashboards.
+ *
+ * <p>An object's pose is the location shown on the dashboard view. Note that for the robot, this
+ * may or may not match the internal odometry. For example, the robot is shown at a particular
+ * starting location, the pose in this class would represent the actual location on the field, but
+ * the robot's internal state might have a 0,0,0 pose (unless it's initialized to something
+ * different).
+ *
+ * <p>As the user is able to edit the pose, code performing updates should get the robot pose,
+ * transform it as appropriate (e.g. based on wheel odometry), and set the new pose.
+ *
+ * <p>This class provides methods to set the robot pose, but other objects can also be shown by
+ * using the getObject() function. Other objects can also have multiple poses (which will show the
+ * object at multiple locations).
+ */
+public class Field2d implements NTSendable {
+  /** Constructor. */
+  public Field2d() {
+    FieldObject2d obj = new FieldObject2d("Robot");
+    obj.setPose(new Pose2d());
+    m_objects.add(obj);
+    SendableRegistry.add(this, "Field");
+  }
+
+  /**
+   * Set the robot pose from a Pose object.
+   *
+   * @param pose 2D pose
+   */
+  public synchronized void setRobotPose(Pose2d pose) {
+    m_objects.get(0).setPose(pose);
+  }
+
+  /**
+   * Set the robot pose from x, y, and rotation.
+   *
+   * @param xMeters X location, in meters
+   * @param yMeters Y location, in meters
+   * @param rotation rotation
+   */
+  @SuppressWarnings("ParameterName")
+  public synchronized void setRobotPose(double xMeters, double yMeters, Rotation2d rotation) {
+    m_objects.get(0).setPose(xMeters, yMeters, rotation);
+  }
+
+  /**
+   * Get the robot pose.
+   *
+   * @return 2D pose
+   */
+  public synchronized Pose2d getRobotPose() {
+    return m_objects.get(0).getPose();
+  }
+
+  /**
+   * Get or create a field object.
+   *
+   * @param name The field object's name.
+   * @return Field object
+   */
+  public synchronized FieldObject2d getObject(String name) {
+    for (FieldObject2d obj : m_objects) {
+      if (obj.m_name.equals(name)) {
+        return obj;
+      }
+    }
+    FieldObject2d obj = new FieldObject2d(name);
+    m_objects.add(obj);
+    if (m_table != null) {
+      synchronized (obj) {
+        obj.m_entry = m_table.getEntry(name);
+      }
+    }
+    return obj;
+  }
+
+  /**
+   * Get the robot object.
+   *
+   * @return Field object for robot
+   */
+  public synchronized FieldObject2d getRobotObject() {
+    return m_objects.get(0);
+  }
+
+  @Override
+  public void initSendable(NTSendableBuilder builder) {
+    builder.setSmartDashboardType("Field2d");
+
+    synchronized (this) {
+      m_table = builder.getTable();
+      for (FieldObject2d obj : m_objects) {
+        synchronized (obj) {
+          obj.m_entry = m_table.getEntry(obj.m_name);
+          obj.updateEntry(true);
+        }
+      }
+    }
+  }
+
+  private NetworkTable m_table;
+  private final List<FieldObject2d> m_objects = new ArrayList<>();
+}
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
new file mode 100644
index 0000000..d6c7a5e
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
@@ -0,0 +1,198 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.smartdashboard;
+
+import edu.wpi.first.math.geometry.Pose2d;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.geometry.Translation2d;
+import edu.wpi.first.math.trajectory.Trajectory;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Game field object on a Field2d. */
+public class FieldObject2d {
+  /**
+   * Package-local constructor.
+   *
+   * @param name name
+   */
+  FieldObject2d(String name) {
+    m_name = name;
+  }
+
+  /**
+   * Set the pose from a Pose object.
+   *
+   * @param pose 2D pose
+   */
+  public synchronized void setPose(Pose2d pose) {
+    setPoses(pose);
+  }
+
+  /**
+   * Set the pose from x, y, and rotation.
+   *
+   * @param xMeters X location, in meters
+   * @param yMeters Y location, in meters
+   * @param rotation rotation
+   */
+  @SuppressWarnings("ParameterName")
+  public synchronized void setPose(double xMeters, double yMeters, Rotation2d rotation) {
+    setPose(new Pose2d(xMeters, yMeters, rotation));
+  }
+
+  /**
+   * Get the pose.
+   *
+   * @return 2D pose
+   */
+  public synchronized Pose2d getPose() {
+    updateFromEntry();
+    if (m_poses.isEmpty()) {
+      return new Pose2d();
+    }
+    return m_poses.get(0);
+  }
+
+  /**
+   * Set multiple poses from an list of Pose objects. The total number of poses is limited to 85.
+   *
+   * @param poses list of 2D poses
+   */
+  public synchronized void setPoses(List<Pose2d> poses) {
+    m_poses.clear();
+    for (Pose2d pose : poses) {
+      m_poses.add(pose);
+    }
+    updateEntry();
+  }
+
+  /**
+   * Set multiple poses from an list of Pose objects. The total number of poses is limited to 85.
+   *
+   * @param poses list of 2D poses
+   */
+  public synchronized void setPoses(Pose2d... poses) {
+    m_poses.clear();
+    for (Pose2d pose : poses) {
+      m_poses.add(pose);
+    }
+    updateEntry();
+  }
+
+  /**
+   * Sets poses from a trajectory.
+   *
+   * @param trajectory The trajectory from which the poses should be added.
+   */
+  public synchronized void setTrajectory(Trajectory trajectory) {
+    m_poses.clear();
+    for (Trajectory.State state : trajectory.getStates()) {
+      m_poses.add(state.poseMeters);
+    }
+    updateEntry();
+  }
+
+  /**
+   * Get multiple poses.
+   *
+   * @return list of 2D poses
+   */
+  public synchronized List<Pose2d> getPoses() {
+    updateFromEntry();
+    return new ArrayList<Pose2d>(m_poses);
+  }
+
+  void updateEntry() {
+    updateEntry(false);
+  }
+
+  synchronized void updateEntry(boolean setDefault) {
+    if (m_entry == null) {
+      return;
+    }
+
+    if (m_poses.size() < (255 / 3)) {
+      double[] arr = new double[m_poses.size() * 3];
+      int ndx = 0;
+      for (Pose2d pose : m_poses) {
+        Translation2d translation = pose.getTranslation();
+        arr[ndx + 0] = translation.getX();
+        arr[ndx + 1] = translation.getY();
+        arr[ndx + 2] = pose.getRotation().getDegrees();
+        ndx += 3;
+      }
+
+      if (setDefault) {
+        m_entry.setDefaultDoubleArray(arr);
+      } else {
+        m_entry.setDoubleArray(arr);
+      }
+    } else {
+      // send as raw array of doubles if too big for NT array
+      ByteBuffer output = ByteBuffer.allocate(m_poses.size() * 3 * 8);
+      output.order(ByteOrder.BIG_ENDIAN);
+
+      for (Pose2d pose : m_poses) {
+        Translation2d translation = pose.getTranslation();
+        output.putDouble(translation.getX());
+        output.putDouble(translation.getY());
+        output.putDouble(pose.getRotation().getDegrees());
+      }
+
+      if (setDefault) {
+        m_entry.setDefaultRaw(output.array());
+      } else {
+        m_entry.forceSetRaw(output.array());
+      }
+    }
+  }
+
+  private synchronized void updateFromEntry() {
+    if (m_entry == null) {
+      return;
+    }
+
+    double[] arr = m_entry.getDoubleArray((double[]) null);
+    if (arr != null) {
+      if ((arr.length % 3) != 0) {
+        return;
+      }
+
+      m_poses.clear();
+      for (int i = 0; i < arr.length; i += 3) {
+        m_poses.add(new Pose2d(arr[i], arr[i + 1], Rotation2d.fromDegrees(arr[i + 2])));
+      }
+    } else {
+      // read as raw array of doubles
+      byte[] data = m_entry.getRaw((byte[]) null);
+      if (data == null) {
+        return;
+      }
+
+      // must be triples of doubles
+      if ((data.length % (3 * 8)) != 0) {
+        return;
+      }
+      ByteBuffer input = ByteBuffer.wrap(data);
+      input.order(ByteOrder.BIG_ENDIAN);
+
+      m_poses.clear();
+      for (int i = 0; i < (data.length / (3 * 8)); i++) {
+        double x = input.getDouble();
+        double y = input.getDouble();
+        double rot = input.getDouble();
+        m_poses.add(new Pose2d(x, y, Rotation2d.fromDegrees(rot)));
+      }
+    }
+  }
+
+  String m_name;
+  NetworkTableEntry m_entry;
+  private final List<Pose2d> m_poses = new ArrayList<>();
+}
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 274c7a8..19a3fc1 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
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard;
 
@@ -31,9 +28,7 @@
     }
   }
 
-  /**
-   * Runs all posted tasks.  Called periodically from main thread.
-   */
+  /** 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<>();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java
new file mode 100644
index 0000000..8967944
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java
@@ -0,0 +1,113 @@
+// 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.smartdashboard;
+
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.wpilibj.util.Color8Bit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Visual 2D representation of arms, elevators, and general mechanisms through a node-based API.
+ *
+ * <p>A Mechanism2d object is published and contains at least one root node. A root is the anchor
+ * point of other nodes (such as ligaments). Other nodes are recursively appended based on other
+ * nodes.
+ *
+ * @see MechanismObject2d
+ * @see MechanismLigament2d
+ * @see MechanismRoot2d
+ */
+public final class Mechanism2d implements NTSendable {
+  private static final String kBackgroundColor = "backgroundColor";
+  private NetworkTable m_table;
+  private final Map<String, MechanismRoot2d> m_roots;
+  private final double[] m_dims = new double[2];
+  private String m_color;
+
+  /**
+   * Create a new Mechanism2d with the given dimensions and default color (dark blue).
+   *
+   * <p>The dimensions represent the canvas that all the nodes are drawn on.
+   *
+   * @param width the width
+   * @param height the height
+   */
+  public Mechanism2d(double width, double height) {
+    this(width, height, new Color8Bit(0, 0, 32));
+  }
+
+  /**
+   * Create a new Mechanism2d with the given dimensions.
+   *
+   * <p>The dimensions represent the canvas that all the nodes are drawn on.
+   *
+   * @param width the width
+   * @param height the height
+   * @param backgroundColor the background color. Defaults to dark blue.
+   */
+  public Mechanism2d(double width, double height, Color8Bit backgroundColor) {
+    m_roots = new HashMap<>();
+    m_dims[0] = width;
+    m_dims[1] = height;
+    setBackgroundColor(backgroundColor);
+  }
+
+  /**
+   * Get or create a root in this Mechanism2d with the given name and position.
+   *
+   * <p>If a root with the given name already exists, the given x and y coordinates are not used.
+   *
+   * @param name the root name
+   * @param x the root x coordinate
+   * @param y the root y coordinate
+   * @return a new root joint object, or the existing one with the given name.
+   */
+  public synchronized MechanismRoot2d getRoot(String name, double x, double y) {
+    var existing = m_roots.get(name);
+    if (existing != null) {
+      return existing;
+    }
+
+    var root = new MechanismRoot2d(name, x, y);
+    m_roots.put(name, root);
+    if (m_table != null) {
+      root.update(m_table.getSubTable(name));
+    }
+    return root;
+  }
+
+  /**
+   * Set the Mechanism2d background color.
+   *
+   * @param color the new color
+   */
+  public synchronized void setBackgroundColor(Color8Bit color) {
+    this.m_color = String.format("#%02X%02X%02X", color.red, color.green, color.blue);
+    if (m_table != null) {
+      m_table.getEntry(kBackgroundColor).setString(m_color);
+    }
+  }
+
+  @Override
+  public void initSendable(NTSendableBuilder builder) {
+    builder.setSmartDashboardType("Mechanism2d");
+    synchronized (this) {
+      m_table = builder.getTable();
+      m_table.getEntry("dims").setDoubleArray(m_dims);
+      m_table.getEntry(kBackgroundColor).setString(m_color);
+      for (Entry<String, MechanismRoot2d> entry : m_roots.entrySet()) {
+        String name = entry.getKey();
+        MechanismRoot2d root = entry.getValue();
+        synchronized (root) {
+          root.update(m_table.getSubTable(name));
+        }
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..8579936
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
@@ -0,0 +1,155 @@
+// 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.smartdashboard;
+
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.wpilibj.util.Color8Bit;
+
+/**
+ * Ligament node on a Mechanism2d. A ligament can have its length changed (like an elevator) or
+ * angle changed, like an arm.
+ *
+ * @see Mechanism2d
+ */
+public class MechanismLigament2d extends MechanismObject2d {
+  private double m_angle;
+  private NetworkTableEntry m_angleEntry;
+  private String m_color;
+  private NetworkTableEntry m_colorEntry;
+  private double m_length;
+  private NetworkTableEntry m_lengthEntry;
+  private double m_weight;
+  private NetworkTableEntry m_weightEntry;
+
+  /**
+   * Create a new ligament.
+   *
+   * @param name The ligament name.
+   * @param length The ligament length.
+   * @param angle The ligament angle.
+   * @param lineWidth The ligament's line width.
+   * @param color The ligament's color.
+   */
+  public MechanismLigament2d(
+      String name, double length, double angle, double lineWidth, Color8Bit color) {
+    super(name);
+    setColor(color);
+    setLength(length);
+    setAngle(angle);
+    setLineWeight(lineWidth);
+  }
+
+  /**
+   * Create a new ligament with the default color (orange) and thickness (6).
+   *
+   * @param name The ligament's name.
+   * @param length The ligament's length.
+   * @param angle The ligament's angle relative to its parent.
+   */
+  public MechanismLigament2d(String name, double length, double angle) {
+    this(name, length, angle, 10, new Color8Bit(235, 137, 52));
+  }
+
+  /**
+   * Set the ligament's angle relative to its parent.
+   *
+   * @param degrees the angle, in degrees
+   */
+  public synchronized void setAngle(double degrees) {
+    m_angle = degrees;
+    flush();
+  }
+
+  /**
+   * Set the ligament's angle relative to its parent.
+   *
+   * @param angle the angle
+   */
+  public synchronized void setAngle(Rotation2d angle) {
+    setAngle(angle.getDegrees());
+  }
+
+  /**
+   * Get the ligament's angle relative to its parent.
+   *
+   * @return the angle, in degrees
+   */
+  public synchronized double getAngle() {
+    if (m_angleEntry != null) {
+      m_angle = m_angleEntry.getDouble(0.0);
+    }
+    return m_angle;
+  }
+
+  /**
+   * Set the ligament's length.
+   *
+   * @param length the line length
+   */
+  public synchronized void setLength(double length) {
+    m_length = length;
+    flush();
+  }
+
+  /**
+   * Get the ligament length.
+   *
+   * @return the line length
+   */
+  public synchronized double getLength() {
+    if (m_lengthEntry != null) {
+      m_length = m_lengthEntry.getDouble(0.0);
+    }
+    return m_length;
+  }
+
+  /**
+   * Set the ligament color.
+   *
+   * @param color the color of the line
+   */
+  public synchronized void setColor(Color8Bit color) {
+    m_color = String.format("#%02X%02X%02X", color.red, color.green, color.blue);
+    flush();
+  }
+
+  /**
+   * Set the line thickness.
+   *
+   * @param weight the line thickness
+   */
+  public synchronized void setLineWeight(double weight) {
+    m_weight = weight;
+    flush();
+  }
+
+  @Override
+  protected synchronized void updateEntries(NetworkTable table) {
+    table.getEntry(".type").setString("line");
+    m_angleEntry = table.getEntry("angle");
+    m_lengthEntry = table.getEntry("length");
+    m_colorEntry = table.getEntry("color");
+    m_weightEntry = table.getEntry("weight");
+    flush();
+  }
+
+  /** Flush latest data to NT. */
+  private synchronized void flush() {
+    if (m_angleEntry != null) {
+      m_angleEntry.setDouble(m_angle);
+    }
+    if (m_lengthEntry != null) {
+      m_lengthEntry.setDouble(m_length);
+    }
+    if (m_colorEntry != null) {
+      m_colorEntry.setString(m_color);
+    }
+    if (m_weightEntry != null) {
+      m_weightEntry.setDouble(m_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
new file mode 100644
index 0000000..4598f7e
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
@@ -0,0 +1,73 @@
+// 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.smartdashboard;
+
+import edu.wpi.first.networktables.NetworkTable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Common base class for all Mechanism2d node types.
+ *
+ * <p>To append another node, call {@link #append(MechanismObject2d)}. Objects that aren't appended
+ * to a published {@link Mechanism2d} container are nonfunctional.
+ *
+ * @see Mechanism2d
+ */
+public abstract class MechanismObject2d {
+  /** Relative to parent. */
+  private final String m_name;
+
+  private NetworkTable m_table;
+  private final Map<String, MechanismObject2d> m_objects = new HashMap<>(1);
+
+  /**
+   * Create a new Mechanism node object.
+   *
+   * @param name the node's name, must be unique.
+   */
+  protected MechanismObject2d(String name) {
+    m_name = name;
+  }
+
+  /**
+   * Append a Mechanism object that is based on this one.
+   *
+   * @param <T> The object type.
+   * @param object the object to add.
+   * @return the object given as a parameter, useful for variable assignments and call chaining.
+   * @throws UnsupportedOperationException if the object's name is already used - object names must
+   *     be unique.
+   */
+  public final synchronized <T extends MechanismObject2d> T append(T object) {
+    if (m_objects.containsKey(object.getName())) {
+      throw new UnsupportedOperationException("Mechanism object names must be unique!");
+    }
+    m_objects.put(object.getName(), object);
+    if (m_table != null) {
+      object.update(m_table.getSubTable(object.getName()));
+    }
+    return object;
+  }
+
+  final void update(NetworkTable table) {
+    m_table = table;
+    updateEntries(m_table);
+    for (MechanismObject2d obj : m_objects.values()) {
+      obj.update(m_table.getSubTable(obj.m_name));
+    }
+  }
+
+  /**
+   * Update all entries with new ones from a new table.
+   *
+   * @param table the new table.
+   */
+  protected abstract void updateEntries(NetworkTable table);
+
+  public final String getName() {
+    return m_name;
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java
new file mode 100644
index 0000000..39f4515
--- /dev/null
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java
@@ -0,0 +1,97 @@
+// 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.smartdashboard;
+
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Root Mechanism2d node.
+ *
+ * <p>A root is the anchor point of other nodes (such as ligaments).
+ *
+ * <p>Do not create objects of this class directly! Obtain instances from the {@link
+ * Mechanism2d#getRoot(String, double, double)} factory method.
+ *
+ * <p>Append other nodes by using {@link #append(MechanismObject2d)}.
+ */
+public final class MechanismRoot2d {
+  private final String m_name;
+  private NetworkTable m_table;
+  private final Map<String, MechanismObject2d> m_objects = new HashMap<>(1);
+  private double m_x;
+  private NetworkTableEntry m_xEntry;
+  private double m_y;
+  private NetworkTableEntry m_yEntry;
+
+  /**
+   * Package-private constructor for roots.
+   *
+   * @param name name
+   * @param x x coordinate of root (provide only when constructing a root node)
+   * @param y y coordinate of root (provide only when constructing a root node)
+   */
+  MechanismRoot2d(String name, double x, double y) {
+    m_name = name;
+    m_x = x;
+    m_y = y;
+  }
+
+  /**
+   * Append a Mechanism object that is based on this one.
+   *
+   * @param <T> The object type.
+   * @param object the object to add.
+   * @return the object given as a parameter, useful for variable assignments and call chaining.
+   * @throws UnsupportedOperationException if the object's name is already used - object names must
+   *     be unique.
+   */
+  public synchronized <T extends MechanismObject2d> T append(T object) {
+    if (m_objects.containsKey(object.getName())) {
+      throw new UnsupportedOperationException("Mechanism object names must be unique!");
+    }
+    m_objects.put(object.getName(), object);
+    if (m_table != null) {
+      object.update(m_table.getSubTable(object.getName()));
+    }
+    return object;
+  }
+
+  /**
+   * Set the root's position.
+   *
+   * @param x new x coordinate
+   * @param y new y coordinate
+   */
+  public synchronized void setPosition(double x, double y) {
+    m_x = x;
+    m_y = y;
+  }
+
+  synchronized void update(NetworkTable table) {
+    m_table = table;
+    m_xEntry = m_table.getEntry("x");
+    m_yEntry = m_table.getEntry("y");
+    flush();
+    for (MechanismObject2d obj : m_objects.values()) {
+      obj.update(m_table.getSubTable(obj.getName()));
+    }
+  }
+
+  public String getName() {
+    return m_name;
+  }
+
+  private synchronized void flush() {
+    if (m_xEntry != null) {
+      m_xEntry.setDouble(m_x);
+    }
+    if (m_yEntry != null) {
+      m_yEntry.setDouble(m_y);
+    }
+  }
+}
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilder.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilder.java
deleted file mode 100644
index 35a8b54..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilder.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.smartdashboard;
-
-import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
-import java.util.function.DoubleConsumer;
-import java.util.function.DoubleSupplier;
-import java.util.function.Supplier;
-
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.networktables.NetworkTableValue;
-
-public interface SendableBuilder {
-  /**
-   * Set the string representation of the named data type that will be used
-   * by the smart dashboard for this sendable.
-   *
-   * @param type    data type
-   */
-  void setSmartDashboardType(String type);
-
-  /**
-   * Set a flag indicating if this sendable should be treated as an actuator.
-   * By default this flag is false.
-   *
-   * @param value   true if actuator, false if not
-   */
-  void setActuator(boolean value);
-
-  /**
-   * Set the function that should be called to set the Sendable into a safe
-   * state.  This is called when entering and exiting Live Window mode.
-   *
-   * @param func    function
-   */
-  void setSafeState(Runnable func);
-
-  /**
-   * Set the function that should be called to update the network table
-   * for things other than properties.  Note this function is not passed
-   * the network table object; instead it should use the entry handles
-   * returned by getEntry().
-   *
-   * @param func    function
-   */
-  void setUpdateTable(Runnable func);
-
-  /**
-   * Add a property without getters or setters.  This can be used to get
-   * entry handles for the function called by setUpdateTable().
-   *
-   * @param key   property name
-   * @return Network table entry
-   */
-  NetworkTableEntry getEntry(String key);
-
-  /**
-   * Represents an operation that accepts a single boolean-valued argument and
-   * returns no result. This is the primitive type specialization of Consumer
-   * for boolean. Unlike most other functional interfaces, BooleanConsumer is
-   * expected to operate via side-effects.
-   *
-   * <p>This is a functional interface whose functional method is accept(boolean).
-   */
-  @FunctionalInterface
-  interface BooleanConsumer {
-    /**
-     * Performs the operation on the given value.
-     * @param value the value
-     */
-    void accept(boolean value);
-  }
-
-  /**
-   * Add a boolean property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter);
-
-  /**
-   * Add a double property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addDoubleProperty(String key, DoubleSupplier getter, DoubleConsumer setter);
-
-  /**
-   * Add a string property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addStringProperty(String key, Supplier<String> getter, Consumer<String> setter);
-
-  /**
-   * Add a boolean array property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addBooleanArrayProperty(String key, Supplier<boolean[]> getter, Consumer<boolean[]> setter);
-
-  /**
-   * Add a double array property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addDoubleArrayProperty(String key, Supplier<double[]> getter, Consumer<double[]> setter);
-
-  /**
-   * Add a string array property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addStringArrayProperty(String key, Supplier<String[]> getter, Consumer<String[]> setter);
-
-  /**
-   * Add a raw property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addRawProperty(String key, Supplier<byte[]> getter, Consumer<byte[]> setter);
-
-  /**
-   * Add a NetworkTableValue property.
-   *
-   * @param key     property name
-   * @param getter  getter function (returns current value)
-   * @param setter  setter function (sets new value)
-   */
-  void addValueProperty(String key, Supplier<NetworkTableValue> getter,
-                        Consumer<NetworkTableValue> setter);
-}
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 a82f26b..6c07fe3 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
@@ -1,12 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard;
 
+import edu.wpi.first.networktables.EntryListenerFlags;
+import edu.wpi.first.networktables.NTSendableBuilder;
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.NetworkTableValue;
+import edu.wpi.first.util.function.BooleanConsumer;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BooleanSupplier;
@@ -16,12 +19,7 @@
 import java.util.function.Function;
 import java.util.function.Supplier;
 
-import edu.wpi.first.networktables.EntryListenerFlags;
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.networktables.NetworkTableValue;
-
-public class SendableBuilderImpl implements SendableBuilder {
+public class SendableBuilderImpl implements NTSendableBuilder {
   private static class Property {
     Property(NetworkTable table, String key) {
       m_entry = table.getEntry(key);
@@ -60,7 +58,7 @@
   private boolean m_actuator;
 
   /**
-   * Set the network table.  Must be called prior to any Add* functions being called.
+   * Set the network table. Must be called prior to any Add* functions being called.
    *
    * @param table Network table
    */
@@ -74,15 +72,18 @@
    *
    * @return The network table
    */
+  @Override
   public NetworkTable getTable() {
     return m_table;
   }
 
   /**
    * Return whether this sendable has an associated table.
+   *
    * @return True if it has a table, false if not.
    */
-  public boolean hasTable() {
+  @Override
+  public boolean isPublished() {
     return m_table != null;
   }
 
@@ -95,10 +96,9 @@
     return m_actuator;
   }
 
-  /**
-   * Update the network table values by calling the getters for all properties.
-   */
-  public void updateTable() {
+  /** Update the network table values by calling the getters for all properties. */
+  @Override
+  public void update() {
     for (Property property : m_properties) {
       if (property.m_update != null) {
         property.m_update.accept(property.m_entry);
@@ -109,9 +109,7 @@
     }
   }
 
-  /**
-   * Hook setters for all properties.
-   */
+  /** Hook setters for all properties. */
   public void startListeners() {
     for (Property property : m_properties) {
       property.startListener();
@@ -121,9 +119,7 @@
     }
   }
 
-  /**
-   * Unhook setters for all properties.
-   */
+  /** Unhook setters for all properties. */
   public void stopListeners() {
     for (Property property : m_properties) {
       property.stopListener();
@@ -134,7 +130,7 @@
   }
 
   /**
-   * Start LiveWindow mode by hooking the setters for all properties.  Also calls the safeState
+   * Start LiveWindow mode by hooking the setters for all properties. Also calls the safeState
    * function if one was provided.
    */
   public void startLiveWindowMode() {
@@ -145,7 +141,7 @@
   }
 
   /**
-   * Stop LiveWindow mode by unhooking the setters for all properties.  Also calls the safeState
+   * Stop LiveWindow mode by unhooking the setters for all properties. Also calls the safeState
    * function if one was provided.
    */
   public void stopLiveWindowMode() {
@@ -155,9 +151,8 @@
     }
   }
 
-  /**
-   * Clear properties.
-   */
+  /** Clear properties. */
+  @Override
   public void clearProperties() {
     stopListeners();
     m_properties.clear();
@@ -187,7 +182,7 @@
   }
 
   /**
-   * Set the function that should be called to set the Sendable into a safe state.  This is called
+   * Set the function that should be called to set the Sendable into a safe state. This is called
    * when entering and exiting Live Window mode.
    *
    * @param func function
@@ -199,7 +194,7 @@
 
   /**
    * Set the function that should be called to update the network table for things other than
-   * properties.  Note this function is not passed the network table object; instead it should use
+   * properties. Note this function is not passed the network table object; instead it should use
    * the entry handles returned by getEntry().
    *
    * @param func function
@@ -210,7 +205,7 @@
   }
 
   /**
-   * Add a property without getters or setters.  This can be used to get entry handles for the
+   * Add a property without getters or setters. This can be used to get entry handles for the
    * function called by setUpdateTable().
    *
    * @param key property name
@@ -224,7 +219,7 @@
   /**
    * Add a boolean property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
@@ -235,11 +230,18 @@
       property.m_update = entry -> entry.setBoolean(getter.getAsBoolean());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isBoolean()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getBoolean()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isBoolean()) {
+                      SmartDashboard.postListenerTask(
+                          () -> setter.accept(event.value.getBoolean()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -247,7 +249,7 @@
   /**
    * Add a double property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
@@ -258,11 +260,17 @@
       property.m_update = entry -> entry.setDouble(getter.getAsDouble());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isDouble()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getDouble()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isDouble()) {
+                      SmartDashboard.postListenerTask(() -> setter.accept(event.value.getDouble()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -270,7 +278,7 @@
   /**
    * Add a string property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
@@ -281,11 +289,17 @@
       property.m_update = entry -> entry.setString(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isString()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getString()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isString()) {
+                      SmartDashboard.postListenerTask(() -> setter.accept(event.value.getString()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -293,23 +307,30 @@
   /**
    * Add a boolean array property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
   @Override
-  public void addBooleanArrayProperty(String key, Supplier<boolean[]> getter,
-                                      Consumer<boolean[]> setter) {
+  public void addBooleanArrayProperty(
+      String key, Supplier<boolean[]> getter, Consumer<boolean[]> setter) {
     Property property = new Property(m_table, key);
     if (getter != null) {
       property.m_update = entry -> entry.setBooleanArray(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isBooleanArray()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getBooleanArray()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isBooleanArray()) {
+                      SmartDashboard.postListenerTask(
+                          () -> setter.accept(event.value.getBooleanArray()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -317,23 +338,30 @@
   /**
    * Add a double array property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
   @Override
-  public void addDoubleArrayProperty(String key, Supplier<double[]> getter,
-                                     Consumer<double[]> setter) {
+  public void addDoubleArrayProperty(
+      String key, Supplier<double[]> getter, Consumer<double[]> setter) {
     Property property = new Property(m_table, key);
     if (getter != null) {
       property.m_update = entry -> entry.setDoubleArray(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isDoubleArray()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getDoubleArray()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isDoubleArray()) {
+                      SmartDashboard.postListenerTask(
+                          () -> setter.accept(event.value.getDoubleArray()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -341,23 +369,30 @@
   /**
    * Add a string array property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
   @Override
-  public void addStringArrayProperty(String key, Supplier<String[]> getter,
-                                     Consumer<String[]> setter) {
+  public void addStringArrayProperty(
+      String key, Supplier<String[]> getter, Consumer<String[]> setter) {
     Property property = new Property(m_table, key);
     if (getter != null) {
       property.m_update = entry -> entry.setStringArray(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isStringArray()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getStringArray()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isStringArray()) {
+                      SmartDashboard.postListenerTask(
+                          () -> setter.accept(event.value.getStringArray()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -365,7 +400,7 @@
   /**
    * Add a raw property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
@@ -376,11 +411,17 @@
       property.m_update = entry -> entry.setRaw(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        if (event.value.isRaw()) {
-          SmartDashboard.postListenerTask(() -> setter.accept(event.value.getRaw()));
-        }
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    if (event.value.isRaw()) {
+                      SmartDashboard.postListenerTask(() -> setter.accept(event.value.getRaw()));
+                    }
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
@@ -388,21 +429,27 @@
   /**
    * Add a NetworkTableValue property.
    *
-   * @param key    property name
+   * @param key property name
    * @param getter getter function (returns current value)
    * @param setter setter function (sets new value)
    */
   @Override
-  public void addValueProperty(String key, Supplier<NetworkTableValue> getter,
-                               Consumer<NetworkTableValue> setter) {
+  public void addValueProperty(
+      String key, Supplier<NetworkTableValue> getter, Consumer<NetworkTableValue> setter) {
     Property property = new Property(m_table, key);
     if (getter != null) {
       property.m_update = entry -> entry.setValue(getter.get());
     }
     if (setter != null) {
-      property.m_createListener = entry -> entry.addListener(event -> {
-        SmartDashboard.postListenerTask(() -> setter.accept(event.value));
-      }, EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);
+      property.m_createListener =
+          entry ->
+              entry.addListener(
+                  event -> {
+                    SmartDashboard.postListenerTask(() -> setter.accept(event.value));
+                  },
+                  EntryListenerFlags.kImmediate
+                      | EntryListenerFlags.kNew
+                      | EntryListenerFlags.kUpdate);
     }
     m_properties.add(property);
   }
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 cdf1ffb..2614edf 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
@@ -1,12 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard;
 
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+
+import edu.wpi.first.networktables.NTSendable;
+import edu.wpi.first.networktables.NTSendableBuilder;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.util.sendable.SendableRegistry;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -14,11 +17,6 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantLock;
 
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.wpilibj.Sendable;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-
 /**
  * The {@link SendableChooser} class is a useful tool for presenting a selection of options to the
  * {@link SmartDashboard}.
@@ -31,38 +29,25 @@
  *
  * @param <V> The type of the values to be stored
  */
-public class SendableChooser<V> implements Sendable, AutoCloseable {
-  /**
-   * The key for the default value.
-   */
+public class SendableChooser<V> implements NTSendable, AutoCloseable {
+  /** The key for the default value. */
   private static final String DEFAULT = "default";
-  /**
-   * The key for the selected option.
-   */
+  /** The key for the selected option. */
   private static final String SELECTED = "selected";
-  /**
-   * The key for the active option.
-   */
+  /** The key for the active option. */
   private static final String ACTIVE = "active";
-  /**
-   * The key for the option array.
-   */
+  /** The key for the option array. */
   private static final String OPTIONS = "options";
-  /**
-   * The key for the instance number.
-   */
+  /** The key for the instance number. */
   private static final String INSTANCE = ".instance";
-  /**
-   * A map linking strings to the objects the represent.
-   */
+  /** A map linking strings to the objects the represent. */
   private final Map<String, V> m_map = new LinkedHashMap<>();
+
   private String m_defaultChoice = "";
   private final int m_instance;
   private static final AtomicInteger s_instances = new AtomicInteger();
 
-  /**
-   * Instantiates a {@link SendableChooser}.
-   */
+  /** Instantiates a {@link SendableChooser}. */
   public SendableChooser() {
     m_instance = s_instances.getAndIncrement();
     SendableRegistry.add(this, "SendableChooser", m_instance);
@@ -77,7 +62,7 @@
    * Adds the given object to the list of options. On the {@link SmartDashboard} on the desktop, the
    * object will appear as the given name.
    *
-   * @param name   the name of the option
+   * @param name the name of the option
    * @param object the option
    */
   public void addOption(String name, V object) {
@@ -88,8 +73,7 @@
    * Adds the given object to the list of options.
    *
    * @deprecated Use {@link #addOption(String, Object)} instead.
-   *
-   * @param name   the name of the option
+   * @param name the name of the option
    * @param object the option
    */
   @Deprecated
@@ -102,7 +86,7 @@
    * very close to {@link #addOption(String, Object)} except that it will use this as the default
    * option if none other is explicitly selected.
    *
-   * @param name   the name of the option
+   * @param name the name of the option
    * @param object the option
    */
   public void setDefaultOption(String name, V object) {
@@ -116,8 +100,7 @@
    * Adds the given object to the list of options and marks it as the default.
    *
    * @deprecated Use {@link #setDefaultOption(String, Object)} instead.
-   *
-   * @param name   the name of the option
+   * @param name the name of the option
    * @param object the option
    */
   @Deprecated
@@ -149,39 +132,45 @@
   private final ReentrantLock m_mutex = new ReentrantLock();
 
   @Override
-  public void initSendable(SendableBuilder builder) {
+  public void initSendable(NTSendableBuilder builder) {
     builder.setSmartDashboardType("String Chooser");
     builder.getEntry(INSTANCE).setDouble(m_instance);
     builder.addStringProperty(DEFAULT, () -> m_defaultChoice, null);
     builder.addStringArrayProperty(OPTIONS, () -> m_map.keySet().toArray(new String[0]), null);
-    builder.addStringProperty(ACTIVE, () -> {
-      m_mutex.lock();
-      try {
-        if (m_selected != null) {
-          return m_selected;
-        } else {
-          return m_defaultChoice;
-        }
-      } finally {
-        m_mutex.unlock();
-      }
-    }, null);
+    builder.addStringProperty(
+        ACTIVE,
+        () -> {
+          m_mutex.lock();
+          try {
+            if (m_selected != null) {
+              return m_selected;
+            } else {
+              return m_defaultChoice;
+            }
+          } finally {
+            m_mutex.unlock();
+          }
+        },
+        null);
     m_mutex.lock();
     try {
       m_activeEntries.add(builder.getEntry(ACTIVE));
     } finally {
       m_mutex.unlock();
     }
-    builder.addStringProperty(SELECTED, null, val -> {
-      m_mutex.lock();
-      try {
-        m_selected = val;
-        for (NetworkTableEntry entry : m_activeEntries) {
-          entry.setString(val);
-        }
-      } finally {
-        m_mutex.unlock();
-      }
-    });
+    builder.addStringProperty(
+        SELECTED,
+        null,
+        val -> {
+          m_mutex.lock();
+          try {
+            m_selected = val;
+            for (NetworkTableEntry entry : m_activeEntries) {
+              entry.setString(val);
+            }
+          } finally {
+            m_mutex.unlock();
+          }
+        });
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableRegistry.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableRegistry.java
deleted file mode 100644
index 3cafbf7..0000000
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableRegistry.java
+++ /dev/null
@@ -1,520 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.smartdashboard;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.WeakHashMap;
-import java.util.function.Consumer;
-
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.wpilibj.DriverStation;
-import edu.wpi.first.wpilibj.Sendable;
-
-
-/**
- * The SendableRegistry class is the public interface for registering sensors
- * and actuators for use on dashboards and LiveWindow.
- */
-public class SendableRegistry {
-  private static class Component {
-    Component() {}
-
-    Component(Sendable sendable) {
-      m_sendable = new WeakReference<>(sendable);
-    }
-
-    WeakReference<Sendable> m_sendable;
-    SendableBuilderImpl m_builder = new SendableBuilderImpl();
-    String m_name;
-    String m_subsystem = "Ungrouped";
-    WeakReference<Sendable> m_parent;
-    boolean m_liveWindow;
-    Object[] m_data;
-
-    void setName(String moduleType, int channel) {
-      m_name = moduleType + "[" + channel + "]";
-    }
-
-    void setName(String moduleType, int moduleNumber, int channel) {
-      m_name = moduleType + "[" + moduleNumber + "," + channel + "]";
-    }
-  }
-
-  private static final Map<Object, Component> components = new WeakHashMap<>();
-  private static int nextDataHandle;
-
-  private static Component getOrAdd(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp == null) {
-      comp = new Component(sendable);
-      components.put(sendable, comp);
-    } else {
-      if (comp.m_sendable == null) {
-        comp.m_sendable = new WeakReference<>(sendable);
-      }
-    }
-    return comp;
-  }
-
-  private SendableRegistry() {
-    throw new UnsupportedOperationException("This is a utility class!");
-  }
-
-  /**
-   * Adds an object to the registry.
-   *
-   * @param sendable object to add
-   * @param name component name
-   */
-  public static synchronized void add(Sendable sendable, String name) {
-    Component comp = getOrAdd(sendable);
-    comp.m_name = name;
-  }
-
-  /**
-   * Adds an object to the registry.
-   *
-   * @param sendable     object to add
-   * @param moduleType   A string that defines the module name in the label for
-   *                     the value
-   * @param channel      The channel number the device is plugged into
-   */
-  public static synchronized void add(Sendable sendable, String moduleType, int channel) {
-    Component comp = getOrAdd(sendable);
-    comp.setName(moduleType, channel);
-  }
-
-  /**
-   * Adds an object to the registry.
-   *
-   * @param sendable     object to add
-   * @param moduleType   A string that defines the module name in the label for
-   *                     the value
-   * @param moduleNumber The number of the particular module type
-   * @param channel      The channel number the device is plugged into
-   */
-  public static synchronized void add(Sendable sendable, String moduleType, int moduleNumber,
-      int channel) {
-    Component comp = getOrAdd(sendable);
-    comp.setName(moduleType, moduleNumber, channel);
-  }
-
-  /**
-   * Adds an object to the registry.
-   *
-   * @param sendable object to add
-   * @param subsystem subsystem name
-   * @param name component name
-   */
-  public static synchronized void add(Sendable sendable, String subsystem, String name) {
-    Component comp = getOrAdd(sendable);
-    comp.m_name = name;
-    comp.m_subsystem = subsystem;
-  }
-
-  /**
-   * Adds an object to the registry and LiveWindow.
-   *
-   * @param sendable object to add
-   * @param name component name
-   */
-  public static synchronized void addLW(Sendable sendable, String name) {
-    Component comp = getOrAdd(sendable);
-    comp.m_liveWindow = true;
-    comp.m_name = name;
-  }
-
-  /**
-   * Adds an object to the registry and LiveWindow.
-   *
-   * @param sendable     object to add
-   * @param moduleType   A string that defines the module name in the label for
-   *                     the value
-   * @param channel      The channel number the device is plugged into
-   */
-  public static synchronized void addLW(Sendable sendable, String moduleType, int channel) {
-    Component comp = getOrAdd(sendable);
-    comp.m_liveWindow = true;
-    comp.setName(moduleType, channel);
-  }
-
-  /**
-   * Adds an object to the registry and LiveWindow.
-   *
-   * @param sendable     object to add
-   * @param moduleType   A string that defines the module name in the label for
-   *                     the value
-   * @param moduleNumber The number of the particular module type
-   * @param channel      The channel number the device is plugged into
-   */
-  public static synchronized void addLW(Sendable sendable, String moduleType, int moduleNumber,
-      int channel) {
-    Component comp = getOrAdd(sendable);
-    comp.m_liveWindow = true;
-    comp.setName(moduleType, moduleNumber, channel);
-  }
-
-  /**
-   * Adds an object to the registry and LiveWindow.
-   *
-   * @param sendable object to add
-   * @param subsystem subsystem name
-   * @param name component name
-   */
-  public static synchronized void addLW(Sendable sendable, String subsystem, String name) {
-    Component comp = getOrAdd(sendable);
-    comp.m_liveWindow = true;
-    comp.m_name = name;
-    comp.m_subsystem = subsystem;
-  }
-
-  /**
-   * Adds a child object to an object.  Adds the child object to the registry
-   * if it's not already present.
-   *
-   * @param parent parent object
-   * @param child child object
-   */
-  public static synchronized void addChild(Sendable parent, Object child) {
-    Component comp = components.get(child);
-    if (comp == null) {
-      comp = new Component();
-      components.put(child, comp);
-    }
-    comp.m_parent = new WeakReference<>(parent);
-  }
-
-  /**
-   * Removes an object from the registry.
-   *
-   * @param sendable object to remove
-   * @return true if the object was removed; false if it was not present
-   */
-  public static synchronized boolean remove(Sendable sendable) {
-    return components.remove(sendable) != null;
-  }
-
-  /**
-   * Determines if an object is in the registry.
-   *
-   * @param sendable object to check
-   * @return True if in registry, false if not.
-   */
-  public static synchronized boolean contains(Sendable sendable) {
-    return components.containsKey(sendable);
-  }
-
-  /**
-   * Gets the name of an object.
-   *
-   * @param sendable object
-   * @return Name (empty if object is not in registry)
-   */
-  public static synchronized String getName(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp == null) {
-      return "";
-    }
-    return comp.m_name;
-  }
-
-  /**
-   * Sets the name of an object.
-   *
-   * @param sendable object
-   * @param name name
-   */
-  public static synchronized void setName(Sendable sendable, String name) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_name = name;
-    }
-  }
-
-  /**
-   * Sets the name of an object with a channel number.
-   *
-   * @param sendable   object
-   * @param moduleType A string that defines the module name in the label for
-   *                   the value
-   * @param channel    The channel number the device is plugged into
-   */
-  public static synchronized void setName(Sendable sendable, String moduleType, int channel) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.setName(moduleType, channel);
-    }
-  }
-
-  /**
-   * Sets the name of an object with a module and channel number.
-   *
-   * @param sendable     object
-   * @param moduleType   A string that defines the module name in the label for
-   *                     the value
-   * @param moduleNumber The number of the particular module type
-   * @param channel      The channel number the device is plugged into
-   */
-  public static synchronized void setName(Sendable sendable, String moduleType, int moduleNumber,
-      int channel) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.setName(moduleType, moduleNumber, channel);
-    }
-  }
-
-  /**
-   * Sets both the subsystem name and device name of an object.
-   *
-   * @param sendable object
-   * @param subsystem subsystem name
-   * @param name device name
-   */
-  public static synchronized void setName(Sendable sendable, String subsystem, String name) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_name = name;
-      comp.m_subsystem = subsystem;
-    }
-  }
-
-  /**
-   * Gets the subsystem name of an object.
-   *
-   * @param sendable object
-   * @return Subsystem name (empty if object is not in registry)
-   */
-  public static synchronized String getSubsystem(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp == null) {
-      return "";
-    }
-    return comp.m_subsystem;
-  }
-
-  /**
-   * Sets the subsystem name of an object.
-   *
-   * @param sendable object
-   * @param subsystem subsystem name
-   */
-  public static synchronized void setSubsystem(Sendable sendable, String subsystem) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_subsystem = subsystem;
-    }
-  }
-
-  /**
-   * Gets a unique handle for setting/getting data with setData() and getData().
-   *
-   * @return Handle
-   */
-  public static synchronized int getDataHandle() {
-    return nextDataHandle++;
-  }
-
-  /**
-   * Associates arbitrary data with an object in the registry.
-   *
-   * @param sendable object
-   * @param handle data handle returned by getDataHandle()
-   * @param data data to set
-   * @return Previous data (may be null)
-   */
-  public static synchronized Object setData(Sendable sendable, int handle, Object data) {
-    Component comp = components.get(sendable);
-    if (comp == null) {
-      return null;
-    }
-    Object rv = null;
-    if (comp.m_data == null) {
-      comp.m_data = new Object[handle + 1];
-    } else if (handle < comp.m_data.length) {
-      rv = comp.m_data[handle];
-    } else {
-      comp.m_data = Arrays.copyOf(comp.m_data, handle + 1);
-    }
-    comp.m_data[handle] = data;
-    return rv;
-  }
-
-  /**
-   * Gets arbitrary data associated with an object in the registry.
-   *
-   * @param sendable object
-   * @param handle data handle returned by getDataHandle()
-   * @return data (may be null if none associated)
-   */
-  public static synchronized Object getData(Sendable sendable, int handle) {
-    Component comp = components.get(sendable);
-    if (comp == null || comp.m_data == null || handle >= comp.m_data.length) {
-      return null;
-    }
-    return comp.m_data[handle];
-  }
-
-  /**
-   * Enables LiveWindow for an object.
-   *
-   * @param sendable object
-   */
-  public static synchronized void enableLiveWindow(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_liveWindow = true;
-    }
-  }
-
-  /**
-   * Disables LiveWindow for an object.
-   *
-   * @param sendable object
-   */
-  public static synchronized void disableLiveWindow(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_liveWindow = false;
-    }
-  }
-
-  /**
-   * Publishes an object in the registry to a network table.
-   *
-   * @param sendable object
-   * @param table network table
-   */
-  public static synchronized void publish(Sendable sendable, NetworkTable table) {
-    Component comp = getOrAdd(sendable);
-    comp.m_builder.clearProperties();
-    comp.m_builder.setTable(table);
-    sendable.initSendable(comp.m_builder);
-    comp.m_builder.updateTable();
-    comp.m_builder.startListeners();
-  }
-
-  /**
-   * Updates network table information from an object.
-   *
-   * @param sendable object
-   */
-  public static synchronized void update(Sendable sendable) {
-    Component comp = components.get(sendable);
-    if (comp != null) {
-      comp.m_builder.updateTable();
-    }
-  }
-
-  /**
-   * Data passed to foreachLiveWindow() callback function.
-   */
-  public static class CallbackData {
-    /**
-     * Sendable object.
-     */
-    @SuppressWarnings("MemberName")
-    public Sendable sendable;
-
-    /**
-     * Name.
-     */
-    @SuppressWarnings("MemberName")
-    public String name;
-
-    /**
-     * Subsystem.
-     */
-    @SuppressWarnings("MemberName")
-    public String subsystem;
-
-    /**
-     * Parent sendable object.
-     */
-    @SuppressWarnings("MemberName")
-    public Sendable parent;
-
-    /**
-     * Data stored in object with setData().  Update this to change the data.
-     */
-    @SuppressWarnings("MemberName")
-    public Object data;
-
-    /**
-     * Sendable builder for the sendable.
-     */
-    @SuppressWarnings("MemberName")
-    public SendableBuilderImpl builder;
-  }
-
-  // As foreachLiveWindow is single threaded, cache the components it
-  // iterates over to avoid risk of ConcurrentModificationException
-  private static List<Component> foreachComponents = new ArrayList<>();
-
-  /**
-   * Iterates over LiveWindow-enabled objects in the registry.
-   * It is *not* safe to call other SendableRegistry functions from the
-   * callback.
-   *
-   * @param dataHandle data handle to get data object passed to callback
-   * @param callback function to call for each object
-   */
-  @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops",
-                     "PMD.AvoidCatchingThrowable"})
-  public static synchronized void foreachLiveWindow(int dataHandle,
-      Consumer<CallbackData> callback) {
-    CallbackData cbdata = new CallbackData();
-    foreachComponents.clear();
-    foreachComponents.addAll(components.values());
-    for (Component comp : foreachComponents) {
-      if (comp.m_sendable == null) {
-        continue;
-      }
-      cbdata.sendable = comp.m_sendable.get();
-      if (cbdata.sendable != null && comp.m_liveWindow) {
-        cbdata.name = comp.m_name;
-        cbdata.subsystem = comp.m_subsystem;
-        if (comp.m_parent != null) {
-          cbdata.parent = comp.m_parent.get();
-        } else {
-          cbdata.parent = null;
-        }
-        if (comp.m_data != null && dataHandle < comp.m_data.length) {
-          cbdata.data = comp.m_data[dataHandle];
-        } else {
-          cbdata.data = null;
-        }
-        cbdata.builder = comp.m_builder;
-        try {
-          callback.accept(cbdata);
-        } catch (Throwable throwable) {
-          Throwable cause = throwable.getCause();
-          if (cause != null) {
-            throwable = cause;
-          }
-          DriverStation.reportError(
-              "Unhandled exception calling LiveWindow for " + comp.m_name + ": "
-                  + throwable.toString(), false);
-          comp.m_liveWindow = false;
-        }
-        if (cbdata.data != null) {
-          if (comp.m_data == null) {
-            comp.m_data = new Object[dataHandle + 1];
-          } else if (dataHandle >= comp.m_data.length) {
-            comp.m_data = Arrays.copyOf(comp.m_data, dataHandle + 1);
-          }
-          comp.m_data[dataHandle] = cbdata.data;
-        }
-      }
-    }
-    foreachComponents.clear();
-  }
-}
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 1b022c1..be5684b 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
@@ -1,23 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard;
 
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
-import edu.wpi.first.wpilibj.Sendable;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableRegistry;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * The {@link SmartDashboard} class is the bridge between robot programs and the SmartDashboard on
@@ -26,24 +23,18 @@
  * <p>When a value is put into the SmartDashboard here, it pops up on the SmartDashboard on the
  * laptop. Users can put values into and get values from the SmartDashboard.
  */
-@SuppressWarnings("PMD.GodClass")
 public final class SmartDashboard {
-  /**
-   * The {@link NetworkTable} used by {@link SmartDashboard}.
-   */
+  /** The {@link NetworkTable} used by {@link SmartDashboard}. */
   private static final NetworkTable table =
       NetworkTableInstance.getDefault().getTable("SmartDashboard");
 
   /**
-   * A table linking tables in the SmartDashboard to the {@link Sendable} objects they
-   * came from.
+   * A table linking tables in the SmartDashboard to the {@link Sendable} objects they came from.
    */
   @SuppressWarnings("PMD.UseConcurrentHashMap")
   private static final Map<String, Sendable> tablesToData = new HashMap<>();
 
-  /**
-   * The executor for listener tasks; calls listener tasks synchronously from main thread.
-   */
+  /** The executor for listener tasks; calls listener tasks synchronously from main thread. */
   private static final ListenerExecutor listenerExecutor = new ListenerExecutor();
 
   static {
@@ -58,7 +49,7 @@
    * Maps the specified key to the specified value in this table. The key can not be null. The value
    * can be retrieved by calling the get method with a key that is equal to the original key.
    *
-   * @param key  the key
+   * @param key the key
    * @param data the value
    * @throws IllegalArgumentException If key is null
    */
@@ -68,15 +59,18 @@
     if (sddata == null || sddata != data) {
       tablesToData.put(key, data);
       NetworkTable dataTable = table.getSubTable(key);
-      SendableRegistry.publish(data, dataTable);
+      SendableBuilderImpl builder = new SendableBuilderImpl();
+      builder.setTable(dataTable);
+      SendableRegistry.publish(data, builder);
+      builder.startListeners();
       dataTable.getEntry(".name").setString(key);
     }
   }
 
   /**
-   * Maps the specified key (where the key is the name of the {@link Sendable}
-   * to the specified value in this table. The value can be retrieved by
-   * calling the get method with a key that is equal to the original key.
+   * Maps the specified key (where the key is the name of the {@link Sendable} to the specified
+   * value in this table. The value can be retrieved by calling the get method with a key that is
+   * equal to the original key.
    *
    * @param value the value
    * @throws IllegalArgumentException If key is null
@@ -93,7 +87,7 @@
    *
    * @param key the key
    * @return the value
-   * @throws IllegalArgumentException  if the key is null
+   * @throws IllegalArgumentException if the key is null
    */
   public static synchronized Sendable getData(String key) {
     Sendable data = tablesToData.get(key);
@@ -106,6 +100,7 @@
 
   /**
    * Gets the entry for the specified key.
+   *
    * @param key the key name
    * @return Network table entry.
    */
@@ -143,8 +138,7 @@
   }
 
   /**
-   * Makes a key's value persistent through program restarts.
-   * The key cannot be null.
+   * Makes a key's value persistent through program restarts. The key cannot be null.
    *
    * @param key the key name
    */
@@ -153,8 +147,7 @@
   }
 
   /**
-   * Stop making a key's value persistent through program restarts.
-   * The key cannot be null.
+   * Stop making a key's value persistent through program restarts. The key cannot be null.
    *
    * @param key the key name
    */
@@ -163,8 +156,7 @@
   }
 
   /**
-   * Returns whether the value is persistent through program restarts.
-   * The key cannot be null.
+   * Returns whether the value is persistent through program restarts. The key cannot be null.
    *
    * @param key the key name
    * @return True if the value is persistent.
@@ -174,8 +166,7 @@
   }
 
   /**
-   * Sets flags on the specified key in this table. The key can
-   * not be null.
+   * Sets flags on the specified key in this table. The key can not be null.
    *
    * @param key the key name
    * @param flags the flags to set (bitmask)
@@ -185,8 +176,7 @@
   }
 
   /**
-   * Clears flags on the specified key in this table. The key can
-   * not be null.
+   * Clears flags on the specified key in this table. The key can not be null.
    *
    * @param key the key name
    * @param flags the flags to clear (bitmask)
@@ -206,8 +196,7 @@
   }
 
   /**
-   * Deletes the specified key in this table. The key can
-   * not be null.
+   * Deletes the specified key in this table. The key can not be null.
    *
    * @param key the key name
    */
@@ -217,6 +206,7 @@
 
   /**
    * Put a boolean in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -227,6 +217,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -236,12 +227,13 @@
   }
 
   /**
-   * Returns the boolean the key maps to. If the key does not exist or is of
-   *     different type, it will return the default value.
+   * Returns the boolean the key maps to. If the key does not exist or is of different type, it will
+   * return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static boolean getBoolean(String key, boolean defaultValue) {
     return getEntry(key).getBoolean(defaultValue);
@@ -249,6 +241,7 @@
 
   /**
    * Put a number in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -259,6 +252,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -268,12 +262,13 @@
   }
 
   /**
-   * Returns the number the key maps to. If the key does not exist or is of
-   *     different type, it will return the default value.
+   * Returns the number the key maps to. If the key does not exist or is of different type, it will
+   * return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static double getNumber(String key, double defaultValue) {
     return getEntry(key).getDouble(defaultValue);
@@ -281,6 +276,7 @@
 
   /**
    * Put a string in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -291,6 +287,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -300,12 +297,13 @@
   }
 
   /**
-   * Returns the string the key maps to. If the key does not exist or is of
-   *     different type, it will return the default value.
+   * Returns the string the key maps to. If the key does not exist or is of different type, it will
+   * return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static String getString(String key, String defaultValue) {
     return getEntry(key).getString(defaultValue);
@@ -313,6 +311,7 @@
 
   /**
    * Put a boolean array in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -323,6 +322,7 @@
 
   /**
    * Put a boolean array in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -333,6 +333,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -343,6 +344,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -352,24 +354,26 @@
   }
 
   /**
-   * Returns the boolean array the key maps to. If the key does not exist or is
-   *     of different type, it will return the default value.
+   * Returns the boolean array the key maps to. If the key does not exist or is of different type,
+   * it will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static boolean[] getBooleanArray(String key, boolean[] defaultValue) {
     return getEntry(key).getBooleanArray(defaultValue);
   }
 
   /**
-   * Returns the boolean array the key maps to. If the key does not exist or is
-   *     of different type, it will return the default value.
+   * Returns the boolean array the key maps to. If the key does not exist or is of different type,
+   * it will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static Boolean[] getBooleanArray(String key, Boolean[] defaultValue) {
     return getEntry(key).getBooleanArray(defaultValue);
@@ -377,6 +381,7 @@
 
   /**
    * Put a number array in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -387,6 +392,7 @@
 
   /**
    * Put a number array in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -397,6 +403,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -407,6 +414,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -416,24 +424,26 @@
   }
 
   /**
-   * Returns the number array the key maps to. If the key does not exist or is
-   *     of different type, it will return the default value.
+   * Returns the number array the key maps to. If the key does not exist or is of different type, it
+   * will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static double[] getNumberArray(String key, double[] defaultValue) {
     return getEntry(key).getDoubleArray(defaultValue);
   }
 
   /**
-   * Returns the number array the key maps to. If the key does not exist or is
-   *     of different type, it will return the default value.
+   * Returns the number array the key maps to. If the key does not exist or is of different type, it
+   * will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static Double[] getNumberArray(String key, Double[] defaultValue) {
     return getEntry(key).getDoubleArray(defaultValue);
@@ -441,6 +451,7 @@
 
   /**
    * Put a string array in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -451,6 +462,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -460,12 +472,13 @@
   }
 
   /**
-   * Returns the string array the key maps to. If the key does not exist or is
-   *     of different type, it will return the default value.
+   * Returns the string array the key maps to. If the key does not exist or is of different type, it
+   * will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static String[] getStringArray(String key, String[] defaultValue) {
     return getEntry(key).getStringArray(defaultValue);
@@ -473,6 +486,7 @@
 
   /**
    * Put a raw value (byte array) in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @return False if the table key already exists with a different type
@@ -483,6 +497,7 @@
 
   /**
    * Put a raw value (bytes from a byte buffer) in the table.
+   *
    * @param key the key to be assigned to
    * @param value the value that will be assigned
    * @param len the length of the value
@@ -494,6 +509,7 @@
 
   /**
    * Gets the current value in the table, setting it if it does not exist.
+   *
    * @param key the key
    * @param defaultValue the default value to set if key does not exist.
    * @return False if the table key exists with a different type
@@ -503,20 +519,21 @@
   }
 
   /**
-   * Returns the raw value (byte array) the key maps to. If the key does not
-   *     exist or is of different type, it will return the default value.
+   * Returns the raw value (byte array) the key maps to. If the key does not exist or is of
+   * different type, it will return the default value.
+   *
    * @param key the key to look up
    * @param defaultValue the value to be returned if no value is found
-   * @return the value associated with the given key or the given default value
-   *     if there is no value associated with the key
+   * @return the value associated with the given key or the given default value if there is no value
+   *     associated with the key
    */
   public static byte[] getRaw(String key, byte[] defaultValue) {
     return getEntry(key).getRaw(defaultValue);
   }
 
   /**
-   * Posts a task from a listener to the ListenerExecutor, so that it can be run synchronously
-   * from the main loop on the next call to {@link SmartDashboard#updateValues()}.
+   * Posts a task from a listener to the ListenerExecutor, so that it can be run synchronously from
+   * the main loop on the next call to {@link SmartDashboard#updateValues()}.
    *
    * @param task The task to run synchronously from the main thread.
    */
@@ -524,14 +541,12 @@
     listenerExecutor.execute(task);
   }
 
-  /**
-   * Puts all sendable data to the dashboard.
-   */
+  /** Puts all sendable data to the dashboard. */
   public static synchronized void updateValues() {
+    // Execute posted listener tasks
+    listenerExecutor.runListenerTasks();
     for (Sendable data : tablesToData.values()) {
       SendableRegistry.update(data);
     }
-    // Execute posted listener tasks
-    listenerExecutor.runListenerTasks();
   }
 }
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 a576991..1287f4a 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
@@ -1,16 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 edu.wpi.first.math.MathUtil;
 import java.util.Objects;
 
-import edu.wpi.first.wpiutil.math.MathUtil;
-
 /**
  * Represents colors.
  *
@@ -27,9 +23,9 @@
   /**
    * Constructs a Color.
    *
-   * @param red   Red value (0-1)
+   * @param red Red value (0-1)
    * @param green Green value (0-1)
-   * @param blue  Blue value (0-1)
+   * @param blue Blue value (0-1)
    */
   public Color(double red, double green, double blue) {
     this.red = roundAndClamp(red);
@@ -43,9 +39,7 @@
    * @param color The color
    */
   public Color(Color8Bit color) {
-    this(color.red / 255.0,
-        color.green / 255.0,
-        color.blue / 255.0);
+    this(color.red / 255.0, color.green / 255.0, color.blue / 255.0);
   }
 
   /**
@@ -114,722 +108,436 @@
    * FIRST Colors
    */
 
-  /**
-   * #1560BD.
-   */
+  /** 0x1560BD. */
   public static final Color kDenim = new Color(0.0823529412, 0.376470589, 0.7411764706);
 
-  /**
-   * #0066B3.
-   */
+  /** 0x0066B3. */
   public static final Color kFirstBlue = new Color(0.0, 0.4, 0.7019607844);
 
-  /**
-   * #ED1C24.
-   */
+  /** 0xED1C24. */
   public static final Color kFirstRed = new Color(0.9294117648, 0.1098039216, 0.1411764706);
 
   /*
    * Standard Colors
    */
 
-  /**
-   * #F0F8FF.
-   */
+  /** 0xF0F8FF. */
   public static final Color kAliceBlue = new Color(0.9411765f, 0.972549f, 1.0f);
 
-  /**
-   * #FAEBD7.
-   */
+  /** 0xFAEBD7. */
   public static final Color kAntiqueWhite = new Color(0.98039216f, 0.92156863f, 0.84313726f);
 
-  /**
-   * #00FFFF.
-   */
+  /** 0x00FFFF. */
   public static final Color kAqua = new Color(0.0f, 1.0f, 1.0f);
 
-  /**
-   * #7FFFD4.
-    */
+  /** 0x7FFFD4. */
   public static final Color kAquamarine = new Color(0.49803922f, 1.0f, 0.83137256f);
 
-  /**
-   * #F0FFFF.
-   */
+  /** 0xF0FFFF. */
   public static final Color kAzure = new Color(0.9411765f, 1.0f, 1.0f);
 
-  /**
-   * #F5F5DC.
-   */
+  /** 0xF5F5DC. */
   public static final Color kBeige = new Color(0.9607843f, 0.9607843f, 0.8627451f);
 
-  /**
-   * #FFE4C4.
-   */
+  /** 0xFFE4C4. */
   public static final Color kBisque = new Color(1.0f, 0.89411765f, 0.76862746f);
 
-  /**
-   * #000000.
-   */
+  /** 0x000000. */
   public static final Color kBlack = new Color(0.0f, 0.0f, 0.0f);
 
-  /**
-   * #FFEBCD.
-   */
+  /** 0xFFEBCD. */
   public static final Color kBlanchedAlmond = new Color(1.0f, 0.92156863f, 0.8039216f);
 
-  /**
-   * #0000FF.
-   */
+  /** 0x0000FF. */
   public static final Color kBlue = new Color(0.0f, 0.0f, 1.0f);
 
-  /**
-   * #8A2BE2.
-   */
+  /** 0x8A2BE2. */
   public static final Color kBlueViolet = new Color(0.5411765f, 0.16862746f, 0.8862745f);
 
-  /**
-   * #A52A2A.
-   */
+  /** 0xA52A2A. */
   public static final Color kBrown = new Color(0.64705884f, 0.16470589f, 0.16470589f);
 
-  /**
-   * #DEB887.
-   */
+  /** 0xDEB887. */
   public static final Color kBurlywood = new Color(0.87058824f, 0.72156864f, 0.5294118f);
 
-  /**
-   * #5F9EA0.
-   */
+  /** 0x5F9EA0. */
   public static final Color kCadetBlue = new Color(0.37254903f, 0.61960787f, 0.627451f);
 
-  /**
-   * #7FFF00.
-   */
+  /** 0x7FFF00. */
   public static final Color kChartreuse = new Color(0.49803922f, 1.0f, 0.0f);
 
-  /**
-   * #D2691E.
-   */
+  /** 0xD2691E. */
   public static final Color kChocolate = new Color(0.8235294f, 0.4117647f, 0.11764706f);
 
-  /**
-   * #FF7F50.
-   */
+  /** 0xFF7F50. */
   public static final Color kCoral = new Color(1.0f, 0.49803922f, 0.3137255f);
 
-  /**
-   * #6495ED.
-   */
+  /** 0x6495ED. */
   public static final Color kCornflowerBlue = new Color(0.39215687f, 0.58431375f, 0.92941177f);
 
-  /**
-   * #FFF8DC.
-   */
+  /** 0xFFF8DC. */
   public static final Color kCornsilk = new Color(1.0f, 0.972549f, 0.8627451f);
 
-  /**
-   * #DC143C.
-   */
+  /** 0xDC143C. */
   public static final Color kCrimson = new Color(0.8627451f, 0.078431375f, 0.23529412f);
 
-  /**
-   * #00FFFF.
-   */
+  /** 0x00FFFF. */
   public static final Color kCyan = new Color(0.0f, 1.0f, 1.0f);
 
-  /**
-   * #00008B.
-   */
+  /** 0x00008B. */
   public static final Color kDarkBlue = new Color(0.0f, 0.0f, 0.54509807f);
 
-  /**
-   * #008B8B.
-   */
+  /** 0x008B8B. */
   public static final Color kDarkCyan = new Color(0.0f, 0.54509807f, 0.54509807f);
 
-  /**
-   * #B8860B.
-   */
+  /** 0xB8860B. */
   public static final Color kDarkGoldenrod = new Color(0.72156864f, 0.5254902f, 0.043137256f);
 
-  /**
-   * #A9A9A9.
-   */
+  /** 0xA9A9A9. */
   public static final Color kDarkGray = new Color(0.6627451f, 0.6627451f, 0.6627451f);
 
-  /**
-   * #006400.
-   */
+  /** 0x006400. */
   public static final Color kDarkGreen = new Color(0.0f, 0.39215687f, 0.0f);
 
-  /**
-   * #BDB76B.
-   */
+  /** 0xBDB76B. */
   public static final Color kDarkKhaki = new Color(0.7411765f, 0.7176471f, 0.41960785f);
 
-  /**
-   * #8B008B.
-   */
+  /** 0x8B008B. */
   public static final Color kDarkMagenta = new Color(0.54509807f, 0.0f, 0.54509807f);
 
-  /**
-   * #556B2F.
-   */
+  /** 0x556B2F. */
   public static final Color kDarkOliveGreen = new Color(0.33333334f, 0.41960785f, 0.18431373f);
 
-  /**
-   * #FF8C00.
-   */
+  /** 0xFF8C00. */
   public static final Color kDarkOrange = new Color(1.0f, 0.54901963f, 0.0f);
 
-  /**
-   * #9932CC.
-   */
+  /** 0x9932CC. */
   public static final Color kDarkOrchid = new Color(0.6f, 0.19607843f, 0.8f);
 
-  /**
-   * #8B0000.
-   */
+  /** 0x8B0000. */
   public static final Color kDarkRed = new Color(0.54509807f, 0.0f, 0.0f);
 
-  /**
-   * #E9967A.
-   */
+  /** 0xE9967A. */
   public static final Color kDarkSalmon = new Color(0.9137255f, 0.5882353f, 0.47843137f);
 
-  /**
-   * #8FBC8F.
-   */
+  /** 0x8FBC8F. */
   public static final Color kDarkSeaGreen = new Color(0.56078434f, 0.7372549f, 0.56078434f);
 
-  /**
-   * #483D8B.
-   */
+  /** 0x483D8B. */
   public static final Color kDarkSlateBlue = new Color(0.28235295f, 0.23921569f, 0.54509807f);
 
-  /**
-   * #2F4F4F.
-   */
+  /** 0x2F4F4F. */
   public static final Color kDarkSlateGray = new Color(0.18431373f, 0.30980393f, 0.30980393f);
 
-  /**
-   * #00CED1.
-   */
+  /** 0x00CED1. */
   public static final Color kDarkTurquoise = new Color(0.0f, 0.80784315f, 0.81960785f);
 
-  /**
-   * #9400D3.
-   */
+  /** 0x9400D3. */
   public static final Color kDarkViolet = new Color(0.5803922f, 0.0f, 0.827451f);
 
-  /**
-   * #FF1493.
-   */
+  /** 0xFF1493. */
   public static final Color kDeepPink = new Color(1.0f, 0.078431375f, 0.5764706f);
 
-  /**
-   * #00BFFF.
-   */
+  /** 0x00BFFF. */
   public static final Color kDeepSkyBlue = new Color(0.0f, 0.7490196f, 1.0f);
 
-  /**
-   * #696969.
-   */
+  /** 0x696969. */
   public static final Color kDimGray = new Color(0.4117647f, 0.4117647f, 0.4117647f);
 
-  /**
-   * #1E90FF.
-   */
+  /** 0x1E90FF. */
   public static final Color kDodgerBlue = new Color(0.11764706f, 0.5647059f, 1.0f);
 
-  /**
-   * #B22222.
-   */
+  /** 0xB22222. */
   public static final Color kFirebrick = new Color(0.69803923f, 0.13333334f, 0.13333334f);
 
-  /**
-   * #FFFAF0.
-   */
+  /** 0xFFFAF0. */
   public static final Color kFloralWhite = new Color(1.0f, 0.98039216f, 0.9411765f);
 
-  /**
-   * #228B22.
-   */
+  /** 0x228B22. */
   public static final Color kForestGreen = new Color(0.13333334f, 0.54509807f, 0.13333334f);
 
-  /**
-   * #FF00FF.
-   */
+  /** 0xFF00FF. */
   public static final Color kFuchsia = new Color(1.0f, 0.0f, 1.0f);
 
-  /**
-   * #DCDCDC.
-   */
+  /** 0xDCDCDC. */
   public static final Color kGainsboro = new Color(0.8627451f, 0.8627451f, 0.8627451f);
 
-  /**
-   * #F8F8FF.
-   */
+  /** 0xF8F8FF. */
   public static final Color kGhostWhite = new Color(0.972549f, 0.972549f, 1.0f);
 
-  /**
-   * #FFD700.
-   */
+  /** 0xFFD700. */
   public static final Color kGold = new Color(1.0f, 0.84313726f, 0.0f);
 
-  /**
-   * #DAA520.
-   */
+  /** 0xDAA520. */
   public static final Color kGoldenrod = new Color(0.85490197f, 0.64705884f, 0.1254902f);
 
-  /**
-   * #808080.
-   */
+  /** 0x808080. */
   public static final Color kGray = new Color(0.5019608f, 0.5019608f, 0.5019608f);
 
-  /**
-   * #008000.
-   */
+  /** 0x008000. */
   public static final Color kGreen = new Color(0.0f, 0.5019608f, 0.0f);
 
-  /**
-   * #ADFF2F.
-   */
+  /** 0xADFF2F. */
   public static final Color kGreenYellow = new Color(0.6784314f, 1.0f, 0.18431373f);
 
-  /**
-   * #F0FFF0.
-   */
+  /** 0xF0FFF0. */
   public static final Color kHoneydew = new Color(0.9411765f, 1.0f, 0.9411765f);
 
-  /**
-   * #FF69B4.
-   */
+  /** 0xFF69B4. */
   public static final Color kHotPink = new Color(1.0f, 0.4117647f, 0.7058824f);
 
-  /**
-   * #CD5C5C.
-   */
+  /** 0xCD5C5C. */
   public static final Color kIndianRed = new Color(0.8039216f, 0.36078432f, 0.36078432f);
 
-  /**
-   * #4B0082.
-   */
+  /** 0x4B0082. */
   public static final Color kIndigo = new Color(0.29411766f, 0.0f, 0.50980395f);
 
-  /**
-   * #FFFFF0.
-   */
+  /** 0xFFFFF0. */
   public static final Color kIvory = new Color(1.0f, 1.0f, 0.9411765f);
 
-  /**
-   * #F0E68C.
-   */
+  /** 0xF0E68C. */
   public static final Color kKhaki = new Color(0.9411765f, 0.9019608f, 0.54901963f);
 
-  /**
-   * #E6E6FA.
-   */
+  /** 0xE6E6FA. */
   public static final Color kLavender = new Color(0.9019608f, 0.9019608f, 0.98039216f);
 
-  /**
-   * #FFF0F5.
-   */
+  /** 0xFFF0F5. */
   public static final Color kLavenderBlush = new Color(1.0f, 0.9411765f, 0.9607843f);
 
-  /**
-   * #7CFC00.
-   */
+  /** 0x7CFC00. */
   public static final Color kLawnGreen = new Color(0.4862745f, 0.9882353f, 0.0f);
 
-  /**
-   * #FFFACD.
-   */
+  /** 0xFFFACD. */
   public static final Color kLemonChiffon = new Color(1.0f, 0.98039216f, 0.8039216f);
 
-  /**
-   * #ADD8E6.
-   */
+  /** 0xADD8E6. */
   public static final Color kLightBlue = new Color(0.6784314f, 0.84705883f, 0.9019608f);
 
-  /**
-   * #F08080.
-   */
+  /** 0xF08080. */
   public static final Color kLightCoral = new Color(0.9411765f, 0.5019608f, 0.5019608f);
 
-  /**
-   * #E0FFFF.
-   */
+  /** 0xE0FFFF. */
   public static final Color kLightCyan = new Color(0.8784314f, 1.0f, 1.0f);
 
-  /**
-   * #FAFAD2.
-   */
+  /** 0xFAFAD2. */
   public static final Color kLightGoldenrodYellow = new Color(0.98039216f, 0.98039216f, 0.8235294f);
 
-  /**
-   * #D3D3D3.
-   */
+  /** 0xD3D3D3. */
   public static final Color kLightGray = new Color(0.827451f, 0.827451f, 0.827451f);
 
-  /**
-   * #90EE90.
-   */
+  /** 0x90EE90. */
   public static final Color kLightGreen = new Color(0.5647059f, 0.93333334f, 0.5647059f);
 
-  /**
-   * #FFB6C1.
-   */
+  /** 0xFFB6C1. */
   public static final Color kLightPink = new Color(1.0f, 0.7137255f, 0.75686276f);
 
-  /**
-   * #FFA07A.
-   */
+  /** 0xFFA07A. */
   public static final Color kLightSalmon = new Color(1.0f, 0.627451f, 0.47843137f);
 
-  /**
-   * #20B2AA.
-   */
+  /** 0x20B2AA. */
   public static final Color kLightSeaGreen = new Color(0.1254902f, 0.69803923f, 0.6666667f);
 
-  /**
-   * #87CEFA.
-   */
+  /** 0x87CEFA. */
   public static final Color kLightSkyBlue = new Color(0.5294118f, 0.80784315f, 0.98039216f);
 
-  /**
-   * #778899.
-   */
+  /** 0x778899. */
   public static final Color kLightSlateGray = new Color(0.46666667f, 0.53333336f, 0.6f);
 
-  /**
-   * #B0C4DE.
-   */
+  /** 0xB0C4DE. */
   public static final Color kLightSteelBlue = new Color(0.6901961f, 0.76862746f, 0.87058824f);
 
-  /**
-   * #FFFFE0.
-   */
+  /** 0xFFFFE0. */
   public static final Color kLightYellow = new Color(1.0f, 1.0f, 0.8784314f);
 
-  /**
-   * #00FF00.
-   */
+  /** 0x00FF00. */
   public static final Color kLime = new Color(0.0f, 1.0f, 0.0f);
 
-  /**
-   * #32CD32.
-   */
+  /** 0x32CD32. */
   public static final Color kLimeGreen = new Color(0.19607843f, 0.8039216f, 0.19607843f);
 
-  /**
-   * #FAF0E6.
-   */
+  /** 0xFAF0E6. */
   public static final Color kLinen = new Color(0.98039216f, 0.9411765f, 0.9019608f);
 
-  /**
-   * #FF00FF.
-   */
+  /** 0xFF00FF. */
   public static final Color kMagenta = new Color(1.0f, 0.0f, 1.0f);
 
-  /**
-   * #800000.
-   */
+  /** 0x800000. */
   public static final Color kMaroon = new Color(0.5019608f, 0.0f, 0.0f);
 
-  /**
-   * #66CDAA.
-   */
+  /** 0x66CDAA. */
   public static final Color kMediumAquamarine = new Color(0.4f, 0.8039216f, 0.6666667f);
 
-  /**
-   * #0000CD.
-   */
+  /** 0x0000CD. */
   public static final Color kMediumBlue = new Color(0.0f, 0.0f, 0.8039216f);
 
-  /**
-   * #BA55D3.
-   */
+  /** 0xBA55D3. */
   public static final Color kMediumOrchid = new Color(0.7294118f, 0.33333334f, 0.827451f);
 
-  /**
-   * #9370DB.
-   */
+  /** 0x9370DB. */
   public static final Color kMediumPurple = new Color(0.5764706f, 0.4392157f, 0.85882354f);
 
-  /**
-   * #3CB371.
-   */
+  /** 0x3CB371. */
   public static final Color kMediumSeaGreen = new Color(0.23529412f, 0.7019608f, 0.44313726f);
 
-  /**
-   * #7B68EE.
-   */
+  /** 0x7B68EE. */
   public static final Color kMediumSlateBlue = new Color(0.48235294f, 0.40784314f, 0.93333334f);
 
-  /**
-   * #00FA9A.
-   */
+  /** 0x00FA9A. */
   public static final Color kMediumSpringGreen = new Color(0.0f, 0.98039216f, 0.6039216f);
 
-  /**
-   * #48D1CC.
-   */
+  /** 0x48D1CC. */
   public static final Color kMediumTurquoise = new Color(0.28235295f, 0.81960785f, 0.8f);
 
-  /**
-   * #C71585.
-   */
+  /** 0xC71585. */
   public static final Color kMediumVioletRed = new Color(0.78039217f, 0.08235294f, 0.52156866f);
 
-  /**
-   * #191970.
-   */
+  /** 0x191970. */
   public static final Color kMidnightBlue = new Color(0.09803922f, 0.09803922f, 0.4392157f);
 
-  /**
-   * #F5FFFA.
-   */
+  /** 0xF5FFFA. */
   public static final Color kMintcream = new Color(0.9607843f, 1.0f, 0.98039216f);
 
-  /**
-   * #FFE4E1.
-   */
+  /** 0xFFE4E1. */
   public static final Color kMistyRose = new Color(1.0f, 0.89411765f, 0.88235295f);
 
-  /**
-   * #FFE4B5.
-   */
+  /** 0xFFE4B5. */
   public static final Color kMoccasin = new Color(1.0f, 0.89411765f, 0.70980394f);
 
-  /**
-   * #FFDEAD.
-   */
+  /** 0xFFDEAD. */
   public static final Color kNavajoWhite = new Color(1.0f, 0.87058824f, 0.6784314f);
 
-  /**
-   * #000080.
-   */
+  /** 0x000080. */
   public static final Color kNavy = new Color(0.0f, 0.0f, 0.5019608f);
 
-  /**
-   * #FDF5E6.
-   */
+  /** 0xFDF5E6. */
   public static final Color kOldLace = new Color(0.99215686f, 0.9607843f, 0.9019608f);
 
-  /**
-   * #808000.
-   */
+  /** 0x808000. */
   public static final Color kOlive = new Color(0.5019608f, 0.5019608f, 0.0f);
 
-  /**
-   * #6B8E23.
-   */
+  /** 0x6B8E23. */
   public static final Color kOliveDrab = new Color(0.41960785f, 0.5568628f, 0.13725491f);
 
-  /**
-   * #FFA500.
-   */
+  /** 0xFFA500. */
   public static final Color kOrange = new Color(1.0f, 0.64705884f, 0.0f);
 
-  /**
-   * #FF4500.
-   */
+  /** 0xFF4500. */
   public static final Color kOrangeRed = new Color(1.0f, 0.27058825f, 0.0f);
 
-  /**
-   * #DA70D6.
-   */
+  /** 0xDA70D6. */
   public static final Color kOrchid = new Color(0.85490197f, 0.4392157f, 0.8392157f);
 
-  /**
-   * #EEE8AA.
-   */
+  /** 0xEEE8AA. */
   public static final Color kPaleGoldenrod = new Color(0.93333334f, 0.9098039f, 0.6666667f);
 
-  /**
-   * #98FB98.
-   */
+  /** 0x98FB98. */
   public static final Color kPaleGreen = new Color(0.59607846f, 0.9843137f, 0.59607846f);
 
-  /**
-   * #AFEEEE.
-   */
+  /** 0xAFEEEE. */
   public static final Color kPaleTurquoise = new Color(0.6862745f, 0.93333334f, 0.93333334f);
 
-  /**
-   * #DB7093.
-   */
+  /** 0xDB7093. */
   public static final Color kPaleVioletRed = new Color(0.85882354f, 0.4392157f, 0.5764706f);
 
-  /**
-   * #FFEFD5.
-   */
+  /** 0xFFEFD5. */
   public static final Color kPapayaWhip = new Color(1.0f, 0.9372549f, 0.8352941f);
 
-  /**
-   * #FFDAB9.
-   */
+  /** 0xFFDAB9. */
   public static final Color kPeachPuff = new Color(1.0f, 0.85490197f, 0.7254902f);
 
-  /**
-   * #CD853F.
-   */
+  /** 0xCD853F. */
   public static final Color kPeru = new Color(0.8039216f, 0.52156866f, 0.24705882f);
 
-  /**
-   * #FFC0CB.
-   */
+  /** 0xFFC0CB. */
   public static final Color kPink = new Color(1.0f, 0.7529412f, 0.79607844f);
 
-  /**
-   * #DDA0DD.
-   */
+  /** 0xDDA0DD. */
   public static final Color kPlum = new Color(0.8666667f, 0.627451f, 0.8666667f);
 
-  /**
-   * #B0E0E6.
-   */
+  /** 0xB0E0E6. */
   public static final Color kPowderBlue = new Color(0.6901961f, 0.8784314f, 0.9019608f);
 
-  /**
-   * #800080.
-   */
+  /** 0x800080. */
   public static final Color kPurple = new Color(0.5019608f, 0.0f, 0.5019608f);
 
-  /**
-   * #FF0000.
-   */
+  /** 0xFF0000. */
   public static final Color kRed = new Color(1.0f, 0.0f, 0.0f);
 
-  /**
-   * #BC8F8F.
-   */
+  /** 0xBC8F8F. */
   public static final Color kRosyBrown = new Color(0.7372549f, 0.56078434f, 0.56078434f);
 
-  /**
-   * #4169E1.
-   */
+  /** 0x4169E1. */
   public static final Color kRoyalBlue = new Color(0.25490198f, 0.4117647f, 0.88235295f);
 
-  /**
-   * #8B4513.
-   */
+  /** 0x8B4513. */
   public static final Color kSaddleBrown = new Color(0.54509807f, 0.27058825f, 0.07450981f);
 
-  /**
-   * #FA8072.
-   */
+  /** 0xFA8072. */
   public static final Color kSalmon = new Color(0.98039216f, 0.5019608f, 0.44705883f);
 
-  /**
-   * #F4A460.
-   */
+  /** 0xF4A460. */
   public static final Color kSandyBrown = new Color(0.95686275f, 0.6431373f, 0.3764706f);
 
-  /**
-   * #2E8B57.
-   */
+  /** 0x2E8B57. */
   public static final Color kSeaGreen = new Color(0.18039216f, 0.54509807f, 0.34117648f);
 
-  /**
-   * #FFF5EE.
-   */
+  /** 0xFFF5EE. */
   public static final Color kSeashell = new Color(1.0f, 0.9607843f, 0.93333334f);
 
-  /**
-   * #A0522D.
-   */
+  /** 0xA0522D. */
   public static final Color kSienna = new Color(0.627451f, 0.32156864f, 0.1764706f);
 
-  /**
-   * #C0C0C0.
-   */
+  /** 0xC0C0C0. */
   public static final Color kSilver = new Color(0.7529412f, 0.7529412f, 0.7529412f);
 
-  /**
-   * #87CEEB.
-   */
+  /** 0x87CEEB. */
   public static final Color kSkyBlue = new Color(0.5294118f, 0.80784315f, 0.92156863f);
 
-  /**
-   * #6A5ACD.
-   */
+  /** 0x6A5ACD. */
   public static final Color kSlateBlue = new Color(0.41568628f, 0.3529412f, 0.8039216f);
 
-  /**
-   * #708090.
-   */
+  /** 0x708090. */
   public static final Color kSlateGray = new Color(0.4392157f, 0.5019608f, 0.5647059f);
 
-  /**
-   * #FFFAFA.
-   */
+  /** 0xFFFAFA. */
   public static final Color kSnow = new Color(1.0f, 0.98039216f, 0.98039216f);
 
-  /**
-   * #00FF7F.
-   */
+  /** 0x00FF7F. */
   public static final Color kSpringGreen = new Color(0.0f, 1.0f, 0.49803922f);
 
-  /**
-   * #4682B4.
-   */
+  /** 0x4682B4. */
   public static final Color kSteelBlue = new Color(0.27450982f, 0.50980395f, 0.7058824f);
 
-  /**
-   * #D2B48C.
-   */
+  /** 0xD2B48C. */
   public static final Color kTan = new Color(0.8235294f, 0.7058824f, 0.54901963f);
 
-  /**
-   * #008080.
-   */
+  /** 0x008080. */
   public static final Color kTeal = new Color(0.0f, 0.5019608f, 0.5019608f);
 
-  /**
-   * #D8BFD8.
-   */
+  /** 0xD8BFD8. */
   public static final Color kThistle = new Color(0.84705883f, 0.7490196f, 0.84705883f);
 
-  /**
-   * #FF6347.
-   */
+  /** 0xFF6347. */
   public static final Color kTomato = new Color(1.0f, 0.3882353f, 0.2784314f);
 
-  /**
-   * #40E0D0.
-   */
+  /** 0x40E0D0. */
   public static final Color kTurquoise = new Color(0.2509804f, 0.8784314f, 0.8156863f);
 
-  /**
-   * #EE82EE.
-   */
+  /** 0xEE82EE. */
   public static final Color kViolet = new Color(0.93333334f, 0.50980395f, 0.93333334f);
 
-  /**
-   * #F5DEB3.
-   */
+  /** 0xF5DEB3. */
   public static final Color kWheat = new Color(0.9607843f, 0.87058824f, 0.7019608f);
 
-  /**
-   * #FFFFFF.
-   */
+  /** 0xFFFFFF. */
   public static final Color kWhite = new Color(1.0f, 1.0f, 1.0f);
 
-  /**
-   * #F5F5F5.
-   */
+  /** 0xF5F5F5. */
   public static final Color kWhiteSmoke = new Color(0.9607843f, 0.9607843f, 0.9607843f);
 
-  /**
-   * #FFFF00.
-   */
+  /** 0xFFFF00. */
   public static final Color kYellow = new Color(1.0f, 1.0f, 0.0f);
 
-  /**
-   * #9ACD32.
-   */
+  /** 0x9ACD32. */
   public static final Color kYellowGreen = new Color(0.6039216f, 0.8039216f, 0.19607843f);
 }
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 50d42fc..cd7154d 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
@@ -1,19 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 edu.wpi.first.math.MathUtil;
 import java.util.Objects;
 
-import edu.wpi.first.wpiutil.math.MathUtil;
-
-/**
- * Represents colors with 8 bits of precision.
- */
+/** Represents colors with 8 bits of precision. */
 @SuppressWarnings("MemberName")
 public class Color8Bit {
   public final int red;
@@ -23,9 +17,9 @@
   /**
    * Constructs a Color8Bit.
    *
-   * @param red   Red value (0-255)
+   * @param red Red value (0-255)
    * @param green Green value (0-255)
-   * @param blue  Blue value (0-255)
+   * @param blue Blue value (0-255)
    */
   public Color8Bit(int red, int green, int blue) {
     this.red = MathUtil.clamp(red, 0, 255);
@@ -39,9 +33,7 @@
    * @param color The color
    */
   public Color8Bit(Color color) {
-    this((int) (color.red * 255),
-        (int) (color.green * 255),
-        (int) (color.blue * 255));
+    this((int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255));
   }
 
   @Override
@@ -54,9 +46,7 @@
     }
 
     Color8Bit color8Bit = (Color8Bit) other;
-    return red == color8Bit.red
-        && green == color8Bit.green
-        && blue == color8Bit.blue;
+    return red == color8Bit.red && green == color8Bit.green && blue == color8Bit.blue;
   }
 
   @Override
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/ErrorMessages.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/ErrorMessages.java
index 8bc86b2..c5b5832 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/ErrorMessages.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/ErrorMessages.java
@@ -1,36 +1,36 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.Objects.requireNonNull;
 
-/**
- * Utility class for common WPILib error messages.
- */
+/** Utility class for common WPILib error messages. */
 public final class ErrorMessages {
-  /**
-   * Utility class, so constructor is private.
-   */
+  /** Utility class, so constructor is private. */
   private ErrorMessages() {
     throw new UnsupportedOperationException("This is a utility class!");
   }
 
   /**
-   * Requires that a parameter of a method not be null; prints an error message with
-   * helpful debugging instructions if the parameter is null.
+   * Requires that a parameter of a method not be null; prints an error message with helpful
+   * debugging instructions if the parameter is null.
    *
+   * @param <T> Type of object.
    * @param obj The parameter that must not be null.
    * @param paramName The name of the parameter.
    * @param methodName The name of the method.
+   * @return The object parameter confirmed not to be null.
    */
   public static <T> T requireNonNullParam(T obj, String paramName, String methodName) {
-    return requireNonNull(obj,
-        "Parameter " + paramName + " in method " + methodName + " was null when it"
+    return requireNonNull(
+        obj,
+        "Parameter "
+            + paramName
+            + " in method "
+            + methodName
+            + " was null when it"
             + " should not have been!  Check the stacktrace to find the responsible line of code - "
             + "usually, it is the first line of user-written code indicated in the stacktrace.  "
             + "Make sure all objects passed to the method in question were properly initialized -"
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java
index 2c0f8f2..d888d3b 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java
@@ -1,29 +1,26 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.vision;
 
 import org.opencv.core.Mat;
 
 /**
- * A vision pipeline is responsible for running a group of
- * OpenCV algorithms to extract data from an image.
+ * A vision pipeline is responsible for running a group of OpenCV algorithms to extract data from an
+ * image.
  *
  * @see VisionRunner
  * @see VisionThread
- *
  * @deprecated Replaced with edu.wpi.first.vision.VisionPipeline
  */
 @Deprecated
 public interface VisionPipeline {
   /**
-   * Processes the image input and sets the result objects.
-   * Implementations should make these objects accessible.
+   * Processes the image input and sets the result objects. Implementations should make these
+   * objects accessible.
+   *
+   * @param image The image to process.
    */
   void process(Mat image);
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java
index 328686a..a1002f5 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java
@@ -1,27 +1,22 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.vision;
 
+import edu.wpi.first.cameraserver.CameraServerSharedStore;
+import edu.wpi.first.cscore.CvSink;
+import edu.wpi.first.cscore.VideoSource;
 import org.opencv.core.Mat;
 
-import edu.wpi.cscore.CvSink;
-import edu.wpi.cscore.VideoSource;
-import edu.wpi.first.cameraserver.CameraServerSharedStore;
-
 /**
- * A vision runner is a convenient wrapper object to make it easy to run vision pipelines
- * from robot code. The easiest  way to use this is to run it in a {@link VisionThread}
- * and use the listener to take snapshots of the pipeline's outputs.
+ * A vision runner is a convenient wrapper object to make it easy to run vision pipelines from robot
+ * code. The easiest way to use this is to run it in a {@link VisionThread} and use the listener to
+ * take snapshots of the pipeline's outputs.
  *
  * @see VisionPipeline
  * @see VisionThread
  * @see <a href="package-summary.html">vision</a>
- *
  * @deprecated Replaced with edu.wpi.first.vision.VisionRunner
  */
 @Deprecated
@@ -48,17 +43,16 @@
      * @param pipeline the vision pipeline that ran
      */
     void copyPipelineOutputs(P pipeline);
-
   }
 
   /**
-   * Creates a new vision runner. It will take images from the {@code videoSource}, send them to
-   * the {@code pipeline}, and call the {@code listener} when the pipeline has finished to alert
-   * user code when it is safe to access the pipeline's outputs.
+   * Creates a new vision runner. It will take images from the {@code videoSource}, send them to the
+   * {@code pipeline}, and call the {@code listener} when the pipeline has finished to alert user
+   * code when it is safe to access the pipeline's outputs.
    *
    * @param videoSource the video source to use to supply images for the pipeline
-   * @param pipeline    the vision pipeline to run
-   * @param listener    a function to call after the pipeline has finished running
+   * @param pipeline the vision pipeline to run
+   * @param listener a function to call after the pipeline has finished running
    */
   public VisionRunner(VideoSource videoSource, P pipeline, Listener<? super P> listener) {
     this.m_pipeline = pipeline;
@@ -67,15 +61,15 @@
   }
 
   /**
-   * Runs the pipeline one time, giving it the next image from the video source specified
-   * in the constructor. This will block until the source either has an image or throws an error.
-   * If the source successfully supplied a frame, the pipeline's image input will be set,
-   * the pipeline will run, and the listener specified in the constructor will be called to notify
-   * it that the pipeline ran.
+   * Runs the pipeline one time, giving it the next image from the video source specified in the
+   * constructor. This will block until the source either has an image or throws an error. If the
+   * source successfully supplied a frame, the pipeline's image input will be set, the pipeline will
+   * run, and the listener specified in the constructor will be called to notify it that the
+   * pipeline ran.
    *
-   * <p>This method is exposed to allow teams to add additional functionality or have their own
-   * ways to run the pipeline. Most teams, however, should just use {@link #runForever} in its own
-   * thread using a {@link VisionThread}.</p>
+   * <p>This method is exposed to allow teams to add additional functionality or have their own ways
+   * to run the pipeline. Most teams, however, should just use {@link #runForever} in its own thread
+   * using a {@link VisionThread}.
    */
   public void runOnce() {
     Long id = CameraServerSharedStore.getCameraServerShared().getRobotMainThreadId();
@@ -101,11 +95,11 @@
   }
 
   /**
-   * A convenience method that calls {@link #runOnce()} in an infinite loop. This must
-   * be run in a dedicated thread, and cannot be used in the main robot thread because
-   * it will freeze the robot program.
+   * A convenience method that calls {@link #runOnce()} in an infinite loop. This must be run in a
+   * dedicated thread, and cannot be used in the main robot thread because it will freeze the robot
+   * program.
    *
-   * <p><strong>Do not call this method directly from the main thread.</strong></p>
+   * <p><strong>Do not call this method directly from the main thread.</strong>
    *
    * @throws IllegalStateException if this is called from the main robot thread
    * @see VisionThread
@@ -122,9 +116,7 @@
     }
   }
 
-  /**
-   * Stop a RunForever() loop.
-   */
+  /** Stop a RunForever() loop. */
   public void stop() {
     m_enabled = false;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java
index 5184ba0..5f23fe1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java
@@ -1,23 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.vision;
 
-import edu.wpi.cscore.VideoSource;
+import edu.wpi.first.cscore.VideoSource;
 
 /**
- * A vision thread is a special thread that runs a vision pipeline. It is a <i>daemon</i> thread;
- * it does not prevent the program from exiting when all other non-daemon threads
- * have finished running.
+ * A vision thread is a special thread that runs a vision pipeline. It is a <i>daemon</i> thread; it
+ * does not prevent the program from exiting when all other non-daemon threads have finished
+ * running.
  *
  * @see VisionPipeline
  * @see VisionRunner
  * @see Thread#setDaemon(boolean)
- *
  * @deprecated Replaced with edu.wpi.first.vision.VisionThread
  */
 @Deprecated
@@ -37,14 +33,12 @@
    * equivalent to {@code new VisionThread(new VisionRunner<>(videoSource, pipeline, listener))}.
    *
    * @param videoSource the source for images the pipeline should process
-   * @param pipeline    the pipeline to run
-   * @param listener    the listener to copy outputs from the pipeline after it runs
-   * @param <P>         the type of the pipeline
+   * @param pipeline the pipeline to run
+   * @param listener the listener to copy outputs from the pipeline after it runs
+   * @param <P> the type of the pipeline
    */
-  public <P extends VisionPipeline> VisionThread(VideoSource videoSource,
-                                                 P pipeline,
-                                                 VisionRunner.Listener<? super P> listener) {
+  public <P extends VisionPipeline> VisionThread(
+      VideoSource videoSource, P pipeline, VisionRunner.Listener<? super P> listener) {
     this(new VisionRunner<>(videoSource, pipeline, listener));
   }
-
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java
index 01c8d73..b52f4ed 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java
@@ -1,22 +1,19 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.
 
 /**
- * Classes in the {@code edu.wpi.first.wpilibj.vision} package are designed to
- * simplify using OpenCV vision processing code from a robot program.
+ * Classes in the {@code edu.wpi.first.wpilibj.vision} package are designed to simplify using OpenCV
+ * vision processing code from a robot program.
  *
- * <p>An example use case for grabbing a yellow tote from 2015 in autonomous:
- * <br>
+ * <p>An example use case for grabbing a yellow tote from 2015 in autonomous: <br>
+ *
  * <pre><code>
  * public class Robot extends IterativeRobot
  *     implements VisionRunner.Listener&lt;MyFindTotePipeline&gt; {
  *
  *      // A USB camera connected to the roboRIO.
- *      private {@link edu.wpi.cscore.VideoSource VideoSource} usbCamera;
+ *      private {@link edu.wpi.first.cscore.VideoSource VideoSource} usbCamera;
  *
  *      // A vision pipeline. This could be handwritten or generated by GRIP.
  *      // This has to implement {@link edu.wpi.first.wpilibj.vision.VisionPipeline}.
@@ -47,7 +44,7 @@
  *
  *     {@literal @}Override
  *      public void robotInit() {
- *          usbCamera = CameraServer.getInstance().startAutomaticCapture(0);
+ *          usbCamera = CameraServer.startAutomaticCapture(0);
  *          findTotePipeline = new MyFindTotePipeline();
  *          findToteThread = new VisionThread(usbCamera, findTotePipeline, this);
  *      }
diff --git a/wpilibj/src/test/java/edu/wpi/first/math/util/ColorTest.java b/wpilibj/src/test/java/edu/wpi/first/math/util/ColorTest.java
new file mode 100644
index 0000000..6685314
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/math/util/ColorTest.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.math.util;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import edu.wpi.first.wpilibj.util.Color;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class ColorTest {
+  private static final double kEpsilon = 1e-3;
+
+  void assertColorMatches(double red, double green, double blue, Color color) {
+    assertAll(
+        () -> assertEquals(red, color.red, kEpsilon),
+        () -> assertEquals(green, color.green, kEpsilon),
+        () -> assertEquals(blue, color.blue, kEpsilon));
+  }
+
+  @ParameterizedTest
+  @MethodSource("staticColorProvider")
+  void staticColorTest(double red, double green, double blue, Color color) {
+    assertColorMatches(red, green, blue, color);
+  }
+
+  @ParameterizedTest
+  @MethodSource("staticColorProvider")
+  void colorEqualsTest(double red, double green, double blue, Color color) {
+    assertEquals(color, new Color(red, green, blue));
+  }
+
+  static Stream<Arguments> staticColorProvider() {
+    return Stream.of(
+        arguments(0.0823529412, 0.376470589, 0.7411764706, Color.kDenim),
+        arguments(0.0, 0.4, 0.7019607844, Color.kFirstBlue),
+        arguments(0.9294117648, 0.1098039216, 0.1411764706, Color.kFirstRed));
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/math/util/ErrorMessagesTest.java b/wpilibj/src/test/java/edu/wpi/first/math/util/ErrorMessagesTest.java
new file mode 100644
index 0000000..5153b00
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/math/util/ErrorMessagesTest.java
@@ -0,0 +1,30 @@
+// 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.math.util;
+
+import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import edu.wpi.first.wpilibj.UtilityClassTest;
+import edu.wpi.first.wpilibj.util.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+class ErrorMessagesTest extends UtilityClassTest<ErrorMessages> {
+  ErrorMessagesTest() {
+    super(ErrorMessages.class);
+  }
+
+  @Test
+  void requireNonNullParamNullTest() {
+    assertThrows(
+        NullPointerException.class, () -> requireNonNullParam(null, "testParam", "testMethod"));
+  }
+
+  @Test
+  void requireNonNullParamNotNullTest() {
+    assertDoesNotThrow(() -> requireNonNullParam("null", "testParam", "testMethod"));
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/AddressableLEDBufferTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AddressableLEDBufferTest.java
index 0bd0113..2a24dbe 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/AddressableLEDBufferTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AddressableLEDBufferTest.java
@@ -1,27 +1,22 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.stream.Stream;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import edu.wpi.first.wpilibj.util.Color;
-import edu.wpi.first.wpilibj.util.Color8Bit;
-
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.params.provider.Arguments.arguments;
 
+import edu.wpi.first.wpilibj.util.Color;
+import edu.wpi.first.wpilibj.util.Color8Bit;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
 class AddressableLEDBufferTest {
   @ParameterizedTest
   @MethodSource("hsvToRgbProvider")
@@ -32,8 +27,7 @@
     assertAll(
         () -> assertEquals((byte) r, buffer.m_buffer[2], "R value didn't match"),
         () -> assertEquals((byte) g, buffer.m_buffer[1], "G value didn't match"),
-        () -> assertEquals((byte) b, buffer.m_buffer[0], "B value didn't match")
-    );
+        () -> assertEquals((byte) b, buffer.m_buffer[0], "B value didn't match"));
   }
 
   static Stream<Arguments> hsvToRgbProvider() {
@@ -54,7 +48,7 @@
         arguments(150, 255, 128, 128, 0, 127), // Purple (ish)
         arguments(90, 255, 128, 0, 127, 128), // Teal (ish)
         arguments(120, 255, 128, 0, 0, 128) // Navy
-    );
+        );
   }
 
   @Test
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogGyroTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogGyroTest.java
new file mode 100644
index 0000000..c8eaad9
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogGyroTest.java
@@ -0,0 +1,38 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.AnalogGyroSim;
+import org.junit.jupiter.api.Test;
+
+public class AnalogGyroTest {
+  @Test
+  void testInitializeWithAnalogInput() {
+    HAL.initialize(500, 0);
+    AnalogGyroSim sim = new AnalogGyroSim(0);
+    assertFalse(sim.getInitialized());
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogGyro gyro = new AnalogGyro(ai, 229, 17.4)) {
+      assertEquals(ai, gyro.getAnalogInput());
+    }
+  }
+
+  @Test
+  void testInitializeWithChannel() {
+    HAL.initialize(500, 0);
+    AnalogGyroSim sim = new AnalogGyroSim(0);
+    assertFalse(sim.getInitialized());
+
+    try (AnalogGyro gyro = new AnalogGyro(1, 191, 35.04)) {
+      assertNotNull(gyro.getAnalogInput());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogPotentiometerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogPotentiometerTest.java
new file mode 100644
index 0000000..fa299a5
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/AnalogPotentiometerTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.AnalogInputSim;
+import edu.wpi.first.wpilibj.simulation.RoboRioSim;
+import org.junit.jupiter.api.Test;
+
+public class AnalogPotentiometerTest {
+  @Test
+  void testInitializeWithAnalogInput() {
+    HAL.initialize(500, 0);
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogPotentiometer pot = new AnalogPotentiometer(ai)) {
+      AnalogInputSim sim = new AnalogInputSim(ai);
+
+      RoboRioSim.resetData();
+      sim.setVoltage(4.0);
+      assertEquals(0.8, pot.get());
+    }
+  }
+
+  @Test
+  void testInitializeWithAnalogInputAndScale() {
+    HAL.initialize(500, 0);
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogPotentiometer pot = new AnalogPotentiometer(ai, 270.0)) {
+      RoboRioSim.resetData();
+      AnalogInputSim sim = new AnalogInputSim(ai);
+
+      sim.setVoltage(5.0);
+      assertEquals(270.0, pot.get());
+
+      sim.setVoltage(2.5);
+      assertEquals(135, pot.get());
+
+      sim.setVoltage(0.0);
+      assertEquals(0.0, pot.get());
+    }
+  }
+
+  @Test
+  void testInitializeWithChannel() {
+    HAL.initialize(500, 0);
+
+    try (AnalogPotentiometer pot = new AnalogPotentiometer(1)) {
+      RoboRioSim.resetData();
+      AnalogInputSim sim = new AnalogInputSim(1);
+
+      sim.setVoltage(5.0);
+      assertEquals(1.0, pot.get());
+    }
+  }
+
+  @Test
+  void testInitializeWithChannelAndScale() {
+    HAL.initialize(500, 0);
+
+    try (AnalogPotentiometer pot = new AnalogPotentiometer(1, 180.0)) {
+      RoboRioSim.resetData();
+      AnalogInputSim sim = new AnalogInputSim(1);
+
+      sim.setVoltage(5.0);
+      assertEquals(180.0, pot.get());
+
+      sim.setVoltage(0.0);
+      assertEquals(0.0, pot.get());
+    }
+  }
+
+  @Test
+  void testWithModifiedBatteryVoltage() {
+    try (AnalogPotentiometer pot = new AnalogPotentiometer(1, 180.0, 90.0)) {
+      RoboRioSim.resetData();
+      AnalogInputSim sim = new AnalogInputSim(1);
+
+      // Test at 5v
+      sim.setVoltage(5.0);
+      assertEquals(270, pot.get());
+
+      sim.setVoltage(0.0);
+      assertEquals(90, pot.get());
+
+      // Simulate a lower battery voltage
+      RoboRioSim.setUserVoltage5V(2.5);
+
+      sim.setVoltage(2.5);
+      assertEquals(270, pot.get());
+
+      sim.setVoltage(2.0);
+      assertEquals(234, pot.get());
+
+      sim.setVoltage(0.0);
+      assertEquals(90, pot.get());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DebouncerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DebouncerTest.java
new file mode 100644
index 0000000..5a51f6c
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DebouncerTest.java
@@ -0,0 +1,54 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.wpilibj.simulation.SimHooks;
+import org.junit.jupiter.api.Test;
+
+public class DebouncerTest {
+  @Test
+  void debounceRisingTest() {
+    var debouncer = new Debouncer(0.02, Debouncer.DebounceType.kRising);
+
+    debouncer.calculate(false);
+    assertFalse(debouncer.calculate(true));
+
+    SimHooks.stepTiming(0.1);
+
+    assertTrue(debouncer.calculate(true));
+  }
+
+  @Test
+  void debounceFallingTest() {
+    var debouncer = new Debouncer(0.02, Debouncer.DebounceType.kFalling);
+
+    debouncer.calculate(true);
+    assertTrue(debouncer.calculate(false));
+
+    SimHooks.stepTiming(0.1);
+
+    assertFalse(debouncer.calculate(false));
+  }
+
+  @Test
+  void debounceBothTest() {
+    var debouncer = new Debouncer(0.02, Debouncer.DebounceType.kBoth);
+
+    debouncer.calculate(false);
+    assertFalse(debouncer.calculate(true));
+
+    SimHooks.stepTiming(0.1);
+
+    assertTrue(debouncer.calculate(true));
+    assertTrue(debouncer.calculate(false));
+
+    SimHooks.stepTiming(0.1);
+
+    assertFalse(debouncer.calculate(false));
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DigitalOutputTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DigitalOutputTest.java
new file mode 100644
index 0000000..47b7eda
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DigitalOutputTest.java
@@ -0,0 +1,47 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.DIOSim;
+import org.junit.jupiter.api.Test;
+
+public class DigitalOutputTest {
+  @Test
+  void testDefaultFunctions() {
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      assertFalse(output.isAnalogTrigger());
+      assertEquals(0, output.getAnalogTriggerTypeForRouting());
+      assertFalse(output.isPulsing());
+    }
+  }
+
+  @Test
+  void testPwmFunctionsWithoutInitialization() {
+    HAL.initialize(500, 0);
+
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      assertDoesNotThrow(() -> output.updateDutyCycle(0.6));
+      assertDoesNotThrow(output::disablePWM);
+    }
+  }
+
+  @Test
+  void testPwmFunctionsWithInitialization() {
+    HAL.initialize(500, 0);
+
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      DIOSim sim = new DIOSim(output);
+      assertEquals(0, sim.getPulseLength());
+
+      output.enablePWM(0.5);
+      output.updateDutyCycle(0.6);
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestCTRE.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestCTRE.java
new file mode 100644
index 0000000..72a3e89
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestCTRE.java
@@ -0,0 +1,94 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import edu.wpi.first.hal.util.AllocationException;
+import org.junit.jupiter.api.Test;
+
+public class DoubleSolenoidTestCTRE {
+  @Test
+  void testValidInitialization() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(3, PneumaticsModuleType.CTREPCM, 2, 3)) {
+      solenoid.set(DoubleSolenoid.Value.kReverse);
+      assertEquals(DoubleSolenoid.Value.kReverse, solenoid.get());
+
+      solenoid.set(DoubleSolenoid.Value.kForward);
+      assertEquals(DoubleSolenoid.Value.kForward, solenoid.get());
+
+      solenoid.set(DoubleSolenoid.Value.kOff);
+      assertEquals(DoubleSolenoid.Value.kOff, solenoid.get());
+    }
+  }
+
+  @Test
+  void testThrowForwardPortAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid = new Solenoid(5, PneumaticsModuleType.CTREPCM, 2)) {
+      assertThrows(
+          AllocationException.class,
+          () -> new DoubleSolenoid(5, PneumaticsModuleType.CTREPCM, 2, 3));
+    }
+  }
+
+  @Test
+  void testThrowReversePortAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid = new Solenoid(6, PneumaticsModuleType.CTREPCM, 3)) {
+      assertThrows(
+          AllocationException.class,
+          () -> new DoubleSolenoid(6, PneumaticsModuleType.CTREPCM, 2, 3));
+    }
+  }
+
+  @Test
+  void testThrowBothPortsAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid0 = new Solenoid(6, PneumaticsModuleType.CTREPCM, 2);
+        Solenoid solenoid1 = new Solenoid(6, PneumaticsModuleType.CTREPCM, 3)) {
+      assertThrows(
+          AllocationException.class,
+          () -> new DoubleSolenoid(6, PneumaticsModuleType.CTREPCM, 2, 3));
+    }
+  }
+
+  @Test
+  void testToggle() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(4, PneumaticsModuleType.CTREPCM, 2, 3)) {
+      // Bootstrap it into reverse
+      solenoid.set(DoubleSolenoid.Value.kReverse);
+
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kForward, solenoid.get());
+
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kReverse, solenoid.get());
+
+      // Of shouldn't do anything on toggle
+      solenoid.set(DoubleSolenoid.Value.kOff);
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kOff, solenoid.get());
+    }
+  }
+
+  @Test
+  void testInvalidForwardPort() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> new DoubleSolenoid(0, PneumaticsModuleType.CTREPCM, 100, 1));
+  }
+
+  @Test
+  void testInvalidReversePort() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> new DoubleSolenoid(0, PneumaticsModuleType.CTREPCM, 0, 100));
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestREV.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestREV.java
new file mode 100644
index 0000000..888b677
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DoubleSolenoidTestREV.java
@@ -0,0 +1,91 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import edu.wpi.first.hal.util.AllocationException;
+import org.junit.jupiter.api.Test;
+
+public class DoubleSolenoidTestREV {
+  @Test
+  void testValidInitialization() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(3, PneumaticsModuleType.REVPH, 2, 3)) {
+      solenoid.set(DoubleSolenoid.Value.kReverse);
+      assertEquals(DoubleSolenoid.Value.kReverse, solenoid.get());
+
+      solenoid.set(DoubleSolenoid.Value.kForward);
+      assertEquals(DoubleSolenoid.Value.kForward, solenoid.get());
+
+      solenoid.set(DoubleSolenoid.Value.kOff);
+      assertEquals(DoubleSolenoid.Value.kOff, solenoid.get());
+    }
+  }
+
+  @Test
+  void testThrowForwardPortAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid = new Solenoid(5, PneumaticsModuleType.REVPH, 2)) {
+      assertThrows(
+          AllocationException.class, () -> new DoubleSolenoid(5, PneumaticsModuleType.REVPH, 2, 3));
+    }
+  }
+
+  @Test
+  void testThrowReversePortAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid = new Solenoid(6, PneumaticsModuleType.REVPH, 3)) {
+      assertThrows(
+          AllocationException.class, () -> new DoubleSolenoid(6, PneumaticsModuleType.REVPH, 2, 3));
+    }
+  }
+
+  @Test
+  void testThrowBothPortsAlreadyInitialized() {
+    try (
+    // Single solenoid that is reused for forward port
+    Solenoid solenoid0 = new Solenoid(6, PneumaticsModuleType.REVPH, 2);
+        Solenoid solenoid1 = new Solenoid(6, PneumaticsModuleType.REVPH, 3)) {
+      assertThrows(
+          AllocationException.class, () -> new DoubleSolenoid(6, PneumaticsModuleType.REVPH, 2, 3));
+    }
+  }
+
+  @Test
+  void testToggle() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(4, PneumaticsModuleType.REVPH, 2, 3)) {
+      // Bootstrap it into reverse
+      solenoid.set(DoubleSolenoid.Value.kReverse);
+
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kForward, solenoid.get());
+
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kReverse, solenoid.get());
+
+      // Of shouldn't do anything on toggle
+      solenoid.set(DoubleSolenoid.Value.kOff);
+      solenoid.toggle();
+      assertEquals(DoubleSolenoid.Value.kOff, solenoid.get());
+    }
+  }
+
+  @Test
+  void testInvalidForwardPort() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> new DoubleSolenoid(1, PneumaticsModuleType.REVPH, 100, 1));
+  }
+
+  @Test
+  void testInvalidReversePort() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> new DoubleSolenoid(1, PneumaticsModuleType.REVPH, 0, 100));
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DriverStationTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DriverStationTest.java
index 50e16c8..5a44615 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/DriverStationTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/DriverStationTest.java
@@ -1,23 +1,18 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.stream.Stream;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
 
+import edu.wpi.first.wpilibj.simulation.DriverStationSim;
+import java.util.stream.Stream;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
-import edu.wpi.first.wpilibj.simulation.DriverStationSim;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.params.provider.Arguments.arguments;
-
 class DriverStationTest {
   @ParameterizedTest
   @MethodSource("isConnectedProvider")
@@ -28,18 +23,17 @@
 
     DriverStationSim.notifyNewData();
 
-    assertEquals(expected, DriverStation.getInstance().isJoystickConnected(1));
+    assertEquals(expected, DriverStation.isJoystickConnected(1));
   }
 
   static Stream<Arguments> isConnectedProvider() {
     return Stream.of(
-      arguments(0, 0, 0, false),
-      arguments(1, 0, 0, true),
-      arguments(0, 1, 0, true),
-      arguments(0, 0, 1, true),
-      arguments(1, 1, 1, true),
-      arguments(4, 10, 1, true)
-    );
+        arguments(0, 0, 0, false),
+        arguments(1, 0, 0, true),
+        arguments(0, 1, 0, true),
+        arguments(0, 0, 1, true),
+        arguments(1, 1, 1, true),
+        arguments(4, 10, 1, true));
   }
 
   @MethodSource("connectionWarningProvider")
@@ -47,17 +41,15 @@
     DriverStationSim.setFmsAttached(fms);
     DriverStationSim.notifyNewData();
 
-    DriverStation.getInstance().silenceJoystickConnectionWarning(silence);
-    assertEquals(expected,
-        DriverStation.getInstance().isJoystickConnectionWarningSilenced());
+    DriverStation.silenceJoystickConnectionWarning(silence);
+    assertEquals(expected, DriverStation.isJoystickConnectionWarningSilenced());
   }
 
   static Stream<Arguments> connectionWarningProvider() {
     return Stream.of(
-      arguments(false, true, true),
-      arguments(false, false, false),
-      arguments(true, true, false),
-      arguments(true, false, false)
-    );
+        arguments(false, true, true),
+        arguments(false, false, false),
+        arguments(true, true, false),
+        arguments(true, false, false));
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/InterruptTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/InterruptTest.java
new file mode 100644
index 0000000..2eecd66
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/InterruptTest.java
@@ -0,0 +1,46 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.wpilibj.simulation.DIOSim;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+public class InterruptTest {
+  @Test
+  void testAsynchronousInterrupt() {
+    AtomicBoolean hasFired = new AtomicBoolean(false);
+    AtomicInteger counter = new AtomicInteger(0);
+
+    try (DigitalInput di = new DigitalInput(0);
+        AsynchronousInterrupt interrupt =
+            new AsynchronousInterrupt(
+                di,
+                (a, b) -> {
+                  counter.incrementAndGet();
+                  hasFired.set(true);
+                })) {
+      interrupt.enable();
+      Timer.delay(0.5);
+      DIOSim digitalSim = new DIOSim(di);
+      digitalSim.setValue(false);
+      Timer.delay(0.01);
+      digitalSim.setValue(true);
+      Timer.delay(0.01);
+
+      int count = 0;
+      while (!hasFired.get()) {
+        Timer.delay(0.005);
+        count++;
+        assertTrue(count < 1000);
+      }
+      assertEquals(1, counter.get(), "The interrupt did not fire the expected number of times");
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/JoystickTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/JoystickTest.java
index 7b1cfbb..bc79d0d 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/JoystickTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/JoystickTest.java
@@ -1,18 +1,16 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.wpilibj.simulation.JoystickSim;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
 
 class JoystickTest {
   @Test
@@ -44,4 +42,132 @@
     joysim.notifyNewData();
     assertEquals(0.0, joy.getY(), 0.001);
   }
+
+  @Test
+  void testGetZ() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    joysim.setZ(0.25);
+    joysim.notifyNewData();
+    assertEquals(0.25, joy.getZ(), 0.001);
+
+    joysim.setZ(0);
+    joysim.notifyNewData();
+    assertEquals(0.0, joy.getZ(), 0.001);
+  }
+
+  @Test
+  void testGetTwist() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    joysim.setTwist(0.25);
+    joysim.notifyNewData();
+    assertEquals(0.25, joy.getTwist(), 0.001);
+
+    joysim.setTwist(0);
+    joysim.notifyNewData();
+    assertEquals(0.0, joy.getTwist(), 0.001);
+  }
+
+  @Test
+  void testGetThrottle() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    joysim.setThrottle(0.25);
+    joysim.notifyNewData();
+    assertEquals(0.25, joy.getThrottle(), 0.001);
+
+    joysim.setThrottle(0);
+    joysim.notifyNewData();
+    assertEquals(0.0, joy.getThrottle(), 0.001);
+  }
+
+  @Test
+  void testGetTrigger() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    joysim.setTrigger(true);
+    joysim.notifyNewData();
+    assertTrue(joy.getTrigger());
+
+    joysim.setTrigger(false);
+    joysim.notifyNewData();
+    assertFalse(joy.getTrigger());
+  }
+
+  @Test
+  void testGetTop() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    joysim.setTop(true);
+    joysim.notifyNewData();
+    assertTrue(joy.getTop());
+
+    joysim.setTop(false);
+    joysim.notifyNewData();
+    assertFalse(joy.getTop());
+  }
+
+  @Test
+  void testGetMagnitude() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    // X Only
+    joysim.setX(0.5);
+    joysim.setY(0.0);
+    joysim.notifyNewData();
+    assertEquals(0.5, joy.getMagnitude(), 0.001);
+
+    // Y Only
+    joysim.setX(0.0);
+    joysim.setY(-0.5);
+    joysim.notifyNewData();
+    assertEquals(0.5, joy.getMagnitude(), 0.001);
+
+    // Both
+    joysim.setX(0.5);
+    joysim.setY(-0.5);
+    joysim.notifyNewData();
+    assertEquals(0.70710678118, joy.getMagnitude(), 0.001);
+  }
+
+  @Test
+  void testGetDirection() {
+    HAL.initialize(500, 0);
+    Joystick joy = new Joystick(1);
+    JoystickSim joysim = new JoystickSim(joy);
+
+    // X Only
+    joysim.setX(0.5);
+    joysim.setY(0.0);
+    joysim.notifyNewData();
+    assertEquals(90, joy.getDirectionDegrees(), 0.001);
+    assertEquals(Math.toRadians(90), joy.getDirectionRadians(), 0.001);
+
+    // Y Only
+    joysim.setX(0.0);
+    joysim.setY(-0.5);
+    joysim.notifyNewData();
+    assertEquals(0, joy.getDirectionDegrees(), 0.001);
+    assertEquals(Math.toRadians(0), joy.getDirectionRadians(), 0.001);
+
+    // Both
+    joysim.setX(0.5);
+    joysim.setY(-0.5);
+    joysim.notifyNewData();
+    assertEquals(45, joy.getDirectionDegrees(), 0.001);
+    assertEquals(Math.toRadians(45), joy.getDirectionRadians(), 0.001);
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockHardwareExtension.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockHardwareExtension.java
index 7406ca9..e70f182 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockHardwareExtension.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockHardwareExtension.java
@@ -1,19 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.DriverStationSim;
 import org.junit.jupiter.api.extension.BeforeAllCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.simulation.DriverStationSim;
-
 public final class MockHardwareExtension implements BeforeAllCallback {
   private static ExtensionContext getRoot(ExtensionContext context) {
     return context.getParent().map(MockHardwareExtension::getRoot).orElse(context);
@@ -21,10 +17,15 @@
 
   @Override
   public void beforeAll(ExtensionContext context) {
-    getRoot(context).getStore(Namespace.GLOBAL).getOrComputeIfAbsent("HAL Initialized", key -> {
-      initializeHardware();
-      return true;
-    }, Boolean.class);
+    getRoot(context)
+        .getStore(Namespace.GLOBAL)
+        .getOrComputeIfAbsent(
+            "HAL Initialized",
+            key -> {
+              initializeHardware();
+              return true;
+            },
+            Boolean.class);
   }
 
   private void initializeHardware() {
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockSpeedController.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockSpeedController.java
deleted file mode 100644
index d183723..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/MockSpeedController.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-public class MockSpeedController implements SpeedController {
-  private double m_speed;
-  private boolean m_isInverted;
-
-  @Override
-  public void set(double speed) {
-    m_speed = m_isInverted ? -speed : speed;
-  }
-
-  @Override
-  public double get() {
-    return m_speed;
-  }
-
-  @Override
-  public void setInverted(boolean isInverted) {
-    m_isInverted = isInverted;
-  }
-
-  @Override
-  public boolean getInverted() {
-    return m_isInverted;
-  }
-
-  @Override
-  public void disable() {
-    m_speed = 0;
-  }
-
-  @Override
-  public void stopMotor() {
-    disable();
-  }
-
-  @Override
-  public void pidWrite(double output) {
-    set(output);
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/PS4ControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PS4ControllerTest.java
new file mode 100644
index 0000000..e969cb3
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PS4ControllerTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.PS4ControllerSim;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+class PS4ControllerTest {
+  @ParameterizedTest
+  @EnumSource(value = PS4Controller.Button.class)
+  @SuppressWarnings({"VariableDeclarationUsageDistance"})
+  public void testButtons(PS4Controller.Button button)
+      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+    HAL.initialize(500, 0);
+    PS4Controller joy = new PS4Controller(2);
+    PS4ControllerSim joysim = new PS4ControllerSim(joy);
+
+    var buttonName = button.toString();
+
+    String simSetMethodName = "set" + buttonName;
+    String joyGetMethodName = "get" + buttonName;
+    String joyPressedMethodName = "get" + buttonName + "Pressed";
+    String joyReleasedMethodName = "get" + buttonName + "Released";
+
+    Method simSetMethod = joysim.getClass().getMethod(simSetMethodName, boolean.class);
+    Method joyGetMethod = joy.getClass().getMethod(joyGetMethodName);
+    Method joyPressedMethod = joy.getClass().getMethod(joyPressedMethodName);
+    Method joyReleasedMethod = joy.getClass().getMethod(joyReleasedMethodName);
+
+    simSetMethod.invoke(joysim, false);
+    joysim.notifyNewData();
+    assertFalse((Boolean) joyGetMethod.invoke(joy));
+    // need to call pressed and released to clear flags
+    joyPressedMethod.invoke(joy);
+    joyReleasedMethod.invoke(joy);
+
+    simSetMethod.invoke(joysim, true);
+    joysim.notifyNewData();
+    assertTrue((Boolean) joyGetMethod.invoke(joy));
+    assertTrue((Boolean) joyPressedMethod.invoke(joy));
+    assertFalse((Boolean) joyReleasedMethod.invoke(joy));
+
+    simSetMethod.invoke(joysim, false);
+    joysim.notifyNewData();
+    assertFalse((Boolean) joyGetMethod.invoke(joy));
+    assertFalse((Boolean) joyPressedMethod.invoke(joy));
+    assertTrue((Boolean) joyReleasedMethod.invoke(joy));
+  }
+
+  @ParameterizedTest
+  @EnumSource(value = PS4Controller.Axis.class)
+  @SuppressWarnings({"VariableDeclarationUsageDistance"})
+  public void testAxes(PS4Controller.Axis axis)
+      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+    HAL.initialize(500, 0);
+    PS4Controller joy = new PS4Controller(2);
+    PS4ControllerSim joysim = new PS4ControllerSim(joy);
+
+    var axisName = axis.toString();
+
+    String simSetMethodName = "set" + axisName;
+    String joyGetMethodName = "get" + axisName;
+
+    Method simSetMethod = joysim.getClass().getMethod(simSetMethodName, double.class);
+    Method joyGetMethod = joy.getClass().getMethod(joyGetMethodName);
+
+    simSetMethod.invoke(joysim, 0.35);
+    joysim.notifyNewData();
+    assertEquals(0.35, (Double) joyGetMethod.invoke(joy), 0.001);
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java
index 7b3735f..2603d5b 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java
@@ -1,12 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
 
 package edu.wpi.first.wpilibj;
 
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.networktables.NetworkTableInstance;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -14,7 +19,6 @@
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Stream;
-
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
@@ -25,15 +29,6 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
-import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableInstance;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
 class PreferencesTest {
   private final Preferences m_prefs = Preferences.getInstance();
   private final NetworkTable m_table = NetworkTableInstance.getDefault().getTable("Preferences");
@@ -76,8 +71,9 @@
     Set<String> keys = m_table.getKeys();
     keys.remove(".type");
 
-    assertTrue(keys.isEmpty(), "Preferences was not empty!  Preferences in table: "
-        + Arrays.toString(keys.toArray()));
+    assertTrue(
+        keys.isEmpty(),
+        "Preferences was not empty!  Preferences in table: " + Arrays.toString(keys.toArray()));
   }
 
   @ParameterizedTest
@@ -99,9 +95,8 @@
         () -> assertEquals(0.2, m_prefs.getDouble("checkedValueDouble", 0), 1e-6),
         () -> assertEquals("Hello. How are you?", m_prefs.getString("checkedValueString", "")),
         () -> assertEquals(2, m_prefs.getInt("checkedValueInt", 0)),
-        () -> assertEquals(3.14, m_prefs.getFloat("checkedValueFloat", 0), 1e-6),
-        () -> assertFalse(m_prefs.getBoolean("checkedValueBoolean", true))
-    );
+        () -> assertEquals(3.4, m_prefs.getFloat("checkedValueFloat", 0), 1e-6),
+        () -> assertFalse(m_prefs.getBoolean("checkedValueBoolean", true)));
   }
 
   @Test
@@ -114,8 +109,7 @@
         () -> assertEquals("", m_prefs.getString("checkedValueString", "")),
         () -> assertEquals(0, m_prefs.getInt("checkedValueInt", 0)),
         () -> assertEquals(0, m_prefs.getFloat("checkedValueFloat", 0), 1e-6),
-        () -> assertTrue(m_prefs.getBoolean("checkedValueBoolean", true))
-    );
+        () -> assertTrue(m_prefs.getBoolean("checkedValueBoolean", true)));
   }
 
   @Nested
@@ -129,8 +123,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getInt(key, -1)),
-          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).intValue())
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).intValue()));
     }
 
     @Test
@@ -142,8 +135,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getLong(key, -1)),
-          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).longValue())
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).longValue()));
     }
 
     @Test
@@ -155,8 +147,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getFloat(key, -1), 1e-6),
-          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).floatValue(), 1e-6)
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).floatValue(), 1e-6));
     }
 
     @Test
@@ -168,8 +159,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getDouble(key, -1), 1e-6),
-          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).doubleValue(), 1e-6)
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getNumber(-1).doubleValue(), 1e-6));
     }
 
     @Test
@@ -181,8 +171,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getString(key, "")),
-          () -> assertEquals(value, m_table.getEntry(key).getString(""))
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getString("")));
     }
 
     @Test
@@ -194,8 +183,7 @@
 
       assertAll(
           () -> assertEquals(value, m_prefs.getBoolean(key, false)),
-          () -> assertEquals(value, m_table.getEntry(key).getBoolean(false))
-      );
+          () -> assertEquals(value, m_table.getEntry(key).getBoolean(false)));
     }
   }
 
@@ -206,7 +194,6 @@
         "checkedValueString",
         "checkedValueInt",
         "checkedValueFloat",
-        "checkedValueBoolean"
-    );
+        "checkedValueBoolean");
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/RobotControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/RobotControllerTest.java
index 4bce18f..3c5647c 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/RobotControllerTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/RobotControllerTest.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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;
 
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SensorUtilTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SensorUtilTest.java
new file mode 100644
index 0000000..a33716c
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SensorUtilTest.java
@@ -0,0 +1,42 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class SensorUtilTest {
+  @Test
+  void checkAnalogInputChannel() {
+    assertThrows(IllegalArgumentException.class, () -> SensorUtil.checkAnalogInputChannel(100));
+  }
+
+  @Test
+  void checkAnalogOutputChannel() {
+    assertThrows(IllegalArgumentException.class, () -> SensorUtil.checkAnalogOutputChannel(100));
+  }
+
+  @Test
+  void testInvalidDigitalChannel() {
+    assertThrows(IllegalArgumentException.class, () -> SensorUtil.checkDigitalChannel(100));
+  }
+
+  @Test
+  void testInvalidPwmChannel() {
+    assertThrows(IllegalArgumentException.class, () -> SensorUtil.checkPWMChannel(100));
+  }
+
+  @Test
+  void testInvalidRelayModule() {
+    assertThrows(IllegalArgumentException.class, () -> SensorUtil.checkRelayChannel(100));
+  }
+
+  @Test
+  void testgetDefaultCtrePcmModule() {
+    assertEquals(0, SensorUtil.getDefaultCTREPCMModule());
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SlewRateLimiterTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SlewRateLimiterTest.java
deleted file mode 100644
index e47b70b..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SlewRateLimiterTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class SlewRateLimiterTest {
-  @Test
-  void slewRateLimitTest() {
-    SlewRateLimiter limiter = new SlewRateLimiter(1);
-    Timer.delay(1);
-    assertTrue(limiter.calculate(2) < 2);
-  }
-
-  @Test
-  void slewRateNoLimitTest() {
-    SlewRateLimiter limiter = new SlewRateLimiter(1);
-    Timer.delay(1);
-    assertEquals(limiter.calculate(0.5), 0.5);
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestCTRE.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestCTRE.java
new file mode 100644
index 0000000..0077650
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestCTRE.java
@@ -0,0 +1,64 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.util.AllocationException;
+import org.junit.jupiter.api.Test;
+
+public class SolenoidTestCTRE {
+  @Test
+  void testValidInitialization() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.CTREPCM, 2)) {
+      assertEquals(2, solenoid.getChannel());
+
+      solenoid.set(true);
+      assertTrue(solenoid.get());
+
+      solenoid.set(false);
+      assertFalse(solenoid.get());
+    }
+  }
+
+  @Test
+  void testDoubleInitialization() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.CTREPCM, 2)) {
+      assertThrows(
+          AllocationException.class, () -> new Solenoid(3, PneumaticsModuleType.CTREPCM, 2));
+    }
+  }
+
+  @Test
+  void testDoubleInitializationFromDoubleSolenoid() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(3, PneumaticsModuleType.CTREPCM, 2, 3)) {
+      assertThrows(
+          AllocationException.class, () -> new Solenoid(3, PneumaticsModuleType.CTREPCM, 2));
+    }
+  }
+
+  @Test
+  void testInvalidChannel() {
+    assertThrows(
+        IllegalArgumentException.class, () -> new Solenoid(3, PneumaticsModuleType.CTREPCM, 100));
+  }
+
+  @Test
+  void testToggle() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.CTREPCM, 2)) {
+      solenoid.set(true);
+      assertTrue(solenoid.get());
+
+      solenoid.toggle();
+      assertFalse(solenoid.get());
+
+      solenoid.toggle();
+      assertTrue(solenoid.get());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestREV.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestREV.java
new file mode 100644
index 0000000..3f07406
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SolenoidTestREV.java
@@ -0,0 +1,62 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.util.AllocationException;
+import org.junit.jupiter.api.Test;
+
+public class SolenoidTestREV {
+  @Test
+  void testValidInitialization() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.REVPH, 2)) {
+      assertEquals(2, solenoid.getChannel());
+
+      solenoid.set(true);
+      assertTrue(solenoid.get());
+
+      solenoid.set(false);
+      assertFalse(solenoid.get());
+    }
+  }
+
+  @Test
+  void testDoubleInitialization() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.REVPH, 2)) {
+      assertThrows(AllocationException.class, () -> new Solenoid(3, PneumaticsModuleType.REVPH, 2));
+    }
+  }
+
+  @Test
+  void testDoubleInitializationFromDoubleSolenoid() {
+    try (DoubleSolenoid solenoid = new DoubleSolenoid(3, PneumaticsModuleType.REVPH, 2, 3)) {
+      assertThrows(AllocationException.class, () -> new Solenoid(3, PneumaticsModuleType.REVPH, 2));
+    }
+  }
+
+  @Test
+  void testInvalidChannel() {
+    assertThrows(
+        IllegalArgumentException.class, () -> new Solenoid(3, PneumaticsModuleType.REVPH, 100));
+  }
+
+  @Test
+  void testToggle() {
+    try (Solenoid solenoid = new Solenoid(3, PneumaticsModuleType.REVPH, 2)) {
+      solenoid.set(true);
+      assertTrue(solenoid.get());
+
+      solenoid.toggle();
+      assertFalse(solenoid.get());
+
+      solenoid.toggle();
+      assertTrue(solenoid.get());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SpeedControllerGroupTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/SpeedControllerGroupTest.java
deleted file mode 100644
index 62e2f46..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/SpeedControllerGroupTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj;
-
-import java.util.Arrays;
-import java.util.stream.DoubleStream;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class SpeedControllerGroupTest {
-  private static Stream<Arguments> speedControllerArguments() {
-    return IntStream.of(1, 2, 3).mapToObj(number -> {
-      SpeedController[] speedControllers = Stream.generate(MockSpeedController::new)
-          .limit(number)
-          .toArray(SpeedController[]::new);
-      SpeedControllerGroup group = new SpeedControllerGroup(speedControllers[0],
-          Arrays.copyOfRange(speedControllers, 1, speedControllers.length));
-      return Arguments.of(group, speedControllers);
-    });
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void setTest(final SpeedControllerGroup group, final SpeedController[] speedControllers) {
-    group.set(1.0);
-
-    assertArrayEquals(DoubleStream.generate(() -> 1.0).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).mapToDouble(SpeedController::get).toArray(),
-        0.00005);
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void getInvertedTest(final SpeedControllerGroup group,
-                       final SpeedController[] speedControllers) {
-    group.setInverted(true);
-
-    assertTrue(group.getInverted());
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void setInvertedDoesNotModifySpeedControllersTest(final SpeedControllerGroup group,
-                                                    final SpeedController[] speedControllers) {
-    group.setInverted(true);
-
-    assertArrayEquals(Stream.generate(() -> false).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).map(SpeedController::getInverted).toArray());
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void setInvertedDoesInvertTest(final SpeedControllerGroup group,
-                                 final SpeedController[] speedControllers) {
-    group.setInverted(true);
-    group.set(1.0);
-
-    assertArrayEquals(DoubleStream.generate(() -> -1.0).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).mapToDouble(SpeedController::get).toArray(),
-        0.00005);
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void disableTest(final SpeedControllerGroup group,
-                   final SpeedController[] speedControllers) {
-    group.set(1.0);
-    group.disable();
-
-    assertArrayEquals(DoubleStream.generate(() -> 0.0).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).mapToDouble(SpeedController::get).toArray(),
-        0.00005);
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void stopMotorTest(final SpeedControllerGroup group,
-                     final SpeedController[] speedControllers) {
-    group.set(1.0);
-    group.stopMotor();
-
-    assertArrayEquals(DoubleStream.generate(() -> 0.0).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).mapToDouble(SpeedController::get).toArray(),
-        0.00005);
-  }
-
-  @ParameterizedTest
-  @MethodSource("speedControllerArguments")
-  void pidWriteTest(final SpeedControllerGroup group,
-                    final SpeedController[] speedControllers) {
-    group.pidWrite(1.0);
-
-    assertArrayEquals(DoubleStream.generate(() -> 1.0).limit(speedControllers.length).toArray(),
-        Arrays.stream(speedControllers).mapToDouble(SpeedController::get).toArray(),
-        0.00005);
-  }
-}
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 6942ca0..de5fd1c 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimedRobotTest.java
@@ -1,26 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.concurrent.atomic.AtomicInteger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import edu.wpi.first.wpilibj.simulation.DriverStationSim;
+import edu.wpi.first.wpilibj.simulation.SimHooks;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.parallel.ResourceLock;
 
-import edu.wpi.first.wpilibj.simulation.DriverStationSim;
-import edu.wpi.first.wpilibj.simulation.SimHooks;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
 class TimedRobotTest {
-  class MockRobot extends TimedRobot {
+  static class MockRobot extends TimedRobot {
     public final AtomicInteger m_robotInitCount = new AtomicInteger(0);
     public final AtomicInteger m_simulationInitCount = new AtomicInteger(0);
     public final AtomicInteger m_disabledInitCount = new AtomicInteger(0);
@@ -35,6 +30,11 @@
     public final AtomicInteger m_teleopPeriodicCount = new AtomicInteger(0);
     public final AtomicInteger m_testPeriodicCount = new AtomicInteger(0);
 
+    public final AtomicInteger m_disabledExitCount = new AtomicInteger(0);
+    public final AtomicInteger m_autonomousExitCount = new AtomicInteger(0);
+    public final AtomicInteger m_teleopExitCount = new AtomicInteger(0);
+    public final AtomicInteger m_testExitCount = new AtomicInteger(0);
+
     @Override
     public void robotInit() {
       m_robotInitCount.addAndGet(1);
@@ -94,6 +94,26 @@
     public void testPeriodic() {
       m_testPeriodicCount.addAndGet(1);
     }
+
+    @Override
+    public void disabledExit() {
+      m_disabledExitCount.addAndGet(1);
+    }
+
+    @Override
+    public void autonomousExit() {
+      m_autonomousExitCount.addAndGet(1);
+    }
+
+    @Override
+    public void teleopExit() {
+      m_teleopExitCount.addAndGet(1);
+    }
+
+    @Override
+    public void testExit() {
+      m_testExitCount.addAndGet(1);
+    }
   }
 
   @BeforeEach
@@ -108,17 +128,19 @@
 
   @Test
   @ResourceLock("timing")
-  void disabledTest() {
+  void disabledModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(1, robot.m_robotInitCount.get());
     assertEquals(1, robot.m_simulationInitCount.get());
@@ -134,6 +156,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -150,6 +177,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -166,6 +198,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     robot.endCompetition();
     try {
       robotThread.interrupt();
@@ -178,19 +215,21 @@
 
   @Test
   @ResourceLock("timing")
-  void autonomousTest() {
+  void autonomousModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
     DriverStationSim.setAutonomous(true);
     DriverStationSim.setTest(false);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(1, robot.m_robotInitCount.get());
     assertEquals(1, robot.m_simulationInitCount.get());
@@ -206,6 +245,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -222,6 +266,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -238,6 +287,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     robot.endCompetition();
     try {
       robotThread.interrupt();
@@ -250,19 +304,21 @@
 
   @Test
   @ResourceLock("timing")
-  void teleopTest() {
+  void teleopModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
     DriverStationSim.setAutonomous(false);
     DriverStationSim.setTest(false);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(1, robot.m_robotInitCount.get());
     assertEquals(1, robot.m_simulationInitCount.get());
@@ -278,6 +334,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -294,6 +355,11 @@
     assertEquals(1, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -310,6 +376,11 @@
     assertEquals(2, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     robot.endCompetition();
     try {
       robotThread.interrupt();
@@ -322,19 +393,21 @@
 
   @Test
   @ResourceLock("timing")
-  void testTest() {
+  void testModeTest() {
     MockRobot robot = new MockRobot();
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(true);
     DriverStationSim.setAutonomous(false);
     DriverStationSim.setTest(true);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(1, robot.m_robotInitCount.get());
     assertEquals(1, robot.m_simulationInitCount.get());
@@ -350,6 +423,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(0, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -366,6 +444,11 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(1, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
     SimHooks.stepTiming(0.02);
 
     assertEquals(1, robot.m_robotInitCount.get());
@@ -382,6 +465,134 @@
     assertEquals(0, robot.m_teleopPeriodicCount.get());
     assertEquals(2, robot.m_testPeriodicCount.get());
 
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    robot.endCompetition();
+    try {
+      robotThread.interrupt();
+      robotThread.join();
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+    robot.close();
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void modeChangeTest() {
+    MockRobot robot = new MockRobot();
+
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
+    robotThread.start();
+
+    // Start in disabled
+    DriverStationSim.setEnabled(false);
+    DriverStationSim.setAutonomous(false);
+    DriverStationSim.setTest(false);
+    DriverStationSim.notifyNewData();
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
+
+    assertEquals(0, robot.m_disabledInitCount.get());
+    assertEquals(0, robot.m_autonomousInitCount.get());
+    assertEquals(0, robot.m_teleopInitCount.get());
+    assertEquals(0, robot.m_testInitCount.get());
+
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    SimHooks.stepTiming(0.02);
+
+    assertEquals(1, robot.m_disabledInitCount.get());
+    assertEquals(0, robot.m_autonomousInitCount.get());
+    assertEquals(0, robot.m_teleopInitCount.get());
+    assertEquals(0, robot.m_testInitCount.get());
+
+    assertEquals(0, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    // Transition to autonomous
+    DriverStationSim.setEnabled(true);
+    DriverStationSim.setAutonomous(true);
+    DriverStationSim.setTest(false);
+    DriverStationSim.notifyNewData();
+
+    SimHooks.stepTiming(0.02);
+
+    assertEquals(1, robot.m_disabledInitCount.get());
+    assertEquals(1, robot.m_autonomousInitCount.get());
+    assertEquals(0, robot.m_teleopInitCount.get());
+    assertEquals(0, robot.m_testInitCount.get());
+
+    assertEquals(1, robot.m_disabledExitCount.get());
+    assertEquals(0, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    // Transition to teleop
+    DriverStationSim.setEnabled(true);
+    DriverStationSim.setAutonomous(false);
+    DriverStationSim.setTest(false);
+    DriverStationSim.notifyNewData();
+
+    SimHooks.stepTiming(0.02);
+
+    assertEquals(1, robot.m_disabledInitCount.get());
+    assertEquals(1, robot.m_autonomousInitCount.get());
+    assertEquals(1, robot.m_teleopInitCount.get());
+    assertEquals(0, robot.m_testInitCount.get());
+
+    assertEquals(1, robot.m_disabledExitCount.get());
+    assertEquals(1, robot.m_autonomousExitCount.get());
+    assertEquals(0, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    // Transition to test
+    DriverStationSim.setEnabled(true);
+    DriverStationSim.setAutonomous(false);
+    DriverStationSim.setTest(true);
+    DriverStationSim.notifyNewData();
+
+    SimHooks.stepTiming(0.02);
+
+    assertEquals(1, robot.m_disabledInitCount.get());
+    assertEquals(1, robot.m_autonomousInitCount.get());
+    assertEquals(1, robot.m_teleopInitCount.get());
+    assertEquals(1, robot.m_testInitCount.get());
+
+    assertEquals(1, robot.m_disabledExitCount.get());
+    assertEquals(1, robot.m_autonomousExitCount.get());
+    assertEquals(1, robot.m_teleopExitCount.get());
+    assertEquals(0, robot.m_testExitCount.get());
+
+    // Transition to disabled
+    DriverStationSim.setEnabled(false);
+    DriverStationSim.setAutonomous(false);
+    DriverStationSim.setTest(false);
+    DriverStationSim.notifyNewData();
+
+    SimHooks.stepTiming(0.02);
+
+    assertEquals(2, robot.m_disabledInitCount.get());
+    assertEquals(1, robot.m_autonomousInitCount.get());
+    assertEquals(1, robot.m_teleopInitCount.get());
+    assertEquals(1, robot.m_testInitCount.get());
+
+    assertEquals(1, robot.m_disabledExitCount.get());
+    assertEquals(1, robot.m_autonomousExitCount.get());
+    assertEquals(1, robot.m_teleopExitCount.get());
+    assertEquals(1, robot.m_testExitCount.get());
+
     robot.endCompetition();
     try {
       robotThread.interrupt();
@@ -398,18 +609,22 @@
     MockRobot robot = new MockRobot();
 
     final AtomicInteger callbackCount = new AtomicInteger(0);
-    robot.addPeriodic(() -> {
-      callbackCount.addAndGet(1);
-    }, 0.01);
+    robot.addPeriodic(
+        () -> {
+          callbackCount.addAndGet(1);
+        },
+        0.01);
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(0, robot.m_disabledInitCount.get());
     assertEquals(0, robot.m_disabledPeriodicCount.get());
@@ -443,9 +658,12 @@
     MockRobot robot = new MockRobot();
 
     final AtomicInteger callbackCount = new AtomicInteger(0);
-    robot.addPeriodic(() -> {
-      callbackCount.addAndGet(1);
-    }, 0.01, 0.005);
+    robot.addPeriodic(
+        () -> {
+          callbackCount.addAndGet(1);
+        },
+        0.01,
+        0.005);
 
     // Expirations in this test (ms)
     //
@@ -454,14 +672,16 @@
     //    20 |      15
     //    40 |      25
 
-    Thread robotThread = new Thread(() -> {
-      robot.startCompetition();
-    });
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
     robotThread.start();
 
     DriverStationSim.setEnabled(false);
     DriverStationSim.notifyNewData();
-    SimHooks.stepTiming(0.0);  // Wait for Notifiers
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
 
     assertEquals(0, robot.m_disabledInitCount.get());
     assertEquals(0, robot.m_disabledPeriodicCount.get());
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimerTest.java
new file mode 100644
index 0000000..89619dd
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimerTest.java
@@ -0,0 +1,132 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.SimHooks;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
+class TimerTest {
+  @BeforeEach
+  void setup() {
+    HAL.initialize(500, 0);
+    SimHooks.pauseTiming();
+    SimHooks.restartTiming();
+  }
+
+  @AfterEach
+  void cleanup() {
+    SimHooks.resumeTiming();
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void startStopTest() {
+    var timer = new Timer();
+
+    // Verify timer is initialized as stopped
+    assertEquals(timer.get(), 0.0);
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.0);
+
+    // Verify timer increments after it's started
+    timer.start();
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.5, 1e-9);
+
+    // Verify timer stops incrementing after it's stopped
+    timer.stop();
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.5, 1e-9);
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void resetTest() {
+    var timer = new Timer();
+    timer.start();
+
+    // Advance timer to 500 ms
+    assertEquals(timer.get(), 0.0);
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.5, 1e-9);
+
+    // Verify timer reports 0 ms after reset
+    timer.reset();
+    assertEquals(timer.get(), 0.0);
+
+    // Verify timer continues incrementing
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.5, 1e-9);
+
+    // Verify timer doesn't start incrementing after reset if it was stopped
+    timer.stop();
+    timer.reset();
+    SimHooks.stepTiming(0.5);
+    assertEquals(timer.get(), 0.0);
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void hasElapsedTest() {
+    var timer = new Timer();
+
+    // Verify 0 ms has elapsed since timer hasn't started
+    assertTrue(timer.hasElapsed(0.0));
+
+    // Verify timer doesn't report elapsed time when stopped
+    SimHooks.stepTiming(0.5);
+    assertFalse(timer.hasElapsed(0.4));
+
+    timer.start();
+
+    // Verify timer reports >= 400 ms has elapsed after multiple calls
+    SimHooks.stepTiming(0.5);
+    assertTrue(timer.hasElapsed(0.4));
+    assertTrue(timer.hasElapsed(0.4));
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void advanceIfElapsedTest() {
+    var timer = new Timer();
+
+    // Verify 0 ms has elapsed since timer hasn't started
+    assertTrue(timer.advanceIfElapsed(0.0));
+
+    // Verify timer doesn't report elapsed time when stopped
+    SimHooks.stepTiming(0.5);
+    assertFalse(timer.advanceIfElapsed(0.4));
+
+    timer.start();
+
+    // Verify timer reports >= 400 ms has elapsed for only first call
+    SimHooks.stepTiming(0.5);
+    assertTrue(timer.advanceIfElapsed(0.4));
+    assertFalse(timer.advanceIfElapsed(0.4));
+
+    // Verify timer reports >= 400 ms has elapsed for two calls
+    SimHooks.stepTiming(1.0);
+    assertTrue(timer.advanceIfElapsed(0.4));
+    assertTrue(timer.advanceIfElapsed(0.4));
+    assertFalse(timer.advanceIfElapsed(0.4));
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void getFPGATimestampTest() {
+    double start = Timer.getFPGATimestamp();
+    SimHooks.stepTiming(0.5);
+    double end = Timer.getFPGATimestamp();
+    assertEquals(start + 0.5, end, 1e-9);
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java
new file mode 100644
index 0000000..ee4860d
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/TimesliceRobotTest.java
@@ -0,0 +1,131 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import edu.wpi.first.wpilibj.simulation.DriverStationSim;
+import edu.wpi.first.wpilibj.simulation.SimHooks;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
+class TimesliceRobotTest {
+  static class MockRobot extends TimesliceRobot {
+    public final AtomicInteger m_robotPeriodicCount = new AtomicInteger(0);
+
+    MockRobot() {
+      super(0.002, 0.005);
+    }
+
+    @Override
+    public void robotPeriodic() {
+      m_robotPeriodicCount.addAndGet(1);
+    }
+  }
+
+  @BeforeEach
+  void setup() {
+    SimHooks.pauseTiming();
+  }
+
+  @AfterEach
+  void cleanup() {
+    SimHooks.resumeTiming();
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void scheduleTest() {
+    MockRobot robot = new MockRobot();
+
+    final AtomicInteger callbackCount1 = new AtomicInteger(0);
+    final AtomicInteger callbackCount2 = new AtomicInteger(0);
+
+    // Timeslice allocation table
+    //
+    // |       Name      | Offset (ms) | Allocation (ms)|
+    // |-----------------|-------------|----------------|
+    // | 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);
+
+    Thread robotThread =
+        new Thread(
+            () -> {
+              robot.startCompetition();
+            });
+    robotThread.start();
+
+    DriverStationSim.setEnabled(false);
+    DriverStationSim.notifyNewData();
+    SimHooks.stepTiming(0.0); // Wait for Notifiers
+
+    // Functions scheduled with addPeriodic() are delayed by one period before
+    // their first run (5 ms for this test's callbacks here and 20 ms for
+    // robotPeriodic()).
+    SimHooks.stepTiming(0.005);
+
+    assertEquals(0, robot.m_robotPeriodicCount.get());
+    assertEquals(0, callbackCount1.get());
+    assertEquals(0, callbackCount2.get());
+
+    // Step to 1.5 ms
+    SimHooks.stepTiming(0.0015);
+    assertEquals(0, robot.m_robotPeriodicCount.get());
+    assertEquals(0, callbackCount1.get());
+    assertEquals(0, callbackCount2.get());
+
+    // Step to 2.25 ms
+    SimHooks.stepTiming(0.00075);
+    assertEquals(0, robot.m_robotPeriodicCount.get());
+    assertEquals(1, callbackCount1.get());
+    assertEquals(0, callbackCount2.get());
+
+    // Step to 2.75 ms
+    SimHooks.stepTiming(0.0005);
+    assertEquals(0, robot.m_robotPeriodicCount.get());
+    assertEquals(1, callbackCount1.get());
+    assertEquals(1, callbackCount2.get());
+
+    robot.endCompetition();
+    try {
+      robotThread.interrupt();
+      robotThread.join();
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+    robot.close();
+  }
+
+  @Test
+  @ResourceLock("timing")
+  void scheduleOverrunTest() {
+    MockRobot robot = new MockRobot();
+
+    robot.schedule(() -> {}, 0.0005);
+    robot.schedule(() -> {}, 0.001);
+
+    // offset = 2 ms + 0.5 ms + 1 ms = 3.5 ms
+    // 3.5 ms + 3 ms allocation = 6.5 ms > max of 5 ms
+    assertThrows(IllegalStateException.class, () -> robot.schedule(() -> {}, 0.003));
+
+    robot.endCompetition();
+    robot.close();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/UltrasonicTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/UltrasonicTest.java
new file mode 100644
index 0000000..1b50572
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/UltrasonicTest.java
@@ -0,0 +1,31 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.wpilibj.simulation.UltrasonicSim;
+import org.junit.jupiter.api.Test;
+
+public class UltrasonicTest {
+  @Test
+  public void testUltrasonic() {
+    try (Ultrasonic ultrasonic = new Ultrasonic(0, 1)) {
+      UltrasonicSim sim = new UltrasonicSim(ultrasonic);
+
+      assertEquals(0, ultrasonic.getRangeInches());
+      assertTrue(ultrasonic.isRangeValid());
+
+      sim.setRangeInches(35.04);
+      assertEquals(35.04, ultrasonic.getRangeInches());
+
+      sim.setRangeValid(false);
+      assertFalse(ultrasonic.isRangeValid());
+      assertEquals(0, ultrasonic.getRangeInches());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/UtilityClassTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/UtilityClassTest.java
index a66cd13..8eed93f 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/UtilityClassTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/UtilityClassTest.java
@@ -1,28 +1,30 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Modifier;
-import java.util.Arrays;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.api.DynamicTest;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestFactory;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.DynamicTest.dynamicTest;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+
+// Declaring this class abstract prevents UtilityClassTest from running on itself and throwing the
+// following exception:
+//
+// org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered
+// for parameter [java.lang.Class<T> arg0] in constructor [protected
+// edu.wpi.first.wpilibj.UtilityClassTest(java.lang.Class<T>)].
 @SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod")
 public abstract class UtilityClassTest<T> {
   private final Class<T> m_clazz;
@@ -33,8 +35,7 @@
 
   @Test
   public void singleConstructorTest() {
-    assertEquals(1, m_clazz.getDeclaredConstructors().length,
-        "More than one constructor defined");
+    assertEquals(1, m_clazz.getDeclaredConstructors().length, "More than one constructor defined");
   }
 
   @Test
@@ -55,7 +56,9 @@
   Stream<DynamicTest> publicMethodsStaticTestFactory() {
     return Arrays.stream(m_clazz.getDeclaredMethods())
         .filter(method -> Modifier.isPublic(method.getModifiers()))
-        .map(method -> dynamicTest(method.getName(),
-            () -> assertTrue(Modifier.isStatic(method.getModifiers()))));
+        .map(
+            method ->
+                dynamicTest(
+                    method.getName(), () -> assertTrue(Modifier.isStatic(method.getModifiers()))));
   }
 }
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 d99d7db..c1676e0 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/WatchdogTest.java
@@ -1,26 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 java.util.concurrent.atomic.AtomicInteger;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.parallel.ResourceLock;
-
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.simulation.SimHooks;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.SimHooks;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
 class WatchdogTest {
   @BeforeEach
   void setup() {
@@ -38,9 +33,12 @@
   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);
@@ -54,8 +52,8 @@
       SimHooks.stepTiming(0.4);
       watchdog.disable();
 
-      assertEquals(1, watchdogCounter.get(),
-          "Watchdog either didn't trigger or triggered more than once");
+      assertEquals(
+          1, watchdogCounter.get(), "Watchdog either didn't trigger or triggered more than once");
 
       // Run 3
       watchdogCounter.set(0);
@@ -63,8 +61,8 @@
       SimHooks.stepTiming(1.0);
       watchdog.disable();
 
-      assertEquals(1, watchdogCounter.get(),
-          "Watchdog either didn't trigger or triggered more than once");
+      assertEquals(
+          1, watchdogCounter.get(), "Watchdog either didn't trigger or triggered more than once");
     }
   }
 
@@ -73,9 +71,12 @@
   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();
@@ -91,9 +92,12 @@
   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);
@@ -104,16 +108,15 @@
       SimHooks.stepTiming(0.3);
       watchdog.disable();
 
-      assertEquals(1, watchdogCounter.get(),
-          "Watchdog either didn't trigger or triggered more than once");
+      assertEquals(
+          1, watchdogCounter.get(), "Watchdog either didn't trigger or triggered more than once");
     }
   }
 
   @Test
   @ResourceLock("timing")
   void isExpiredTest() {
-    try (Watchdog watchdog = new Watchdog(0.2, () -> {
-    })) {
+    try (Watchdog watchdog = new Watchdog(0.2, () -> {})) {
       assertFalse(watchdog.isExpired());
       watchdog.enable();
 
@@ -134,9 +137,12 @@
   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");
@@ -167,12 +173,18 @@
     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");
@@ -184,8 +196,8 @@
       watchdog1.disable();
       watchdog2.disable();
 
-      assertEquals(1, watchdogCounter1.get(),
-          "Watchdog either didn't trigger or triggered more than once");
+      assertEquals(
+          1, watchdogCounter1.get(), "Watchdog either didn't trigger or triggered more than once");
       assertEquals(0, watchdogCounter2.get(), "Watchdog triggered early");
     }
   }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/XboxControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/XboxControllerTest.java
index 34fce9a..d472377 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/XboxControllerTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/XboxControllerTest.java
@@ -1,86 +1,81 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 org.junit.jupiter.api.Test;
-
-import edu.wpi.first.hal.HAL;
-import edu.wpi.first.wpilibj.simulation.XboxControllerSim;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.simulation.XboxControllerSim;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
 class XboxControllerTest {
-  @Test
-  void testGetX() {
+  @ParameterizedTest
+  @EnumSource(value = XboxController.Button.class)
+  @SuppressWarnings({"VariableDeclarationUsageDistance"})
+  public void testButtons(XboxController.Button button)
+      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
     HAL.initialize(500, 0);
     XboxController joy = new XboxController(2);
     XboxControllerSim joysim = new XboxControllerSim(joy);
 
-    joysim.setX(XboxController.Hand.kLeft, 0.35);
-    joysim.setX(XboxController.Hand.kRight, 0.45);
+    var buttonName = button.toString();
+
+    String simSetMethodName = "set" + buttonName;
+    String joyGetMethodName = "get" + buttonName;
+    String joyPressedMethodName = "get" + buttonName + "Pressed";
+    String joyReleasedMethodName = "get" + buttonName + "Released";
+
+    Method simSetMethod = joysim.getClass().getMethod(simSetMethodName, boolean.class);
+    Method joyGetMethod = joy.getClass().getMethod(joyGetMethodName);
+    Method joyPressedMethod = joy.getClass().getMethod(joyPressedMethodName);
+    Method joyReleasedMethod = joy.getClass().getMethod(joyReleasedMethodName);
+
+    simSetMethod.invoke(joysim, false);
     joysim.notifyNewData();
-    assertEquals(0.35, joy.getX(XboxController.Hand.kLeft), 0.001);
-    assertEquals(0.45, joy.getX(XboxController.Hand.kRight), 0.001);
+    assertFalse((Boolean) joyGetMethod.invoke(joy));
+    // need to call pressed and released to clear flags
+    joyPressedMethod.invoke(joy);
+    joyReleasedMethod.invoke(joy);
+
+    simSetMethod.invoke(joysim, true);
+    joysim.notifyNewData();
+    assertTrue((Boolean) joyGetMethod.invoke(joy));
+    assertTrue((Boolean) joyPressedMethod.invoke(joy));
+    assertFalse((Boolean) joyReleasedMethod.invoke(joy));
+
+    simSetMethod.invoke(joysim, false);
+    joysim.notifyNewData();
+    assertFalse((Boolean) joyGetMethod.invoke(joy));
+    assertFalse((Boolean) joyPressedMethod.invoke(joy));
+    assertTrue((Boolean) joyReleasedMethod.invoke(joy));
   }
 
-  @Test
-  void testGetBumper() {
+  @ParameterizedTest
+  @EnumSource(value = XboxController.Axis.class)
+  @SuppressWarnings({"VariableDeclarationUsageDistance"})
+  public void testAxes(XboxController.Axis axis)
+      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
     HAL.initialize(500, 0);
     XboxController joy = new XboxController(2);
     XboxControllerSim joysim = new XboxControllerSim(joy);
 
-    joysim.setBumper(XboxController.Hand.kLeft, false);
-    joysim.setBumper(XboxController.Hand.kRight, true);
-    joysim.notifyNewData();
-    assertFalse(joy.getBumper(XboxController.Hand.kLeft));
-    assertTrue(joy.getBumper(XboxController.Hand.kRight));
-    // need to call pressed and released to clear flags
-    joy.getBumperPressed(XboxController.Hand.kLeft);
-    joy.getBumperReleased(XboxController.Hand.kLeft);
-    joy.getBumperPressed(XboxController.Hand.kRight);
-    joy.getBumperReleased(XboxController.Hand.kRight);
+    var axisName = axis.toString();
 
-    joysim.setBumper(XboxController.Hand.kLeft, true);
-    joysim.setBumper(XboxController.Hand.kRight, false);
-    joysim.notifyNewData();
-    assertTrue(joy.getBumper(XboxController.Hand.kLeft));
-    assertTrue(joy.getBumperPressed(XboxController.Hand.kLeft));
-    assertFalse(joy.getBumperReleased(XboxController.Hand.kLeft));
-    assertFalse(joy.getBumper(XboxController.Hand.kRight));
-    assertFalse(joy.getBumperPressed(XboxController.Hand.kRight));
-    assertTrue(joy.getBumperReleased(XboxController.Hand.kRight));
-  }
+    String simSetMethodName = "set" + axisName;
+    String joyGetMethodName = "get" + axisName;
 
-  @Test
-  void testGetAButton() {
-    HAL.initialize(500, 0);
-    XboxController joy = new XboxController(2);
-    XboxControllerSim joysim = new XboxControllerSim(joy);
+    Method simSetMethod = joysim.getClass().getMethod(simSetMethodName, double.class);
+    Method joyGetMethod = joy.getClass().getMethod(joyGetMethodName);
 
-    joysim.setAButton(false);
+    simSetMethod.invoke(joysim, 0.35);
     joysim.notifyNewData();
-    assertFalse(joy.getAButton());
-    // need to call pressed and released to clear flags
-    joy.getAButtonPressed();
-    joy.getAButtonReleased();
-
-    joysim.setAButton(true);
-    joysim.notifyNewData();
-    assertTrue(joy.getAButton());
-    assertTrue(joy.getAButtonPressed());
-    assertFalse(joy.getAButtonReleased());
-
-    joysim.setAButton(false);
-    joysim.notifyNewData();
-    assertFalse(joy.getAButton());
-    assertFalse(joy.getAButtonPressed());
-    assertTrue(joy.getAButtonReleased());
+    assertEquals(0.35, (Double) joyGetMethod.invoke(joy), 0.001);
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/can/CANStatusTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/can/CANStatusTest.java
index e804d0c..71104fc 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/can/CANStatusTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/can/CANStatusTest.java
@@ -1,25 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.can;
 
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.can.CANJNI;
 import edu.wpi.first.hal.can.CANStatus;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import org.junit.jupiter.api.Test;
 
 class CANStatusTest {
   @Test
   void canStatusGetDoesntThrow() {
     HAL.initialize(500, 0);
     CANStatus status = new CANStatus();
-    assertDoesNotThrow(() -> CANJNI.GetCANStatus(status));
+    assertDoesNotThrow(() -> CANJNI.getCANStatus(status));
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ControllerUtilTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ControllerUtilTest.java
deleted file mode 100644
index 307eccd..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ControllerUtilTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class ControllerUtilTest {
-  @Test
-  void testGetModulusError() {
-    // Test symmetric range
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0, -170.0, -180.0, 180.0));
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0 + 360.0, -170.0, -180.0, 180.0));
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0, -170.0 + 360.0, -180.0, 180.0));
-    assertEquals(20.0, ControllerUtil.getModulusError(-170.0, 170.0, -180.0, 180.0));
-    assertEquals(20.0, ControllerUtil.getModulusError(-170.0 + 360.0, 170.0, -180.0, 180.0));
-    assertEquals(20.0, ControllerUtil.getModulusError(-170.0, 170.0 + 360.0, -180.0, 180.0));
-
-    // Test range start at zero
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0, 190.0, 0.0, 360.0));
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0 + 360.0, 190.0, 0.0, 360.0));
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0, 190.0 + 360, 0.0, 360.0));
-
-    // Test asymmetric range that doesn't start at zero
-    assertEquals(-20.0, ControllerUtil.getModulusError(170.0, -170.0, -170.0, 190.0));
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/HolonomicDriveControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/HolonomicDriveControllerTest.java
deleted file mode 100644
index 2bf7900..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/HolonomicDriveControllerTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.geometry.Twist2d;
-import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
-import edu.wpi.first.wpilibj.trajectory.Trajectory;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryConfig;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryGenerator;
-import edu.wpi.first.wpilibj.trajectory.TrapezoidProfile;
-import edu.wpi.first.wpiutil.math.MathUtil;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class HolonomicDriveControllerTest {
-  private static final double kTolerance = 1 / 12.0;
-  private static final double kAngularTolerance = Math.toRadians(2);
-
-  @Test
-  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
-  void testReachesReference() {
-    HolonomicDriveController controller = new HolonomicDriveController(
-        new PIDController(1.0, 0.0, 0.0),
-        new PIDController(1.0, 0.0, 0.0),
-        new ProfiledPIDController(1.0, 0.0, 0.0,
-            new TrapezoidProfile.Constraints(6.28, 3.14))
-    );
-    Pose2d robotPose = new Pose2d(2.7, 23.0, Rotation2d.fromDegrees(0.0));
-
-    List<Pose2d> waypoints = new ArrayList<>();
-    waypoints.add(new Pose2d(2.75, 22.521, new Rotation2d(0)));
-    waypoints.add(new Pose2d(24.73, 19.68, new Rotation2d(5.8)));
-
-    TrajectoryConfig config = new TrajectoryConfig(8.0, 4.0);
-    Trajectory trajectory = TrajectoryGenerator.generateTrajectory(waypoints, config);
-
-    final double kDt = 0.02;
-    final double kTotalTime = trajectory.getTotalTimeSeconds();
-
-    for (int i = 0; i < (kTotalTime / kDt); i++) {
-      Trajectory.State state = trajectory.sample(kDt * i);
-      ChassisSpeeds output = controller.calculate(robotPose, state, new Rotation2d());
-
-      robotPose = robotPose.exp(new Twist2d(output.vxMetersPerSecond * kDt,
-          output.vyMetersPerSecond * kDt, output.omegaRadiansPerSecond * kDt));
-    }
-
-    final List<Trajectory.State> states = trajectory.getStates();
-    final Pose2d endPose = states.get(states.size() - 1).poseMeters;
-
-    // Java lambdas require local variables referenced from a lambda expression
-    // must be final or effectively final.
-    final Pose2d finalRobotPose = robotPose;
-
-    assertAll(
-        () -> assertEquals(endPose.getX(), finalRobotPose.getX(),
-            kTolerance),
-        () -> assertEquals(endPose.getY(), finalRobotPose.getY(),
-            kTolerance),
-        () -> assertEquals(0.0,
-            MathUtil.normalizeAngle(finalRobotPose.getRotation().getRadians()),
-            kAngularTolerance)
-    );
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDInputOutputTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDInputOutputTest.java
deleted file mode 100644
index 873d03f..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDInputOutputTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class PIDInputOutputTest {
-  private PIDController m_controller;
-
-  @BeforeEach
-  void setUp() {
-    m_controller = new PIDController(0, 0, 0);
-  }
-
-  @Test
-  void continuousInputTest() {
-    m_controller.setP(1);
-    m_controller.enableContinuousInput(-180, 180);
-
-    assertTrue(m_controller.calculate(-179, 179) < 0.0);
-  }
-
-  @Test
-  void proportionalGainOutputTest() {
-    m_controller.setP(4);
-
-    assertEquals(-0.1, m_controller.calculate(0.025, 0), 1e-5);
-  }
-
-  @Test
-  void integralGainOutputTest() {
-    m_controller.setI(4);
-
-    double out = 0;
-
-    for (int i = 0; i < 5; i++) {
-      out = m_controller.calculate(0.025, 0);
-    }
-
-    assertEquals(-0.5 * m_controller.getPeriod(), out, 1e-5);
-  }
-
-  @Test
-  void derivativeGainOutputTest() {
-    m_controller.setD(4);
-
-    m_controller.calculate(0, 0);
-
-    assertEquals(-0.01 / m_controller.getPeriod(), m_controller.calculate(0.0025, 0), 1e-5);
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDToleranceTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDToleranceTest.java
deleted file mode 100644
index b2d43c6..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/PIDToleranceTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class PIDToleranceTest {
-  private static final double kSetpoint = 50.0;
-  private static final double kTolerance = 10.0;
-  private static final double kRange = 200;
-
-  @Test
-  void initialToleranceTest() {
-    try (var controller = new PIDController(0.05, 0.0, 0.0)) {
-      controller.enableContinuousInput(-kRange / 2, kRange / 2);
-
-      assertTrue(controller.atSetpoint());
-    }
-  }
-
-  @Test
-  void absoluteToleranceTest() {
-    try (var controller = new PIDController(0.05, 0.0, 0.0)) {
-      controller.enableContinuousInput(-kRange / 2, kRange / 2);
-
-      controller.setTolerance(kTolerance);
-      controller.setSetpoint(kSetpoint);
-
-      controller.calculate(0.0);
-
-      assertFalse(controller.atSetpoint(),
-          "Error was in tolerance when it should not have been. Error was "
-          + controller.getPositionError());
-
-      controller.calculate(kSetpoint + kTolerance / 2);
-
-      assertTrue(controller.atSetpoint(),
-          "Error was not in tolerance when it should have been. Error was "
-          + controller.getPositionError());
-
-      controller.calculate(kSetpoint + 10 * kTolerance);
-
-      assertFalse(controller.atSetpoint(),
-          "Error was in tolerance when it should not have been. Error was "
-          + controller.getPositionError());
-    }
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDControllerTest.java
deleted file mode 100644
index 6babcc2..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDControllerTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.trajectory.TrapezoidProfile;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class ProfiledPIDControllerTest {
-  @Test
-  void testStartFromNonZeroPosition() {
-    ProfiledPIDController controller = new ProfiledPIDController(1.0, 0.0, 0.0,
-        new TrapezoidProfile.Constraints(1.0, 1.0));
-
-    controller.reset(20);
-
-    assertEquals(0.0, controller.calculate(20), 0.05);
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDInputOutputTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDInputOutputTest.java
deleted file mode 100644
index 9fca2a4..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/ProfiledPIDInputOutputTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.trajectory.TrapezoidProfile;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class ProfiledPIDInputOutputTest {
-  private ProfiledPIDController m_controller;
-
-  @BeforeEach
-  void setUp() {
-    m_controller = new ProfiledPIDController(0, 0, 0,
-        new TrapezoidProfile.Constraints(360, 180));
-  }
-
-  @Test
-  void continuousInputTest1() {
-    m_controller.setP(1);
-    m_controller.enableContinuousInput(-180, 180);
-
-    final double kSetpoint = -179.0;
-    final double kMeasurement = -179.0;
-    final double kGoal = 179.0;
-
-    m_controller.reset(kSetpoint);
-    assertTrue(m_controller.calculate(kMeasurement, kGoal) < 0.0);
-
-    // Error must be less than half the input range at all times
-    assertTrue(Math.abs(m_controller.getSetpoint().position - kMeasurement) < 180.0);
-  }
-
-  @Test
-  void continuousInputTest2() {
-    m_controller.setP(1);
-    m_controller.enableContinuousInput(-Math.PI, Math.PI);
-
-    final double kSetpoint = -3.4826633343199735;
-    final double kMeasurement = -3.1352207333939606;
-    final double kGoal = -3.534162788601621;
-
-    m_controller.reset(kSetpoint);
-    assertTrue(m_controller.calculate(kMeasurement, kGoal) < 0.0);
-
-    // Error must be less than half the input range at all times
-    assertTrue(Math.abs(m_controller.getSetpoint().position - kMeasurement) < Math.PI);
-  }
-
-  @Test
-  void continuousInputTest3() {
-    m_controller.setP(1);
-    m_controller.enableContinuousInput(-Math.PI, Math.PI);
-
-    final double kSetpoint = -3.5176604690006377;
-    final double kMeasurement = 3.1191729343822456;
-    final double kGoal = 2.709680418117445;
-
-    m_controller.reset(kSetpoint);
-    assertTrue(m_controller.calculate(kMeasurement, kGoal) < 0.0);
-
-    // Error must be less than half the input range at all times
-    assertTrue(Math.abs(m_controller.getSetpoint().position - kMeasurement) < Math.PI);
-  }
-
-  @Test
-  void proportionalGainOutputTest() {
-    m_controller.setP(4);
-
-    assertEquals(-0.1, m_controller.calculate(0.025, 0), 1e-5);
-  }
-
-  @Test
-  void integralGainOutputTest() {
-    m_controller.setI(4);
-
-    double out = 0;
-
-    for (int i = 0; i < 5; i++) {
-      out = m_controller.calculate(0.025, 0);
-    }
-
-    assertEquals(-0.5 * m_controller.getPeriod(), out, 1e-5);
-  }
-
-  @Test
-  void derivativeGainOutputTest() {
-    m_controller.setD(4);
-
-    m_controller.calculate(0, 0);
-
-    assertEquals(-0.01 / m_controller.getPeriod(), m_controller.calculate(0.0025, 0), 1e-5);
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/RamseteControllerTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/RamseteControllerTest.java
deleted file mode 100644
index a2b6e22..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/controller/RamseteControllerTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.controller;
-
-import java.util.ArrayList;
-
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.geometry.Twist2d;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryConfig;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryGenerator;
-import edu.wpi.first.wpiutil.math.MathUtil;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class RamseteControllerTest {
-  private static final double kTolerance = 1 / 12.0;
-  private static final double kAngularTolerance = Math.toRadians(2);
-
-  @Test
-  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
-  void testReachesReference() {
-    final var controller = new RamseteController(2.0, 0.7);
-    var robotPose = new Pose2d(2.7, 23.0, Rotation2d.fromDegrees(0.0));
-
-    final var waypoints = new ArrayList<Pose2d>();
-    waypoints.add(new Pose2d(2.75, 22.521, new Rotation2d(0)));
-    waypoints.add(new Pose2d(24.73, 19.68, new Rotation2d(5.846)));
-    var config = new TrajectoryConfig(8.8, 0.1);
-    final var trajectory = TrajectoryGenerator.generateTrajectory(waypoints, config);
-
-    final double kDt = 0.02;
-    final var totalTime = trajectory.getTotalTimeSeconds();
-    for (int i = 0; i < (totalTime / kDt); ++i) {
-      var state = trajectory.sample(kDt * i);
-
-      var output = controller.calculate(robotPose, state);
-      robotPose = robotPose.exp(new Twist2d(output.vxMetersPerSecond * kDt, 0,
-          output.omegaRadiansPerSecond * kDt));
-    }
-
-    final var states = trajectory.getStates();
-    final var endPose = states.get(states.size() - 1).poseMeters;
-
-    // Java lambdas require local variables referenced from a lambda expression
-    // must be final or effectively final.
-    final var finalRobotPose = robotPose;
-    assertAll(
-        () -> assertEquals(endPose.getX(), finalRobotPose.getX(),
-            kTolerance),
-        () -> assertEquals(endPose.getY(), finalRobotPose.getY(),
-            kTolerance),
-        () -> assertEquals(0.0,
-            MathUtil.normalizeAngle(endPose.getRotation().getRadians()
-                - finalRobotPose.getRotation().getRadians()),
-            kAngularTolerance)
-    );
-  }
-}
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
new file mode 100644
index 0000000..e7f2e31
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DifferentialDriveTest.java
@@ -0,0 +1,240 @@
+// 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.drive;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.wpilibj.motorcontrol.MockMotorController;
+import org.junit.jupiter.api.Test;
+
+class DifferentialDriveTest {
+  @Test
+  void testArcadeDrive() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.arcadeDrive(1.0, 0.0, false);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.arcadeDrive(0.5, -0.5, false);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(0.5, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.arcadeDrive(0.5, 0.5, false);
+    assertEquals(0.5, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward
+    drive.arcadeDrive(-1.0, 0.0, false);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.arcadeDrive(-0.5, -0.5, false);
+    assertEquals(-0.5, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.arcadeDrive(-0.5, 0.5, false);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(-0.5, right.get(), 1e-9);
+  }
+
+  @Test
+  void testArcadeDriveSquared() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.arcadeDrive(1.0, 0.0, true);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.arcadeDrive(0.5, -0.5, true);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(0.25, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.arcadeDrive(0.5, 0.5, true);
+    assertEquals(0.25, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward
+    drive.arcadeDrive(-1.0, 0.0, true);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.arcadeDrive(-0.5, -0.5, true);
+    assertEquals(-0.25, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.arcadeDrive(-0.5, 0.5, true);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(-0.25, right.get(), 1e-9);
+  }
+
+  @Test
+  void testCurvatureDrive() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.curvatureDrive(1.0, 0.0, false);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.curvatureDrive(0.5, -0.5, false);
+    assertEquals(0.25, left.get(), 1e-9);
+    assertEquals(0.75, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.curvatureDrive(0.5, 0.5, false);
+    assertEquals(0.75, left.get(), 1e-9);
+    assertEquals(0.25, right.get(), 1e-9);
+
+    // Backward
+    drive.curvatureDrive(-1.0, 0.0, false);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.curvatureDrive(-0.5, -0.5, false);
+    assertEquals(-0.75, left.get(), 1e-9);
+    assertEquals(-0.25, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.curvatureDrive(-0.5, 0.5, false);
+    assertEquals(-0.25, left.get(), 1e-9);
+    assertEquals(-0.75, right.get(), 1e-9);
+  }
+
+  @Test
+  void testCurvatureDriveTurnInPlace() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.curvatureDrive(1.0, 0.0, true);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.curvatureDrive(0.5, -0.5, true);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.curvatureDrive(0.5, 0.5, true);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward
+    drive.curvatureDrive(-1.0, 0.0, true);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.curvatureDrive(-0.5, -0.5, true);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(0.0, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.curvatureDrive(-0.5, 0.5, true);
+    assertEquals(0.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+  }
+
+  @Test
+  void testTankDrive() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.tankDrive(1.0, 1.0, false);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.tankDrive(0.5, 1.0, false);
+    assertEquals(0.5, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.tankDrive(1.0, 0.5, false);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(0.5, right.get(), 1e-9);
+
+    // Backward
+    drive.tankDrive(-1.0, -1.0, false);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.tankDrive(-0.5, -1.0, false);
+    assertEquals(-0.5, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.tankDrive(-0.5, 1.0, false);
+    assertEquals(-0.5, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+  }
+
+  @Test
+  void testTankDriveSquared() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var drive = new DifferentialDrive(left, right);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.tankDrive(1.0, 1.0, true);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward left turn
+    drive.tankDrive(0.5, 1.0, true);
+    assertEquals(0.25, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+
+    // Forward right turn
+    drive.tankDrive(1.0, 0.5, true);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(0.25, right.get(), 1e-9);
+
+    // Backward
+    drive.tankDrive(-1.0, -1.0, true);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward left turn
+    drive.tankDrive(-0.5, -1.0, true);
+    assertEquals(-0.25, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+
+    // Backward right turn
+    drive.tankDrive(-1.0, -0.5, true);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-0.25, right.get(), 1e-9);
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DriveTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DriveTest.java
deleted file mode 100644
index 14e9401..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/DriveTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.drive;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.MockSpeedController;
-import edu.wpi.first.wpilibj.RobotDrive;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * Tests DifferentialDrive and MecanumDrive.
- */
-public class DriveTest {
-  private final MockSpeedController m_rdFrontLeft = new MockSpeedController();
-  private final MockSpeedController m_rdRearLeft = new MockSpeedController();
-  private final MockSpeedController m_rdFrontRight = new MockSpeedController();
-  private final MockSpeedController m_rdRearRight = new MockSpeedController();
-  private final MockSpeedController m_frontLeft = new MockSpeedController();
-  private final MockSpeedController m_rearLeft = new MockSpeedController();
-  private final MockSpeedController m_frontRight = new MockSpeedController();
-  private final MockSpeedController m_rearRight = new MockSpeedController();
-  private final RobotDrive m_robotDrive =
-      new RobotDrive(m_rdFrontLeft, m_rdRearLeft, m_rdFrontRight, m_rdRearRight);
-  private final DifferentialDrive m_differentialDrive =
-      new DifferentialDrive(m_frontLeft, m_frontRight);
-  private final MecanumDrive m_mecanumDrive =
-      new MecanumDrive(m_frontLeft, m_rearLeft, m_frontRight, m_rearRight);
-
-  private final double[] m_testJoystickValues = {1.0, 0.9, 0.5, 0.01, 0.0, -0.01, -0.5, -0.9,
-                                                 -1.0};
-  private final double[] m_testGyroValues = {0, 30, 45, 90, 135, 180, 225, 270, 305, 360, 540,
-                                             -45, -90, -135, -180, -225, -270, -305, -360, -540};
-
-  @BeforeEach
-  void setUp() {
-    m_differentialDrive.setDeadband(0.0);
-    m_differentialDrive.setSafetyEnabled(false);
-    m_mecanumDrive.setDeadband(0.0);
-    m_mecanumDrive.setSafetyEnabled(false);
-    m_robotDrive.setSafetyEnabled(false);
-  }
-
-  @Test
-  public void testTankDriveSquared() {
-    for (double leftJoystick : m_testJoystickValues) {
-      for (double rightJoystick : m_testJoystickValues) {
-        m_robotDrive.tankDrive(leftJoystick, rightJoystick);
-        m_differentialDrive.tankDrive(leftJoystick, rightJoystick);
-        assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-            "Left Motor squared didn't match. Left Joystick: " + leftJoystick + " Right Joystick: "
-            + rightJoystick);
-        assertEquals(m_rdFrontRight.get(), m_frontRight.get(), 0.01,
-            "Right Motor squared didn't match. Left Joystick: " + leftJoystick + " Right Joystick: "
-            + rightJoystick);
-      }
-    }
-  }
-
-  @Test
-  void testTankDrive() {
-    for (double leftJoystick : m_testJoystickValues) {
-      for (double rightJoystick : m_testJoystickValues) {
-        m_robotDrive.tankDrive(leftJoystick, rightJoystick, false);
-        m_differentialDrive.tankDrive(leftJoystick, rightJoystick, false);
-        assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-            "Left Motor didn't match. Left Joystick: " + leftJoystick + " Right Joystick: "
-            + rightJoystick);
-        assertEquals(m_rdFrontRight.get(), m_frontRight.get(), 0.01,
-            "Right Motor didn't match. Left Joystick: " + leftJoystick + " Right Joystick: "
-            + rightJoystick);
-      }
-    }
-  }
-
-  @Test
-  void testArcadeDriveSquared() {
-    for (double moveJoystick : m_testJoystickValues) {
-      for (double rotateJoystick : m_testJoystickValues) {
-        m_robotDrive.arcadeDrive(moveJoystick, rotateJoystick);
-        m_differentialDrive.arcadeDrive(moveJoystick, -rotateJoystick);
-        assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-            "Left Motor squared didn't match. Move Joystick: " + moveJoystick + " Rotate Joystick: "
-            + rotateJoystick);
-        assertEquals(m_rdFrontRight.get(), m_frontRight.get(), 0.01,
-            "Right Motor squared didn't match. Move Joystick: " + moveJoystick
-            + " Rotate Joystick: " + rotateJoystick);
-      }
-    }
-  }
-
-  @Test
-  void testArcadeDrive() {
-    for (double moveJoystick : m_testJoystickValues) {
-      for (double rotateJoystick : m_testJoystickValues) {
-        m_robotDrive.arcadeDrive(moveJoystick, rotateJoystick, false);
-        m_differentialDrive.arcadeDrive(moveJoystick, -rotateJoystick, false);
-        assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-            "Left Motor didn't match. Move Joystick: " + moveJoystick + " Rotate Joystick: "
-            + rotateJoystick);
-        assertEquals(m_rdFrontRight.get(), m_frontRight.get(), 0.01,
-            "Right Motor didn't match. Move Joystick: " + moveJoystick + " Rotate Joystick: "
-            + rotateJoystick);
-      }
-    }
-  }
-
-  @Test
-  void testMecanumPolar() {
-    for (double magnitudeJoystick : m_testJoystickValues) {
-      for (double directionJoystick : m_testGyroValues) {
-        for (double rotationJoystick : m_testJoystickValues) {
-          m_robotDrive.mecanumDrive_Polar(magnitudeJoystick, directionJoystick, rotationJoystick);
-          m_mecanumDrive.drivePolar(magnitudeJoystick, directionJoystick, rotationJoystick);
-          assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-              "Left Front Motor didn't match. Magnitude Joystick: " + magnitudeJoystick
-              + " Direction Joystick: " + directionJoystick + " RotationJoystick: "
-              + rotationJoystick);
-          assertEquals(m_rdFrontRight.get(), -m_frontRight.get(), 0.01,
-              "Right Front Motor didn't match. Magnitude Joystick: " + magnitudeJoystick
-              + " Direction Joystick: " + directionJoystick + " RotationJoystick: "
-              + rotationJoystick);
-          assertEquals(m_rdRearLeft.get(), m_rearLeft.get(), 0.01,
-              "Left Rear Motor didn't match. Magnitude Joystick: " + magnitudeJoystick
-              + " Direction Joystick: " + directionJoystick + " RotationJoystick: "
-              + rotationJoystick);
-          assertEquals(m_rdRearRight.get(), -m_rearRight.get(), 0.01,
-              "Right Rear Motor didn't match. Magnitude Joystick: " + magnitudeJoystick
-              + " Direction Joystick: " + directionJoystick + " RotationJoystick: "
-              + rotationJoystick);
-        }
-      }
-    }
-  }
-
-  @Test
-  @SuppressWarnings("checkstyle:LocalVariableName")
-  void testMecanumCartesian() {
-    for (double x_Joystick : m_testJoystickValues) {
-      for (double y_Joystick : m_testJoystickValues) {
-        for (double rotationJoystick : m_testJoystickValues) {
-          for (double gyroValue : m_testGyroValues) {
-            m_robotDrive.mecanumDrive_Cartesian(x_Joystick, y_Joystick, rotationJoystick,
-                                                gyroValue);
-            m_mecanumDrive.driveCartesian(x_Joystick, -y_Joystick, rotationJoystick, -gyroValue);
-            assertEquals(m_rdFrontLeft.get(), m_frontLeft.get(), 0.01,
-                "Left Front Motor didn't match. X Joystick: " + x_Joystick + " Y Joystick: "
-                + y_Joystick + " RotationJoystick: " + rotationJoystick + " Gyro: " + gyroValue);
-            assertEquals(m_rdFrontRight.get(), -m_frontRight.get(), 0.01,
-                "Right Front Motor didn't match. X Joystick: " + x_Joystick + " Y Joystick: "
-                + y_Joystick + " RotationJoystick: " + rotationJoystick + " Gyro: " + gyroValue);
-            assertEquals(m_rdRearLeft.get(), m_rearLeft.get(), 0.01,
-                "Left Rear Motor didn't match. X Joystick: " + x_Joystick + " Y Joystick: "
-                + y_Joystick + " RotationJoystick: " + rotationJoystick + " Gyro: " + gyroValue);
-            assertEquals(m_rdRearRight.get(), -m_rearRight.get(), 0.01,
-                "Right Rear Motor didn't match. X Joystick: " + x_Joystick + " Y Joystick: "
-                + y_Joystick + " RotationJoystick: " + rotationJoystick + " Gyro: " + gyroValue);
-          }
-        }
-      }
-    }
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/KilloughDriveTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/KilloughDriveTest.java
new file mode 100644
index 0000000..7d06d9f
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/KilloughDriveTest.java
@@ -0,0 +1,129 @@
+// 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.drive;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.wpilibj.motorcontrol.MockMotorController;
+import org.junit.jupiter.api.Test;
+
+class KilloughDriveTest {
+  @Test
+  void testCartesian() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var back = new MockMotorController();
+    var drive = new KilloughDrive(left, right, back);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.driveCartesian(1.0, 0.0, 0.0);
+    assertEquals(0.5, left.get(), 1e-9);
+    assertEquals(-0.5, right.get(), 1e-9);
+    assertEquals(0.0, back.get(), 1e-9);
+
+    // Left
+    drive.driveCartesian(0.0, -1.0, 0.0);
+    assertEquals(-Math.sqrt(3) / 2, left.get(), 1e-9);
+    assertEquals(-Math.sqrt(3) / 2, right.get(), 1e-9);
+    assertEquals(1.0, back.get(), 1e-9);
+
+    // Right
+    drive.driveCartesian(0.0, 1.0, 0.0);
+    assertEquals(Math.sqrt(3) / 2, left.get(), 1e-9);
+    assertEquals(Math.sqrt(3) / 2, right.get(), 1e-9);
+    assertEquals(-1.0, back.get(), 1e-9);
+
+    // Rotate CCW
+    drive.driveCartesian(0.0, 0.0, -1.0);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+    assertEquals(-1.0, back.get(), 1e-9);
+
+    // Rotate CW
+    drive.driveCartesian(0.0, 0.0, 1.0);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+    assertEquals(1.0, back.get(), 1e-9);
+  }
+
+  @Test
+  void testCartesiangyro90CW() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var back = new MockMotorController();
+    var drive = new KilloughDrive(left, right, back);
+    drive.setDeadband(0.0);
+
+    // Forward in global frame; left in robot frame
+    drive.driveCartesian(1.0, 0.0, 0.0, 90.0);
+    assertEquals(-Math.sqrt(3) / 2, left.get(), 1e-9);
+    assertEquals(-Math.sqrt(3) / 2, right.get(), 1e-9);
+    assertEquals(1.0, back.get(), 1e-9);
+
+    // Left in global frame; backward in robot frame
+    drive.driveCartesian(0.0, -1.0, 0.0, 90.0);
+    assertEquals(-0.5, left.get(), 1e-9);
+    assertEquals(0.5, right.get(), 1e-9);
+    assertEquals(0.0, back.get(), 1e-9);
+
+    // Right in global frame; forward in robot frame
+    drive.driveCartesian(0.0, 1.0, 0.0, 90.0);
+    assertEquals(0.5, left.get(), 1e-9);
+    assertEquals(-0.5, right.get(), 1e-9);
+    assertEquals(0.0, back.get(), 1e-9);
+
+    // Rotate CCW
+    drive.driveCartesian(0.0, 0.0, -1.0, 90.0);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+    assertEquals(-1.0, back.get(), 1e-9);
+
+    // Rotate CW
+    drive.driveCartesian(0.0, 0.0, 1.0, 90.0);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+    assertEquals(1.0, back.get(), 1e-9);
+  }
+
+  @Test
+  void testPolar() {
+    var left = new MockMotorController();
+    var right = new MockMotorController();
+    var back = new MockMotorController();
+    var drive = new KilloughDrive(left, right, back);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.drivePolar(1.0, 0.0, 0.0);
+    assertEquals(Math.sqrt(3) / 2, left.get(), 1e-9);
+    assertEquals(Math.sqrt(3) / 2, right.get(), 1e-9);
+    assertEquals(-1.0, back.get(), 1e-9);
+
+    // Left
+    drive.drivePolar(1.0, -90.0, 0.0);
+    assertEquals(-0.5, left.get(), 1e-9);
+    assertEquals(0.5, right.get(), 1e-9);
+    assertEquals(0.0, back.get(), 1e-9);
+
+    // Right
+    drive.drivePolar(1.0, 90.0, 0.0);
+    assertEquals(0.5, left.get(), 1e-9);
+    assertEquals(-0.5, right.get(), 1e-9);
+    assertEquals(0.0, back.get(), 1e-9);
+
+    // Rotate CCW
+    drive.drivePolar(0.0, 0.0, -1.0);
+    assertEquals(-1.0, left.get(), 1e-9);
+    assertEquals(-1.0, right.get(), 1e-9);
+    assertEquals(-1.0, back.get(), 1e-9);
+
+    // Rotate CW
+    drive.drivePolar(0.0, 0.0, 1.0);
+    assertEquals(1.0, left.get(), 1e-9);
+    assertEquals(1.0, right.get(), 1e-9);
+    assertEquals(1.0, back.get(), 1e-9);
+  }
+}
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
new file mode 100644
index 0000000..70a07a2
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/drive/MecanumDriveTest.java
@@ -0,0 +1,147 @@
+// 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.drive;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.wpilibj.motorcontrol.MockMotorController;
+import org.junit.jupiter.api.Test;
+
+class MecanumDriveTest {
+  @Test
+  void testCartesian() {
+    var fl = new MockMotorController();
+    var fr = new MockMotorController();
+    var rl = new MockMotorController();
+    var rr = new MockMotorController();
+    var drive = new MecanumDrive(fl, fr, rl, rr);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.driveCartesian(1.0, 0.0, 0.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Left
+    drive.driveCartesian(0.0, -1.0, 0.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+
+    // Right
+    drive.driveCartesian(0.0, 1.0, 0.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CCW
+    drive.driveCartesian(0.0, 0.0, -1.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CW
+    drive.driveCartesian(0.0, 0.0, 1.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+  }
+
+  @Test
+  void testCartesiangyro90CW() {
+    var fl = new MockMotorController();
+    var fr = new MockMotorController();
+    var rl = new MockMotorController();
+    var rr = new MockMotorController();
+    var drive = new MecanumDrive(fl, fr, rl, rr);
+    drive.setDeadband(0.0);
+
+    // Forward in global frame; left in robot frame
+    drive.driveCartesian(1.0, 0.0, 0.0, 90.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+
+    // Left in global frame; backward in robot frame
+    drive.driveCartesian(0.0, -1.0, 0.0, 90.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+
+    // Right in global frame; forward in robot frame
+    drive.driveCartesian(0.0, 1.0, 0.0, 90.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CCW
+    drive.driveCartesian(0.0, 0.0, -1.0, 90.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CW
+    drive.driveCartesian(0.0, 0.0, 1.0, 90.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+  }
+
+  @Test
+  void testPolar() {
+    var fl = new MockMotorController();
+    var fr = new MockMotorController();
+    var rl = new MockMotorController();
+    var rr = new MockMotorController();
+    var drive = new MecanumDrive(fl, fr, rl, rr);
+    drive.setDeadband(0.0);
+
+    // Forward
+    drive.drivePolar(1.0, 0.0, 0.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Left
+    drive.drivePolar(1.0, -90.0, 0.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+
+    // Right
+    drive.drivePolar(1.0, 90.0, 0.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CCW
+    drive.drivePolar(0.0, 0.0, -1.0);
+    assertEquals(-1.0, fl.get(), 1e-9);
+    assertEquals(-1.0, fr.get(), 1e-9);
+    assertEquals(1.0, rl.get(), 1e-9);
+    assertEquals(1.0, rr.get(), 1e-9);
+
+    // Rotate CW
+    drive.drivePolar(0.0, 0.0, 1.0);
+    assertEquals(1.0, fl.get(), 1e-9);
+    assertEquals(1.0, fr.get(), 1e-9);
+    assertEquals(-1.0, rl.get(), 1e-9);
+    assertEquals(-1.0, rr.get(), 1e-9);
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/JNITest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/JNITest.java
index f70f3f8..083e82c 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/JNITest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/JNITest.java
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.hal;
 
-import org.junit.jupiter.api.Test;
-
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.HALUtil;
 import edu.wpi.first.networktables.NetworkTablesJNI;
+import org.junit.jupiter.api.Test;
 
 class JNITest {
   @Test
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/MatchInfoDataTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/MatchInfoDataTest.java
index 3887d91..855ace5 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/MatchInfoDataTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/hal/MatchInfoDataTest.java
@@ -1,26 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.hal;
 
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.hal.MatchInfoData;
 import edu.wpi.first.hal.simulation.DriverStationDataJNI;
 import edu.wpi.first.wpilibj.DriverStation.MatchType;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
 
 class MatchInfoDataTest {
   @Test
   void testSetMatchInfo() {
-
     MatchType matchType = MatchType.Qualification;
     DriverStationDataJNI.setMatchInfo("Event Name", "Game Message", 174, 191, matchType.ordinal());
 
@@ -32,7 +27,6 @@
         () -> assertEquals(matchType.ordinal(), outMatchInfo.matchType),
         () -> assertEquals(174, outMatchInfo.matchNumber),
         () -> assertEquals(191, outMatchInfo.replayNumber),
-        () -> assertEquals("Game Message", outMatchInfo.gameSpecificMessage)
-    );
+        () -> assertEquals("Game Message", outMatchInfo.gameSpecificMessage));
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/livewindow/LiveWindowTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/livewindow/LiveWindowTest.java
index dd33945..6d4a666 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/livewindow/LiveWindowTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/livewindow/LiveWindowTest.java
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.livewindow;
 
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
new file mode 100644
index 0000000..e7a6b8e
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MockMotorController.java
@@ -0,0 +1,40 @@
+// 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 MockMotorController implements MotorController {
+  private double m_speed;
+  private boolean m_isInverted;
+
+  @Override
+  public void set(double speed) {
+    m_speed = m_isInverted ? -speed : speed;
+  }
+
+  @Override
+  public double get() {
+    return m_speed;
+  }
+
+  @Override
+  public void setInverted(boolean isInverted) {
+    m_isInverted = isInverted;
+  }
+
+  @Override
+  public boolean getInverted() {
+    return m_isInverted;
+  }
+
+  @Override
+  public void disable() {
+    m_speed = 0;
+  }
+
+  @Override
+  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
new file mode 100644
index 0000000..6efb3de
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/motorcontrol/MotorControllerGroupTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.motorcontrol;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class MotorControllerGroupTest {
+  private static Stream<Arguments> motorControllerArguments() {
+    return IntStream.of(1, 2, 3)
+        .mapToObj(
+            number -> {
+              MotorController[] motorControllers =
+                  Stream.generate(MockMotorController::new)
+                      .limit(number)
+                      .toArray(MotorController[]::new);
+              MotorControllerGroup group =
+                  new MotorControllerGroup(
+                      motorControllers[0],
+                      Arrays.copyOfRange(motorControllers, 1, motorControllers.length));
+              return Arguments.of(group, motorControllers);
+            });
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void setTest(final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.set(1.0);
+
+    assertArrayEquals(
+        DoubleStream.generate(() -> 1.0).limit(motorControllers.length).toArray(),
+        Arrays.stream(motorControllers).mapToDouble(MotorController::get).toArray(),
+        0.00005);
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void getInvertedTest(final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.setInverted(true);
+
+    assertTrue(group.getInverted());
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void setInvertedDoesNotModifyMotorControllersTest(
+      final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.setInverted(true);
+
+    assertArrayEquals(
+        Stream.generate(() -> false).limit(motorControllers.length).toArray(),
+        Arrays.stream(motorControllers).map(MotorController::getInverted).toArray());
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void setInvertedDoesInvertTest(
+      final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.setInverted(true);
+    group.set(1.0);
+
+    assertArrayEquals(
+        DoubleStream.generate(() -> -1.0).limit(motorControllers.length).toArray(),
+        Arrays.stream(motorControllers).mapToDouble(MotorController::get).toArray(),
+        0.00005);
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void disableTest(final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.set(1.0);
+    group.disable();
+
+    assertArrayEquals(
+        DoubleStream.generate(() -> 0.0).limit(motorControllers.length).toArray(),
+        Arrays.stream(motorControllers).mapToDouble(MotorController::get).toArray(),
+        0.00005);
+  }
+
+  @ParameterizedTest
+  @MethodSource("motorControllerArguments")
+  void stopMotorTest(final MotorControllerGroup group, final MotorController[] motorControllers) {
+    group.set(1.0);
+    group.stopMotor();
+
+    assertArrayEquals(
+        DoubleStream.generate(() -> 0.0).limit(motorControllers.length).toArray(),
+        Arrays.stream(motorControllers).mapToDouble(MotorController::get).toArray(),
+        0.00005);
+  }
+}
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 89ca3ec..04397c2 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
@@ -1,19 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import edu.wpi.first.wpilibj.Sendable;
-import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder;
-import edu.wpi.first.wpilibj.smartdashboard.SendableRegistry;
+import edu.wpi.first.util.sendable.Sendable;
+import edu.wpi.first.util.sendable.SendableBuilder;
+import edu.wpi.first.util.sendable.SendableRegistry;
 
-/**
- * A mock sendable that marks itself as an actuator.
- */
+/** A mock sendable that marks itself as an actuator. */
 public class MockActuatorSendable implements Sendable {
   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 9824f11..61db14a 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
@@ -1,24 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.networktables.NetworkTableInstance;
-
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.NetworkTableInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
 public class ShuffleboardInstanceTest {
   private NetworkTableInstance m_ntInstance;
   private ShuffleboardInstance m_shuffleboardInstance;
@@ -36,32 +32,42 @@
 
   @Test
   void testPathFluent() {
-    NetworkTableEntry entry = m_shuffleboardInstance.getTab("Tab Title")
-                                                  .getLayout("Layout Title", "List Layout")
-                                                  .add("Data", "string")
-                                                  .withWidget("Text View")
-                                                  .getEntry();
+    NetworkTableEntry entry =
+        m_shuffleboardInstance
+            .getTab("Tab Title")
+            .getLayout("Layout Title", "List Layout")
+            .add("Data", "string")
+            .withWidget("Text View")
+            .getEntry();
 
     assertAll(
         () -> assertEquals("string", entry.getString(null), "Wrong entry value"),
-        () -> assertEquals("/Shuffleboard/Tab Title/Layout Title/Data", entry.getName(),
-                           "Entry path generated incorrectly"));
+        () ->
+            assertEquals(
+                "/Shuffleboard/Tab Title/Layout Title/Data",
+                entry.getName(),
+                "Entry path generated incorrectly"));
   }
 
   @Test
   void testNestedLayoutsFluent() {
-    NetworkTableEntry entry = m_shuffleboardInstance.getTab("Tab")
-                                                  .getLayout("First", "List")
-                                                  .getLayout("Second", "List")
-                                                  .getLayout("Third", "List")
-                                                  .getLayout("Fourth", "List")
-                                                  .add("Value", "string")
-                                                  .getEntry();
+    NetworkTableEntry entry =
+        m_shuffleboardInstance
+            .getTab("Tab")
+            .getLayout("First", "List")
+            .getLayout("Second", "List")
+            .getLayout("Third", "List")
+            .getLayout("Fourth", "List")
+            .add("Value", "string")
+            .getEntry();
 
     assertAll(
         () -> assertEquals("string", entry.getString(null), "Wrong entry value"),
-        () -> assertEquals("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", entry.getName(),
-                           "Entry path generated incorrectly"));
+        () ->
+            assertEquals(
+                "/Shuffleboard/Tab/First/Second/Third/Fourth/Value",
+                entry.getName(),
+                "Entry path generated incorrectly"));
   }
 
   @Test
@@ -76,26 +82,29 @@
 
     assertAll(
         () -> assertEquals("string", entry.getString(null), "Wrong entry value"),
-        () -> assertEquals("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", entry.getName(),
-                           "Entry path generated incorrectly"));
+        () ->
+            assertEquals(
+                "/Shuffleboard/Tab/First/Second/Third/Fourth/Value",
+                entry.getName(),
+                "Entry path generated incorrectly"));
   }
 
   @Test
   void testLayoutTypeIsSet() {
     String layoutType = "Type";
-    m_shuffleboardInstance.getTab("Tab")
-                          .getLayout("Title", layoutType);
+    m_shuffleboardInstance.getTab("Tab").getLayout("Title", layoutType);
     m_shuffleboardInstance.update();
-    NetworkTableEntry entry = m_ntInstance.getEntry(
-        "/Shuffleboard/.metadata/Tab/Title/PreferredComponent");
+    NetworkTableEntry entry =
+        m_ntInstance.getEntry("/Shuffleboard/.metadata/Tab/Title/PreferredComponent");
     assertEquals(layoutType, entry.getString("Not Set"), "Layout type not set");
   }
 
   @Test
   void testNestedActuatorWidgetsAreDisabled() {
-    m_shuffleboardInstance.getTab("Tab")
-                          .getLayout("Title", "Layout")
-                          .add(new MockActuatorSendable("Actuator"));
+    m_shuffleboardInstance
+        .getTab("Tab")
+        .getLayout("Title", "Layout")
+        .add(new MockActuatorSendable("Actuator"));
     NetworkTableEntry controllableEntry =
         m_ntInstance.getEntry("/Shuffleboard/Tab/Title/Actuator/.controllable");
 
@@ -111,5 +120,4 @@
     controllable = controllableEntry.getValue().getBoolean();
     assertFalse(controllable, "The nested actuator widget should have been disabled");
   }
-
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java
index 1469170..963d262 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTest.java
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertSame;
 
 import edu.wpi.first.wpilibj.UtilityClassTest;
-
-import static org.junit.jupiter.api.Assertions.assertSame;
+import org.junit.jupiter.api.Test;
 
 public class ShuffleboardTest extends UtilityClassTest<Shuffleboard> {
   public ShuffleboardTest() {
@@ -26,5 +22,4 @@
     ShuffleboardTab tab2 = Shuffleboard.getTab("testTabObjectsCached");
     assertSame(tab1, tab2, "Tab objects were not cached");
   }
-
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidgetTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidgetTest.java
index 6e0da8e..036c73d 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidgetTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidgetTest.java
@@ -1,25 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.shuffleboard;
 
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.networktables.NetworkTableEntry;
-import edu.wpi.first.networktables.NetworkTableInstance;
-
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.NetworkTableInstance;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
 class SuppliedValueWidgetTest {
   private NetworkTableInstance m_ntInstance;
   private ShuffleboardInstance m_instance;
@@ -33,8 +28,7 @@
   @Test
   void testAddString() {
     AtomicInteger count = new AtomicInteger(0);
-    m_instance.getTab("Tab")
-        .addString("Title", () -> Integer.toString(count.incrementAndGet()));
+    m_instance.getTab("Tab").addString("Title", () -> Integer.toString(count.incrementAndGet()));
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -47,8 +41,7 @@
   @Test
   void testAddDouble() {
     AtomicInteger num = new AtomicInteger(0);
-    m_instance.getTab("Tab")
-        .addNumber("Title", num::incrementAndGet);
+    m_instance.getTab("Tab").addNumber("Title", num::incrementAndGet);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -61,8 +54,7 @@
   @Test
   void testAddBoolean() {
     boolean[] bool = {false};
-    m_instance.getTab("Tab")
-        .addBoolean("Title", () -> bool[0] = !bool[0]);
+    m_instance.getTab("Tab").addBoolean("Title", () -> bool[0] = !bool[0]);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -75,8 +67,7 @@
   @Test
   void testAddStringArray() {
     String[] arr = {"foo", "bar"};
-    m_instance.getTab("Tab")
-        .addStringArray("Title", () -> arr);
+    m_instance.getTab("Tab").addStringArray("Title", () -> arr);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -86,8 +77,7 @@
   @Test
   void testAddDoubleArray() {
     double[] arr = {0, 1};
-    m_instance.getTab("Tab")
-        .addDoubleArray("Title", () -> arr);
+    m_instance.getTab("Tab").addDoubleArray("Title", () -> arr);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -97,8 +87,7 @@
   @Test
   void testAddBooleanArray() {
     boolean[] arr = {true, false};
-    m_instance.getTab("Tab")
-        .addBooleanArray("Title", () -> arr);
+    m_instance.getTab("Tab").addBooleanArray("Title", () -> arr);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
@@ -108,12 +97,10 @@
   @Test
   void testAddRawBytes() {
     byte[] arr = {0, 1, 2, 3};
-    m_instance.getTab("Tab")
-        .addRaw("Title", () -> arr);
+    m_instance.getTab("Tab").addRaw("Title", () -> arr);
     NetworkTableEntry entry = m_ntInstance.getEntry("/Shuffleboard/Tab/Title");
 
     m_instance.update();
     assertArrayEquals(arr, entry.getRaw(new byte[0]));
   }
-
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL345SimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL345SimTest.java
new file mode 100644
index 0000000..7f41038
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL345SimTest.java
@@ -0,0 +1,64 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.ADXL345_I2C;
+import edu.wpi.first.wpilibj.ADXL345_SPI;
+import edu.wpi.first.wpilibj.I2C;
+import edu.wpi.first.wpilibj.SPI;
+import edu.wpi.first.wpilibj.interfaces.Accelerometer;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class ADXL345SimTest {
+  @ParameterizedTest
+  @EnumSource(Accelerometer.Range.class)
+  void testInitI2C(Accelerometer.Range range) {
+    HAL.initialize(500, 0);
+
+    try (ADXL345_I2C accel = new ADXL345_I2C(I2C.Port.kMXP, range)) {
+      ADXL345Sim sim = new ADXL345Sim(accel);
+
+      sim.setX(1.91);
+      sim.setY(-3.405);
+      sim.setZ(2.29);
+
+      assertEquals(1.91, accel.getX());
+      assertEquals(-3.405, accel.getY());
+      assertEquals(2.29, accel.getZ());
+
+      ADXL345_I2C.AllAxes allAccel = accel.getAccelerations();
+      assertEquals(1.91, allAccel.XAxis);
+      assertEquals(-3.405, allAccel.YAxis);
+      assertEquals(2.29, allAccel.ZAxis);
+    }
+  }
+
+  @ParameterizedTest
+  @EnumSource(Accelerometer.Range.class)
+  void testInitSPi(Accelerometer.Range range) {
+    HAL.initialize(500, 0);
+
+    try (ADXL345_SPI accel = new ADXL345_SPI(SPI.Port.kMXP, range)) {
+      ADXL345Sim sim = new ADXL345Sim(accel);
+
+      sim.setX(1.91);
+      sim.setY(-3.405);
+      sim.setZ(2.29);
+
+      assertEquals(1.91, accel.getX());
+      assertEquals(-3.405, accel.getY());
+      assertEquals(2.29, accel.getZ());
+
+      ADXL345_SPI.AllAxes allAccel = accel.getAccelerations();
+      assertEquals(1.91, allAccel.XAxis);
+      assertEquals(-3.405, allAccel.YAxis);
+      assertEquals(2.29, allAccel.ZAxis);
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL362SimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL362SimTest.java
new file mode 100644
index 0000000..f361381
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXL362SimTest.java
@@ -0,0 +1,42 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.ADXL362;
+import edu.wpi.first.wpilibj.SPI;
+import edu.wpi.first.wpilibj.interfaces.Accelerometer;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class ADXL362SimTest {
+  @ParameterizedTest
+  @EnumSource(Accelerometer.Range.class)
+  void testAccel(Accelerometer.Range range) {
+    HAL.initialize(500, 0);
+
+    try (ADXL362 accel = new ADXL362(SPI.Port.kMXP, range)) {
+      assertEquals(0, accel.getX());
+      assertEquals(0, accel.getY());
+      assertEquals(0, accel.getZ());
+
+      ADXL362Sim sim = new ADXL362Sim(accel);
+      sim.setX(1.91);
+      sim.setY(-3.405);
+      sim.setZ(2.29);
+
+      assertEquals(1.91, accel.getX());
+      assertEquals(-3.405, accel.getY());
+      assertEquals(2.29, accel.getZ());
+
+      ADXL362.AllAxes allAccel = accel.getAccelerations();
+      assertEquals(1.91, allAccel.XAxis);
+      assertEquals(-3.405, allAccel.YAxis);
+      assertEquals(2.29, allAccel.ZAxis);
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSimTest.java
new file mode 100644
index 0000000..301cef1
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ADXRS450_GyroSimTest.java
@@ -0,0 +1,35 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.ADXRS450_Gyro;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings({"TypeName"})
+class ADXRS450_GyroSimTest {
+  @Test
+  void testCallbacks() {
+    HAL.initialize(500, 0);
+
+    try (ADXRS450_Gyro gyro = new ADXRS450_Gyro()) {
+      ADXRS450_GyroSim sim = new ADXRS450_GyroSim(gyro);
+
+      assertEquals(0, gyro.getAngle());
+      assertEquals(0, gyro.getRate());
+
+      sim.setAngle(123.456);
+      sim.setRate(229.3504);
+
+      assertEquals(123.456, gyro.getAngle());
+      assertEquals(229.3504, gyro.getRate());
+
+      gyro.reset();
+      assertEquals(0, gyro.getAngle());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AccelerometerSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AccelerometerSimTest.java
index 05ea290..998b66a 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AccelerometerSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AccelerometerSimTest.java
@@ -1,42 +1,142 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.hal.AccelerometerJNI;
-import edu.wpi.first.hal.HAL;
-
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-class AccelerometerSimTest {
-  static class TriggeredStore {
-    public boolean m_wasTriggered;
-    public boolean m_setValue = true;
-  }
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.BuiltInAccelerometer;
+import edu.wpi.first.wpilibj.interfaces.Accelerometer;
+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 org.junit.jupiter.api.Test;
 
+class AccelerometerSimTest {
   @Test
   void testCallbacks() {
     HAL.initialize(500, 0);
     BuiltInAccelerometerSim sim = new BuiltInAccelerometerSim();
     sim.resetData();
 
-    TriggeredStore store = new TriggeredStore();
+    BooleanCallback store = new BooleanCallback();
 
-    try (CallbackStore cb = sim.registerActiveCallback((s, v) -> {
-      store.m_wasTriggered = true;
-      store.m_setValue = v.getBoolean();
-    }, false)) {
-      assertFalse(store.m_wasTriggered);
-      AccelerometerJNI.setAccelerometerActive(true);
-      assertTrue(store.m_wasTriggered);
-      assertTrue(store.m_setValue);
+    try (CallbackStore cb = sim.registerActiveCallback(store, false)) {
+      assertFalse(store.wasTriggered());
+      sim.setActive(true);
+      assertTrue(sim.getActive());
+      assertTrue(store.wasTriggered());
+      assertTrue(store.getSetValue());
+    }
+  }
+
+  @Test
+  void testX() {
+    HAL.initialize(500, 0);
+    BuiltInAccelerometerSim sim = new BuiltInAccelerometerSim();
+    sim.resetData();
+
+    DoubleCallback callback = new DoubleCallback();
+    final double kTestValue = 1.91;
+
+    try (BuiltInAccelerometer accel = new BuiltInAccelerometer();
+        CallbackStore cb = sim.registerXCallback(callback, false)) {
+      sim.setX(kTestValue);
+      assertEquals(kTestValue, accel.getX());
+      assertEquals(kTestValue, sim.getX());
+      assertTrue(callback.wasTriggered());
+      assertEquals(kTestValue, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testY() {
+    HAL.initialize(500, 0);
+    BuiltInAccelerometerSim sim = new BuiltInAccelerometerSim();
+    sim.resetData();
+
+    DoubleCallback callback = new DoubleCallback();
+    final double kTestValue = 2.29;
+
+    try (BuiltInAccelerometer accel = new BuiltInAccelerometer();
+        CallbackStore cb = sim.registerYCallback(callback, false)) {
+      sim.setY(kTestValue);
+      assertEquals(kTestValue, accel.getY());
+      assertEquals(kTestValue, sim.getY());
+      assertTrue(callback.wasTriggered());
+      assertEquals(kTestValue, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testZ() {
+    HAL.initialize(500, 0);
+
+    BuiltInAccelerometerSim sim = new BuiltInAccelerometerSim();
+    sim.resetData();
+
+    DoubleCallback callback = new DoubleCallback();
+    final double kTestValue = 3.405;
+
+    try (BuiltInAccelerometer accel = new BuiltInAccelerometer();
+        CallbackStore cb = sim.registerZCallback(callback, false)) {
+      sim.setZ(kTestValue);
+      assertEquals(kTestValue, accel.getZ());
+      assertEquals(kTestValue, sim.getZ());
+      assertTrue(callback.wasTriggered());
+      assertEquals(kTestValue, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testRange() {
+    HAL.initialize(500, 0);
+
+    BuiltInAccelerometerSim sim = new BuiltInAccelerometerSim();
+    sim.resetData();
+
+    EnumCallback callback = new EnumCallback();
+
+    Accelerometer.Range range = Accelerometer.Range.k4G;
+    try (CallbackStore cb = sim.registerRangeCallback(callback, false);
+        BuiltInAccelerometer accel = new BuiltInAccelerometer(range)) {
+      assertTrue(callback.wasTriggered());
+      assertEquals(range.ordinal(), sim.getRange());
+      assertEquals(range.ordinal(), callback.getSetValue());
+
+      // 2G
+      callback.reset();
+      range = Accelerometer.Range.k2G;
+      accel.setRange(range);
+      assertTrue(callback.wasTriggered());
+      assertEquals(range.ordinal(), sim.getRange());
+      assertEquals(range.ordinal(), callback.getSetValue());
+
+      // 4G
+      callback.reset();
+      range = Accelerometer.Range.k4G;
+      accel.setRange(range);
+      assertTrue(callback.wasTriggered());
+      assertEquals(range.ordinal(), sim.getRange());
+      assertEquals(range.ordinal(), callback.getSetValue());
+
+      // 8G
+      callback.reset();
+      range = Accelerometer.Range.k8G;
+      accel.setRange(range);
+      assertTrue(callback.wasTriggered());
+      assertEquals(range.ordinal(), sim.getRange());
+      assertEquals(range.ordinal(), callback.getSetValue());
+
+      // 16G - Not supported
+      callback.reset();
+      assertThrows(IllegalArgumentException.class, () -> accel.setRange(Accelerometer.Range.k16G));
+      assertFalse(callback.wasTriggered());
     }
   }
 }
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
new file mode 100644
index 0000000..096f77e
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AddressableLEDSimTest.java
@@ -0,0 +1,133 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.AddressableLED;
+import edu.wpi.first.wpilibj.AddressableLEDBuffer;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.BufferCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+
+class AddressableLEDSimTest {
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
+    AddressableLEDSim sim = new AddressableLEDSim();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback initializedCallback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(initializedCallback, false);
+        AddressableLED led = new AddressableLED(0)) {
+      assertTrue(sim.getInitialized());
+      assertTrue(initializedCallback.wasTriggered());
+      assertTrue(initializedCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testLength() {
+    AddressableLEDSim sim = new AddressableLEDSim();
+    IntCallback callback = new IntCallback();
+    try (CallbackStore cb = sim.registerLengthCallback(callback, false);
+        AddressableLED led = new AddressableLED(0)) {
+      assertEquals(1, sim.getLength()); // Defaults to 1 led
+
+      AddressableLEDBuffer ledData = new AddressableLEDBuffer(50);
+      led.setLength(ledData.getLength());
+      led.setData(ledData);
+
+      assertEquals(50, sim.getLength());
+      assertTrue(callback.wasTriggered());
+      assertEquals(50, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetRunning() {
+    AddressableLEDSim sim = AddressableLEDSim.createForIndex(0);
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = sim.registerRunningCallback(callback, false);
+        AddressableLED led = new AddressableLED(0)) {
+      assertFalse(sim.getRunning());
+
+      led.start();
+      assertTrue(sim.getRunning());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+
+      callback.reset();
+      led.stop();
+      assertFalse(sim.getRunning());
+      assertTrue(callback.wasTriggered());
+      assertFalse(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetData() {
+    AddressableLEDSim sim = new AddressableLEDSim();
+    BufferCallback callback = new BufferCallback();
+
+    try (AddressableLED led = new AddressableLED(0);
+        CallbackStore cb = sim.registerDataCallback(callback); ) {
+      assertFalse(sim.getRunning());
+
+      assertEquals(1, sim.getLength()); // Defaults to 1 led
+
+      AddressableLEDBuffer ledData = new AddressableLEDBuffer(3);
+      led.setLength(ledData.getLength());
+
+      ledData.setRGB(0, 255, 0, 0);
+      ledData.setRGB(1, 0, 255, 0);
+      ledData.setRGB(2, 0, 0, 255);
+      led.setData(ledData);
+
+      byte[] data = sim.getData();
+      System.out.println(Arrays.toString(data));
+      assertEquals(12, data.length);
+
+      assertEquals((byte) 0, data[0]);
+      assertEquals((byte) 0, data[1]);
+      assertEquals((byte) 255, data[2]);
+      assertEquals((byte) 0, data[3]);
+
+      assertEquals((byte) 0, data[4]);
+      assertEquals((byte) 255, data[5]);
+      assertEquals((byte) 0, data[6]);
+      assertEquals((byte) 0, data[7]);
+
+      assertEquals((byte) 255, data[8]);
+      assertEquals((byte) 0, data[9]);
+      assertEquals((byte) 0, data[10]);
+      assertEquals((byte) 0, data[11]);
+
+      assertTrue(callback.wasTriggered());
+      data = callback.getSetValue();
+
+      assertEquals((byte) 0, data[0]);
+      assertEquals((byte) 0, data[1]);
+      assertEquals((byte) 255, data[2]);
+      assertEquals((byte) 0, data[3]);
+
+      assertEquals((byte) 0, data[4]);
+      assertEquals((byte) 255, data[5]);
+      assertEquals((byte) 0, data[6]);
+      assertEquals((byte) 0, data[7]);
+
+      assertEquals((byte) 255, data[8]);
+      assertEquals((byte) 0, data[9]);
+      assertEquals((byte) 0, data[10]);
+      assertEquals((byte) 0, data[11]);
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSimTest.java
index 677c5eb..5a04441 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogEncoderSimTest.java
@@ -1,29 +1,27 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.math.geometry.Rotation2d;
 import edu.wpi.first.wpilibj.AnalogEncoder;
 import edu.wpi.first.wpilibj.AnalogInput;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
 public class AnalogEncoderSimTest {
   @Test
   public void testBasic() {
-    var analogInput = new AnalogInput(0);
-    var analogEncoder = new AnalogEncoder(analogInput);
-    var encoderSim = new AnalogEncoderSim(analogEncoder);
+    try (var analogInput = new AnalogInput(0);
+        var analogEncoder = new AnalogEncoder(analogInput)) {
+      var encoderSim = new AnalogEncoderSim(analogEncoder);
 
-    encoderSim.setPosition(Rotation2d.fromDegrees(180));
-    assertEquals(analogEncoder.get(), 0.5, 1E-8);
-    assertEquals(encoderSim.getTurns(), 0.5, 1E-8);
-    assertEquals(encoderSim.getPosition().getRadians(), Math.PI, 1E-8);
+      encoderSim.setPosition(Rotation2d.fromDegrees(180));
+      assertEquals(analogEncoder.get(), 0.5, 1E-8);
+      assertEquals(encoderSim.getTurns(), 0.5, 1E-8);
+      assertEquals(encoderSim.getPosition().getRadians(), Math.PI, 1E-8);
+    }
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSimTest.java
new file mode 100644
index 0000000..5ce5093
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogGyroSimTest.java
@@ -0,0 +1,97 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.AnalogGyro;
+import edu.wpi.first.wpilibj.AnalogInput;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import org.junit.jupiter.api.Test;
+
+class AnalogGyroSimTest {
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
+    AnalogGyroSim sim = new AnalogGyroSim(0);
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback initializedCallback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(initializedCallback, false);
+        AnalogInput ai = new AnalogInput(0);
+        AnalogGyro gyro = new AnalogGyro(ai)) {
+      assertTrue(sim.getInitialized());
+      assertTrue(initializedCallback.wasTriggered());
+      assertTrue(initializedCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetAngle() {
+    HAL.initialize(500, 0);
+
+    AnalogGyroSim sim = new AnalogGyroSim(0);
+    DoubleCallback callback = new DoubleCallback();
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogGyro gyro = new AnalogGyro(ai);
+        CallbackStore cb = sim.registerAngleCallback(callback, false)) {
+      assertEquals(0, gyro.getAngle());
+
+      final double TEST_ANGLE = 35.04;
+      sim.setAngle(TEST_ANGLE);
+      assertEquals(TEST_ANGLE, sim.getAngle());
+      assertEquals(TEST_ANGLE, gyro.getAngle());
+      assertEquals(-TEST_ANGLE, gyro.getRotation2d().getDegrees());
+      assertTrue(callback.wasTriggered());
+      assertEquals(TEST_ANGLE, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetRate() {
+    HAL.initialize(500, 0);
+
+    AnalogGyroSim sim = new AnalogGyroSim(0);
+    DoubleCallback callback = new DoubleCallback();
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogGyro gyro = new AnalogGyro(ai);
+        CallbackStore cb = sim.registerRateCallback(callback, false)) {
+      assertEquals(0, gyro.getRate());
+
+      final double TEST_RATE = -19.1;
+      sim.setRate(TEST_RATE);
+      assertEquals(TEST_RATE, sim.getRate());
+      assertEquals(TEST_RATE, gyro.getRate());
+
+      assertTrue(callback.wasTriggered());
+      assertEquals(TEST_RATE, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testReset() {
+    HAL.initialize(500, 0);
+
+    try (AnalogInput ai = new AnalogInput(0);
+        AnalogGyro gyro = new AnalogGyro(ai)) {
+      AnalogGyroSim sim = new AnalogGyroSim(gyro);
+      sim.setAngle(12.34);
+      sim.setRate(43.21);
+
+      sim.resetData();
+      assertEquals(0, sim.getAngle());
+      assertEquals(0, sim.getRate());
+      assertEquals(0, gyro.getAngle());
+      assertEquals(0, gyro.getRate());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogInputSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogInputSimTest.java
index a707725..ada4ebf 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogInputSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogInputSimTest.java
@@ -1,42 +1,86 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.hal.util.AllocationException;
+import edu.wpi.first.wpilibj.AnalogInput;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
 import org.junit.jupiter.api.Test;
 
-import edu.wpi.first.wpilibj.AnalogInput;
-import edu.wpi.first.hal.HAL;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
 class AnalogInputSimTest {
-  static class DoubleStore {
-    public boolean m_wasTriggered;
-    public boolean m_wasCorrectType;
-    public double m_setValue0;
+  @Test
+  void setInitializeTest() {
+    HAL.initialize(500, 0);
+
+    AnalogInputSim sim = new AnalogInputSim(5);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        AnalogInput input = new AnalogInput(5)) {
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
   }
 
   @Test
-  void setCallbackTest() {
+  void testSetVoltage() {
     HAL.initialize(500, 0);
 
-    try (AnalogInput input = new AnalogInput(5)) {
-      AnalogInputSim inputSim = new AnalogInputSim(input);
+    AnalogInputSim sim = new AnalogInputSim(5);
+    DoubleCallback callback = new DoubleCallback();
+
+    try (CallbackStore cb = sim.registerVoltageCallback(callback, false);
+        AnalogInput input = new AnalogInput(5)) {
+      // Bootstrap the voltage to be non-zero
+      sim.setVoltage(1.0);
 
       for (double i = 0; i < 5.0; i += 0.1) {
-        inputSim.setVoltage(0);
+        callback.reset();
 
+        sim.setVoltage(0);
         assertEquals(input.getVoltage(), 0, 0.001);
 
-        inputSim.setVoltage(i);
-
+        callback.reset();
+        sim.setVoltage(i);
         assertEquals(input.getVoltage(), i, 0.001);
       }
     }
   }
+
+  @Test
+  void testSetOverSampleBits() {
+    HAL.initialize(500, 0);
+    try (AnalogInput input = new AnalogInput(5)) {
+      input.setOversampleBits(3504);
+      assertEquals(3504, input.getOversampleBits());
+    }
+  }
+
+  @Test
+  void tesInitAccumulator() {
+    HAL.initialize(500, 0);
+    try (AnalogInput input = new AnalogInput(0)) {
+      // First initialization works fine
+      assertDoesNotThrow(input::initAccumulator);
+
+      input.resetAccumulator();
+    }
+  }
+
+  @Test
+  void tesInitAccumulatorOnInvalidPort() {
+    HAL.initialize(500, 0);
+    try (AnalogInput input = new AnalogInput(5)) {
+      assertThrows(AllocationException.class, input::initAccumulator);
+    }
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSimTest.java
index fe1bba8..85a7494 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogOutputSimTest.java
@@ -1,31 +1,32 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.AnalogOutput;
-import edu.wpi.first.hal.HAL;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.AnalogOutput;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
 import org.junit.jupiter.api.Test;
 
 class AnalogOutputSimTest {
-  static class DoubleStore {
-    public boolean m_wasTriggered;
-    public boolean m_wasCorrectType;
-    public double m_setValue = -1;
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
 
-    public void reset() {
-      m_wasCorrectType = false;
-      m_wasTriggered = false;
-      m_setValue = -1;
+    AnalogOutputSim outputSim = new AnalogOutputSim(0);
+    assertFalse(outputSim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = outputSim.registerInitializedCallback(callback, false);
+        AnalogOutput output = new AnalogOutput(0)) {
+      assertTrue(outputSim.getInitialized());
     }
   }
 
@@ -33,37 +34,51 @@
   void setCallbackTest() {
     HAL.initialize(500, 0);
 
-
     try (AnalogOutput output = new AnalogOutput(0)) {
       output.setVoltage(0.5);
 
       AnalogOutputSim outputSim = new AnalogOutputSim(output);
 
-      DoubleStore store = new DoubleStore();
+      DoubleCallback voltageCallback = new DoubleCallback();
 
-      try (CallbackStore cb = outputSim.registerVoltageCallback((name, value) -> {
-        store.m_wasTriggered = true;
-        store.m_wasCorrectType = true;
-        store.m_setValue = value.getDouble();
-      }, false)) {
-        assertFalse(store.m_wasTriggered);
+      try (CallbackStore cb = outputSim.registerVoltageCallback(voltageCallback, false)) {
+        assertFalse(voltageCallback.wasTriggered());
 
         for (double i = 0.1; i < 5.0; i += 0.1) {
-          store.reset();
+          voltageCallback.reset();
 
           output.setVoltage(0);
 
-          assertTrue(store.m_wasTriggered);
-          assertEquals(store.m_setValue, 0, 0.001);
+          assertEquals(0, output.getVoltage());
+          assertEquals(0, outputSim.getVoltage());
+          assertTrue(voltageCallback.wasTriggered());
+          assertEquals(voltageCallback.getSetValue(), 0, 0.001);
 
-          store.reset();
+          voltageCallback.reset();
 
           output.setVoltage(i);
 
-          assertTrue(store.m_wasTriggered);
-          assertEquals(store.m_setValue, i, 0.001);
+          assertEquals(i, output.getVoltage());
+          assertEquals(i, outputSim.getVoltage());
+          assertTrue(voltageCallback.wasTriggered());
+          assertEquals(voltageCallback.getSetValue(), i, 0.001);
         }
       }
     }
   }
+
+  @Test
+  void testReset() {
+    HAL.initialize(500, 0);
+
+    AnalogOutputSim outputSim = new AnalogOutputSim(0);
+
+    try (AnalogOutput output = new AnalogOutput(0)) {
+      output.setVoltage(1.2);
+
+      outputSim.resetData();
+      assertEquals(0, output.getVoltage());
+      assertEquals(0, outputSim.getVoltage());
+    }
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSimTest.java
new file mode 100644
index 0000000..1142914
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/AnalogTriggerSimTest.java
@@ -0,0 +1,59 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.AnalogTrigger;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import org.junit.jupiter.api.Test;
+
+class AnalogTriggerSimTest {
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
+
+    AnalogTriggerSim sim = AnalogTriggerSim.createForIndex(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        AnalogTrigger trigger = new AnalogTrigger(0)) {
+      assertTrue(sim.getInitialized());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void triggerLowerBoundTest() {
+    HAL.initialize(500, 0);
+
+    try (AnalogTrigger trigger = new AnalogTrigger(0)) {
+      AnalogTriggerSim sim = new AnalogTriggerSim(trigger);
+      DoubleCallback lowerCallback = new DoubleCallback();
+      DoubleCallback upperCallback = new DoubleCallback();
+      try (CallbackStore lowerCb = sim.registerTriggerLowerBoundCallback(lowerCallback, false);
+          CallbackStore upperCb = sim.registerTriggerUpperBoundCallback(upperCallback, false)) {
+        trigger.setLimitsVoltage(0.299, 1.91);
+
+        assertEquals(0.299, sim.getTriggerLowerBound());
+        assertEquals(1.91, sim.getTriggerUpperBound());
+
+        assertTrue(lowerCallback.wasTriggered());
+        assertEquals(0.299, lowerCallback.getSetValue());
+
+        assertTrue(upperCallback.wasTriggered());
+        assertEquals(1.91, upperCallback.getSetValue());
+      }
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/CTREPCMSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/CTREPCMSimTest.java
new file mode 100644
index 0000000..29cac3a
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/CTREPCMSimTest.java
@@ -0,0 +1,168 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DoubleSolenoid;
+import edu.wpi.first.wpilibj.PneumaticsControlModule;
+import edu.wpi.first.wpilibj.PneumaticsModuleType;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings("AbbreviationAsWordInName")
+class CTREPCMSimTest {
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
+
+    CTREPCMSim sim = new CTREPCMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        PneumaticsControlModule pcm = new PneumaticsControlModule(0)) {
+      assertTrue(sim.getInitialized());
+    }
+    assertFalse(sim.getInitialized());
+  }
+
+  @Test
+  void solenoidOutputTest() {
+    HAL.initialize(500, 0);
+
+    try (PneumaticsControlModule pcm = new PneumaticsControlModule(0);
+        DoubleSolenoid doubleSolenoid = new DoubleSolenoid(0, PneumaticsModuleType.CTREPCM, 3, 4)) {
+      CTREPCMSim sim = new CTREPCMSim(0);
+      sim.resetData();
+
+      BooleanCallback callback3 = new BooleanCallback();
+      BooleanCallback callback4 = new BooleanCallback();
+
+      try (CallbackStore cb3 = sim.registerSolenoidOutputCallback(3, callback3, false);
+          CallbackStore cb4 = sim.registerSolenoidOutputCallback(4, callback4, false)) {
+        // Reverse
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kReverse);
+        assertFalse(callback3.wasTriggered());
+        assertNull(callback3.getSetValue());
+        assertTrue(callback4.wasTriggered());
+        assertTrue(callback4.getSetValue());
+        assertFalse(sim.getSolenoidOutput(3));
+        assertTrue(sim.getSolenoidOutput(4));
+        assertEquals(0x10, pcm.getSolenoids());
+
+        // Forward
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kForward);
+        assertTrue(callback3.wasTriggered());
+        assertTrue(callback3.getSetValue());
+        assertTrue(callback4.wasTriggered());
+        assertFalse(callback4.getSetValue());
+        assertTrue(sim.getSolenoidOutput(3));
+        assertFalse(sim.getSolenoidOutput(4));
+        assertEquals(0x8, pcm.getSolenoids());
+
+        // Off
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kOff);
+        assertTrue(callback3.wasTriggered());
+        assertFalse(callback3.getSetValue());
+        assertFalse(callback4.wasTriggered());
+        assertNull(callback4.getSetValue());
+        assertFalse(sim.getSolenoidOutput(3));
+        assertFalse(sim.getSolenoidOutput(4));
+        assertEquals(0x0, pcm.getSolenoids());
+      }
+    }
+  }
+
+  @Test
+  void setCompressorOnTest() {
+    HAL.initialize(500, 0);
+
+    CTREPCMSim sim = new CTREPCMSim(0);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticsControlModule pcm = new PneumaticsControlModule(0);
+        CallbackStore cb = sim.registerCompressorOnCallback(callback, false)) {
+      assertFalse(pcm.getCompressor());
+      assertFalse(sim.getCompressorOn());
+      sim.setCompressorOn(true);
+      assertTrue(pcm.getCompressor());
+      assertTrue(sim.getCompressorOn());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setClosedLoopEnabled() {
+    HAL.initialize(500, 0);
+
+    CTREPCMSim sim = new CTREPCMSim(0);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticsControlModule pcm = new PneumaticsControlModule(0);
+        CallbackStore cb = sim.registerClosedLoopEnabledCallback(callback, false)) {
+      pcm.setClosedLoopControl(false);
+      assertFalse(pcm.getClosedLoopControl());
+
+      pcm.setClosedLoopControl(true);
+      assertTrue(sim.getClosedLoopEnabled());
+      assertTrue(pcm.getClosedLoopControl());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setPressureSwitchEnabledTest() {
+    HAL.initialize(500, 0);
+
+    CTREPCMSim sim = new CTREPCMSim(0);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticsControlModule pcm = new PneumaticsControlModule(0);
+        CallbackStore cb = sim.registerPressureSwitchCallback(callback, false)) {
+      assertFalse(pcm.getPressureSwitch());
+
+      sim.setPressureSwitch(true);
+      assertTrue(sim.getPressureSwitch());
+      assertTrue(pcm.getPressureSwitch());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setCompressorCurrentTest() {
+    HAL.initialize(500, 0);
+
+    CTREPCMSim sim = new CTREPCMSim(0);
+    DoubleCallback callback = new DoubleCallback();
+
+    try (PneumaticsControlModule pcm = new PneumaticsControlModule(0);
+        CallbackStore cb = sim.registerCompressorCurrentCallback(callback, false)) {
+      assertFalse(pcm.getPressureSwitch());
+
+      sim.setCompressorCurrent(35.04);
+      assertEquals(35.04, sim.getCompressorCurrent());
+      assertEquals(35.04, pcm.getCompressorCurrent());
+      assertTrue(callback.wasTriggered());
+      assertEquals(35.04, callback.getSetValue());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DIOSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DIOSimTest.java
new file mode 100644
index 0000000..1c742fe
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DIOSimTest.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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DigitalInput;
+import edu.wpi.first.wpilibj.DigitalOutput;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import org.junit.jupiter.api.Test;
+
+class DIOSimTest {
+  @Test
+  void testInitialization() {
+    DIOSim sim = new DIOSim(2);
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback initializedCallback = new BooleanCallback();
+    BooleanCallback isInputCallback = new BooleanCallback();
+
+    try (CallbackStore initializeCb = sim.registerInitializedCallback(initializedCallback, false);
+        CallbackStore inputCb = sim.registerIsInputCallback(isInputCallback, false);
+        DigitalOutput output = new DigitalOutput(2)) {
+      assertTrue(sim.getInitialized());
+      assertTrue(initializedCallback.wasTriggered());
+      assertTrue(initializedCallback.getSetValue());
+
+      assertFalse(sim.getIsInput());
+      assertTrue(isInputCallback.wasTriggered());
+      assertFalse(isInputCallback.getSetValue());
+
+      initializedCallback.reset();
+      sim.setInitialized(false);
+      assertTrue(initializedCallback.wasTriggered());
+      assertFalse(initializedCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testInput() {
+    HAL.initialize(500, 0);
+
+    try (DigitalInput input = new DigitalInput(0)) {
+      DIOSim sim = new DIOSim(input);
+      assertTrue(sim.getIsInput());
+
+      BooleanCallback valueCallback = new BooleanCallback();
+
+      try (CallbackStore cb = sim.registerValueCallback(valueCallback, false)) {
+        assertTrue(input.get());
+        assertTrue(sim.getValue());
+
+        assertFalse(valueCallback.wasTriggered());
+        sim.setValue(false);
+        assertTrue(valueCallback.wasTriggered());
+        assertFalse(valueCallback.getSetValue());
+      }
+    }
+  }
+
+  @Test
+  void testOutput() {
+    HAL.initialize(500, 0);
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      DIOSim sim = new DIOSim(output);
+      assertFalse(sim.getIsInput());
+
+      BooleanCallback valueCallback = new BooleanCallback();
+
+      try (CallbackStore cb = sim.registerValueCallback(valueCallback, false)) {
+        assertFalse(output.get());
+        assertFalse(sim.getValue());
+
+        assertFalse(valueCallback.wasTriggered());
+        output.set(true);
+        assertTrue(valueCallback.wasTriggered());
+        assertTrue(valueCallback.getSetValue());
+      }
+    }
+  }
+}
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 4a0eab3..62fdcc9 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
@@ -1,53 +1,50 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import edu.wpi.first.wpilibj.controller.LinearPlantInversionFeedforward;
-import edu.wpi.first.wpilibj.controller.RamseteController;
-import edu.wpi.first.wpilibj.geometry.Pose2d;
-import edu.wpi.first.wpilibj.geometry.Rotation2d;
-import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
-import edu.wpi.first.wpilibj.system.RungeKutta;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryConfig;
-import edu.wpi.first.wpilibj.trajectory.TrajectoryGenerator;
-import edu.wpi.first.wpilibj.trajectory.constraint.DifferentialDriveKinematicsConstraint;
-import edu.wpi.first.wpilibj.util.Units;
-import edu.wpi.first.wpiutil.math.Matrix;
-import edu.wpi.first.wpiutil.math.Nat;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-import edu.wpi.first.wpiutil.math.Vector;
-import edu.wpi.first.wpiutil.math.numbers.N1;
-import edu.wpi.first.wpiutil.math.numbers.N7;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Nat;
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.Vector;
+import edu.wpi.first.math.controller.LinearPlantInversionFeedforward;
+import edu.wpi.first.math.controller.RamseteController;
+import edu.wpi.first.math.geometry.Pose2d;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.kinematics.DifferentialDriveKinematics;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.numbers.N7;
+import edu.wpi.first.math.system.NumericalIntegration;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
+import edu.wpi.first.math.trajectory.TrajectoryConfig;
+import edu.wpi.first.math.trajectory.TrajectoryGenerator;
+import edu.wpi.first.math.trajectory.constraint.DifferentialDriveKinematicsConstraint;
+import edu.wpi.first.math.util.Units;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
 class DifferentialDrivetrainSimTest {
   @Test
   public void testConvergence() {
     var motor = DCMotor.getNEO(2);
-    var plant = LinearSystemId.createDrivetrainVelocitySystem(
-        motor,
-        50,
-        Units.inchesToMeters(2),
-        Units.inchesToMeters(12),
-        0.5,
-        1.0);
+    var plant =
+        LinearSystemId.createDrivetrainVelocitySystem(
+            motor, 50, Units.inchesToMeters(2), Units.inchesToMeters(12), 0.5, 1.0);
 
     var kinematics = new DifferentialDriveKinematics(Units.inchesToMeters(24));
-    var sim = new DifferentialDrivetrainSim(plant,
-        motor, 1, kinematics.trackWidthMeters, Units.inchesToMeters(2));
+    var sim =
+        new DifferentialDrivetrainSim(
+            plant,
+            motor,
+            1,
+            kinematics.trackWidthMeters,
+            Units.inchesToMeters(2),
+            VecBuilder.fill(0, 0, 0.0001, 0.1, 0.1, 0.005, 0.005));
 
     var feedforward = new LinearPlantInversionFeedforward<>(plant, 0.020);
     var ramsete = new RamseteController();
@@ -56,12 +53,13 @@
     // ground truth
     Matrix<N7, N1> groundTruthX = new Vector<>(Nat.N7());
 
-    var traj = TrajectoryGenerator.generateTrajectory(
-        new Pose2d(),
-        List.of(),
-        new Pose2d(2, 2, new Rotation2d()),
-        new TrajectoryConfig(1, 1)
-            .addConstraint(new DifferentialDriveKinematicsConstraint(kinematics, 1)));
+    var traj =
+        TrajectoryGenerator.generateTrajectory(
+            new Pose2d(),
+            List.of(),
+            new Pose2d(2, 2, new Rotation2d()),
+            new TrajectoryConfig(1, 1)
+                .addConstraint(new DifferentialDriveKinematicsConstraint(kinematics, 1)));
 
     for (double t = 0; t < traj.getTotalTimeSeconds(); t += 0.020) {
       var state = traj.sample(0.020);
@@ -69,37 +67,44 @@
 
       var wheelSpeeds = kinematics.toWheelSpeeds(ramseteOut);
 
-      var voltages = feedforward.calculate(
-          VecBuilder.fill(wheelSpeeds.leftMetersPerSecond, wheelSpeeds.rightMetersPerSecond));
+      var voltages =
+          feedforward.calculate(
+              VecBuilder.fill(wheelSpeeds.leftMetersPerSecond, wheelSpeeds.rightMetersPerSecond));
 
       // Sim periodic code
       sim.setInputs(voltages.get(0, 0), voltages.get(1, 0));
       sim.update(0.020);
 
       // Update our ground truth
-      groundTruthX = RungeKutta.rungeKutta(sim::getDynamics, groundTruthX, voltages, 0.020);
+      groundTruthX = NumericalIntegration.rk4(sim::getDynamics, groundTruthX, voltages, 0.020);
     }
 
-    assertEquals(groundTruthX.get(DifferentialDrivetrainSim.State.kX.value, 0),
-        sim.getState(DifferentialDrivetrainSim.State.kX));
-    assertEquals(groundTruthX.get(DifferentialDrivetrainSim.State.kY.value, 0),
-        sim.getState(DifferentialDrivetrainSim.State.kY));
-    assertEquals(groundTruthX.get(DifferentialDrivetrainSim.State.kHeading.value, 0),
-        sim.getState(DifferentialDrivetrainSim.State.kHeading));
+    // 2 inch tolerance is OK since our ground truth is an approximation of the
+    // ODE solution using RK4 anyway
+    assertEquals(
+        groundTruthX.get(DifferentialDrivetrainSim.State.kX.value, 0),
+        sim.getState(DifferentialDrivetrainSim.State.kX),
+        0.05);
+    assertEquals(
+        groundTruthX.get(DifferentialDrivetrainSim.State.kY.value, 0),
+        sim.getState(DifferentialDrivetrainSim.State.kY),
+        0.05);
+    assertEquals(
+        groundTruthX.get(DifferentialDrivetrainSim.State.kHeading.value, 0),
+        sim.getState(DifferentialDrivetrainSim.State.kHeading),
+        0.01);
   }
 
   @Test
   public void testCurrent() {
     var motor = DCMotor.getNEO(2);
-    var plant = LinearSystemId.createDrivetrainVelocitySystem(
-        motor,
-        50,
-        Units.inchesToMeters(2),
-        Units.inchesToMeters(12),
-        0.5,
-        1.0);
+    var plant =
+        LinearSystemId.createDrivetrainVelocitySystem(
+            motor, 50, Units.inchesToMeters(2), Units.inchesToMeters(12), 0.5, 1.0);
     var kinematics = new DifferentialDriveKinematics(Units.inchesToMeters(24));
-    var sim = new DifferentialDrivetrainSim(plant, motor, 1, kinematics.trackWidthMeters, Units.inchesToMeters(2));
+    var sim =
+        new DifferentialDrivetrainSim(
+            plant, motor, 1, kinematics.trackWidthMeters, Units.inchesToMeters(2), null);
 
     sim.setInputs(-12, -12);
     for (int i = 0; i < 10; i++) {
@@ -123,16 +128,19 @@
   @Test
   public void testModelStability() {
     var motor = DCMotor.getNEO(2);
-    var plant = LinearSystemId.createDrivetrainVelocitySystem(
-        motor,
-        50,
-        Units.inchesToMeters(2),
-        Units.inchesToMeters(12),
-        2.0,
-        5.0);
+    var plant =
+        LinearSystemId.createDrivetrainVelocitySystem(
+            motor, 50, Units.inchesToMeters(2), Units.inchesToMeters(12), 2.0, 5.0);
 
     var kinematics = new DifferentialDriveKinematics(Units.inchesToMeters(24));
-    var sim = new DifferentialDrivetrainSim(plant, motor, 5, kinematics.trackWidthMeters, Units.inchesToMeters(2));
+    var sim =
+        new DifferentialDrivetrainSim(
+            plant,
+            motor,
+            5,
+            kinematics.trackWidthMeters,
+            Units.inchesToMeters(2),
+            VecBuilder.fill(0, 0, 0.0001, 0.1, 0.1, 0.005, 0.005));
 
     sim.setInputs(2, 4);
 
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
new file mode 100644
index 0000000..9f29aac
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DigitalPWMSimTest.java
@@ -0,0 +1,58 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DigitalOutput;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import org.junit.jupiter.api.Test;
+
+class DigitalPWMSimTest {
+  @Test
+  void testInitialization() {
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      DigitalPWMSim sim = new DigitalPWMSim(output);
+      assertFalse(sim.getInitialized());
+
+      BooleanCallback initializeCallback = new BooleanCallback();
+      DoubleCallback dutyCycleCallback = new DoubleCallback();
+      try (CallbackStore initCb = sim.registerInitializedCallback(initializeCallback, false);
+          CallbackStore dutyCycleCb = sim.registerDutyCycleCallback(dutyCycleCallback, false); ) {
+        final double kTestDutyCycle = 0.191;
+        output.enablePWM(kTestDutyCycle);
+
+        assertTrue(sim.getInitialized());
+        assertTrue(initializeCallback.wasTriggered());
+        assertTrue(initializeCallback.getSetValue());
+
+        assertEquals(kTestDutyCycle, sim.getDutyCycle());
+        assertTrue(dutyCycleCallback.wasTriggered());
+        assertEquals(kTestDutyCycle, dutyCycleCallback.getSetValue());
+      }
+    }
+  }
+
+  @Test
+  void setPinTest() {
+    HAL.initialize(500, 0);
+
+    try (DigitalOutput output = new DigitalOutput(0)) {
+      DigitalPWMSim sim = new DigitalPWMSim(output);
+      IntCallback callback = new IntCallback();
+      try (CallbackStore cb = sim.registerPinCallback(callback, false)) {
+        sim.setPin(191);
+        assertEquals(191, sim.getPin());
+        assertTrue(callback.wasTriggered());
+        assertEquals(191, callback.getSetValue());
+      }
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DriverStationSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DriverStationSimTest.java
new file mode 100644
index 0000000..ce50afc
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DriverStationSimTest.java
@@ -0,0 +1,261 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.AllianceStationID;
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DriverStation;
+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 org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class DriverStationSimTest {
+  @Test
+  void testEnabled() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    assertFalse(DriverStation.isEnabled());
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerEnabledCallback(callback, false)) {
+      DriverStationSim.setEnabled(true);
+      DriverStationSim.notifyNewData();
+      assertTrue(DriverStationSim.getEnabled());
+      assertTrue(DriverStation.isEnabled());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testAutonomus() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    assertFalse(DriverStation.isAutonomous());
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerAutonomousCallback(callback, false)) {
+      DriverStationSim.setAutonomous(true);
+      DriverStationSim.notifyNewData();
+      assertTrue(DriverStationSim.getAutonomous());
+      assertTrue(DriverStation.isAutonomous());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testTest() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    assertFalse(DriverStation.isTest());
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerTestCallback(callback, false)) {
+      DriverStationSim.setTest(true);
+      DriverStationSim.notifyNewData();
+      assertTrue(DriverStationSim.getTest());
+      assertTrue(DriverStation.isTest());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testEstop() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    assertFalse(DriverStation.isEStopped());
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerEStopCallback(callback, false)) {
+      DriverStationSim.setEStop(true);
+      DriverStationSim.notifyNewData();
+      assertTrue(DriverStationSim.getEStop());
+      assertTrue(DriverStation.isEStopped());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testFmsAttached() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    assertFalse(DriverStation.isFMSAttached());
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerFmsAttachedCallback(callback, false)) {
+      DriverStationSim.setFmsAttached(true);
+      DriverStationSim.notifyNewData();
+      assertTrue(DriverStationSim.getFmsAttached());
+      assertTrue(DriverStation.isFMSAttached());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testDsAttached() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    DriverStationSim.notifyNewData();
+    assertTrue(DriverStation.isDSAttached());
+
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = DriverStationSim.registerDsAttachedCallback(callback, false)) {
+      DriverStationSim.setDsAttached(false);
+      DriverStationSim.notifyNewData();
+      assertFalse(DriverStationSim.getDsAttached());
+      assertFalse(DriverStation.isDSAttached());
+      assertTrue(callback.wasTriggered());
+      assertFalse(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testAllianceStationId() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    EnumCallback callback = new EnumCallback();
+
+    AllianceStationID allianceStation = AllianceStationID.Blue2;
+    DriverStationSim.setAllianceStationId(allianceStation);
+
+    try (CallbackStore cb = DriverStationSim.registerAllianceStationIdCallback(callback, false)) {
+      // B1
+      allianceStation = AllianceStationID.Blue1;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Blue, DriverStation.getAlliance());
+      assertEquals(1, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+
+      // B2
+      allianceStation = AllianceStationID.Blue2;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Blue, DriverStation.getAlliance());
+      assertEquals(2, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+
+      // B3
+      allianceStation = AllianceStationID.Blue3;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Blue, DriverStation.getAlliance());
+      assertEquals(3, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+
+      // R1
+      allianceStation = AllianceStationID.Red1;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Red, DriverStation.getAlliance());
+      assertEquals(1, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+
+      // R2
+      allianceStation = AllianceStationID.Red2;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Red, DriverStation.getAlliance());
+      assertEquals(2, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+
+      // R3
+      allianceStation = AllianceStationID.Red3;
+      DriverStationSim.setAllianceStationId(allianceStation);
+      assertEquals(allianceStation, DriverStationSim.getAllianceStationId());
+      assertEquals(DriverStation.Alliance.Red, DriverStation.getAlliance());
+      assertEquals(3, DriverStation.getLocation());
+      assertTrue(callback.wasTriggered());
+      assertEquals(allianceStation.ordinal(), callback.getSetValue());
+    }
+  }
+
+  @ParameterizedTest
+  @EnumSource(DriverStation.MatchType.class)
+  public void testMatchType(DriverStation.MatchType matchType) {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    DriverStationSim.setMatchType(matchType);
+    DriverStationSim.notifyNewData();
+    assertEquals(matchType, DriverStation.getMatchType());
+  }
+
+  @Test
+  public void testReplayNumber() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    DriverStationSim.setReplayNumber(4);
+    DriverStationSim.notifyNewData();
+    assertEquals(4, DriverStation.getReplayNumber());
+  }
+
+  @Test
+  public void testMatchNumber() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    DriverStationSim.setMatchNumber(3);
+    DriverStationSim.notifyNewData();
+    assertEquals(3, DriverStation.getMatchNumber());
+  }
+
+  @Test
+  public void testMatchTime() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    DoubleCallback callback = new DoubleCallback();
+    try (CallbackStore cb = DriverStationSim.registerMatchTimeCallback(callback, false)) {
+      final double testTime = 19.174;
+      DriverStationSim.setMatchTime(testTime);
+      assertEquals(testTime, DriverStationSim.getMatchTime());
+      assertEquals(testTime, DriverStation.getMatchTime());
+      assertTrue(callback.wasTriggered());
+      assertEquals(testTime, callback.getSetValue());
+    }
+  }
+
+  @Test
+  public void testSetGameSpecificMessage() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    final String message = "Hello World!";
+    DriverStationSim.setGameSpecificMessage(message);
+    DriverStationSim.notifyNewData();
+    assertEquals(message, DriverStation.getGameSpecificMessage());
+  }
+
+  @Test
+  public void testSetEventName() {
+    HAL.initialize(500, 0);
+    DriverStationSim.resetData();
+
+    final String message = "The Best Event";
+    DriverStationSim.setEventName(message);
+    DriverStationSim.notifyNewData();
+    assertEquals(message, DriverStation.getEventName());
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSimTest.java
new file mode 100644
index 0000000..49f8e77
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleEncoderSimTest.java
@@ -0,0 +1,35 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DutyCycleEncoder;
+import org.junit.jupiter.api.Test;
+
+class DutyCycleEncoderSimTest {
+  @Test
+  void setTest() {
+    try (DutyCycleEncoder encoder = new DutyCycleEncoder(0)) {
+      DutyCycleEncoderSim sim = new DutyCycleEncoderSim(encoder);
+
+      sim.set(5.67);
+      assertEquals(5.67, encoder.get());
+    }
+  }
+
+  @Test
+  void setDistanceTest() {
+    HAL.initialize(500, 0);
+
+    try (DutyCycleEncoder encoder = new DutyCycleEncoder(0)) {
+      DutyCycleEncoderSim sim = new DutyCycleEncoderSim(encoder);
+
+      sim.setDistance(19.1);
+      assertEquals(19.1, encoder.getDistance());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleSimTest.java
new file mode 100644
index 0000000..3c2a7fe
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/DutyCycleSimTest.java
@@ -0,0 +1,71 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DigitalInput;
+import edu.wpi.first.wpilibj.DutyCycle;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import org.junit.jupiter.api.Test;
+
+class DutyCycleSimTest {
+  @Test
+  void testInitialization() {
+    DutyCycleSim sim = DutyCycleSim.createForIndex(0);
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        DigitalInput di = new DigitalInput(2);
+        DutyCycle dc = new DutyCycle(di)) {
+      assertTrue(sim.getInitialized());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setFrequencyTest() {
+    HAL.initialize(500, 0);
+
+    try (DigitalInput di = new DigitalInput(2);
+        DutyCycle dc = new DutyCycle(di)) {
+      IntCallback callback = new IntCallback();
+      DutyCycleSim sim = new DutyCycleSim(dc);
+      try (CallbackStore cb = sim.registerFrequencyCallback(callback, false)) {
+        sim.setFrequency(191);
+        assertEquals(191, sim.getFrequency());
+        assertEquals(191, dc.getFrequency());
+        assertTrue(callback.wasTriggered());
+        assertEquals(191, callback.getSetValue());
+      }
+    }
+  }
+
+  @Test
+  void setOutputTest() {
+    HAL.initialize(500, 0);
+
+    try (DigitalInput di = new DigitalInput(2);
+        DutyCycle dc = new DutyCycle(di)) {
+      DoubleCallback callback = new DoubleCallback();
+      DutyCycleSim sim = new DutyCycleSim(dc);
+      try (CallbackStore cb = sim.registerOutputCallback(callback, false)) {
+        sim.setOutput(229.174);
+        assertEquals(229.174, sim.getOutput());
+        assertEquals(229.174, dc.getOutput());
+        assertTrue(callback.wasTriggered());
+        assertEquals(229.174, callback.getSetValue());
+      }
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ElevatorSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ElevatorSimTest.java
index c4d78d0..d655a90 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ElevatorSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/ElevatorSimTest.java
@@ -1,70 +1,80 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.Encoder;
-import edu.wpi.first.wpilibj.PWMVictorSPX;
-import edu.wpi.first.wpilibj.RobotController;
-import edu.wpi.first.wpilibj.controller.PIDController;
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.system.plant.LinearSystemId;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.controller.PIDController;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
+import edu.wpi.first.math.util.Units;
+import edu.wpi.first.wpilibj.Encoder;
+import edu.wpi.first.wpilibj.RobotController;
+import edu.wpi.first.wpilibj.motorcontrol.PWMVictorSPX;
+import org.junit.jupiter.api.Test;
+
 public class ElevatorSimTest {
   @Test
-  @SuppressWarnings("LocalVariableName")
+  @SuppressWarnings({"LocalVariableName", "resource"})
   public void testStateSpaceSimWithElevator() {
+    RoboRioSim.resetData();
 
     var controller = new PIDController(10, 0, 0);
 
-    var sim = new ElevatorSim(DCMotor.getVex775Pro(4), 14.67, 8, 0.75 * 25.4 / 1000.0,
-        0.0, 3.0, VecBuilder.fill(0.01));
+    var sim =
+        new ElevatorSim(
+            DCMotor.getVex775Pro(4),
+            14.67,
+            8,
+            0.75 * 25.4 / 1000.0,
+            0.0,
+            3.0,
+            VecBuilder.fill(0.01));
 
-    var motor = new PWMVictorSPX(0);
-    var encoder = new Encoder(0, 1);
-    var encoderSim = new EncoderSim(encoder);
+    try (var motor = new PWMVictorSPX(0);
+        var encoder = new Encoder(0, 1)) {
+      var encoderSim = new EncoderSim(encoder);
 
-    for (int i = 0; i < 100; i++) {
-      controller.setSetpoint(2.0);
+      for (int i = 0; i < 100; i++) {
+        controller.setSetpoint(2.0);
 
-      double nextVoltage = controller.calculate(encoderSim.getDistance());
+        double nextVoltage = controller.calculate(encoderSim.getDistance());
 
-      double currentBatteryVoltage = RobotController.getBatteryVoltage();
-      motor.set(nextVoltage / currentBatteryVoltage);
+        double currentBatteryVoltage = RobotController.getBatteryVoltage();
+        motor.set(nextVoltage / currentBatteryVoltage);
 
-      // ------ SimulationPeriodic() happens after user code -------
+        // ------ SimulationPeriodic() happens after user code -------
 
-      var u = VecBuilder.fill(motor.get() * currentBatteryVoltage);
-      sim.setInput(u);
-      sim.update(0.020);
-      var y = sim.getOutput();
-      encoderSim.setDistance(y.get(0, 0));
+        var u = VecBuilder.fill(motor.get() * currentBatteryVoltage);
+        sim.setInput(u);
+        sim.update(0.020);
+        var y = sim.getOutput();
+        encoderSim.setDistance(y.get(0, 0));
+      }
+
+      assertEquals(controller.getSetpoint(), sim.getPositionMeters(), 0.2);
     }
-
-    assertEquals(controller.getSetpoint(), sim.getPositionMeters(), 0.2);
   }
 
   @Test
   public void testMinMax() {
-    var plant = LinearSystemId.createElevatorSystem(
-        DCMotor.getVex775Pro(4),
-        8.0,
-        0.75 * 25.4 / 1000.0,
-        14.67);
+    var plant =
+        LinearSystemId.createElevatorSystem(
+            DCMotor.getVex775Pro(4), 8.0, 0.75 * 25.4 / 1000.0, 14.67);
 
-    var sim = new ElevatorSim(plant,
-        DCMotor.getVex775Pro(4),
-        14.67, 0.75 * 25.4 / 1000.0, 0.0, 1.0, VecBuilder.fill(0.01));
+    var sim =
+        new ElevatorSim(
+            plant,
+            DCMotor.getVex775Pro(4),
+            14.67,
+            0.75 * 25.4 / 1000.0,
+            0.0,
+            1.0,
+            VecBuilder.fill(0.01));
 
     for (int i = 0; i < 100; i++) {
       sim.setInput(VecBuilder.fill(0));
@@ -80,4 +90,20 @@
       assertTrue(height <= 1.05);
     }
   }
+
+  @Test
+  public void testStability() {
+    var sim = new ElevatorSim(DCMotor.getVex775Pro(4), 100, 4, Units.inchesToMeters(0.5), 0, 10);
+
+    sim.setState(VecBuilder.fill(0, 0));
+    sim.setInput(12);
+    for (int i = 0; i < 50; i++) {
+      sim.update(0.02);
+    }
+
+    assertEquals(
+        sim.m_plant.calculateX(VecBuilder.fill(0, 0), VecBuilder.fill(12), 0.02 * 50.0).get(0, 0),
+        sim.getPositionMeters(),
+        0.1);
+  }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/EncoderSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/EncoderSimTest.java
new file mode 100644
index 0000000..931c0fe
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/EncoderSimTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.wpilibj.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.Encoder;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import org.junit.jupiter.api.Test;
+
+class EncoderSimTest {
+  private static final double DEFAULT_DISTANCE_PER_PULSE = 0.0005;
+
+  @Test
+  void testRate() {
+    HAL.initialize(500, 0);
+
+    try (Encoder encoder = new Encoder(0, 1)) {
+      EncoderSim sim = new EncoderSim(encoder);
+      sim.resetData();
+
+      encoder.setDistancePerPulse(DEFAULT_DISTANCE_PER_PULSE);
+
+      sim.setRate(1.91);
+      assertEquals(1.91, sim.getRate());
+    }
+  }
+
+  @Test
+  void testCount() {
+    HAL.initialize(500, 0);
+
+    try (Encoder encoder = new Encoder(0, 1)) {
+      EncoderSim sim = new EncoderSim(encoder);
+      sim.resetData();
+
+      encoder.setDistancePerPulse(DEFAULT_DISTANCE_PER_PULSE);
+
+      IntCallback callback = new IntCallback();
+      try (CallbackStore cb = sim.registerCountCallback(callback, false)) {
+        sim.setCount(3504);
+        assertEquals(3504, sim.getCount());
+
+        assertTrue(callback.wasTriggered());
+        assertEquals(3504, encoder.get());
+        assertEquals(3504, callback.getSetValue());
+      }
+    }
+  }
+
+  @Test
+  void testDistance() {
+    HAL.initialize(500, 0);
+
+    try (Encoder encoder = new Encoder(0, 1)) {
+      EncoderSim sim = new EncoderSim(encoder);
+      sim.resetData();
+
+      encoder.setDistancePerPulse(DEFAULT_DISTANCE_PER_PULSE);
+
+      sim.setDistance(229.174);
+      assertEquals(229.174, sim.getDistance());
+      assertEquals(229.174, encoder.getDistance());
+    }
+  }
+
+  @Test
+  void testPeriod() {
+    HAL.initialize(500, 0);
+
+    try (Encoder encoder = new Encoder(0, 1)) {
+      EncoderSim sim = new EncoderSim(encoder);
+      sim.resetData();
+
+      encoder.setDistancePerPulse(DEFAULT_DISTANCE_PER_PULSE);
+
+      DoubleCallback callback = new DoubleCallback();
+      try (CallbackStore cb = sim.registerPeriodCallback(callback, false)) {
+        sim.setPeriod(123.456);
+        assertEquals(123.456, sim.getPeriod());
+        assertEquals(123.456, encoder.getPeriod());
+        assertEquals(DEFAULT_DISTANCE_PER_PULSE / 123.456, encoder.getRate());
+      }
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/PWMSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/PWMSimTest.java
new file mode 100644
index 0000000..2e76aa2
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/PWMSimTest.java
@@ -0,0 +1,135 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.PWM;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import org.junit.jupiter.api.Test;
+
+class PWMSimTest {
+  @Test
+  void testInitialize() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      assertTrue(sim.getInitialized());
+    }
+  }
+
+  @Test
+  void testSetRawValue() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    IntCallback callback = new IntCallback();
+
+    try (CallbackStore cb = sim.registerRawValueCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      sim.setRawValue(229);
+      assertEquals(229, sim.getRawValue());
+      assertEquals(229, pwm.getRaw());
+      assertTrue(callback.wasTriggered());
+      assertEquals(229, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetSpeed() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    DoubleCallback callback = new DoubleCallback();
+
+    try (CallbackStore cb = sim.registerSpeedCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      final double kTestValue = 0.3504;
+      pwm.setSpeed(kTestValue);
+
+      assertEquals(kTestValue, sim.getSpeed());
+      assertEquals(kTestValue, pwm.getSpeed());
+      assertTrue(callback.wasTriggered());
+      assertEquals(kTestValue, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetPosition() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    DoubleCallback callback = new DoubleCallback();
+
+    try (CallbackStore cb = sim.registerPositionCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      final double kTestValue = 0.3504;
+      pwm.setPosition(kTestValue);
+
+      assertEquals(kTestValue, sim.getPosition());
+      assertEquals(kTestValue, pwm.getPosition());
+      assertTrue(callback.wasTriggered());
+      assertEquals(kTestValue, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetPeriodScale() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    IntCallback callback = new IntCallback();
+
+    try (CallbackStore cb = sim.registerPeriodScaleCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      sim.setPeriodScale(3504);
+      assertEquals(3504, sim.getPeriodScale());
+      assertTrue(callback.wasTriggered());
+      assertEquals(3504, callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetZeroLatch() {
+    HAL.initialize(500, 0);
+
+    PWMSim sim = new PWMSim(0);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerZeroLatchCallback(callback, false);
+        PWM pwm = new PWM(0)) {
+      pwm.setZeroLatch();
+
+      assertTrue(callback.wasTriggered());
+    }
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/REVPHSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/REVPHSimTest.java
new file mode 100644
index 0000000..8bdf23e
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/REVPHSimTest.java
@@ -0,0 +1,168 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.DoubleSolenoid;
+import edu.wpi.first.wpilibj.PneumaticHub;
+import edu.wpi.first.wpilibj.PneumaticsModuleType;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings("AbbreviationAsWordInName")
+class REVPHSimTest {
+  @Test
+  void testInitialization() {
+    HAL.initialize(500, 0);
+
+    REVPHSim sim = new REVPHSim(1);
+    sim.resetData();
+    assertFalse(sim.getInitialized());
+
+    BooleanCallback callback = new BooleanCallback();
+
+    try (CallbackStore cb = sim.registerInitializedCallback(callback, false);
+        PneumaticHub ph = new PneumaticHub(1)) {
+      assertTrue(sim.getInitialized());
+    }
+    assertFalse(sim.getInitialized());
+  }
+
+  @Test
+  void solenoidOutputTest() {
+    HAL.initialize(500, 0);
+
+    try (PneumaticHub ph = new PneumaticHub(1);
+        DoubleSolenoid doubleSolenoid = new DoubleSolenoid(1, PneumaticsModuleType.REVPH, 3, 4)) {
+      REVPHSim sim = new REVPHSim(ph);
+      sim.resetData();
+
+      BooleanCallback callback3 = new BooleanCallback();
+      BooleanCallback callback4 = new BooleanCallback();
+
+      try (CallbackStore cb3 = sim.registerSolenoidOutputCallback(3, callback3, false);
+          CallbackStore cb4 = sim.registerSolenoidOutputCallback(4, callback4, false)) {
+        // Reverse
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kReverse);
+        assertFalse(callback3.wasTriggered());
+        assertNull(callback3.getSetValue());
+        assertTrue(callback4.wasTriggered());
+        assertTrue(callback4.getSetValue());
+        assertFalse(sim.getSolenoidOutput(3));
+        assertTrue(sim.getSolenoidOutput(4));
+        assertEquals(0x10, ph.getSolenoids());
+
+        // Forward
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kForward);
+        assertTrue(callback3.wasTriggered());
+        assertTrue(callback3.getSetValue());
+        assertTrue(callback4.wasTriggered());
+        assertFalse(callback4.getSetValue());
+        assertTrue(sim.getSolenoidOutput(3));
+        assertFalse(sim.getSolenoidOutput(4));
+        assertEquals(0x8, ph.getSolenoids());
+
+        // Off
+        callback3.reset();
+        callback4.reset();
+        doubleSolenoid.set(DoubleSolenoid.Value.kOff);
+        assertTrue(callback3.wasTriggered());
+        assertFalse(callback3.getSetValue());
+        assertFalse(callback4.wasTriggered());
+        assertNull(callback4.getSetValue());
+        assertFalse(sim.getSolenoidOutput(3));
+        assertFalse(sim.getSolenoidOutput(4));
+        assertEquals(0x0, ph.getSolenoids());
+      }
+    }
+  }
+
+  @Test
+  void setCompressorOnTest() {
+    HAL.initialize(500, 0);
+
+    REVPHSim sim = new REVPHSim(1);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticHub ph = new PneumaticHub(1);
+        CallbackStore cb = sim.registerCompressorOnCallback(callback, false)) {
+      assertFalse(ph.getCompressor());
+      assertFalse(sim.getCompressorOn());
+      sim.setCompressorOn(true);
+      assertTrue(ph.getCompressor());
+      assertTrue(sim.getCompressorOn());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setClosedLoopEnabled() {
+    HAL.initialize(500, 0);
+
+    REVPHSim sim = new REVPHSim(1);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticHub ph = new PneumaticHub(1);
+        CallbackStore cb = sim.registerClosedLoopEnabledCallback(callback, false)) {
+      ph.setClosedLoopControl(false);
+      assertFalse(ph.getClosedLoopControl());
+
+      ph.setClosedLoopControl(true);
+      assertTrue(sim.getClosedLoopEnabled());
+      assertTrue(ph.getClosedLoopControl());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setPressureSwitchEnabledTest() {
+    HAL.initialize(500, 0);
+
+    REVPHSim sim = new REVPHSim(1);
+    BooleanCallback callback = new BooleanCallback();
+
+    try (PneumaticHub ph = new PneumaticHub(1);
+        CallbackStore cb = sim.registerPressureSwitchCallback(callback, false)) {
+      assertFalse(ph.getPressureSwitch());
+
+      sim.setPressureSwitch(true);
+      assertTrue(sim.getPressureSwitch());
+      assertTrue(ph.getPressureSwitch());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void setCompressorCurrentTest() {
+    HAL.initialize(500, 0);
+
+    REVPHSim sim = new REVPHSim(1);
+    DoubleCallback callback = new DoubleCallback();
+
+    try (PneumaticHub ph = new PneumaticHub(1);
+        CallbackStore cb = sim.registerCompressorCurrentCallback(callback, false)) {
+      assertFalse(ph.getPressureSwitch());
+
+      sim.setCompressorCurrent(35.04);
+      assertEquals(35.04, sim.getCompressorCurrent());
+      assertEquals(35.04, ph.getCompressorCurrent());
+      assertTrue(callback.wasTriggered());
+      assertEquals(35.04, callback.getSetValue());
+    }
+  }
+}
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
new file mode 100644
index 0000000..ba84fc5
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RelaySimTest.java
@@ -0,0 +1,258 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HAL;
+import edu.wpi.first.wpilibj.Relay;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import org.junit.jupiter.api.Test;
+
+class RelaySimTest {
+  @Test
+  void testInitializationBidrectional() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    sim.resetData();
+
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    assertFalse(sim.getInitializedForward());
+    assertFalse(sim.getInitializedReverse());
+
+    try (CallbackStore fwdCb = sim.registerInitializedForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerInitializedReverseCallback(reverseCallback, false);
+        Relay relay = new Relay(0)) {
+      assertTrue(sim.getInitializedForward());
+      assertTrue(sim.getInitializedReverse());
+
+      assertTrue(forwardCallback.wasTriggered());
+      assertTrue(forwardCallback.getSetValue());
+      assertTrue(reverseCallback.wasTriggered());
+      assertTrue(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testInitializationForwardOnly() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    sim.resetData();
+
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    assertFalse(sim.getInitializedForward());
+    assertFalse(sim.getInitializedReverse());
+
+    try (CallbackStore fwdCb = sim.registerInitializedForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerInitializedReverseCallback(reverseCallback, false);
+        Relay relay = new Relay(0, Relay.Direction.kForward); ) {
+      assertTrue(sim.getInitializedForward());
+      assertFalse(sim.getInitializedReverse());
+
+      assertTrue(forwardCallback.wasTriggered());
+      assertTrue(forwardCallback.getSetValue());
+      assertFalse(reverseCallback.wasTriggered());
+      assertNull(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testInitializationReverseOnly() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    sim.resetData();
+
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    assertFalse(sim.getInitializedForward());
+    assertFalse(sim.getInitializedReverse());
+
+    try (CallbackStore fwdCb = sim.registerInitializedForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerInitializedReverseCallback(reverseCallback, false);
+        Relay relay = new Relay(0, Relay.Direction.kReverse); ) {
+      assertFalse(sim.getInitializedForward());
+      assertTrue(sim.getInitializedReverse());
+
+      assertFalse(forwardCallback.wasTriggered());
+      assertNull(forwardCallback.getSetValue());
+      assertTrue(reverseCallback.wasTriggered());
+      assertTrue(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testBidirectionalSetForward() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    try (Relay relay = new Relay(0);
+        CallbackStore fwdCb = sim.registerForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerReverseCallback(reverseCallback, false)) {
+      relay.set(Relay.Value.kForward);
+      assertEquals(Relay.Value.kForward, relay.get());
+      assertTrue(sim.getForward());
+      assertFalse(sim.getReverse());
+
+      assertTrue(forwardCallback.wasTriggered());
+      assertTrue(forwardCallback.getSetValue());
+      assertFalse(reverseCallback.wasTriggered());
+      assertNull(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testBidirectionalSetReverse() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    try (Relay relay = new Relay(0);
+        CallbackStore fwdCb = sim.registerForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerReverseCallback(reverseCallback, false)) {
+      relay.set(Relay.Value.kReverse);
+      assertEquals(Relay.Value.kReverse, relay.get());
+      assertFalse(sim.getForward());
+      assertTrue(sim.getReverse());
+
+      assertFalse(forwardCallback.wasTriggered());
+      assertNull(forwardCallback.getSetValue());
+      assertTrue(reverseCallback.wasTriggered());
+      assertTrue(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testBidirectionalSetOn() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    try (Relay relay = new Relay(0);
+        CallbackStore fwdCb = sim.registerForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerReverseCallback(reverseCallback, false)) {
+      relay.set(Relay.Value.kOn);
+      assertEquals(Relay.Value.kOn, relay.get());
+      assertTrue(sim.getForward());
+      assertTrue(sim.getReverse());
+
+      assertTrue(forwardCallback.wasTriggered());
+      assertTrue(forwardCallback.getSetValue());
+      assertTrue(reverseCallback.wasTriggered());
+      assertTrue(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testBidirectionalSetOff() {
+    HAL.initialize(500, 0);
+
+    RelaySim sim = new RelaySim(0);
+    BooleanCallback forwardCallback = new BooleanCallback();
+    BooleanCallback reverseCallback = new BooleanCallback();
+
+    try (Relay relay = new Relay(0);
+        CallbackStore fwdCb = sim.registerForwardCallback(forwardCallback, false);
+        CallbackStore revCb = sim.registerReverseCallback(reverseCallback, false)) {
+      // Bootstrap into a non-off state to verify the callbacks
+      relay.set(Relay.Value.kOn);
+      forwardCallback.reset();
+      reverseCallback.reset();
+
+      relay.set(Relay.Value.kOff);
+      assertEquals(Relay.Value.kOff, relay.get());
+      assertFalse(sim.getForward());
+      assertFalse(sim.getReverse());
+
+      assertTrue(forwardCallback.wasTriggered());
+      assertFalse(forwardCallback.getSetValue());
+      assertTrue(reverseCallback.wasTriggered());
+      assertFalse(reverseCallback.getSetValue());
+    }
+  }
+
+  @Test
+  void testStopMotor() {
+    try (Relay relay = new Relay(0)) {
+      // Bootstrap into non-off state
+      relay.set(Relay.Value.kOn);
+
+      relay.stopMotor();
+      assertEquals(Relay.Value.kOff, relay.get());
+    }
+  }
+
+  @Test
+  void testForwardOnlyCannotGoReverse() {
+    try (Relay relay = new Relay(0, Relay.Direction.kForward)) {
+      relay.set(Relay.Value.kForward);
+      assertEquals(Relay.Value.kOn, relay.get());
+
+      relay.set(Relay.Value.kOff);
+      assertEquals(Relay.Value.kOff, relay.get());
+
+      assertThrows(Relay.InvalidValueException.class, () -> relay.set(Relay.Value.kReverse));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOn));
+    }
+  }
+
+  @Test
+  void testReverseOnlyCannotGoForwards() {
+    try (Relay relay = new Relay(0, Relay.Direction.kReverse)) {
+      relay.set(Relay.Value.kReverse);
+      assertEquals(Relay.Value.kOn, relay.get());
+
+      relay.set(Relay.Value.kOff);
+      assertEquals(Relay.Value.kOff, relay.get());
+
+      assertThrows(Relay.InvalidValueException.class, () -> relay.set(Relay.Value.kForward));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOn));
+    }
+  }
+
+  @Test
+  void testSwitchDirections() {
+    try (Relay relay = new Relay(0, Relay.Direction.kBoth)) {
+      // Start with both. Should be able to set all 4 values
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOff));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kForward));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kReverse));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOn));
+
+      // Switch it to forward only
+      relay.setDirection(Relay.Direction.kForward);
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOff));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kForward));
+      assertThrows(Relay.InvalidValueException.class, () -> relay.set(Relay.Value.kReverse));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOn));
+
+      // Switch it to Reverse only
+      relay.setDirection(Relay.Direction.kReverse);
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOff));
+      assertThrows(Relay.InvalidValueException.class, () -> relay.set(Relay.Value.kForward));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kReverse));
+      assertDoesNotThrow(() -> relay.set(Relay.Value.kOn));
+    }
+  }
+}
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
new file mode 100644
index 0000000..e9ea9f1
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/RoboRioSimTest.java
@@ -0,0 +1,211 @@
+// 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.simulation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.hal.HALUtil;
+import edu.wpi.first.wpilibj.RobotController;
+import edu.wpi.first.wpilibj.simulation.testutils.BooleanCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.DoubleCallback;
+import edu.wpi.first.wpilibj.simulation.testutils.IntCallback;
+import org.junit.jupiter.api.Test;
+
+public class RoboRioSimTest {
+  @Test
+  void testFPGAButton() {
+    RoboRioSim.resetData();
+
+    BooleanCallback callback = new BooleanCallback();
+    try (CallbackStore cb = RoboRioSim.registerFPGAButtonCallback(callback, false)) {
+      RoboRioSim.setFPGAButton(true);
+      assertTrue(RoboRioSim.getFPGAButton());
+      assertTrue(HALUtil.getFPGAButton());
+      assertTrue(callback.wasTriggered());
+      assertTrue(callback.getSetValue());
+
+      callback.reset();
+      RoboRioSim.setFPGAButton(false);
+      assertFalse(RoboRioSim.getFPGAButton());
+      assertFalse(HALUtil.getFPGAButton());
+      assertTrue(callback.wasTriggered());
+      assertFalse(callback.getSetValue());
+    }
+  }
+
+  @Test
+  void testSetVin() {
+    RoboRioSim.resetData();
+
+    DoubleCallback voltageCallback = new DoubleCallback();
+    DoubleCallback currentCallback = new DoubleCallback();
+    try (CallbackStore voltageCb = RoboRioSim.registerVInVoltageCallback(voltageCallback, false);
+        CallbackStore currentCb = RoboRioSim.registerVInCurrentCallback(currentCallback, false)) {
+      final double kTestVoltage = 1.91;
+      final double kTestCurrent = 35.04;
+
+      RoboRioSim.setVInVoltage(kTestVoltage);
+      assertTrue(voltageCallback.wasTriggered());
+      assertEquals(kTestVoltage, voltageCallback.getSetValue());
+      assertEquals(kTestVoltage, RoboRioSim.getVInVoltage());
+      assertEquals(kTestVoltage, RobotController.getInputVoltage());
+
+      RoboRioSim.setVInCurrent(kTestCurrent);
+      assertTrue(currentCallback.wasTriggered());
+      assertEquals(kTestCurrent, currentCallback.getSetValue());
+      assertEquals(kTestCurrent, RoboRioSim.getVInCurrent());
+      assertEquals(kTestCurrent, RobotController.getInputCurrent());
+    }
+  }
+
+  @Test
+  void testSetBrownout() {
+    RoboRioSim.resetData();
+
+    DoubleCallback voltageCallback = new DoubleCallback();
+    try (CallbackStore voltageCb =
+        RoboRioSim.registerBrownoutVoltageCallback(voltageCallback, false)) {
+      final double kTestVoltage = 1.91;
+
+      RoboRioSim.setBrownoutVoltage(kTestVoltage);
+      assertTrue(voltageCallback.wasTriggered());
+      assertEquals(kTestVoltage, voltageCallback.getSetValue());
+      assertEquals(kTestVoltage, RoboRioSim.getBrownoutVoltage());
+      assertEquals(kTestVoltage, RobotController.getBrownoutVoltage());
+    }
+  }
+
+  @Test
+  void test6V() {
+    RoboRioSim.resetData();
+
+    DoubleCallback voltageCallback = new DoubleCallback();
+    DoubleCallback currentCallback = new DoubleCallback();
+    BooleanCallback activeCallback = new BooleanCallback();
+    IntCallback faultCallback = new IntCallback();
+    try (CallbackStore voltageCb =
+            RoboRioSim.registerUserVoltage6VCallback(voltageCallback, false);
+        CallbackStore currentCb = RoboRioSim.registerUserCurrent6VCallback(currentCallback, false);
+        CallbackStore activeCb = RoboRioSim.registerUserActive6VCallback(activeCallback, false);
+        CallbackStore faultsCb = RoboRioSim.registerUserFaults6VCallback(faultCallback, false)) {
+      final double kTestVoltage = 22.9;
+      final double kTestCurrent = 174;
+      final int kTestFaults = 229;
+
+      RoboRioSim.setUserVoltage6V(kTestVoltage);
+      assertTrue(voltageCallback.wasTriggered());
+      assertEquals(kTestVoltage, voltageCallback.getSetValue());
+      assertEquals(kTestVoltage, RoboRioSim.getUserVoltage6V());
+      assertEquals(kTestVoltage, RobotController.getVoltage6V());
+
+      RoboRioSim.setUserCurrent6V(kTestCurrent);
+      assertTrue(currentCallback.wasTriggered());
+      assertEquals(kTestCurrent, currentCallback.getSetValue());
+      assertEquals(kTestCurrent, RoboRioSim.getUserCurrent6V());
+      assertEquals(kTestCurrent, RobotController.getCurrent6V());
+
+      RoboRioSim.setUserActive6V(false);
+      assertTrue(activeCallback.wasTriggered());
+      assertFalse(activeCallback.getSetValue());
+      assertFalse(RoboRioSim.getUserActive6V());
+      assertFalse(RobotController.getEnabled6V());
+
+      RoboRioSim.setUserFaults6V(kTestFaults);
+      assertTrue(faultCallback.wasTriggered());
+      assertEquals(kTestFaults, faultCallback.getSetValue());
+      assertEquals(kTestFaults, RoboRioSim.getUserFaults6V());
+      assertEquals(kTestFaults, RobotController.getFaultCount6V());
+    }
+  }
+
+  @Test
+  void test5V() {
+    RoboRioSim.resetData();
+
+    DoubleCallback voltageCallback = new DoubleCallback();
+    DoubleCallback currentCallback = new DoubleCallback();
+    BooleanCallback activeCallback = new BooleanCallback();
+    IntCallback faultCallback = new IntCallback();
+    try (CallbackStore voltageCb =
+            RoboRioSim.registerUserVoltage5VCallback(voltageCallback, false);
+        CallbackStore currentCb = RoboRioSim.registerUserCurrent5VCallback(currentCallback, false);
+        CallbackStore activeCb = RoboRioSim.registerUserActive5VCallback(activeCallback, false);
+        CallbackStore faultsCb = RoboRioSim.registerUserFaults5VCallback(faultCallback, false)) {
+      final double kTestVoltage = 22.9;
+      final double kTestCurrent = 174;
+      final int kTestFaults = 229;
+
+      RoboRioSim.setUserVoltage5V(kTestVoltage);
+      assertTrue(voltageCallback.wasTriggered());
+      assertEquals(kTestVoltage, voltageCallback.getSetValue());
+      assertEquals(kTestVoltage, RoboRioSim.getUserVoltage5V());
+      assertEquals(kTestVoltage, RobotController.getVoltage5V());
+
+      RoboRioSim.setUserCurrent5V(kTestCurrent);
+      assertTrue(currentCallback.wasTriggered());
+      assertEquals(kTestCurrent, currentCallback.getSetValue());
+      assertEquals(kTestCurrent, RoboRioSim.getUserCurrent5V());
+      assertEquals(kTestCurrent, RobotController.getCurrent5V());
+
+      RoboRioSim.setUserActive5V(false);
+      assertTrue(activeCallback.wasTriggered());
+      assertFalse(activeCallback.getSetValue());
+      assertFalse(RoboRioSim.getUserActive5V());
+      assertFalse(RobotController.getEnabled5V());
+
+      RoboRioSim.setUserFaults5V(kTestFaults);
+      assertTrue(faultCallback.wasTriggered());
+      assertEquals(kTestFaults, faultCallback.getSetValue());
+      assertEquals(kTestFaults, RoboRioSim.getUserFaults5V());
+      assertEquals(kTestFaults, RobotController.getFaultCount5V());
+    }
+  }
+
+  @Test
+  void test3V3() {
+    RoboRioSim.resetData();
+
+    DoubleCallback voltageCallback = new DoubleCallback();
+    DoubleCallback currentCallback = new DoubleCallback();
+    BooleanCallback activeCallback = new BooleanCallback();
+    IntCallback faultCallback = new IntCallback();
+    try (CallbackStore voltageCb =
+            RoboRioSim.registerUserVoltage3V3Callback(voltageCallback, false);
+        CallbackStore currentCb =
+            RoboRioSim.registerUserCurrent3V3Callback(currentCallback, false);
+        CallbackStore activeCb = RoboRioSim.registerUserActive3V3Callback(activeCallback, false);
+        CallbackStore faultsCb = RoboRioSim.registerUserFaults3V3Callback(faultCallback, false)) {
+      final double kTestVoltage = 22.9;
+      final double kTestCurrent = 174;
+      final int kTestFaults = 229;
+
+      RoboRioSim.setUserVoltage3V3(kTestVoltage);
+      assertTrue(voltageCallback.wasTriggered());
+      assertEquals(kTestVoltage, voltageCallback.getSetValue());
+      assertEquals(kTestVoltage, RoboRioSim.getUserVoltage3V3());
+      assertEquals(kTestVoltage, RobotController.getVoltage3V3());
+
+      RoboRioSim.setUserCurrent3V3(kTestCurrent);
+      assertTrue(currentCallback.wasTriggered());
+      assertEquals(kTestCurrent, currentCallback.getSetValue());
+      assertEquals(kTestCurrent, RoboRioSim.getUserCurrent3V3());
+      assertEquals(kTestCurrent, RobotController.getCurrent3V3());
+
+      RoboRioSim.setUserActive3V3(false);
+      assertTrue(activeCallback.wasTriggered());
+      assertFalse(activeCallback.getSetValue());
+      assertFalse(RoboRioSim.getUserActive3V3());
+      assertFalse(RobotController.getEnabled3V3());
+
+      RoboRioSim.setUserFaults3V3(kTestFaults);
+      assertTrue(faultCallback.wasTriggered());
+      assertEquals(kTestFaults, faultCallback.getSetValue());
+      assertEquals(kTestFaults, RoboRioSim.getUserFaults3V3());
+      assertEquals(kTestFaults, RobotController.getFaultCount3V3());
+    }
+  }
+}
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 6b9fe61..1daea96 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
@@ -1,31 +1,178 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import edu.wpi.first.hal.SimBoolean;
 import edu.wpi.first.hal.SimDevice;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import edu.wpi.first.hal.SimDevice.Direction;
+import edu.wpi.first.hal.SimValue;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
 
 class SimDeviceSimTest {
   @Test
   void testBasic() {
-    SimDevice dev = SimDevice.create("test");
-    SimBoolean devBool = dev.createBoolean("bool", false, false);
+    try (SimDevice dev = SimDevice.create("test")) {
+      SimBoolean devBool = dev.createBoolean("bool", Direction.kBidir, false);
 
-    SimDeviceSim sim = new SimDeviceSim("test");
-    SimBoolean simBool = sim.getBoolean("bool");
+      SimDeviceSim sim = new SimDeviceSim("test");
+      SimBoolean simBool = sim.getBoolean("bool");
 
-    assertFalse(simBool.get());
-    simBool.set(true);
-    assertTrue(devBool.get());
+      assertFalse(simBool.get());
+      simBool.set(true);
+      assertTrue(devBool.get());
+    }
+  }
+
+  @Test
+  void testDeviceCreatedCallback() {
+    AtomicInteger callback1Counter = new AtomicInteger(0);
+    AtomicInteger callback2Counter = new AtomicInteger(0);
+
+    try (SimDevice otherdev = SimDevice.create("testnotDC");
+        SimDevice dev1 = SimDevice.create("testDC1")) {
+      try (CallbackStore callback1 =
+              SimDeviceSim.registerDeviceCreatedCallback(
+                  "testDC",
+                  (name, handle) -> {
+                    callback1Counter.addAndGet(1);
+                  },
+                  false);
+          CallbackStore callback2 =
+              SimDeviceSim.registerDeviceCreatedCallback(
+                  "testDC",
+                  (name, handle) -> {
+                    callback2Counter.addAndGet(1);
+                  },
+                  true)) {
+        assertEquals(0, callback1Counter.get(), "Callback 1 called early");
+        assertEquals(
+            1,
+            callback2Counter.get(),
+            "Callback 2 called early or not initialized with existing devices");
+
+        SimDevice dev2 = SimDevice.create("testDC2");
+        dev2.close();
+
+        assertEquals(
+            1, callback1Counter.get(), "Callback 1 called either more than once or not at all");
+        assertEquals(2, callback2Counter.get(), "Callback 2 called either more or less than twice");
+      }
+
+      SimDevice dev3 = SimDevice.create("testDC3");
+      dev3.close();
+
+      assertEquals(1, callback1Counter.get(), "Callback 1 called after closure");
+      assertEquals(2, callback2Counter.get(), "Callback 2 called after closure");
+    }
+  }
+
+  @Test
+  void testDeviceFreedCallback() {
+    AtomicInteger counter = new AtomicInteger(0);
+
+    SimDevice dev1 = SimDevice.create("testDF1");
+    try (CallbackStore callback =
+        SimDeviceSim.registerDeviceFreedCallback(
+            "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");
+    }
+
+    SimDevice dev2 = SimDevice.create("testDF2");
+    dev2.close();
+
+    assertEquals(1, counter.get(), "Callback called after closure");
+  }
+
+  @Test
+  void testValueCreatedCallback() {
+    AtomicInteger callback1Counter = new AtomicInteger(0);
+    AtomicInteger callback2Counter = new AtomicInteger(0);
+
+    try (SimDevice dev1 = SimDevice.create("testVM1")) {
+      dev1.createBoolean("v1", Direction.kBidir, false);
+      SimDeviceSim sim = new SimDeviceSim("testVM1");
+      try (CallbackStore callback1 =
+              sim.registerValueCreatedCallback(
+                  (name, handle, readonly, value) -> {
+                    callback1Counter.addAndGet(1);
+                  },
+                  false);
+          CallbackStore callback2 =
+              sim.registerValueCreatedCallback(
+                  (name, handle, readonly, value) -> {
+                    callback2Counter.addAndGet(1);
+                  },
+                  true)) {
+        assertEquals(0, callback1Counter.get(), "Callback 1 called early");
+        assertEquals(
+            1,
+            callback2Counter.get(),
+            "Callback 2 called early or not initialized with existing devices");
+
+        dev1.createDouble("v2", Direction.kBidir, 0);
+
+        assertEquals(
+            1, callback1Counter.get(), "Callback 1 called either more than once or not at all");
+        assertEquals(2, callback2Counter.get(), "Callback 2 called either more or less than twice");
+      }
+      dev1.createBoolean("v3", Direction.kBidir, false);
+
+      assertEquals(1, callback1Counter.get(), "Callback 1 called after closure");
+      assertEquals(2, callback2Counter.get(), "Callback 2 called after closure");
+    }
+  }
+
+  @Test
+  void testValueChangedCallback() {
+    AtomicInteger callback1Counter = new AtomicInteger(0);
+    AtomicInteger callback2Counter = new AtomicInteger(0);
+
+    try (SimDevice dev1 = SimDevice.create("testVM1")) {
+      SimBoolean val = dev1.createBoolean("v1", Direction.kBidir, false);
+      SimDeviceSim sim = new SimDeviceSim("testVM1");
+      SimValue simVal = sim.getValue("v1");
+      try (CallbackStore callback1 =
+              sim.registerValueChangedCallback(
+                  simVal,
+                  (name, handle, readonly, value) -> {
+                    callback1Counter.addAndGet(1);
+                  },
+                  false);
+          CallbackStore callback2 =
+              sim.registerValueChangedCallback(
+                  simVal,
+                  (name, handle, readonly, value) -> {
+                    callback2Counter.addAndGet(1);
+                  },
+                  true)) {
+        assertEquals(0, callback1Counter.get(), "Callback 1 called early");
+        assertEquals(
+            1,
+            callback2Counter.get(),
+            "Callback 2 called early or not initialized with existing devices");
+
+        val.set(true);
+
+        assertEquals(
+            1, callback1Counter.get(), "Callback 1 called either more than once or not at all");
+        assertEquals(2, callback2Counter.get(), "Callback 2 called either more or less than twice");
+      }
+      val.set(false);
+      assertEquals(1, callback1Counter.get(), "Callback 1 called after closure");
+      assertEquals(2, callback2Counter.get(), "Callback 2 called after closure");
+    }
   }
 }
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSimTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSimTest.java
index f1b4bc8..26aa6b9 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSimTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/SingleJointedArmSimTest.java
@@ -1,28 +1,27 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.simulation;
 
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.system.plant.DCMotor;
-import edu.wpi.first.wpilibj.util.Units;
-import edu.wpi.first.wpiutil.math.VecBuilder;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import edu.wpi.first.math.VecBuilder;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.util.Units;
+import org.junit.jupiter.api.Test;
+
 public class SingleJointedArmSimTest {
-  SingleJointedArmSim m_sim = new SingleJointedArmSim(
-      DCMotor.getVex775Pro(2),
-      100,
-      3.0,
-      Units.inchesToMeters(19.0 / 2.0),
-      -Math.PI,
-      0.0, 10.0 / 2.2, true);
+  SingleJointedArmSim m_sim =
+      new SingleJointedArmSim(
+          DCMotor.getVex775Pro(2),
+          100,
+          3.0,
+          Units.inchesToMeters(19.0 / 2.0),
+          -Math.PI,
+          0.0,
+          10.0 / 2.2,
+          true);
 
   @Test
   public void testArmDisabled() {
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BooleanCallback.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BooleanCallback.java
new file mode 100644
index 0000000..f368d7f
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BooleanCallback.java
@@ -0,0 +1,19 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.HALValue;
+
+public class BooleanCallback extends CallbackHelperBase<Boolean> {
+  @Override
+  public void callback(String name, HALValue value) {
+    if (value.getType() != HALValue.kBoolean) {
+      throw new IllegalArgumentException("Wrong callback for type " + value.getType());
+    }
+
+    m_wasTriggered = true;
+    m_setValue = value.getBoolean();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BufferCallback.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BufferCallback.java
new file mode 100644
index 0000000..fdd8c06
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/BufferCallback.java
@@ -0,0 +1,27 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.simulation.ConstBufferCallback;
+import java.util.Arrays;
+
+public class BufferCallback implements ConstBufferCallback {
+  private boolean m_wasTriggered;
+  private byte[] m_setValue;
+
+  @Override
+  public void callback(String name, byte[] buffer, int count) {
+    m_wasTriggered = true;
+    m_setValue = Arrays.copyOf(buffer, buffer.length);
+  }
+
+  public boolean wasTriggered() {
+    return m_wasTriggered;
+  }
+
+  public byte[] getSetValue() {
+    return Arrays.copyOf(m_setValue, m_setValue.length);
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/CallbackHelperBase.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/CallbackHelperBase.java
new file mode 100644
index 0000000..4fb455e
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/CallbackHelperBase.java
@@ -0,0 +1,25 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.simulation.NotifyCallback;
+
+public abstract class CallbackHelperBase<T> implements NotifyCallback {
+  protected boolean m_wasTriggered;
+  protected T m_setValue;
+
+  public final boolean wasTriggered() {
+    return m_wasTriggered;
+  }
+
+  public final T getSetValue() {
+    return m_setValue;
+  }
+
+  public final void reset() {
+    m_wasTriggered = false;
+    m_setValue = null;
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/DoubleCallback.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/DoubleCallback.java
new file mode 100644
index 0000000..ccf53f3
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/DoubleCallback.java
@@ -0,0 +1,19 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.HALValue;
+
+public class DoubleCallback extends CallbackHelperBase<Double> {
+  @Override
+  public void callback(String name, HALValue value) {
+    if (value.getType() != HALValue.kDouble) {
+      throw new IllegalArgumentException("Wrong callback for type " + value.getType());
+    }
+
+    m_wasTriggered = true;
+    m_setValue = value.getDouble();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/EnumCallback.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/EnumCallback.java
new file mode 100644
index 0000000..2c585ac
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/EnumCallback.java
@@ -0,0 +1,19 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.HALValue;
+
+public class EnumCallback extends CallbackHelperBase<Long> {
+  @Override
+  public void callback(String name, HALValue value) {
+    if (value.getType() != HALValue.kEnum) {
+      throw new IllegalArgumentException("Wrong callback for type " + value.getType());
+    }
+
+    m_wasTriggered = true;
+    m_setValue = value.getLong();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/IntCallback.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/IntCallback.java
new file mode 100644
index 0000000..a1f65c4
--- /dev/null
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/simulation/testutils/IntCallback.java
@@ -0,0 +1,19 @@
+// 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.simulation.testutils;
+
+import edu.wpi.first.hal.HALValue;
+
+public class IntCallback extends CallbackHelperBase<Integer> {
+  @Override
+  public void callback(String name, HALValue value) {
+    if (value.getType() != HALValue.kInt) {
+      throw new IllegalArgumentException("Wrong callback for type " + value.getType());
+    }
+
+    m_wasTriggered = true;
+    m_setValue = (int) value.getLong();
+  }
+}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java
index cc92ed0..dfefddb 100644
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java
+++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java
@@ -1,21 +1,17 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.smartdashboard;
 
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableInstance;
 import edu.wpi.first.wpilibj.UtilityClassTest;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class SmartDashboardTest extends UtilityClassTest<SmartDashboard> {
   private final NetworkTable m_table = NetworkTableInstance.getDefault().getTable("SmartDashboard");
@@ -107,13 +103,14 @@
 
   @Test
   void putStringNullKeyTest() {
-    assertThrows(NullPointerException.class,
-        () -> SmartDashboard.putString(null, "This should not work"));
+    assertThrows(
+        NullPointerException.class, () -> SmartDashboard.putString(null, "This should not work"));
   }
 
   @Test
   void putStringNullValueTest() {
-    assertThrows(NullPointerException.class,
+    assertThrows(
+        NullPointerException.class,
         () -> SmartDashboard.putString("KEY_SHOULD_NOT_BE_STORED", null));
   }
 }
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
deleted file mode 100644
index 231eab6..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.util;
-
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.params.provider.Arguments.arguments;
-
-class ColorTest {
-  private static final double kEpsilon = 1e-3;
-
-  void assertColorMatches(double red, double green, double blue, Color color) {
-    assertAll(
-        () -> assertEquals(red, color.red, kEpsilon),
-        () -> assertEquals(green, color.green, kEpsilon),
-        () -> assertEquals(blue, color.blue, kEpsilon)
-    );
-  }
-
-  @ParameterizedTest
-  @MethodSource("staticColorProvider")
-  void staticColorTest(double red, double green, double blue, Color color) {
-    assertColorMatches(red, green, blue, color);
-  }
-
-  @ParameterizedTest
-  @MethodSource("staticColorProvider")
-  void colorEqualsTest(double red, double green, double blue, Color color) {
-    assertEquals(color, new Color(red, green, blue));
-  }
-
-  static Stream<Arguments> staticColorProvider() {
-    return Stream.of(
-      arguments(0.0823529412, 0.376470589, 0.7411764706, Color.kDenim),
-      arguments(0.0, 0.4, 0.7019607844, Color.kFirstBlue),
-      arguments(0.9294117648, 0.1098039216, 0.1411764706, Color.kFirstRed)
-    );
-  }
-}
diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ErrorMessagesTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ErrorMessagesTest.java
deleted file mode 100644
index 839e2e3..0000000
--- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ErrorMessagesTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-package edu.wpi.first.wpilibj.util;
-
-import org.junit.jupiter.api.Test;
-
-import edu.wpi.first.wpilibj.UtilityClassTest;
-
-import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-class ErrorMessagesTest extends UtilityClassTest<ErrorMessages> {
-  ErrorMessagesTest() {
-    super(ErrorMessages.class);
-  }
-
-  @Test
-  void requireNonNullParamNullTest() {
-    assertThrows(NullPointerException.class, () -> requireNonNullParam(null, "testParam",
-        "testMethod"));
-  }
-
-  @Test
-  void requireNonNullParamNotNullTest() {
-    assertDoesNotThrow(() -> requireNonNullParam("null", "testParam", "testMethod"));
-  }
-}
diff --git a/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini b/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini
index a685de0..f271944 100644
--- a/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini
+++ b/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini
@@ -1,7 +1,8 @@
 [NetworkTables Storage 3.0]
 double "/Preferences/checkedValueInt"=2
+; The omission of a leading zero is intentional for testing purposes
 double "/Preferences/checkedValueDouble"=.2
-double "/Preferences/checkedValueFloat"=3.14
+double "/Preferences/checkedValueFloat"=3.4
 double "/Preferences/checkedValueLong"=172
 string "/Preferences/checkedValueString"="Hello. How are you?"
 boolean "/Preferences/checkedValueBoolean"=false