Squashed 'third_party/allwpilib/' changes from 83f1860047..f1a82828fe

f1a82828fe [wpiutil] Add DataLog and DataLogManager Stop() (#5860)
2a04e12c6f [apriltag] AprilTagFieldLayout: Add accessors for origin and field dimensions (#5869)
33e0089afb Cleanup usages of std::function<void(void)> (#5864)
d06fa633d5 [build] Fix protobuf generation when building with make (#5867)
049732afb8 [cscore] Make camera connection logging clearer (#5866)
87f7c19f90 [wpimath] Make InterpolatingDoubleTreeMap constructor public (#5865)
6b53ef47cf [wpimath] Don't recreate TrapezoidProfile in ProfiledPIDController calculate() (#5863)
8a3a268ae6 [commands] Add finallyDo with zero-arg lambda (#5862)
1c35d42cd0 [wpilib] Pop diagnostic for deprecated function use (#5859)
ddc8db6c26 [wpimath] Add feedforward constant constructor to ElevatorSim (#5823)
c6aff2c431 [upstream_utils] Update to LLVM 17.0.4 (#5855)
a9c5b18a39 [build] Update OpenCV to 2024-4.8.0-2 (#5854)
9540b6922d [hal] Add CAN IDs for AndyMark and Vivid Hosting (#5852)
83a7d33c47 [glass] Improve display of protobuf/struct type strings (#5850)
a4a8ad9c75 [commands] Make Java SelectCommand generic (#5849)
9eecf2a456 [build] Add CMake option to build Java sources jars (#5768)
9536a311cb [wpilib] Add support for the PS5 DualSense controller (#5257)
8d5e6737fc [wpilibc] SolenoidSim: Add virtual destructor (#5848)
07e13d60a2 [ntcore] Fix write_impl (#5847)
1713386869 [wpiutil] ProtobufMessageDatabase: Fix out-of-order Add() rebuild (#5845)
35472f5fc9 [ntcore] Fix a use-after-free in client close (#5844)
ed168b522c [ntcore] Disable buf pool when asan is enabled (#5843)
3e7ba2cc6f [wpinet] WebSocket: Fix write behavior (#5841)
80c47da237 [sim] Disable the robot program when DS disconnects (#5818)
abe1cec90c [wpilib] Update Usage Reporting ResourceType from NI Libraries (#5842)
cdf981abba [glass] Fix position of data type in NT view (#5840)
04dcd80adb [build] Publish unit tests for examples (#5838)
49920234ac [build] Fix checkstyle rules to allow Windows paths (#5839)
366b715942 [wpilib] Fix SendableChooser test (#5835)
3ba501f947 [commands] Java: Fix CommandXboxController.leftTrigger() parameter order (#5831)
ec569a58ef [wpimath] Make KalmanTypeFilter interface public (#5830)
b91317fd36 [wpiutil] DataLog.addSchema(): Don't add into a set view (#5829)
2ab4fcbc24 [wpiutil] ProtobufMessageDatabase: Clear messages first (#5827)
98c14f1692 [wpimath] Add EKF/UKF u-y-R correct overload (#5832)
60bcdeded9 [ci] Disable java in sanitizer builds (#5833)
c87f8fd538 [commands] Add DeferredCommand (#5566)
ad80eb3a0b [ci] Update actions for comment-command (#5824)
c7d6ad5a0b [ntcore] WebSocketConnection: Use weak capture (#5822)
8a8e220792 [simgui] Add 'Invalid' option for AllianceStation (#5820)
cfc6a47f76 [sim] DS plugin: Fix off-by-one error when setting alliance station (#5819)
8efa586ace [ntcore] Don't check type string on publishing an entry (#5816)
23ea188e60 [glass] Add protobuf decode error log message (#5812)
928e87b4f4 [build] Add combined test meta-task (#5813)
63ef585d4b [wpiutil] Fix compilation of MathExtras.h on Windows with /sdl (#5809)
b03a7668f9 [build] Windows CMake/vcpkg fixes (#5807)
3f08bcde54 [hal] Fix HAL AllianceStation on rio (#5811)
196d963dc4 [ntcore] Fix off-by-one error in stream write (#5810)
f4cbcbc984 Fix typos (NFC) (#5804)
ec0f7fefb0 [myrobot] Update the myRobot JRE (#5805)
3d618bdbfd [wpiutil] Fix Java struct array unpacking (#5801)
1fa7445667 [ntcore] Check for valid client in incoming text and binary (#5799)
269b9647da [ci] Update JDK for combine step (#5794)
bee32f080e [docs] Add wpiunits to JavaDocs (#5793)
25dad5a531 [wpinet] TCPConnector_parallel: Don't use thread_local (#5791)
4a93581f1a [build] cmake: use default library type for libglassnt, libglass, wpigui, and imgui (#5797)
abb2857e03 [wpilib] Counter: Fix default distance per pulse, add distance and rate to C++ (#5796)
b14a61e1c0 [readme] Add link to QuickBuffers release page (#5795)
cf54d9ccb7 [wpiutil, ntcore] Add structured data support (#5391)
ecb7cfa9ef [wpimath] Add Exponential motion profile (#5720)
7c6fe56cf2 [ntcore] Fix crash on disconnect (#5788)
85147bf69e [wpinet] WebSocketSerializer: Fix UB (#5787)
244163acad [wpinet] uv::Stream::TryWrite(): Return 0 on EAGAIN (#5784)
820728503d [hal] Remove extra semicolon in RoboRioData (#5786)
45f307d87e [upstream_utils] Upgrade to LLVM 17.0.3 (#5785)
4ce4d63efc [wpilibj] Fix RobotBase.isSimulation() (#5783)
579007ceb3 [commands] Add requirements parameter to Commands.idle() (#5774)
3f3a169149 [wpilib] Make physics sim setState() functions public (#5779)
7501e4ac88 [wpilib] Close sim device in ADIS IMUs (#5776)
99630d2e78 [wpimath] Upgrade to EJML 0.43.1 (#5778)
02cbbc997d [wpimath] Make Vector-Vector binary operators return Vector (#5772)
ed93889e17 [examples] Fix typo in TimesliceRobot example name (#5773)
da70e4c262 [docs] Add jinja2 to CMake prerequisites (#5771)
e814595ea7 [wpimath] Add ChassisSpeeds.fromRobotRelativeSpeeds() (#5744)
f98c943445 [wpimath] LinearSystemId: Add DCMotorSystem overload (#5770)
b3eb64b0f7 [wpiutil] ct_string: Use inline namespace for literals (#5767)
7d9ba256c2 Revert "[build] Add CMake option to build Java source jars (#5756)" (#5766)
1f6492e3d8 [sysid] Update JSON library usage (#5765)
638f04f626 [wpiutil] Add protobuf to thirdparty sources (#5746)
210255bfff [wpiutil] Update json to 3.11.2 (#5680)
896772c750 [wpimath] Add DCMotor functions for Kraken X60 and Neo Vortex (#5759)
fd427f6c82 [wpimath] Fix hardcoded module count in SwerveDriveKinematics.resetHeading() (#5762)
c0b4c6cce6 [wpimath] Add overloads for Transform2d and Transform3d (#5757)
9a0aafd8ab [examples] Make swerve examples multiply desired module speeds by cosine of heading error (#5758)
1c724884ca [build] Add CMake option to build Java source jars (#5756)
5b0db6b93e [ci] Forward CI as well (#5755)
f8cbbbac12 [ci] Take 2 on passing GITHUB_REF (#5754)
b9944be09c [ci] Pass GITHUB_REF to docker container (#5753)
de5e4eda6c [build] Update apriltag, libssh, googletest for 2024 (#5752)
227e660e20 [upstream_utils] Upgrade to LLVM 17.0.2 (#5750)
36f94c9f21 [commands,romi,xrp] Add frcYear to vendordep (#5747)
741d166457 [glass] NT view: enhance array support (#5732)
1d23513945 [ntcore] Fix string array value comparison (#5745)
ff1849052e [commands] Make command scheduling order consistent (#5470)
58e8474368 [build] Disable armsimulation unit test (#5739)
fb07b0da49 [examples] Add XRP C++ Examples and Templates (#5743)
81893ad73d Run wpiformat with clang-format 17 (#5740)
faa1e665ba [wpimath] Add ElevatorFeedforward.calculate(currentV, nextV) overload (#5715)
a789632052 [build] Update to native utils 2024.3.1 (#5738)
8f60ab5182 [build] Update OpenCV to 2024-4.8.0-1 (#5737)
33243f982b [wpimath] Expand Quaternion class with additional operators (#5600)
420f2f7c80 [ntcore] Add RTT-only subprotocol (#5731)
2b63e35ded [ntcore] Fix moving outgoing queue to new period (#5735)
be939cb636 [ntcore] Fix notification of SetDefaultEntryValue (#5733)
69a54de202 [build] Update enterprise plugin (#5730)
fef03a3ff5 [commands] Clean up C++ includes after Requirements was added (#5719)
8b7c6852cf [ntcore] Networking improvements (#5659)
1d19e09ca9 [wpiutil] Set WPI_{UN}IGNORE_DEPRECATED to empty when all else fails (#5728)
58141d6eb5 [wpilib] Make BooleanEvent more consistent (#5436)
6576d9b474 [wpilib] SendableChooser: implement Sendable instead of NTSendable (#5718)
a4030c670f [build] Update to gradle 8.4, enable win arm builds (#5727)
0960f11eba [wpinet] Revert removal of uv_clock_gettime() (#5723)
cb1bd0a3be [wpiutil] Get more precise system time on Windows (#5722)
4831277ffe [wpigui] Fix loading a maximized window on second monitor (#5721)
3eb372c25a [wpiutil] SendableBuilder: Add PublishConst methods (#5158)
1fec8596a4 [ci] Fix -dirty version (#5716)
f7e47d03f3 [build] Remove unnecessary CMake config installs (#5714)
a331ed2374 [sysid] Add SysId (#5672)
8d2cbfce16 [wpiutil] DataLog: Stop logging if insufficient free space (#5699)
48facb9cef [ntcoreffi] Add DataLogManager (#5702)
aecbcb08fc [ntcore] Correctly start DataLog for existing publishers (#5703)
5e295dfbda [wpiutil] DataLog: Limit total buffer allocation (#5700)
c7c7e05d9d [ci] Unbreak combiner (#5698)
c92bad52cb [wpilib] DataLogManager: Use system time valid function (#5697)
d404af5f24 [wpilib] RobotController: Add isSystemTimeValid() (#5696)
e56f1a3632 [ci] Run combine but skip all steps (#5695)
8f5bcad244 [ci] Use sccache for cmake builds (#5692)
703dedc4a6 [ci] Upgrade get-cmake action to fix node12 deprecation warning (#5694)
c69a0d7504 [ci] Don't run example unit test that segfaults (#5693)
66358d103e Add menu items for online docs to GUI tools (#5689)
4be8384a76 [ci] Disable combine on PR builds (#5691)
90288f06a6 [ci] Fix Gradle disk space issues (#5688)
9e9583412e [wpigui] Make wpi::gui::OpenURL() fork the process first (#5687)
d4fcd80b7b [ci] Gradle: Use container only for build step (#5684)
7b70e66772 [outlineviewer] Fix thirdparty library include sorting (#5683)
5f651df5d5 [build] Clean up Gradle configs (#5685)
65b26738d5 Add CMakeSettings.json to gitignore (#5682)
d0305951ad Fix GitHub inline warnings (#5681)
e8d4a20331 [build][cmake] Fix windows tests and re-enable CI tests (#5674)
2b58bbde0b [xrp] Add Reflectance sensor and rangefinder classes (#5673)
dd5612fbee [json] Add forward definition header (#5676)
eab44534c3 [wpimath] Remove unused SmallString include (#5677)
5ab54ff760 Replace wpi::raw_istream with wpi::MemoryBuffer (#5675)
1b6ec5a95d [wpiutil] Upgrade to LLVM 17.0.1 (#5482)
07a0d22fe6 [build] Build examples in CMake CI (#5667)
97021f074a [build] Upgrade imgui and implot (#5668)
87ce1e3761 [build] Fix wpilibNewCommands CMake install (#5671)
6ef94de9b5 [wpimath] Add tests for ArmFeedforward and ElevatorFeedforward (#5663)
c395b29fb4 [wpinet] Add WebSocket::TrySendFrames() (#5607)
c4643ba047 [romi/xrp] Fix version typo in vendordep json (#5664)
51dcb8b55a [examples] Make Romi/XRP Examples use appropriate vendordeps (#5665)
daf7702007 [build] Test each example in a new environment (#5662)
e67df8c180 [wpilib] Const-qualify EncoderSim getters (#5660)
7be290147c [wpiutil] Refactor SpanMatcher and TestPrinters from ntcore (#5658)
9fe258427a [commands] Add proxy factory to Commands (#5603)
633c5a8a22 [commands] Add C++ Requirements struct (#5504)
b265a68eea [commands] Add interruptor parameter to onCommandInterrupt callbacks (#5461)
e93c233d60 [ntcore] Compute Value memory size when creating value (#5657)
5383589f99 [wpinet] uv::Request: Return shared_ptr from Release() (#5656)
40b552be4a [wpinet] uv::Stream: Return error from TryWrite() (#5655)
202a75fe08 [wpinet] RequestImpl: Avoid infinite loop in shared_from_this() (#5654)
8896515eb7 [wpinet] uv::Buffer: Add bytes() accessor (#5653)
ae59a2fba2 [wpinet] uv::Error: Change default error to 0 (#5652)
3b51ecc35b [wpiutil] SpanExtras: Add take_back and take_front (#5651)
17f1062885 Replace std::snprintf() with wpi::format_to_n_c_str() (#5645)
bb39900353 [romi/xrp] Add Romi and XRP Vendordeps (#5644)
cb99517838 [build] cmake: Use default install location on windows for dlls (#5580)
25b0622d4c [build] Add Windows CMake CI (#5516)
34e7849605 Add warning to development builds instructions (NFC) (#5646)
e9e611c9d8 [cameraserver] Remove CameraServer.SetSize() (#5650)
94f58cc536 [wpilib] Remove Compressor.Enabled() (#5649)
4da5aee88a [wpimath] Remove SlewRateLimiter 2 argument constructor (#5648)
2e3ddf5502 Update versions in development builds instructions to 2024 (#5647)
19a8850fb1 [examples] Add TimesliceRobot templates (#3683)
9047682202 [sim] Add XRP-specific plugin (#5631)
575348b81c [wpilib] Use IsSimulation() consistently (#3534)
12e2043b77 [wpilib] Clean up Notifier (#5630)
4bac4dd0f4 [wpimath] Move PIDController from frc2 to frc namespace (#5640)
494cfd78c1 [wpiutil] Fix deprecation warning in LLVM for C++23 (#5642)
43a727e868 [apriltag] Make loadAprilTagFieldLayout throw an unchecked exception instead (#5629)
ad4b017321 [ci] Use Ninja for faster builds (#5626)
4f2114d6f5 Fix warnings from GCC 13 release build (#5637)
e7e927fe26 [build] Also compress debug info for CMake RelWithDebInfo build type (#5638)
205a40c895 [build] Specify zlib for debug info compression (#5636)
707444f000 [apriltag] Suppress -Wtype-limits warning in asserts from GCC 13 (#5635)
3b79cb6ed3 [commands] Revert SubsystemBase deprecation/removal (#5634)
bc7f23a632 [build] Compress Linux debug info (#5633)
57b2d6f254 [build] Update to image 2024 v1.0 (#5625)
339ef1ea39 [wpilib] DataLogManager: Warn user if logging to RoboRIO 1 internal storage (#5617)
7a9a901a73 [build] Fix cmake config files (#5624)
298f8a6e33 [wpilib] Add Mechanism2d tests and make Java impl match C++ (#5527)
d7ef817bae [apriltag] Update apriltag library (#5619)
c3fb31fd0e [docs] Switch to Java 17 api docs (#5613)
bd64f81cf9 [build] Run Google tests in release mode in CI (#5615)
66e6bd81ea [wpimath] Cleanup wpimath/algorithms.md (NFC) (#5621)
4fa56fd884 [build] Add missing find_dependency call (#5623)
f63d958995 [build] Update to native utils 2024.2.0 (#5601)
a9ab08f48b [wpimath] Rename ChassisSpeeds.fromDiscreteSpeeds() to discretize() (#5616)
8e05983a4a [wpimath] Add math docs to plant inversion feedforward internals (NFC) (#5618)
3a33ce918b [ntcore] Add missing StringMap include (#5620)
a6157f184d [wpiutil] timestamp: Add ShutdownNowRio (#5610)
e9f612f581 [build] Guard policy setting for CMake versions below 3.24 (#5612)
1a6df6fec6 [wpimath] Fix DARE Q decomposition (#5611)
9b3f7fb548 [build] Exclude IntelliJ folders from spotless XML (#5602)
814f18c7f5 [wpimath] Fix computation of C for DARE (A, C) detectability check (#5609)
ac23f92451 [hal] Add GetTeamNumber (#5596)
a750bee54d [wpimath] Use std::norm() in IsStabilizable() (#5599)
8e2465f8a0 [wpimath] Add arithmetic functions to wheel speeds classes (#5465)
10d4f5b5df [wpimath] Clean up notation in DARE precondition docs (#5595)
b2dd59450b [hal] Fix unfinished/incorrect GetCPUTemp functions (#5598)
99f66b1e24 [wpimath] Replace frc/EigenCore.h typedefs with Eigen's where possible (#5597)
383289bc4b [build] Make custom CMake macros use lowercase (#5594)
45e7720ec1 [build] Add error message when downloading files in CMake (#5593)
4e0d785356 [wpimath] ChassisSpeeds: document that values aren't relative to the robot (NFC) (#5551)
3c04580a57 [commands] ProxyCommand: Use inner command name in unique_ptr constructor (#5570)
cf19102c4a [commands] SelectCommand: Fix leakage and multiple composition bug (#5571)
171375f440 [ntcoreffi] Link to NI libraries (#5589)
89add5d05b Disable flaky tests (#5591)
a8d4b162ab [ntcore] Remove RPC manual tests (#5590)
39a73b5b58 [commands] C++: Add CommandPtr supplier constructor to ProxyCommand (#5572)
36d514eae7 [commands] Refactor C++ ScheduleCommand to use SmallSet (#5568)
52297ffe29 [commands] Add idle command (#5555)
67043a8eeb [wpimath] Add angular jerk unit (#5582)
51b0fb1492 [wpimath] Fix incorrect header inclusion in angular_acceleration.h (#5587)
b7657a8e28 [wpimath] Split WPIMathJNI into logical chunks (#5552)
ea17f90f87 [build] Fix tool builds with multiple arm platforms installed (#5586)
f1d7b05723 [wpimath] Clean up unit formatter (#5584)
d7264ff597 Replace wpi::errs() usage with fmtlib (#5560)
ab3bf39e0e [wpiutil] Upgrade to fmt 10.1.1 (#5585)
165ebe4c79 Upgrade to fmt 10.1.0 (#5326)
8e2a7fd306 Include thirdparty libraries with angle brackets (#5578)
e322ab8e46 [wpimath] Fix docs for DARE ABQRN stabilizability check (NFC) (#5579)
360fb835f4 [upstream_utils] Handle edge case in filename matches (#5576)
9d86624c00 [build] Fix CMake configure warnings (#5577)
969979d6c7 [wpiutil] Update to foonathan memory 0.7-3 (#5573)
0d2d989e84 [wpimath] Update to gcem 1.17.0 (#5575)
cf86af7166 [wpiutil] Update to mpack 1.1.1 (#5574)
a0c029a35b [commands] Fix dangling SelectCommand documentation (NFC) (#5567)
349141b91b [upstream_utils] Document adding a patch (NFC) (#5432)
7889b35b67 [wpimath] Add RamseteController comparison to LTV controller docs (NFC) (#5559)
b3ef536677 [build] Ignore nt/sim json files in spotless (#5565)
ed895815b5 [build] Compile Java with UTF-8 encoding (#5564)
2e4ad35e36 [wpiutil] jni_util: Add JSpan and CriticalJSpan (#5554)
8f3d6a1d4b [wpimath] Remove discretizeAQTaylor() (#5562)
7c20fa1b18 [wpimath] Refactor DARE tests to reduce RAM usage at compile time (#5557)
89e738262c [ntcore] Limit buffer pool size to 64KB per connection (#5485)
96f7fa662e Upgrade Maven dependencies (#5553)
7a2d336d52 [wpinet] Leak multicast handles during windows shutdown (#5550)
f9e2757d8f [wpimath] Use JDoubleArrayRef in all JNI functions (#5546)
0cf6e37dc1 [wpimath] Make LTV controller constructors use faster DARE solver (#5543)
6953a303b3 [build] Fix the windows build with fmt (#5544)
7a37e3a496 [wpimath] Correct Rotation3d::RotateBy doc comment (NFC) (#5541)
186b409e16 [wpimath] Remove internal Eigen header include (#5539)
03764dfe93 [wpimath] Add static matrix support to DARE solver (#5536)
394cfeadbd [wpimath] Use SDA algorithm instead of SSCA for DARE solver (#5526)
a4b7fde767 [wpilib] Add mechanism specific SetState overloads to physics sims (#5534)
8121566258 [wpimath] Fix CoordinateSystem.convert() Transform3d overload (#5532)
b542e01a0b [glass] Fix array crash when clearing existing workspace (#5535)
e2e1b763b2 [wpigui] Fix PFD file dialogs not closing after window closing (#5530)
86d7bbc4e4 [examples] Add Java Examples and Templates for the XRP (#5529)
e8b5d44752 [wpimath] Make Java Quaternion use doubles instead of Vector (#5525)
38c198fa64 [myRobot] Add apriltags to myRobot build (#5528)
00450c3548 [wpimath] Upgrade to EJML 0.42 (#5531)
faf3cecd83 [wpimath] Don't copy Matrix and underlying storage in VecBuilder (#5524)
6b896a38dc [build] Don't enforce WITH_FLAT_INSTALL with MSVC (part 2) (#5517)
c01814b80e [wpiutil] Add C API for DataLog (#5509)
b5bd0771eb [wpimath] Document extrinsic vs intrinsic rotations (NFC) (#5508)
84ed8aec05 [build] Don't enforce WITH_FLAT_INSTALL with MSVC (#5515)
999f677d8c [ntcoreffi] Add WPI_Impl_SetupNowRio to exported symbols (#5510)
338f37d302 Fix header sorting of libssh (#5507)
75cbd9d6d0 [glass] Add background color selector to glass plots (#5506)
e2c190487b [examples] Add flywheel bang-bang controller example (#4071)
c52dad609e [wpinet] WebSocket: Send pong in response to ping (#5498)
e2d17a24a6 [hal] Expose power rail disable and cpu temp functionality (#5477)
3ad5d2e42d [hal,wpiutil] Use HMB for FPGA Timestamps (#5499)
b46a872494 [ntcore] Remove pImpl from implementation (#5480)
d8c59ccc71 [wpimath] Add tests for MathUtil clamp() and interpolate() (#5501)
0552c8621d [glass,ov] Improve Glass and OutlineViewer title bar message (#5502)
90e37a129f [wpiutil,wpimath] Add generic InterpolatingTreeMap (#5372)
d83a6edc20 [wpilib] Update GetMatchTime docs and units (#5232)
6db2c42966 [wpimath] Trajectory: Throw on empty lists of States (#5497)
21439b606c [wpimath] Disallow LTV controller max velocities above 15 m/s (#5495)
7496e0d208 [ntcore] Value: More efficiently store arrays (#5484)
0c93aded8a [wpimath] Change kinematics.ToTwist2d(end - start) to kinematics.ToTwist2d(start, end) (#5493)
815a8403e5 [wpimath] Give infeasible trajectory constraints a better exception message (#5492)
35a8b129d9 [wpimath] Add RotateBy() function to pose classes (#5491)
26d6e68c8f [upstream_utils] Add GCEM to CI (#5483)
6aa469ae45 [wpilib] Document how to create LinearSystem object for physics sim classes (NFC) (#5488)
a01b6467d3 [wpimath] Link to docs on LQR and KF tolerances (#5486)
d814f1d123 [wpimath] Fix copy-paste error from Pose2d docs (NFC) (#5490)
98f074b072 [wpimath] Add folder prefix to geometry includes (#5489)
e9858c10e9 [glass] Add tooltips for NT settings (#5476)
12dda24f06 [examples] Fix C robot template not correctly looping (#5474)
fc75d31755 [apriltag] Update apriltaglib (#5475)
a95994fff6 [wpiutil] timestamp: Call FPGA functions directly (#5235)
2ba8fbb6f4 [wpimath] Improve documentation for SwerveModulePosition::operator- (#5468)
b8cdf97621 [build] Prepare for Windows arm64 builds (#5390)
552f4b76b5 [wpimath] Add FOC-enabled Falcon constants to the DCMotor class (#5469)
1938251436 [examples] Add Feedforward to ElevatorProfiledPid (#5300)
873c2a6c10 [examples] Update ElevatorTrapezoidProfile example (#5466)
99b88be4f3 [wpilib] Reduce usage of NTSendable (#5434)
d125711023 [hal] Fix Java REVPH faults bitfield (take 2) (#5464)
c3fab7f1f2 [ntcore] Don't update timestamp when value is unchanged (#5356)
5ec7f18bdc [wpilib] EventLoop docs: Remove BooleanEvent references (NFC) (#5463)
c065ae1fcf [wpiunits] Add subproject for a Java typesafe unit system (#5371)
44acca7c00 [wpiutil] Add ClassPreloader (#5365)
88b11832ec [hal] Fix Java REVPH faults bitfield (#5148)
fb57d82e52 [ntcore] Enhance Java raw value support
3a6e40a44b [wpiutil] Enhance DataLog Java raw value support
8dae5af271 [wpiutil] Add compile-time string utilities (ct_string) (#5462)
fc56f8049a [wpilib] DriverStation: Change alliance station to use optional (#5229)
ef155438bd [build] Consume libuv via cmake config instead of via pkg-config (#5438)
86e91e6724 [wpimath] Refactor TrapezoidProfile API (#5457)
72a4543493 [wpilib] DutyCycleEncoderSim: Expand API (#5443)
657338715d [wpimath] Add ChassisSpeeds method to fix drifting during compound swerve drive maneuvers (#5425)
1af224c21b Add missing <functional> includes (#5459)
0b91ca6d5a [wpilib] SendableChooser: Add onChange listener (#5458)
6f7cdd460e [wpimath] Pose3d: Switch to JNI for exp and log (#5444)
c69e34c80c [wpimath] ChassisSpeeds: Add arithmetic functions (#5293)
335e7dd89d [wpilib] Simulation: Add ctor parameter to set starting state of mechanism sims (#5288)
14f30752ab [wpilib] Deprecate Accelerometer and Gyro interfaces (#5445)
70b60e3a74 [commands] Trigger: Fix method names in requireNonNullParam (#5454)
593767c8c7 [wpimath] Improve Euler angle calculations in gimbal lock (#5437)
daf022d3da [build] Make devImplementation inherit from implementation (#5450)
9b8d90b852 [examples] Convert the unitless joystick inputs to actual physical units (#5451)
1f6428ab63 [ntcore] Fix undefined comparison behavior when array is empty (#5448)
17eb9161cd Update code owners for removal of old commands (#5447)
3c4b58ae1e [wpinet] Upgrade to libuv 1.46.0 (#5446)
aaea85ff16 [commands] Merge CommandBase into Command and SubsystemBase into Subsystem (#5392)
7ac932996a [ci] Use PAT for workflow dispatch (#5442)
efe1987e8b [ci] Trigger pages repo workflow (#5441)
828bc5276f [wpiutil] Upgrade to LLVM 16.0.6 (#5435)
701df9eb87 [ci] Change documentation publish to single-commit (#5440)
e5452e3f69 [wpiutil] Add WPICleaner and an example how to use it (#4850)
7a099cb02a [commands] Remove deprecated classes and functions (#5409)
b250a03944 [wpilib] Add function to wait for DS Connection (#5230)
a6463ed761 [wpiutil] Fix unused variable warning in release build (#5430)
f031513470 [ntcore] NetworkTable::GetSubTables(): Remove duplicates (#5076)
f8e74e2f7c [hal] Unify PWM simulation Speed, Position, and Raw (#5277)
fd5699b240 Remove references to Drake (#5427)
e2d385d80a [build] cmake: Respect USE_SYSTEM_FMTLIB (#5429)
d37f990ce3 [hal] Fix HAL Relay/Main doc module (NFC) (#5422)
a7a8b874ac [docs] Expand HAL_ENUM in doxygen docs (#5421)
3a61deedde [wpimath] Rotation2d: Only use gcem::hypot when constexpr evaluated (#5419)
96145de7db [examples] Fix formatting (NFC) (#5420)
fffe6a7b9a [examples] Improve Pneumatics example coverage in Solenoid and RapidReactCmdBot examples (#4998)
6b5817836d [wpimath] Add tolerance for some tests (#5416)
3233883f3e [cscore] Fix warnings on macos arm (#5415)
c4fc21838f [commands] Add ConditionalCommand getInterruptionBehavior (#5161)
89fc51f0d4 Add tests for SendableChooser and Command Sendable functionality (#5179)
663bf25aaf [docs] Generate docs for symbols in __cplusplus (#5412)
fe32127ea8 [command] Clean up Command doc comments (NFC) (#5321)
c1a01569b4 [wpilib][hal] PWM Raw using microseconds (#5283)
1fca519fb4 [wpiutil] Remove remnants of ghc fs and tcb_span libraries (#5411)
90602cc135 [github] Update issue template to collect more project info (#5090)
34412ac57e [build] Exclude files in bin from Spotless (#5410)
61aa60f0e3 [wpilib] Add robot callback that is called when the DS is initially connected (#5231)
ebae341a91 [commands] Add test for subsystem registration and periodic (#5408)
5d3a133f9f Remove spaces in NOLINT comments (#5407)
3a0e484691 [wpimath] Fix clang-tidy warnings (#5403)
eb3810c765 [wpiutil] Fix clang-tidy warnings (#5406)
c4dc697192 [hal] WS Simulation: Add message filtering capability (#5395)
0eccc3f247 [ntcore] Fix clang-tidy warnings (#5405)
f4dda4bac0 [hal] Add javadocs for JNI (NFC) (#5298)
1c20c69793 [cscore] Fix clang-tidy warnings (#5404)
1501607e48 [commands] Fix clang-tidy warnings (#5402)
991f4b0f62 [wpimath] PIDController: Add IZone (#5315)
f5b0d1484b [wpimath] Add isNear method to MathUtil (#5353)
2ce248f66c [hal] Fix clang-tidy warnings (#5401)
5fc4aee2d2 [wpimath] SwerveDriveKinematics: Rename currentChassisSpeed to desiredChassisSpeed (#5393)
50b90ceb54 [wpimath] SwerveDriveKinematics: Add reset method (#5398)
316cd2a453 [commands] Notify DriverStationSim in CommandTestBaseWithParam (#5400)
d4ea5fa902 [cscore] VideoMode: Add equals override (Java) (#5397)
d6bd72d738 [wpimath] ProfiledPIDController: Add getConstraints (#5399)
25ad5017a9 [wpimath] Refactor kinematics, odometry, and pose estimator (#5355)
5c2addda0f [doc] Add missing pneumatics docs (NFC) (#5389)
c3e04a6ea2 Fix loading tests on macos 12 (#5388)
d5ed9fb859 [wpimath] Create separate archive with just units headers (#5383)
901ab693d4 [wpimath] Use UtilityClassTest for more utility classes (#5384)
9d53231b01 [wpilib] DataLogManager: Add warning for low storage space (#5364)
d466933963 [wpiutil] Group doxygen into MPack module (#5380)
652d1c44e3 [wpiutil] Upgrade to macOS 12 to remove concept shims (#5379)
6414be0e5d [wpimath] Group units doxygen modules (#5382)
7ab5800487 [wpiutil] Fix docs typo in SmallVector (#5381)
59905ea721 Replace WPI_DEPRECATED() macro with [[deprecated]] attribute (#5373)
753cb49a5e [ntcore] Fix doxygen module in generated C types (NFC) (#5374)
1c00a52b67 [hal] Expose CAN timestamp base clock (#5357)
91cbcea841 Replace SFINAE with concepts (#5361)
d57d1a4598 [wpimath] Remove unnecessary template argument from unit formatter (#5367)
5acc5e22aa [wpimath] Only compute eigenvalues with EigenSolvers (#5369)
d3c9316a97 extend shuffleboard test timeout (#5377)
1ea868081a [ci] Fix /format command (#5376)
5fac18ff4a Update formatting to clang-format 16 (#5370)
a94a998002 [wpimath] Generalize Eigen formatter (#5360)
125f6ea101 [wpimath] Make SwerveDriveKinematics::ToChassisSpeeds() take const-ref argument (#5363)
51066a5a8a [wpimath] Move unit formatters into units library (#5358)
282c032b60 [wpilibc] Add unit-aware Joystick.GetDirection() (#5319)
073d19cb69 [build] Fix CMake warning (#5359)
01490fc77b [wpiutil] DataLog: Add documentation for append methods (NFC) (#5348)
c9b612c986 [wpilibcExamples] Make C++ state-space elevator KF and LQR match Java (#5346)
eed1e6e3cb [wpimath] Replace DiscretizeAQTaylor() with DiscretizeAQ() (#5344)
c976f40364 [readme] Document how to run examples in simulation (#5340)
4d28bdc19e [ci] Update Github Pages deploy action parameters (#5343)
e0f851871f [ci] Fix github pages deploy version (#5342)
063c8cbedc Run wpiformat (NFC) (#5341)
96e41c0447 [ci] Update deploy and sshagent actions (#5338)
fd294bdd71 [build] Fix compilation with GCC 13 (#5322)
d223e4040b [dlt] Add delete without download functionality (#5329)
abc19bcb43 [upstream_utils] Zero out commit hashes and show 40 digits in index hashes (#5336)
e909f2e687 [build] Update gradle cache repo name (#5334)
52bd5b972d [wpimath] Rewrite DARE solver (#5328)
3876a2523a [wpimath] Remove unused MatrixImpl() function (#5330)
c82fcb1975 [wpiutil] Add reflection based cleanup helper (#4919)
15ba95df7e [wpiutil] Use std::filesystem (#4941)
77c2124fc5 [wpimath] Remove Eigen's custom STL types (#4945)
27fb47ab10 [glass] Field2D: Embed standard field images (#5159)
102e4f2566 [wpilib] Remove deprecated and broken SPI methods (#5249)
463a90f1df [wpilib, hal] Add function to read the RSL state (#5312)
7a90475eec [wpilib] Update RobotBase documentation (NFC) (#5320)
218cfea16b [wpilib] DutyCycleEncoder: Fix reset behavior (#5287)
91392823ff [build] Update to gradle 8.1 (#5303)
258b7cc48b [wpilibj] Filesystem.getDeployDirectory(): Strip JNI path from user.dir (#5317)
26cc43bee1 [wpilib] Add documentation to SPI mode enum (NFC) (#5324)
ac4da9b1cb [hal] Add HAL docs for Addressable LED (NFC) (#5304)
21d4244cf7 [wpimath] Fix DCMotor docs (NFC) (#5309)
1dff81bea7 [hal] Miscellaneous HAL doc fixes (NFC) (#5306)
7ce75574bf [wpimath] Upgrade to Drake v1.15.0 (#5310)
576bd646ae [hal] Add CANManufacturer for Redux Robotics (#5305)
ee3b4621e5 [commands] Add onlyWhile and onlyIf (#5291)
40ca094686 [commands] Fix RepeatCommand calling end() twice (#5261)
9cbeb841f5 [rtns] Match imaging tool capitalization (#5265)
a63d06ff77 [examples] Add constants to java gearsbot example (#5248)
b6c43322a3 [wpilibc] XboxController: Add return tag to docs (NFC) (#5246)
5162d0001c [hal] Fix and document addressable LED timings (#5272)
90fabe9651 [wpilibj] Use method references in drive class initSendable() (#5251)
24828afd11 [wpimath] Fix desaturateWheelSpeeds to account for negative speeds (#5269)
e099948a77 [wpimath] Clean up rank notation in docs (NFC) (#5274)
fd2d8cb9c1 [hal] Use std::log2() for base-2 logarithm (#5278)
ba8c64bcff [wpimath] Fix misspelled Javadoc parameters in pose estimators (NFC) (#5292)
f53c6813d5 [wpimath] Patch Eigen warnings (#5290)
663703d370 [gitattributes] Mark json files as lf text files (#5256)
aa34aacf6e [wpilib] Shuffleboard: Keep duplicates on SelectTab() (#5198)
63512bbbb8 [wpimath] Fix potential divide-by-zero in RKDP (#5242)
9227b2166e [wpilibj] DriverStation: Fix joystick data logs (#5240)
fbf92e9190 [wpinet] ParallelTcpConnector: don't connect to duplicate addresses (#5169)
2108a61362 [ntcore] NT4 client: close timed-out connections (#5175)
0a66479693 [ntcore] Optimize scan of outgoing messages (#5227)
b510c17ef6 [hal] Fix RobotController.getComments() mishandling quotes inside the comments string (#5197)
e7a7eb2e93 [commands] WaitCommand: Remove subclass doc note (NFC) (#5200)
a465f2d8f0 [examples] Shuffleboard: Correct parameter order (#5204)
a3364422fa LICENSE.md: Bump year to 2023 (#5195)
df3242a40a [wpimath] Fix NaN in C++ MakeCostMatrix() that takes an array (#5194)
00abb8c1e0 [commands] RamseteCommand: default-initialize m_prevSpeeds (#5188)
c886273fd7 [wpilibj] DutyCycleEncoder.setDistancePerRotation(): fix simulation (#5147)
53b5fd2ace [ntcore] Use int64 for datalog type string (#5186)
56b758320f [wpilib] DataLogManager: increase time for datetime to be valid (#5185)
08f298e4cd [wpimath] Fix Pose3d log returning Twist3d NaN for theta between 1E-8 and 1E-7 (#5168)
6d0c5b19db [commands] CommandScheduler.isComposed: Remove incorrect throws clause (NFC) (#5183)
0d22cf5ff7 [wpilib] Fix enableLiveWindowInTest crashing in disabled (#5173)
32ec5b3f75 [wpilib] Add isTestEnabled and minor docs cleanup (#5172)
e5c4c6b1a7 [wpimath] Fix invalid iterator access in TimeInterpolatableBuffer (#5138)
099d048d9e [wpimath] Fix Pose3d log returning Twist3d NaN for theta between 1E-9 and 1E-8 (#5143)
4af84a1c12 Fix Typos (NFC) (#5137)
ce3686b80d [wpimath] Check LTV controller max velocity precondition (#5142)
4b0eecaee0 [commands] Subsystem: Add default command removal method (#5064)
edf4ded412 [wpilib] PH: Revert to 5V rail being fixed 5V (#5122)
4c46b6aff9 [wpilibc] Fix DataLogManager crash on exit in sim (#5125)
490ca4a68a [wpilibc] Fix XboxController::GetBackButton doc (NFC) (#5131)
cbb5b0b802 [hal] Simulation: Fix REV PH solenoids 8+ (#5132)
bb7053d9ee [hal] Fix HAL_GetRuntimeType being slow on the roboRIO (#5130)
9efed9a533 Update .clang-format to c++20 (#5121)
dbbfe1aed2 [wpilib] Use PH voltage to calc Analog pressure switch threshold (#5115)
de65a135c3 [wpilib] DutyCycleEncoderSim: Add channel number constructor (#5118)
3e9788cdff [docs] Strip path from generated NT docs (#5119)
ecb072724d [ntcore] Client::Disconnect(): actually close connection (#5113)
0d462a4561 [glass] NT view: Change string/string array to quoted (#5111)
ba37986561 [ntcore] NetworkClient::Disconnect: Add null check (#5112)
25ab9cda92 [glass,ov] Provide menu item to create topic from root (#5110)
2f6251d4a6 [glass] Set default value when publishing new topic (#5109)
e9a7bed988 [wpimath] Add timestamp getter to MathShared (#5091)
9cc14bbb43 [ntcore] Add stress test to dev executable (#5107)
8068369542 [wpinet] uv: Stop creating handles when closing loop (#5102)
805c837a42 [ntcore] Fix use-after-free in server (#5101)
fd18577ba0 [commands] Improve documentation of addRequirements (NFC) (#5103)
74dea9f05e [wpimath] Fix exception for empty pose buffer in pose estimators (#5106)
9eef79d638 [wpilib] PneumaticHub: Document range of enableCompressorAnalog (NFC) (#5099)
843574a810 [ntcore] Use wpi::Now instead of loop time for transmit time
226ef35212 [wpinet] WebSocket: Reduce server send frame overhead
b30664d630 [ntcore] Reduce initial connection overhead
804e5ce236 [examples] MecanumDrive: Fix axis comment in C++ example (NFC) (#5096)
49af88f2bb [examples] ArmSimulation: Fix flaky test (#5093)
d56314f866 [wpiutil] Disable mock time on the Rio (#5092)
43975ac7cc [examples] ArmSimulation, ElevatorSimulation: Extract mechanism to class (#5052)
5483464158 [examples, templates] Improve descriptions (NFC) (#5051)
785e7dd85c [wpilibc] SendableChooser: static_assert copy- and default-constructibility (#5078)
e57ded8c39 [ntcore] Improve disconnect error reporting (#5085)
01f0394419 [wpinet] Revert WebSocket: When Close() is called, call closed immediately (#5084)
59be120982 [wpimath] Fix Pose3d exp()/log() and add rotation vector constructor to Rotation3d (#5072)
37f065032f [wpilib] Refactor TimedRobot tests (#5068)
22a170bee7 [wpilib] Add Notifier test (#5070)
2f310a748c [wpimath] Fix DCMotor.getSpeed() (#5061)
b43ec87f57 [wpilib] ElevatorSim: Fix WouldHitLimit methods (#5057)
19267bef0c [ntcore] Output warning on property set on unpublished topic (#5059)
84cbd48d84 [ntcore] Handle excludeSelf on SetDefault (#5058)
1f35750865 [cameraserver] Add GetInstance() to all functions (#5054)
8230fc631d [wpilib] Revert throw on nonexistent SimDevice name in SimDeviceSim (#5053)
b879a6f8c6 [wpinet] WebSocket: When Close() is called, call closed immediately (#5047)
49459d3e45 [ntcore] Change wire timeout to fixed 1 second (#5048)
4079eabe9b [wpimath] Discard stale pose estimates (#5045)
fe5d226a19 [glass] Fix option for debug-level NT logging (#5049)
b7535252c2 [ntcore] Don't leak buffers in rare WS shutdown case (#5046)
b61ac6db33 [ntcore] Add client disconnect function (#5022)
7b828ce84f [wpimath] Add nearest to Pose2d and Translation2d (#4882)
08a536291b [examples] Improvements to Elevator Simulation Example (#4937)
193a10d020 [wpigui] Limit frame rate to 120 fps by default (#5030)
7867bbde0e [wpilib] Clarify DS functions provided by FMS (NFC) (#5043)
fa7c01b598 [glass] Add option for debug-level NT logging (#5007)
2b81610248 [wpiutil] Add msgpack to datalog Python example (#5032)
a4a369b8da CONTRIBUTING.md: Add unicodeit CLI to math docs guidelines (#5031)
d991f6e435 [wpilib] Throw on nonexistent SimDevice name in SimDeviceSim constructor (#5041)
a27a047ae8 [hal] Check for null in getSimDeviceName JNI (#5038)
2f96cae31a [examples] Hatchbots: Add telemetry (#5011)
83ef8f9658 [simulation] GUI: Fix buffer overflow in joystick axes copy (#5036)
4054893669 [commands] Fix C++ Select() factory (#5024)
f75acd11ce [commands] Use Timer.restart() (#5023)
8bf67b1b33 [wpimath] PIDController::Calculate(double, double): update setpoint flag (#5021)
49bb1358d8 [wpiutil] MemoryBuffer: Fix GetMemoryBufferForStream (#5017)
9c4c07c0f9 [wpiutil] Remove NDEBUG check for debug-level logging (#5018)
1a47cc2e86 [ntcore] Use full handle when subscribing (#5013)
7cd30cffbc Ignore networktables.json (#5006)
92aecab2ef [commands] Command controllers are not subclasses (NFC) (#5000)
8785bba080 [ntcore] Special-case default timestamps (#5003)
9e5b7b8040 [ntcore] Handle topicsonly followed by value subscribe (#4991)
917906530a [wpilib] Add Timer::Restart() (#4963)
00aa66e4fd [wpimath] Remove extraneous assignments from DiscretizeAB() (#4967)
893320544a [examples] C++ RamseteCommand: Fix units (#4954)
b95d0e060d [wpilib] XboxController: Fix docs discrepancy (NFC) (#4993)
008232b43c [ntcore] Write empty persistent file if none found (#4996)
522be348f4 [examples] Rewrite tags (NFC) (#4961)
d48a83dee2 [wpimath] Update Wikipedia links for quaternion to Euler angle conversion (NFC) (#4995)
504fa22143 [wpimath] Workaround intellisense Eigen issue (#4992)
b2b25bf09f [commands] Fix docs inconsistency for toggleOnFalse(Command) (NFC) (#4978)
ce3dc4eb3b [hal] Properly use control word that is in sync with DS data (#4989)
1ea48caa7d [wpilib] Fix C++ ADXRS450 and Java SPI gyro defs (#4988)
fb101925a7 [build] Include wpimathjni in commands binaries (#4981)
657951f6dd [starter] Add a process starter for use by the installer for launching tools (#4931)
a60ca9d71c [examples] Update AprilTag field load API usage (#4975)
f8a45f1558 [wpimath] Remove print statements from tests (#4977)
ecba8b99a8 [examples] Fix swapped arguments in MecanumControllerCommand example (#4976)
e95e88fdf9 [examples] Add comment to drivedistanceoffboard example (#4877)
371d15dec3 [examples] Add Computer Vision Pose Estimation and Latency Compensation Example (#4901)
cb9b8938af [sim] Enable docking in the GUI (#4960)
3b084ecbe0 [apriltag] AprilTagFieldLayout: Improve API shape for loading builtin JSONs (#4949)
27ba096ea1 [wpilib] Fix MOI calculation error in SingleJointedArmSim (#4968)
42c997a3c4 [wpimath] Fix Pose3d exponential and clean up Pose3d logarithm (#4970)
5f1a025f27 [wpilibj] Fix typo in MecanumDrive docs (NFC) (#4969)
0ebf79b54c [wpimath] Fix typo in Pose3d::Exp() docs (NFC) (#4966)
a8c465f3fb [wpimath] HolonomicDriveController: Add getters for the controllers (#4948)
a7b1ab683d [wpilibc] Add unit test for fast deconstruction of GenericHID (#4953)
bd6479dc29 [build] Add Spotless for JSON (#4956)
5cb0340a8c [hal, wpilib] Load joystick values upon code initialization (#4950)
ab0e8c37a7 [readme] Update build requirements (NFC) (#4947)
b74ac1c645 [build] Add apriltag to C++ cmake example builds (#4944)
cf1a411acf [examples] Add example programs for AprilTags detection (#4932)
1e05b21ab5 [wpimath] Fix PID atSetpoint to not return true prematurely (#4906)
e5a6197633 [wpimath] Fix SwerveDriveKinematics not initializing a new array each time (#4942)
039edcc23f [ntcore] Queue current value on subscriber creation (#4938)
f7f19207e0 [wpimath] Allow multiple vision measurements from same timestamp (#4917)
befd12911c [commands] Delete UB-causing rvalue variants of CommandPtr methods (#4923)
34519de60a [commands] Fix spacing in command composition exception (#4924)
dc4355c031 [hal] Add handle constructor and name getters for sim devices (#4925)
53d8d33bca [hal, wpilibj] Add missing distance per pulse functions to EncoderSim (#4928)
530ae40614 [apriltag] Explain what April tag poses represent (NFC) (#4930)
79f565191e [examples] DigitalCommunication, I2CCommunication: Add tests (#4865)
2cd9be413f [wpilib, examples] Cleanup PotentiometerPID, Ultrasonic, UltrasonicPID examples (#4893)
babb0c1fcf [apriltag] Add 2023 field layout JSON (#4912)
330ba45f9c [wpimath] Fix swerve kinematics util classes equals function (#4907)
51272ef6b3 [fieldImages] Add 2023 field (#4915)
0d105ab771 [commands] Deduplicate command test utils (#4897)
cf4235ea36 [wpiutil] Guard MSVC pragma in SymbolExports.h (#4911)
2d4b7b9147 [build] Update opencv version in opencv.gradle (#4909)
aec6f3d506 [ntcore] Fix client flush behavior (#4903)
bfe346c76a [build] Fix cmake java resources (#4898)

Change-Id: Ia1dd90fe42c6cd5df281b8a5b710e136f54355f4
git-subtree-dir: third_party/allwpilib
git-subtree-split: f1a82828fed8950f9a3f1586c44327027627a0c8
Signed-off-by: James Kuszmaul <jabukuszmaul+collab@gmail.com>
diff --git a/simulation/CMakeLists.txt b/simulation/CMakeLists.txt
index 7a54c48..0a1a953 100644
--- a/simulation/CMakeLists.txt
+++ b/simulation/CMakeLists.txt
@@ -5,3 +5,4 @@
 add_subdirectory(halsim_ws_core)
 add_subdirectory(halsim_ws_client)
 add_subdirectory(halsim_ws_server)
+add_subdirectory(halsim_xrp)
diff --git a/simulation/halsim_ds_socket/CMakeLists.txt b/simulation/halsim_ds_socket/CMakeLists.txt
index 6d770d9..4eb23bb 100644
--- a/simulation/halsim_ds_socket/CMakeLists.txt
+++ b/simulation/halsim_ds_socket/CMakeLists.txt
@@ -13,4 +13,4 @@
 
 set_property(TARGET halsim_ds_socket PROPERTY FOLDER "libraries")
 
-install(TARGETS halsim_ds_socket EXPORT halsim_ds_socket DESTINATION "${main_lib_dest}")
+install(TARGETS halsim_ds_socket EXPORT halsim_ds_socket)
diff --git a/simulation/halsim_ds_socket/build.gradle b/simulation/halsim_ds_socket/build.gradle
index 0e90938..b9a39a8 100644
--- a/simulation/halsim_ds_socket/build.gradle
+++ b/simulation/halsim_ds_socket/build.gradle
@@ -1,3 +1,7 @@
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
+
 description = "A plugin that listens on a socket so that you can use the real Driver Station software to connect to the simulation"
 
 ext {
@@ -7,7 +11,6 @@
 
 apply plugin: 'google-test-test-suite'
 
-
 ext {
     staticGtestConfigs = [:]
 }
@@ -17,28 +20,22 @@
 
 apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
 
-
 model {
     testSuites {
-        def comps = $.components
-        if (!project.hasProperty('onlylinuxathena')) {
-            "${pluginName}Test"(GoogleTestTestSuiteSpec) {
-                for(NativeComponentSpec c : comps) {
-                    if (c.name == pluginName) {
-                        testing c
-                        break
-                    }
+        "${pluginName}Test"(GoogleTestTestSuiteSpec) {
+            for(NativeComponentSpec c : $.components) {
+                if (c.name == pluginName) {
+                    testing c
+                    break
                 }
-                sources {
-                    cpp {
-                        source {
-                            srcDirs 'src/test/native/cpp'
-                            include '**/*.cpp'
-                        }
-                        exportedHeaders {
-                            srcDirs 'src/test/native/include', 'src/main/native/cpp'
-                        }
-                    }
+            }
+            sources.cpp {
+                source {
+                    srcDirs 'src/test/native/cpp'
+                    include '**/*.cpp'
+                }
+                exportedHeaders {
+                    srcDirs 'src/test/native/include', 'src/main/native/cpp'
                 }
             }
         }
diff --git a/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp b/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
index 004752c..5773722 100644
--- a/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
+++ b/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
@@ -146,7 +146,9 @@
   m_lo = packet[1];
   // Comm Version is packet 2, ignore
   SetControl(packet[3], packet[4]);
-  SetAlliance(packet[5]);
+  // DS sends values 0, 1, and 2 for Red, but kUnknown is 0, so the value needs
+  // to be offset by one
+  SetAlliance(packet[5] + 1);
 
   // Return if packet finished
   if (packet.size() == 6) {
diff --git a/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp b/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
index af84632..e602ecc 100644
--- a/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
@@ -16,6 +16,7 @@
 #include <atomic>
 #include <cstdio>
 #include <cstring>
+#include <exception>
 #include <string_view>
 
 #include <DSCommPacket.h>
@@ -124,33 +125,45 @@
     });
   });
   simLoopTimer->Start(Timer::Time{100}, Timer::Time{100});
+  // DS Timeout
+  int timeoutMs = 100;
+  if (auto envTimeout = std::getenv("DS_TIMEOUT_MS")) {
+    try {
+      timeoutMs = std::stoi(envTimeout);
+    } catch (const std::exception& e) {
+      fmt::print(stderr, "Error parsing DS_TIMEOUT_MS: {}\n", e.what());
+    }
+  }
+  auto autoDisableTimer = Timer::Create(loop);
+  autoDisableTimer->timeout.connect([] { HALSIM_SetDriverStationEnabled(0); });
 
   // UDP Receive then send
-  udp->received.connect([udpLocal = udp.get()](Buffer& buf, size_t len,
-                                               const sockaddr& recSock,
-                                               unsigned int port) {
-    auto ds = udpLocal->GetLoop()->GetData<halsim::DSCommPacket>();
-    ds->DecodeUDP({reinterpret_cast<uint8_t*>(buf.base), len});
+  udp->received.connect(
+      [udpLocal = udp.get(), autoDisableTimer, timeoutMs](
+          Buffer& buf, size_t len, const sockaddr& recSock, unsigned int port) {
+        autoDisableTimer->Start(Timer::Time(timeoutMs));
+        auto ds = udpLocal->GetLoop()->GetData<halsim::DSCommPacket>();
+        ds->DecodeUDP({reinterpret_cast<uint8_t*>(buf.base), len});
 
-    struct sockaddr_in outAddr;
-    std::memcpy(&outAddr, &recSock, sizeof(sockaddr_in));
-    outAddr.sin_family = PF_INET;
-    outAddr.sin_port = htons(1150);
+        struct sockaddr_in outAddr;
+        std::memcpy(&outAddr, &recSock, sizeof(sockaddr_in));
+        outAddr.sin_family = PF_INET;
+        outAddr.sin_port = htons(1150);
 
-    wpi::SmallVector<wpi::uv::Buffer, 4> sendBufs;
-    wpi::raw_uv_ostream stream{sendBufs,
-                               [] { return GetBufferPool().Allocate(); }};
-    ds->SetupSendBuffer(stream);
+        wpi::SmallVector<wpi::uv::Buffer, 4> sendBufs;
+        wpi::raw_uv_ostream stream{sendBufs,
+                                   [] { return GetBufferPool().Allocate(); }};
+        ds->SetupSendBuffer(stream);
 
-    udpLocal->Send(outAddr, sendBufs, [](auto bufs, Error err) {
-      GetBufferPool().Release(bufs);
-      if (err) {
-        fmt::print(stderr, "{}\n", err.str());
-        std::fflush(stderr);
-      }
-    });
-    ds->SendUDPToHALSim();
-  });
+        udpLocal->Send(outAddr, sendBufs, [](auto bufs, Error err) {
+          GetBufferPool().Release(bufs);
+          if (err) {
+            fmt::print(stderr, "{}\n", err.str());
+            std::fflush(stderr);
+          }
+        });
+        ds->SendUDPToHALSim();
+      });
 
   udp->StartRecv();
 }
diff --git a/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp b/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
index d1a892a..1c7bfc5 100644
--- a/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
+++ b/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
@@ -2,8 +2,9 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+#include <gtest/gtest.h>
+
 #include "DSCommPacket.h"
-#include "gtest/gtest.h"
 
 class DSCommPacketTest : public ::testing::Test {
  public:
diff --git a/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp b/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
index 6aea19a..d181e39 100644
--- a/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
@@ -2,10 +2,9 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+#include <gtest/gtest.h>
 #include <hal/HALBase.h>
 
-#include "gtest/gtest.h"
-
 int main(int argc, char** argv) {
   HAL_Initialize(500, 0);
   ::testing::InitGoogleTest(&argc, argv);
diff --git a/simulation/halsim_gui/CMakeLists.txt b/simulation/halsim_gui/CMakeLists.txt
index 949f9f1..957a9c2 100644
--- a/simulation/halsim_gui/CMakeLists.txt
+++ b/simulation/halsim_gui/CMakeLists.txt
@@ -16,4 +16,4 @@
 
 set_property(TARGET halsim_gui PROPERTY FOLDER "libraries")
 
-install(TARGETS halsim_gui EXPORT halsim_gui DESTINATION "${main_lib_dest}")
+install(TARGETS halsim_gui EXPORT halsim_gui)
diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle
index a8608e3..72bcd22 100644
--- a/simulation/halsim_gui/build.gradle
+++ b/simulation/halsim_gui/build.gradle
@@ -1,50 +1,51 @@
-if (!project.hasProperty('onlylinuxathena')) {
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
 
-    description = "A plugin that creates a simulation gui"
+description = "A plugin that creates a simulation gui"
 
-    ext {
-        pluginName = 'halsim_gui'
-    }
+ext {
+    pluginName = 'halsim_gui'
+}
 
-    apply plugin: 'google-test-test-suite'
+apply plugin: 'google-test-test-suite'
+
+ext {
+    staticGtestConfigs = [:]
+}
+
+staticGtestConfigs["${pluginName}Test"] = []
+apply from: "${rootDir}/shared/googletest.gradle"
+
+apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
 
 
-    ext {
-        staticGtestConfigs = [:]
-    }
+apply from: "${rootDir}/shared/imgui.gradle"
 
-    staticGtestConfigs["${pluginName}Test"] = []
-    apply from: "${rootDir}/shared/googletest.gradle"
-
-    apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
-
-
-    apply from: "${rootDir}/shared/imgui.gradle"
-
-    model {
-        binaries {
-            all {
-                lib project: ':glass', library: 'glassnt', linkage: 'static'
-                lib project: ':glass', library: 'glass', linkage: 'static'
-                lib project: ':wpigui', library: 'wpigui', linkage: 'static'
-                lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
-                project(':ntcore').addNtcoreDependency(it, 'shared')
-                lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
-                lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
-                nativeUtils.useRequiredLibrary(it, 'imgui')
-                if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                    it.buildable = false
-                    return
-                }
-                if (it.targetPlatform.operatingSystem.isWindows()) {
-                    it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib'
-                } else if (it.targetPlatform.operatingSystem.isMacOsX()) {
-                    it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore'
-                } else {
-                    it.linker.args << '-lX11'
-                    if (it.targetPlatform.name.startsWith('linuxarm')) {
-                        it.linker.args << '-lGL'
-                    }
+model {
+    binaries {
+        all {
+            lib project: ':glass', library: 'glassnt', linkage: 'static'
+            lib project: ':glass', library: 'glass', linkage: 'static'
+            lib project: ':wpigui', library: 'wpigui', linkage: 'static'
+            lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
+            project(':ntcore').addNtcoreDependency(it, 'shared')
+            lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
+            lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
+            lib project: ':fieldImages', library: 'fieldImages', linkage: 'static'
+            nativeUtils.useRequiredLibrary(it, 'imgui')
+            if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
+                it.buildable = false
+                return
+            }
+            if (it.targetPlatform.operatingSystem.isWindows()) {
+                it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib'
+            } else if (it.targetPlatform.operatingSystem.isMacOsX()) {
+                it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore'
+            } else {
+                it.linker.args << '-lX11'
+                if (it.targetPlatform.name.startsWith('linuxarm')) {
+                    it.linker.args << '-lGL'
                 }
             }
         }
diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
index e7271fd..4edaa5c 100644
--- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
@@ -487,7 +487,7 @@
     }
   } else {
     std::memcpy(data->axes.axes, sysAxes,
-                data->axes.count * sizeof(&data->axes.axes[0]));
+                data->axes.count * sizeof(data->axes.axes[0]));
   }
 
   data->povs.count = data->desc.povCount;
@@ -555,8 +555,8 @@
       m_axisStorage{storage.GetChildArray("axisConfig")},
       m_buttonKey{storage.GetIntArray("buttonKeys")},
       m_povStorage{storage.GetChildArray("povConfig")} {
-  std::snprintf(m_name, sizeof(m_name), "Keyboard %d", index);
-  std::snprintf(m_guid, sizeof(m_guid), "Keyboard%d", index);
+  wpi::format_to_n_c_str(m_name, sizeof(m_name), "Keyboard {}", index);
+  wpi::format_to_n_c_str(m_guid, sizeof(m_guid), "Keyboard{}", index);
 
   // init axes
   for (auto&& axisConfig : m_axisStorage) {
@@ -587,9 +587,15 @@
   ImGui::PushID(label);
   ImGui::Text("%s", label);
   ImGui::SameLine();
+
   char editLabel[32];
-  std::snprintf(editLabel, sizeof(editLabel), "%s###edit",
-                s_keyEdit == key ? "(press key)" : GetKeyName(*key));
+  if (s_keyEdit == key) {
+    wpi::format_to_n_c_str(editLabel, sizeof(editLabel), "(press key)###edit");
+  } else {
+    wpi::format_to_n_c_str(editLabel, sizeof(editLabel), "{}###edit",
+                           GetKeyName(*key));
+  }
+
   if (ImGui::SmallButton(editLabel)) {
     s_keyEdit = key;
   }
@@ -633,7 +639,8 @@
       m_axisConfig.emplace_back(*m_axisStorage.back());
     }
     for (int i = 0; i < m_axisCount; ++i) {
-      std::snprintf(label, sizeof(label), "Axis %d", i);
+      wpi::format_to_n_c_str(label, sizeof(label), "Axis {}", i);
+
       if (ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen)) {
         EditKey("Increase", &m_axisConfig[i].incKey);
         EditKey("Decrease", &m_axisConfig[i].decKey);
@@ -666,7 +673,8 @@
       m_buttonKey.emplace_back(-1);
     }
     for (int i = 0; i < m_buttonCount; ++i) {
-      std::snprintf(label, sizeof(label), "Button %d", i + 1);
+      wpi::format_to_n_c_str(label, sizeof(label), "Button {}", i + 1);
+
       EditKey(label, &m_buttonKey[i]);
     }
     ImGui::PopID();
@@ -688,7 +696,8 @@
       m_povConfig.emplace_back(*m_povStorage.back());
     }
     for (int i = 0; i < m_povCount; ++i) {
-      std::snprintf(label, sizeof(label), "POV %d", i);
+      wpi::format_to_n_c_str(label, sizeof(label), "POV {}", i);
+
       if (ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen)) {
         EditKey("  0 deg", &m_povConfig[i].key0);
         EditKey(" 45 deg", &m_povConfig[i].key45);
@@ -1174,7 +1183,7 @@
 
 static void DisplaySystemJoystick(SystemJoystick& joy, int i) {
   char label[64];
-  std::snprintf(label, sizeof(label), "%d: %s", i, joy.GetName());
+  wpi::format_to_n_c_str(label, sizeof(label), "{}: {}", i, joy.GetName());
 
   // highlight if any buttons pressed
   bool anyButtonPressed = joy.IsAnyButtonPressed();
@@ -1208,7 +1217,8 @@
     DisplaySystemJoystick(*joy, i + GLFW_JOYSTICK_LAST + 1);
     if (ImGui::BeginPopupContextItem()) {
       char buf[64];
-      std::snprintf(buf, sizeof(buf), "%s Settings", joy->GetName());
+      wpi::format_to_n_c_str(buf, sizeof(buf), "{} Settings", joy->GetName());
+
       if (ImGui::MenuItem(buf)) {
         if (auto win = DriverStationGui::dsManager->GetWindow(buf)) {
           win->SetVisible(true);
@@ -1293,7 +1303,8 @@
       for (int j = 0; j < joy.data.axes.count; ++j) {
         if (source && source->axes[j]) {
           char label[64];
-          std::snprintf(label, sizeof(label), "Axis[%d]", j);
+          wpi::format_to_n_c_str(label, sizeof(label), "Axis[{}]", j);
+
           ImGui::Selectable(label);
           source->axes[j]->EmitDrag();
           ImGui::SameLine();
@@ -1306,7 +1317,8 @@
       for (int j = 0; j < joy.data.povs.count; ++j) {
         if (source && source->povs[j]) {
           char label[64];
-          std::snprintf(label, sizeof(label), "POVs[%d]", j);
+          wpi::format_to_n_c_str(label, sizeof(label), "POVs[{}]", j);
+
           ImGui::Selectable(label);
           source->povs[j]->EmitDrag();
           ImGui::SameLine();
@@ -1406,7 +1418,9 @@
     int i = 0;
     for (auto&& joy : gKeyboardJoysticks) {
       char label[64];
-      std::snprintf(label, sizeof(label), "%s Settings", joy->GetName());
+      wpi::format_to_n_c_str(label, sizeof(label), "{} Settings",
+                             joy->GetName());
+
       if (auto win = dsManager->AddWindow(
               label, [j = joy.get()] { j->SettingsDisplay(); },
               glass::Window::kHide)) {
diff --git a/simulation/halsim_gui/src/main/native/cpp/main.cpp b/simulation/halsim_gui/src/main/native/cpp/main.cpp
index d0db8d4..0cedd36 100644
--- a/simulation/halsim_gui/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/main.cpp
@@ -116,7 +116,8 @@
     }
   });
 
-  if (!gui::Initialize("Robot Simulation", 1280, 720)) {
+  if (!gui::Initialize("Robot Simulation", 1280, 720,
+                       ImGuiConfigFlags_DockingEnable)) {
     return 0;
   }
   HAL_RegisterExtensionListener(
diff --git a/simulation/halsim_gui/src/main/native/include/HALDataSource.h b/simulation/halsim_gui/src/main/native/include/HALDataSource.h
index c04f358..295b3f8 100644
--- a/simulation/halsim_gui/src/main/native/include/HALDataSource.h
+++ b/simulation/halsim_gui/src/main/native/include/HALDataSource.h
@@ -57,9 +57,13 @@
         HALSIM_Cancel##cbname##Callback(m_index, m_callback);                 \
     }                                                                         \
                                                                               \
-    int32_t GetIndex() const { return m_index; }                              \
+    int32_t GetIndex() const {                                                \
+      return m_index;                                                         \
+    }                                                                         \
                                                                               \
-    int GetChannel() const { return m_channel; }                              \
+    int GetChannel() const {                                                  \
+      return m_channel;                                                       \
+    }                                                                         \
                                                                               \
    private:                                                                   \
     static void CallbackFunc(const char*, void* param,                        \
@@ -96,9 +100,13 @@
         HALSIM_Cancel##cbname##Callback(m_index, m_channel, m_callback);      \
     }                                                                         \
                                                                               \
-    int32_t GetIndex() const { return m_index; }                              \
+    int32_t GetIndex() const {                                                \
+      return m_index;                                                         \
+    }                                                                         \
                                                                               \
-    int32_t GetChannel() const { return m_channel; }                          \
+    int32_t GetChannel() const {                                              \
+      return m_channel;                                                       \
+    }                                                                         \
                                                                               \
    private:                                                                   \
     static void CallbackFunc(const char*, void* param,                        \
diff --git a/simulation/halsim_gui/src/test/native/cpp/main.cpp b/simulation/halsim_gui/src/test/native/cpp/main.cpp
index 6aea19a..d181e39 100644
--- a/simulation/halsim_gui/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_gui/src/test/native/cpp/main.cpp
@@ -2,10 +2,9 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
+#include <gtest/gtest.h>
 #include <hal/HALBase.h>
 
-#include "gtest/gtest.h"
-
 int main(int argc, char** argv) {
   HAL_Initialize(500, 0);
   ::testing::InitGoogleTest(&argc, argv);
diff --git a/simulation/halsim_ws_client/CMakeLists.txt b/simulation/halsim_ws_client/CMakeLists.txt
index 5bc99db..18a2a12 100644
--- a/simulation/halsim_ws_client/CMakeLists.txt
+++ b/simulation/halsim_ws_client/CMakeLists.txt
@@ -13,4 +13,4 @@
 
 set_property(TARGET halsim_ws_client PROPERTY FOLDER "libraries")
 
-install(TARGETS halsim_ws_client EXPORT halsim_ws_client DESTINATION "${main_lib_dest}")
+install(TARGETS halsim_ws_client EXPORT halsim_ws_client)
diff --git a/simulation/halsim_ws_client/build.gradle b/simulation/halsim_ws_client/build.gradle
index 1fe4d29..fb563ab 100644
--- a/simulation/halsim_ws_client/build.gradle
+++ b/simulation/halsim_ws_client/build.gradle
@@ -1,35 +1,35 @@
-if (!project.hasProperty('onlylinuxathena')) {
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
 
-    description = "WebSocket Client Extension"
+description = "WebSocket Client Extension"
 
-    ext {
-        includeWpiutil = true
-        pluginName = 'halsim_ws_client'
-    }
+ext {
+    includeWpiutil = true
+    pluginName = 'halsim_ws_client'
+}
 
-    apply plugin: 'google-test-test-suite'
+apply plugin: 'google-test-test-suite'
 
+ext {
+    staticGtestConfigs = [:]
+}
 
-    ext {
-        staticGtestConfigs = [:]
-    }
+staticGtestConfigs["${pluginName}Test"] = []
+apply from: "${rootDir}/shared/googletest.gradle"
 
-    staticGtestConfigs["${pluginName}Test"] = []
-    apply from: "${rootDir}/shared/googletest.gradle"
+apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
 
-    apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
-
-    model {
-        binaries {
-            all {
-                if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                    it.buildable = false
-                    return
-                }
-
-                lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
-                lib project: ":simulation:halsim_ws_core", library: "halsim_ws_core", linkage: "static"
+model {
+    binaries {
+        all {
+            if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
+                it.buildable = false
+                return
             }
+
+            lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
+            lib project: ":simulation:halsim_ws_core", library: "halsim_ws_core", linkage: "static"
         }
     }
 }
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
index f4c5926..bc38c3c 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
@@ -8,6 +8,8 @@
 
 #include <fmt/format.h>
 #include <wpi/SmallString.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
 #include <wpinet/uv/util.h>
 
 #include "HALSimWSClientConnection.h"
@@ -66,6 +68,22 @@
     m_uri = "/wpilibws";
   }
 
+  const char* msgFilters = std::getenv("HALSIMWS_FILTERS");
+  if (msgFilters != nullptr) {
+    m_useMsgFiltering = true;
+
+    std::string_view filters(msgFilters);
+    filters = wpi::trim(filters);
+    wpi::SmallVector<std::string_view, 16> filtersSplit;
+
+    wpi::split(filters, filtersSplit, ',', -1, false);
+    for (auto val : filtersSplit) {
+      m_msgFilters[wpi::trim(val)] = true;
+    }
+  } else {
+    m_useMsgFiltering = false;
+  }
+
   return true;
 }
 
@@ -87,8 +105,19 @@
 
   m_tcp_client->closed.connect([]() { std::puts("TCP connection closed"); });
 
-  // Set up the connection timer
   std::puts("HALSimWS Initialized");
+
+  // Print any filters we are using
+  if (m_useMsgFiltering) {
+    fmt::print("WS Message Filters:");
+    for (auto filter : m_msgFilters.keys()) {
+      fmt::print("* \"{}\"\n", filter);
+    }
+  } else {
+    fmt::print("No WS Message Filters specified");
+  }
+
+  // Set up the connection timer
   fmt::print("Will attempt to connect to ws://{}:{}{}\n", m_host, m_port,
              m_uri);
 
@@ -171,3 +200,10 @@
     fmt::print(stderr, "Error with incoming message: {}\n", e.what());
   }
 }
+
+bool HALSimWS::CanSendMessage(std::string_view type) {
+  if (!m_useMsgFiltering) {
+    return true;
+  }
+  return m_msgFilters.count(type) > 0;
+}
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
index 10ea8af..cba94c8 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
@@ -74,6 +74,17 @@
   if (msg.empty()) {
     return;
   }
+
+  // Skip sending if this message is not in the allowed filter list
+  try {
+    auto& type = msg.at("type").get_ref<const std::string&>();
+    if (!m_client->CanSendMessage(type)) {
+      return;
+    }
+  } catch (wpi::json::exception& e) {
+    fmt::print(stderr, "Error with message: {}\n", e.what());
+  }
+
   wpi::SmallVector<uv::Buffer, 4> sendBufs;
   wpi::raw_uv_ostream os{sendBufs, [this]() -> uv::Buffer {
                            std::lock_guard lock(m_buffers_mutex);
diff --git a/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h b/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
index 5bec17c..ea01d28 100644
--- a/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
+++ b/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
@@ -7,25 +7,24 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <string_view>
 
 #include <WSProviderContainer.h>
 #include <WSProvider_SimDevice.h>
+#include <wpi/StringMap.h>
+#include <wpi/json_fwd.h>
 #include <wpinet/uv/Async.h>
 #include <wpinet/uv/Loop.h>
 #include <wpinet/uv/Tcp.h>
 #include <wpinet/uv/Timer.h>
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace wpilibws {
 
 class HALSimWSClientConnection;
 
 class HALSimWS : public std::enable_shared_from_this<HALSimWS> {
  public:
-  using LoopFunc = std::function<void(void)>;
+  using LoopFunc = std::function<void()>;
   using UvExecFunc = wpi::uv::Async<LoopFunc>;
 
   HALSimWS(wpi::uv::Loop& loop, ProviderContainer& providers,
@@ -41,6 +40,8 @@
 
   void OnNetValueChanged(const wpi::json& msg);
 
+  bool CanSendMessage(std::string_view type);
+
   const std::string& GetTargetHost() const { return m_host; }
   const std::string& GetTargetUri() const { return m_uri; }
   int GetTargetPort() const { return m_port; }
@@ -67,6 +68,9 @@
   std::string m_host;
   std::string m_uri;
   int m_port;
+
+  bool m_useMsgFiltering;
+  wpi::StringMap<bool> m_msgFilters;
 };
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
index b712e70..005813f 100644
--- a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
+++ b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
@@ -8,6 +8,7 @@
 #include <utility>
 
 #include <HALSimBaseWebSocketConnection.h>
+#include <wpi/json_fwd.h>
 #include <wpi/mutex.h>
 #include <wpinet/WebSocket.h>
 #include <wpinet/uv/Buffer.h>
@@ -15,10 +16,6 @@
 
 #include "HALSimWS.h"
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace wpilibws {
 
 class HALSimWS;
diff --git a/simulation/halsim_ws_core/CMakeLists.txt b/simulation/halsim_ws_core/CMakeLists.txt
index 91bcbb2..f7e240f 100644
--- a/simulation/halsim_ws_core/CMakeLists.txt
+++ b/simulation/halsim_ws_core/CMakeLists.txt
@@ -13,4 +13,4 @@
 
 set_property(TARGET halsim_ws_core PROPERTY FOLDER "libraries")
 
-install(TARGETS halsim_ws_core EXPORT halsim_ws_core DESTINATION "${main_lib_dest}")
+install(TARGETS halsim_ws_core EXPORT halsim_ws_core)
diff --git a/simulation/halsim_ws_core/build.gradle b/simulation/halsim_ws_core/build.gradle
index abdfddc..5dd8d2a 100644
--- a/simulation/halsim_ws_core/build.gradle
+++ b/simulation/halsim_ws_core/build.gradle
@@ -1,58 +1,57 @@
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
+
 apply plugin: 'cpp'
 apply plugin: 'edu.wpi.first.NativeUtils'
 apply plugin: ExtraTasks
 
-if (!project.hasProperty('onlylinuxathena')) {
+description = "Core library for WebSocket extensions"
 
-    description = "Core library for WebSocket extensions"
+ext {
+    includeWpiutil = true
+    includeWpinet = true
+    pluginName = 'halsim_ws_core'
+}
 
-    ext {
-        includeWpiutil = true
-        includeWpinet = true
-        pluginName = 'halsim_ws_core'
-    }
-
-    apply plugin: 'google-test-test-suite'
+apply plugin: 'google-test-test-suite'
 
 
-    ext {
-        staticGtestConfigs = [:]
-    }
+ext {
+    staticGtestConfigs = [:]
+}
 
-    staticGtestConfigs["${pluginName}Test"] = []
-    apply from: "${rootDir}/shared/googletest.gradle"
+staticGtestConfigs["${pluginName}Test"] = []
+apply from: "${rootDir}/shared/googletest.gradle"
 
-    apply from: "${rootDir}/shared/config.gradle"
-    apply from: "${rootDir}/shared/plugins/publish.gradle"
+apply from: "${rootDir}/shared/config.gradle"
+apply from: "${rootDir}/shared/plugins/publish.gradle"
 
-    model {
-        components {
-            halsim_ws_core(NativeLibrarySpec) {
-                sources {
-                    cpp {
-                        source {
-                            srcDirs = ['src/main/native/cpp']
-                            includes = ["**/*.cpp"]
-                        }
-                        exportedHeaders {
-                            srcDirs = ["src/main/native/include"]
-                        }
-                    }
+model {
+    components {
+        halsim_ws_core(NativeLibrarySpec) {
+            sources.cpp {
+                source {
+                    srcDirs = ['src/main/native/cpp']
+                    includes = ["**/*.cpp"]
                 }
-                binaries.all {
-                    project(':hal').addHalDependency(it, 'shared')
-                    lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
-                    lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
+                exportedHeaders {
+                    srcDirs = ["src/main/native/include"]
                 }
-                appendDebugPathToBinaries(binaries)
             }
+            binaries.all {
+                project(':hal').addHalDependency(it, 'shared')
+                lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
+                lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
+            }
+            appendDebugPathToBinaries(binaries)
         }
-        binaries {
-            all {
-                if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                    it.buildable = false
-                    return
-                }
+    }
+    binaries {
+        all {
+            if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
+                it.buildable = false
+                return
             }
         }
     }
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
index 7307b48..ee959b6b 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
@@ -84,6 +84,9 @@
           case HAL_AllianceStationID_kBlue3:
             station = "blue3";
             break;
+          case HAL_AllianceStationID_kUnknown:
+            station = "unknown";
+            break;
         }
         static_cast<HALSimWSProviderDriverStation*>(param)->ProcessHalCallback(
             {{">station", station}});
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_HAL.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_HAL.cpp
new file mode 100644
index 0000000..4010a85
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_HAL.cpp
@@ -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.
+
+#include "WSProvider_HAL.h"
+
+#include <algorithm>
+#include <atomic>
+#include <string_view>
+
+#include <hal/Extensions.h>
+#include <hal/HAL.h>
+#include <hal/Ports.h>
+#include <hal/simulation/MockHooks.h>
+#include <wpi/raw_ostream.h>
+
+namespace wpilibws {
+
+void HALSimWSProviderHAL::Initialize(WSRegisterFunc webRegisterFunc) {
+  CreateSingleProvider<HALSimWSProviderHAL>("HAL", webRegisterFunc);
+}
+
+HALSimWSProviderHAL::~HALSimWSProviderHAL() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderHAL::RegisterCallbacks() {
+  m_simPeriodicBeforeCbKey = HALSIM_RegisterSimPeriodicBeforeCallback(
+      [](void* param) {
+        static_cast<HALSimWSProviderHAL*>(param)->ProcessHalCallback(
+            {{">sim_periodic_before", true}});
+      },
+      this);
+
+  m_simPeriodicAfterCbKey = HALSIM_RegisterSimPeriodicAfterCallback(
+      [](void* param) {
+        static_cast<HALSimWSProviderHAL*>(param)->ProcessHalCallback(
+            {{">sim_periodic_after", true}});
+      },
+      this);
+}
+
+void HALSimWSProviderHAL::CancelCallbacks() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderHAL::DoCancelCallbacks() {
+  HALSIM_CancelSimPeriodicBeforeCallback(m_simPeriodicBeforeCbKey);
+  HALSIM_CancelSimPeriodicAfterCallback(m_simPeriodicAfterCbKey);
+
+  m_simPeriodicBeforeCbKey = 0;
+  m_simPeriodicAfterCbKey = 0;
+}
+
+void HALSimWSProviderHAL::OnNetValueChanged(const wpi::json& json) {
+  // no-op. This is all one way
+}
+
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
index 5792ef5..046e12c 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
@@ -30,7 +30,7 @@
   m_initCbKey = REGISTER(Initialized, "<init", bool, boolean);
   m_speedCbKey = REGISTER(Speed, "<speed", double, double);
   m_positionCbKey = REGISTER(Position, "<position", double, double);
-  m_rawCbKey = REGISTER(RawValue, "<raw", int32_t, int);
+  m_rawCbKey = REGISTER(PulseMicrosecond, "<raw", int32_t, int);
   m_periodScaleCbKey = REGISTER(PeriodScale, "<period_scale", int32_t, int);
   m_zeroLatchCbKey = REGISTER(ZeroLatch, "<zero_latch", bool, boolean);
 }
@@ -43,7 +43,7 @@
   HALSIM_CancelPWMInitializedCallback(m_channel, m_initCbKey);
   HALSIM_CancelPWMSpeedCallback(m_channel, m_speedCbKey);
   HALSIM_CancelPWMPositionCallback(m_channel, m_positionCbKey);
-  HALSIM_CancelPWMRawValueCallback(m_channel, m_rawCbKey);
+  HALSIM_CancelPWMPulseMicrosecondCallback(m_channel, m_rawCbKey);
   HALSIM_CancelPWMPeriodScaleCallback(m_channel, m_periodScaleCbKey);
   HALSIM_CancelPWMZeroLatchCallback(m_channel, m_zeroLatchCbKey);
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h b/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
index bcf29c2..5a699c8 100644
--- a/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
+++ b/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
@@ -6,7 +6,7 @@
 
 #include <memory>
 
-#include <wpi/json.h>
+#include <wpi/json_fwd.h>
 
 namespace wpilibws {
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
index e1bc1f8..849edeb 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
@@ -10,7 +10,7 @@
 #include <string_view>
 
 #include <hal/simulation/NotifyListener.h>
-#include <wpi/json.h>
+#include <wpi/json_fwd.h>
 #include <wpi/mutex.h>
 
 #include "WSBaseProvider.h"
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_HAL.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_HAL.h
new file mode 100644
index 0000000..05ea722
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_HAL.h
@@ -0,0 +1,32 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+
+#include "WSHalProviders.h"
+
+namespace wpilibws {
+
+class HALSimWSProviderHAL : public HALSimWSHalProvider {
+ public:
+  static void Initialize(WSRegisterFunc webRegisterFunc);
+
+  using HALSimWSHalProvider::HALSimWSHalProvider;
+  ~HALSimWSProviderHAL() override;
+
+  void OnNetValueChanged(const wpi::json& json) override;
+
+ protected:
+  void RegisterCallbacks() override;
+  void CancelCallbacks() override;
+  void DoCancelCallbacks();
+
+ private:
+  int32_t m_simPeriodicBeforeCbKey = 0;
+  int32_t m_simPeriodicAfterCbKey = 0;
+};
+
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
index 3a1ff5f..3bc2df1 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <functional>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -86,7 +87,7 @@
 
 class HALSimWSProviderSimDevices {
  public:
-  using LoopFn = std::function<void(void)>;
+  using LoopFn = std::function<void()>;
   using UvExecFn = wpi::uv::AsyncFunction<void(LoopFn)>;
 
   explicit HALSimWSProviderSimDevices(ProviderContainer& providers)
diff --git a/simulation/halsim_ws_server/CMakeLists.txt b/simulation/halsim_ws_server/CMakeLists.txt
index e5b55c8..370d2f8 100644
--- a/simulation/halsim_ws_server/CMakeLists.txt
+++ b/simulation/halsim_ws_server/CMakeLists.txt
@@ -13,4 +13,4 @@
 
 set_property(TARGET halsim_ws_server PROPERTY FOLDER "libraries")
 
-install(TARGETS halsim_ws_server EXPORT halsim_ws_server DESTINATION "${main_lib_dest}")
+install(TARGETS halsim_ws_server EXPORT halsim_ws_server)
diff --git a/simulation/halsim_ws_server/build.gradle b/simulation/halsim_ws_server/build.gradle
index fbdee04..8db1951 100644
--- a/simulation/halsim_ws_server/build.gradle
+++ b/simulation/halsim_ws_server/build.gradle
@@ -1,3 +1,6 @@
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
 
 description = "WebSocket Server Extension"
 
@@ -20,25 +23,20 @@
 
 model {
     testSuites {
-        def comps = $.components
-        if (!project.hasProperty('onlylinuxathena')) {
-            "${pluginName}Test"(GoogleTestTestSuiteSpec) {
-                for(NativeComponentSpec c : comps) {
-                    if (c.name == pluginName) {
-                        testing c
-                        break
-                    }
+        "${pluginName}Test"(GoogleTestTestSuiteSpec) {
+            for(NativeComponentSpec c : $.components) {
+                if (c.name == pluginName) {
+                    testing c
+                    break
                 }
-                sources {
-                    cpp {
-                        source {
-                            srcDirs 'src/test/native/cpp'
-                            include '**/*.cpp'
-                        }
-                        exportedHeaders {
-                            srcDirs 'src/test/native/include', 'src/main/native/cpp'
-                        }
-                    }
+            }
+            sources.cpp {
+                source {
+                    srcDirs 'src/test/native/cpp'
+                    include '**/*.cpp'
+                }
+                exportedHeaders {
+                    srcDirs 'src/test/native/include', 'src/main/native/cpp'
                 }
             }
         }
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp b/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
index 77dec08..ee188b3 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
@@ -9,10 +9,10 @@
 #include <string_view>
 
 #include <fmt/format.h>
+#include <wpi/MemoryBuffer.h>
 #include <wpi/SmallVector.h>
 #include <wpi/StringExtras.h>
 #include <wpi/fs.h>
-#include <wpi/raw_istream.h>
 #include <wpinet/MimeTypes.h>
 #include <wpinet/UrlParser.h>
 #include <wpinet/raw_uv_ostream.h>
@@ -76,6 +76,16 @@
 }
 
 void HALSimHttpConnection::OnSimValueChanged(const wpi::json& msg) {
+  // Skip sending if this message is not in the allowed filter list
+  try {
+    auto& type = msg.at("type").get_ref<const std::string&>();
+    if (!m_server->CanSendMessage(type)) {
+      return;
+    }
+  } catch (wpi::json::exception& e) {
+    fmt::print(stderr, "Error with message: {}\n", e.what());
+  }
+
   // render json to buffers
   wpi::SmallVector<uv::Buffer, 4> sendBufs;
   wpi::raw_uv_ostream os{sendBufs, [this]() -> uv::Buffer {
@@ -115,8 +125,9 @@
   }
 
   // open file
-  wpi::raw_fd_istream is{filename, ec, true};
-  if (ec) {
+  std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
+      wpi::MemoryBuffer::GetFile(filename, ec);
+  if (fileBuffer == nullptr || ec) {
     MySendError(404, "error opening file");
     return;
   }
@@ -132,16 +143,7 @@
   wpi::SmallVector<uv::Buffer, 4> bodyData;
   wpi::raw_uv_ostream bodyOs{bodyData, 4096};
 
-  std::string fileBuf;
-  size_t oldSize = 0;
-
-  while (fileBuf.size() < size) {
-    oldSize = fileBuf.size();
-    fileBuf.resize(oldSize + 1);
-    is.read(&(*fileBuf.begin()) + oldSize, 1);
-  }
-
-  bodyOs << fileBuf;
+  bodyOs << fileBuffer->GetBuffer();
 
   SendData(bodyOs.bufs(), false);
   if (!m_keepAlive) {
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
index 8a6df97..790d0b5 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
@@ -77,6 +77,22 @@
     m_port = 3300;
   }
 
+  const char* msgFilters = std::getenv("HALSIMWS_FILTERS");
+  if (msgFilters != nullptr) {
+    m_useMsgFiltering = true;
+
+    std::string_view filters(msgFilters);
+    filters = wpi::trim(filters);
+    wpi::SmallVector<std::string_view, 16> filtersSplit;
+
+    wpi::split(filters, filtersSplit, ',', -1, false);
+    for (auto val : filtersSplit) {
+      m_msgFilters[wpi::trim(val)] = true;
+    }
+  } else {
+    m_useMsgFiltering = false;
+  }
+
   return true;
 }
 
@@ -100,6 +116,16 @@
   m_server->Listen();
   fmt::print("Listening at http://localhost:{}\n", m_port);
   fmt::print("WebSocket URI: {}\n", m_uri);
+
+  // Print any filters we are using
+  if (m_useMsgFiltering) {
+    fmt::print("WS Message Filters:");
+    for (auto filter : m_msgFilters.keys()) {
+      fmt::print("* \"{}\"\n", filter);
+    }
+  } else {
+    fmt::print("No WS Message Filters specified");
+  }
 }
 
 bool HALSimWeb::RegisterWebsocket(
@@ -157,3 +183,10 @@
     fmt::print(stderr, "Error with incoming message: {}\n", e.what());
   }
 }
+
+bool HALSimWeb::CanSendMessage(std::string_view type) {
+  if (!m_useMsgFiltering) {
+    return true;
+  }
+  return m_msgFilters.count(type) > 0;
+}
diff --git a/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h b/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
index 91cfb61..7073256 100644
--- a/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
+++ b/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
@@ -10,6 +10,7 @@
 #include <utility>
 
 #include <HALSimBaseWebSocketConnection.h>
+#include <wpi/json_fwd.h>
 #include <wpi/mutex.h>
 #include <wpinet/HttpWebSocketServerConnection.h>
 #include <wpinet/uv/AsyncFunction.h>
@@ -17,10 +18,6 @@
 
 #include "HALSimWeb.h"
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace wpilibws {
 
 class HALSimHttpConnection
diff --git a/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h b/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
index de5d1f2..03f2cb6 100644
--- a/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
+++ b/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
@@ -7,23 +7,22 @@
 #include <functional>
 #include <memory>
 #include <string>
+#include <string_view>
 
 #include <WSBaseProvider.h>
 #include <WSProviderContainer.h>
 #include <WSProvider_SimDevice.h>
+#include <wpi/StringMap.h>
+#include <wpi/json_fwd.h>
 #include <wpinet/uv/Async.h>
 #include <wpinet/uv/Loop.h>
 #include <wpinet/uv/Tcp.h>
 
-namespace wpi {
-class json;
-}  // namespace wpi
-
 namespace wpilibws {
 
 class HALSimWeb : public std::enable_shared_from_this<HALSimWeb> {
  public:
-  using LoopFunc = std::function<void(void)>;
+  using LoopFunc = std::function<void()>;
   using UvExecFunc = wpi::uv::Async<LoopFunc>;
 
   HALSimWeb(wpi::uv::Loop& loop, ProviderContainer& providers,
@@ -41,6 +40,8 @@
   // network -> sim
   void OnNetValueChanged(const wpi::json& msg);
 
+  bool CanSendMessage(std::string_view type);
+
   const std::string& GetWebrootSys() const { return m_webroot_sys; }
   const std::string& GetWebrootUser() const { return m_webroot_user; }
   const std::string& GetServerUri() const { return m_uri; }
@@ -69,6 +70,9 @@
 
   std::string m_uri;
   int m_port;
+
+  bool m_useMsgFiltering;
+  wpi::StringMap<bool> m_msgFilters;
 };
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_server/src/test/native/cpp/main.cpp b/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
index a9f739a..61e3a88 100644
--- a/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
@@ -5,6 +5,7 @@
 #include <thread>
 
 #include <fmt/format.h>
+#include <gtest/gtest.h>
 #include <hal/DriverStation.h>
 #include <hal/HALBase.h>
 #include <hal/Main.h>
@@ -13,7 +14,6 @@
 
 #include "HALSimWSServer.h"
 #include "WebServerClientTest.h"
-#include "gtest/gtest.h"
 
 namespace uv = wpi::uv;
 
diff --git a/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h b/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
index cda61dd..6f2995f 100644
--- a/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
+++ b/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <functional>
 #include <memory>
 #include <string>
 
@@ -21,7 +22,7 @@
 class WebServerClientTest {
  public:
   using BufferPool = wpi::uv::SimpleBufferPool<4>;
-  using LoopFunc = std::function<void(void)>;
+  using LoopFunc = std::function<void()>;
   using UvExecFunc = wpi::uv::AsyncFunction<void(LoopFunc)>;
 
   explicit WebServerClientTest(wpi::uv::Loop& loop) : m_loop(loop) {}
diff --git a/simulation/halsim_xrp/CMakeLists.txt b/simulation/halsim_xrp/CMakeLists.txt
new file mode 100644
index 0000000..cc83874
--- /dev/null
+++ b/simulation/halsim_xrp/CMakeLists.txt
@@ -0,0 +1,16 @@
+project(halsim_xrp)
+
+include(CompileWarnings)
+
+file(GLOB halsim_xrp_src src/main/native/cpp/*.cpp)
+
+add_library(halsim_xrp SHARED ${halsim_xrp_src})
+wpilib_target_warnings(halsim_xrp)
+set_target_properties(halsim_xrp PROPERTIES DEBUG_POSTFIX "d")
+target_link_libraries(halsim_xrp PUBLIC hal halsim_ws_core)
+
+target_include_directories(halsim_xrp PRIVATE src/main/native/include)
+
+set_property(TARGET halsim_xrp PROPERTY FOLDER "libraries")
+
+install(TARGETS halsim_xrp EXPORT halsim_xrp DESTINATION "${main_lib_dest}")
diff --git a/simulation/halsim_xrp/README.md b/simulation/halsim_xrp/README.md
new file mode 100644
index 0000000..79d6d52
--- /dev/null
+++ b/simulation/halsim_xrp/README.md
@@ -0,0 +1,125 @@
+# HAL XRP Client
+
+This is an extension that provides a client version of the XRP protocol for transmitting robot hardware interface state to an XRP robot over UDP.
+
+## Configuration
+
+The XRP client has a number of configuration options available through environment variables.
+
+``HALSIMXRP_HOST``: The host to connect to.  Defaults to localhost.
+
+``HALSIMXRP_PORT``: The port number to connect to.  Defaults to 3540.
+
+## XRP Protocol
+
+The WPILib -> XRP protocol is binary-based to save on bandwidth due to hardware limitations of the XRP robot. The messages to/from the XRP follow a the format below:
+
+| 2 bytes             | 1 byte            | n bytes                             |
+|---------------------|-------------------|-------------------------------------|
+| _uint16_t_ sequence | _uint8_t_ control | [&lt;Tagged Data&gt;](#tagged-data) |
+
+### Control Byte
+
+The control byte is used to indicate the current `enabled` state of the WPILib robot code. When this is set to `1`, the robot is enabled, and when it is set to `0` it is disabled.
+
+Messages originating from the XRP have an unspecified value for the control byte.
+
+### Tagged Data
+
+The `Tagged Data` section can contain an arbitrary number of data blocks. Each block has the format below:
+
+| 1 byte         | 1 byte          | n bytes         |
+|----------------|-----------------|-----------------|
+| _uint8_t_ size | _uint8_t_ tagID | &lt;payload&gt; |
+
+The `size` byte encodes the size of the data block, _excluding_ itself. Thus the smallest block size is 2 bytes, with a size value of 1 (1 size byte, 1 tag byte, 0 payload bytes). Maximum size of the payload is 254 bytes.
+
+
+Utilizing tagged data blocks allows us to send multiple pieces of data in a single UDP packet. The tags currently implemented for the XRP are as follows:
+
+| Tag  | Description                   |
+|------|-------------------------------|
+| 0x12 | [XRPMotor](#xrpmotor)         |
+| 0x13 | [XRPServo](#xrpservo)         |
+| 0x14 | [DIO](#dio)                   |
+| 0x15 | [AnalogIn](#analogin)         |
+| 0x16 | [XRPGyro](#xrpgyro)           |
+| 0x17 | [BuiltInAccel](#builtinaccel) |
+| 0x18 | [Encoder](#encoder)           |
+
+
+#### XRPMotor
+
+| Order | Data Type | Description       |
+|-------|-----------|-------------------|
+| 0     | _uint8_t_ | ID                |
+| 1     | _float_   | Value [-1.0, 1.0] |
+
+IDs:
+| ID | Description |
+|----|-------------|
+| 0  | Left Motor  |
+| 1  | Right Motor |
+| 2  | Motor 3     |
+| 3  | Motor 4     |
+
+#### XRPServo
+
+| Order | Data Type | Description      |
+|-------|-----------|------------------|
+| 0     | _uint8_t_ | ID               |
+| 1     | _float_   | Value [0.0, 1.0] |
+
+IDs:
+| ID | Description |
+|----|-------------|
+| 4  | Servo 1     |
+| 5  | Servo 2     |
+
+#### DIO
+
+| Order | Data Type | Description        |
+|-------|-----------|--------------------|
+| 0     | _uint8_t_ | ID                 |
+| 1     | _uint8_t_ | Value (True/False) |
+
+#### AnalogIn
+
+| Order | Data Type | Description |
+|-------|-----------|-------------|
+| 0     | _uint8_t_ | ID          |
+| 1     | _float_   | Value       |
+
+#### XRPGyro
+
+| Order | Data Type | Description   |
+|-------|-----------|---------------|
+| 0     | _float_   | rate_x (dps)  |
+| 1     | _float_   | rate_y (dps)  |
+| 2     | _float_   | rate_z (dps)  |
+| 3     | _float_   | angle_x (deg) |
+| 4     | _float_   | angle_y (deg) |
+| 5     | _float_   | angle_z (deg) |
+
+#### BuiltInAccel
+
+| Order | Data Type | Description |
+|-------|-----------|-------------|
+| 0     | _float_   | accel_x (g) |
+| 1     | _float_   | accel_y (g) |
+| 2     | _float_   | accel_z (g) |
+
+#### Encoder
+
+| Order | Data Type | Description |
+|-------|-----------|-------------|
+| 0     | _uint8_t_ | ID          |
+| 1     | _int32_t_ | Value       |
+
+IDs:
+| ID | Description         |
+|----|---------------------|
+| 0  | Left Motor Encoder  |
+| 1  | Right Motor Encoder |
+| 2  | Motor 3 Encoder     |
+| 3  | Motor 4 Encoder     |
diff --git a/simulation/halsim_xrp/build.gradle b/simulation/halsim_xrp/build.gradle
new file mode 100644
index 0000000..e745820
--- /dev/null
+++ b/simulation/halsim_xrp/build.gradle
@@ -0,0 +1,35 @@
+if (project.hasProperty('onlylinuxathena')) {
+    return;
+}
+
+description = "XRP Extension"
+
+ext {
+    includeWpiutil = true
+    pluginName = 'halsim_xrp'
+}
+
+apply plugin: 'google-test-test-suite'
+
+ext {
+    staticGtestConfigs = [:]
+}
+
+staticGtestConfigs["${pluginName}Test"] = []
+apply from: "${rootDir}/shared/googletest.gradle"
+
+apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
+
+model {
+    binaries {
+        all {
+            if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
+                it.buildable = false
+                return
+            }
+
+            lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
+            lib project: ":simulation:halsim_ws_core", library: "halsim_ws_core", linkage: "static"
+        }
+    }
+}
diff --git a/simulation/halsim_xrp/src/dev/native/cpp/main.cpp b/simulation/halsim_xrp/src/dev/native/cpp/main.cpp
new file mode 100644
index 0000000..a3e363e
--- /dev/null
+++ b/simulation/halsim_xrp/src/dev/native/cpp/main.cpp
@@ -0,0 +1,5 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source 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/simulation/halsim_xrp/src/main/native/cpp/HALSimXRP.cpp b/simulation/halsim_xrp/src/main/native/cpp/HALSimXRP.cpp
new file mode 100644
index 0000000..0f2b93b
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/cpp/HALSimXRP.cpp
@@ -0,0 +1,164 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "HALSimXRP.h"
+
+#include <cstdio>
+
+#include <fmt/format.h>
+#include <wpi/Endian.h>
+#include <wpi/MathExtras.h>
+#include <wpi/SmallString.h>
+#include <wpinet/raw_uv_ostream.h>
+#include <wpinet/uv/util.h>
+
+namespace uv = wpi::uv;
+
+using namespace wpilibxrp;
+
+HALSimXRP::HALSimXRP(wpi::uv::Loop& loop,
+                     wpilibws::ProviderContainer& providers,
+                     wpilibws::HALSimWSProviderSimDevices& simDevicesProvider)
+    : m_loop(loop),
+      m_providers(providers),
+      m_simDevicesProvider(simDevicesProvider) {
+  m_loop.error.connect([](uv::Error err) {
+    fmt::print(stderr, "HALSim XRP Client libuv Error: {}\n", err.str());
+  });
+
+  m_udp_client = uv::Udp::Create(m_loop);
+  m_exec = UvExecFunc::Create(m_loop);
+  if (m_exec) {
+    m_exec->wakeup.connect([](auto func) { func(); });
+  }
+}
+
+bool HALSimXRP::Initialize() {
+  if (!m_udp_client || !m_exec) {
+    return false;
+  }
+
+  const char* host = std::getenv("HALSIMXRP_HOST");
+  if (host != nullptr) {
+    m_host = host;
+  } else {
+    m_host = "localhost";
+  }
+
+  const char* port = std::getenv("HALSIMXRP_PORT");
+  if (port != nullptr) {
+    try {
+      m_port = std::stoi(port);
+    } catch (const std::invalid_argument& err) {
+      fmt::print(stderr, "Error decoding HALSIMXRP_PORT ({})\n", err.what());
+      return false;
+    }
+  } else {
+    m_port = 3540;
+  }
+
+  wpilibxrp::WPILibUpdateFunc func = [&](const wpi::json& data) {
+    OnNetValueChanged(data);
+  };
+
+  m_xrp.SetWPILibUpdateFunc(func);
+
+  return true;
+}
+
+void HALSimXRP::Start() {
+  // struct sockaddr_in dest;
+  uv::NameToAddr(m_host, m_port, &m_dest);
+
+  m_udp_client->Connect(m_dest);
+
+  m_udp_client->received.connect(
+      [this, socket = m_udp_client.get()](auto data, size_t len, auto rinfo,
+                                          unsigned int flags) {
+        ParsePacket({reinterpret_cast<uint8_t*>(data.base), len});
+      });
+
+  m_udp_client->closed.connect([]() { fmt::print("Socket Closed\n"); });
+
+  // Fake the OnNetworkConnected call
+  auto hws = shared_from_this();
+  m_simDevicesProvider.OnNetworkConnected(hws);
+  m_providers.ForEach(
+      [hws](std::shared_ptr<wpilibws::HALSimWSBaseProvider> provider) {
+        provider->OnNetworkConnected(hws);
+      });
+
+  m_udp_client->StartRecv();
+
+  std::puts("HALSimXRP Initialized");
+}
+
+void HALSimXRP::ParsePacket(std::span<const uint8_t> packet) {
+  if (packet.size() < 3) {
+    return;
+  }
+
+  // Hand this off to the XRP object to deal with the messages
+  m_xrp.HandleXRPUpdate(packet);
+}
+
+void HALSimXRP::OnNetValueChanged(const wpi::json& msg) {
+  try {
+    auto& type = msg.at("type").get_ref<const std::string&>();
+    auto& device = msg.at("device").get_ref<const std::string&>();
+
+    wpi::SmallString<64> key;
+    key.append(type);
+    if (!device.empty()) {
+      key.append("/");
+      key.append(device);
+    }
+
+    auto provider = m_providers.Get(key.str());
+    if (provider) {
+      provider->OnNetValueChanged(msg.at("data"));
+    }
+  } catch (wpi::json::exception& e) {
+    fmt::print(stderr, "Error with incoming message: {}\n", e.what());
+  }
+}
+
+void HALSimXRP::OnSimValueChanged(const wpi::json& simData) {
+  // We'll use a signal from robot code to send all the data
+  if (simData["type"] == "HAL") {
+    auto halData = simData["data"];
+    if (halData.find(">sim_periodic_after") != halData.end()) {
+      SendStateToXRP();
+    }
+  } else {
+    m_xrp.HandleWPILibUpdate(simData);
+  }
+}
+
+uv::SimpleBufferPool<4>& HALSimXRP::GetBufferPool() {
+  static uv::SimpleBufferPool<4> bufferPool(128);
+  return bufferPool;
+}
+
+void HALSimXRP::SendStateToXRP() {
+  wpi::SmallVector<uv::Buffer, 4> sendBufs;
+  wpi::raw_uv_ostream stream{sendBufs, [&] {
+                               std::lock_guard lock(m_buffer_mutex);
+                               return GetBufferPool().Allocate();
+                             }};
+  m_xrp.SetupXRPSendBuffer(stream);
+
+  m_exec->Send([this, sendBufs]() mutable {
+    m_udp_client->Send(sendBufs, [&](auto bufs, uv::Error err) {
+      {
+        std::lock_guard lock(m_buffer_mutex);
+        GetBufferPool().Release(bufs);
+      }
+
+      if (err) {
+        // no-op
+      }
+    });
+  });
+}
diff --git a/simulation/halsim_xrp/src/main/native/cpp/HALSimXRPClient.cpp b/simulation/halsim_xrp/src/main/native/cpp/HALSimXRPClient.cpp
new file mode 100644
index 0000000..2cd36c3
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/cpp/HALSimXRPClient.cpp
@@ -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.
+
+#include "HALSimXRPClient.h"
+
+#include <WSProviderContainer.h>
+#include <WSProvider_Analog.h>
+#include <WSProvider_BuiltInAccelerometer.h>
+#include <WSProvider_DIO.h>
+#include <WSProvider_DriverStation.h>
+#include <WSProvider_Encoder.h>
+#include <WSProvider_HAL.h>
+#include <WSProvider_SimDevice.h>
+#include <wpinet/EventLoopRunner.h>
+
+using namespace wpilibxrp;
+using namespace wpilibws;
+
+bool HALSimXRPClient::Initialize() {
+  bool result = true;
+  runner.ExecSync([&](wpi::uv::Loop& loop) {
+    simxrp = std::make_shared<HALSimXRP>(loop, providers, simDevices);
+
+    if (!simxrp->Initialize()) {
+      result = false;
+      return;
+    }
+
+    WSRegisterFunc registerFunc = [&](auto key, auto provider) {
+      providers.Add(key, provider);
+    };
+
+    // Minimized set of HAL providers
+    HALSimWSProviderAnalogIn::Initialize(registerFunc);
+    HALSimWSProviderBuiltInAccelerometer::Initialize(registerFunc);
+    HALSimWSProviderDIO::Initialize(registerFunc);
+    HALSimWSProviderDriverStation::Initialize(registerFunc);
+    HALSimWSProviderEncoder::Initialize(registerFunc);
+    HALSimWSProviderHAL::Initialize(registerFunc);
+
+    simDevices.Initialize(loop);
+
+    simxrp->Start();
+  });
+
+  return result;
+}
diff --git a/simulation/halsim_xrp/src/main/native/cpp/XRP.cpp b/simulation/halsim_xrp/src/main/native/cpp/XRP.cpp
new file mode 100644
index 0000000..ae78469
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/cpp/XRP.cpp
@@ -0,0 +1,386 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "XRP.h"
+
+#include <fmt/format.h>
+#include <wpi/Endian.h>
+#include <wpi/MathExtras.h>
+#include <wpi/json.h>
+
+using namespace wpilibxrp;
+
+XRP::XRP()
+    : m_gyro_name{"XRPGyro"}, m_wpilib_update_func([](const wpi::json&) {}) {
+  // Set up the inputs and outputs
+  m_motor_outputs.emplace(0, 0.0f);
+  m_motor_outputs.emplace(1, 0.0f);
+  m_motor_outputs.emplace(2, 0.0f);
+  m_motor_outputs.emplace(3, 0.0f);
+
+  m_servo_outputs.emplace(4, 0.5f);
+  m_servo_outputs.emplace(5, 0.5f);
+
+  m_encoder_inputs.emplace(1, 0);
+  m_encoder_inputs.emplace(2, 0);
+  m_encoder_inputs.emplace(0, 0);
+  m_encoder_inputs.emplace(3, 0);
+}
+
+void XRP::HandleWPILibUpdate(const wpi::json& data) {
+  if (data.count("type") == 0) {
+    return;
+  }
+
+  if (data["type"] == "DriverStation") {
+    HandleDriverStationSimValueChanged(data);
+  } else if (data["type"] == "XRPMotor") {
+    HandleMotorSimValueChanged(data);
+  } else if (data["type"] == "XRPServo") {
+    HandleServoSimValueChanged(data);
+  } else if (data["type"] == "DIO") {
+    HandleDIOSimValueChanged(data);
+  } else if (data["type"] == "Gyro") {
+    HandleGyroSimValueChanged(data);
+  } else if (data["type"] == "Encoder") {
+    HandleEncoderSimValueChanged(data);
+  }
+}
+
+void XRP::HandleXRPUpdate(std::span<const uint8_t> packet) {
+  uint16_t seq = (packet[0] << 8) + packet[1];
+
+  if (seq <= m_wpilib_bound_seq) {
+    // If the old sequence was within 3 or uint16_t max and the new
+    // sequence is < 3 - we've prob rolled over
+    if (!((0xFFFF - m_wpilib_bound_seq < 3) && seq < 3)) {
+      return;
+    }
+  }
+
+  m_wpilib_bound_seq = seq;
+
+  // Tagged data starts at byte 3
+  packet = packet.subspan(3);
+
+  // Loop to handle multiple tags
+  while (!packet.empty()) {
+    auto tagLength = packet[0];
+    auto tagPacket = packet.subspan(0, tagLength + 1);
+
+    // NOTE: tagPacket contains the size and tag bytes as well
+    // Verify that the packet is indeed the right size
+    if (tagPacket.size() != static_cast<size_t>(tagLength + 1)) {
+      break;
+    }
+
+    switch (packet[1]) {
+      case XRP_TAG_GYRO:
+        ReadGyroTag(tagPacket);
+        break;
+      case XRP_TAG_ACCEL:
+        ReadAccelTag(tagPacket);
+        break;
+      case XRP_TAG_DIO:
+        ReadDIOTag(tagPacket);
+        break;
+      case XRP_TAG_ENCODER:
+        ReadEncoderTag(tagPacket);
+        break;
+      case XRP_TAG_ANALOG:
+        ReadAnalogTag(tagPacket);
+        break;
+    }
+
+    packet = packet.subspan(tagLength + 1);
+  }
+}
+
+void XRP::SetupXRPSendBuffer(wpi::raw_uv_ostream& buf) {
+  SetupSendHeader(buf);
+  SetupMotorTag(buf);
+  SetupServoTag(buf);
+  SetupDigitalOutTag(buf);
+  m_xrp_bound_seq++;
+}
+
+// WPILib Sim Handlers
+void XRP::HandleDriverStationSimValueChanged(const wpi::json& data) {
+  auto dsData = data["data"];
+  if (dsData.find(">enabled") != dsData.end()) {
+    m_robot_enabled = dsData[">enabled"];
+  }
+}
+
+void XRP::HandleMotorSimValueChanged(const wpi::json& data) {
+  int deviceId = -1;
+  auto motorData = data["data"];
+
+  if (data["device"] == "motorL") {
+    deviceId = 0;
+  } else if (data["device"] == "motorR") {
+    deviceId = 1;
+  } else if (data["device"] == "motor3") {
+    deviceId = 2;
+  } else if (data["device"] == "motor4") {
+    deviceId = 3;
+  }
+
+  if (deviceId != -1 && motorData.find("<speed") != motorData.end()) {
+    m_motor_outputs[deviceId] = motorData["<speed"];
+  }
+}
+
+void XRP::HandleServoSimValueChanged(const wpi::json& data) {
+  int deviceId = -1;
+  auto servoData = data["data"];
+
+  if (data["device"] == "servo1") {
+    deviceId = 4;
+  } else if (data["device"] == "servo2") {
+    deviceId = 5;
+  }
+
+  if (deviceId != -1 && servoData.find("<position") != servoData.end()) {
+    m_servo_outputs[deviceId] = servoData["<position"];
+  }
+}
+
+void XRP::HandleDIOSimValueChanged(const wpi::json& data) {
+  int deviceId = -1;
+  auto dioData = data["data"];
+
+  try {
+    deviceId = std::stoi(data["device"].get<std::string>());
+  } catch (const std::invalid_argument&) {
+    deviceId = -1;
+  }
+
+  // Bail out early if device ID is invalid or if it's "spoken for"
+  if (deviceId == -1) {
+    return;
+  }
+
+  if (dioData.find("<init") != dioData.end() && dioData["<init"]) {
+    // All DIOs are initialized as inputs by default
+    m_digital_inputs.emplace(deviceId, false);
+  }
+
+  if (dioData.find("<input") != dioData.end() && dioData["<input"] == false) {
+    // We're registering an output device
+    // Remove from the digital inputs list (if present)
+    m_digital_inputs.erase(deviceId);
+    m_digital_outputs.emplace(deviceId, false);
+  }
+
+  if (dioData.find("<>value") != dioData.end() &&
+      m_digital_outputs.count(deviceId) > 0) {
+    m_digital_outputs[deviceId] = dioData["<>value"];
+  }
+}
+
+void XRP::HandleGyroSimValueChanged(const wpi::json& data) {
+  m_gyro_name = data["device"].get<std::string>();
+}
+
+void XRP::HandleEncoderSimValueChanged(const wpi::json& data) {
+  // We need to handle the various encoder cases
+  // 4/5 -> Encoder 0
+  // 6/7 -> Encoder 1
+  // 8/9 -> Encoder 2
+  // 10/11 -> Encoder 3
+  int deviceId = -1;
+  auto encData = data["data"];
+
+  try {
+    deviceId = std::stoi(data["device"].get<std::string>());
+  } catch (const std::invalid_argument&) {
+    deviceId = -1;
+  }
+
+  if (deviceId == -1) {
+    return;
+  }
+
+  if (encData.find("<init") != encData.end() && encData["<init"]) {
+    // The <channel_a and <channel_b values come with the init message
+    int chA = encData["<channel_a"];
+    int chB = encData["<channel_b"];
+
+    if ((chA == 4 && chB == 5) || (chA == 5 && chB == 4)) {
+      m_encoder_channel_map.emplace(0, deviceId);
+    } else if ((chA == 6 && chB == 7) || (chA == 7 && chB == 6)) {
+      m_encoder_channel_map.emplace(1, deviceId);
+    } else if ((chA == 8 && chB == 9) || (chA == 9 && chB == 8)) {
+      m_encoder_channel_map.emplace(2, deviceId);
+    } else if ((chA == 10 && chB == 11) || (chA == 11 && chB == 10)) {
+      m_encoder_channel_map.emplace(3, deviceId);
+    }
+  }
+}
+
+// ==================================
+// XRP Buffer Generation/Read Methods
+// ==================================
+
+void XRP::SetupSendHeader(wpi::raw_uv_ostream& buf) {
+  uint8_t pktSeq[2];
+  wpi::support::endian::write16be(pktSeq, m_xrp_bound_seq);
+
+  buf << pktSeq[0] << pktSeq[1]
+      << static_cast<uint8_t>(m_robot_enabled ? 1 : 0);
+}
+
+void XRP::SetupMotorTag(wpi::raw_uv_ostream& buf) {
+  uint8_t value[4];
+
+  for (auto motor : m_motor_outputs) {
+    // Motor payload is 6 bytes
+    buf << static_cast<uint8_t>(6)              // Size
+        << static_cast<uint8_t>(XRP_TAG_MOTOR)  // Tag
+        << static_cast<uint8_t>(motor.first);   // Channel
+
+    // Convert the value
+    wpi::support::endian::write32be(value,
+                                    wpi::bit_cast<uint32_t>(motor.second));
+    buf << value[0] << value[1] << value[2] << value[3];
+  }
+}
+
+void XRP::SetupServoTag(wpi::raw_uv_ostream& buf) {
+  uint8_t value[4];
+
+  for (auto servo : m_servo_outputs) {
+    // Servo payload is 6 bytes
+    buf << static_cast<uint8_t>(6)              // Size
+        << static_cast<uint8_t>(XRP_TAG_SERVO)  // Tag
+        << static_cast<uint8_t>(servo.first);   // Channel
+
+    // Convert the value
+    wpi::support::endian::write32be(value,
+                                    wpi::bit_cast<uint32_t>(servo.second));
+    buf << value[0] << value[1] << value[2] << value[3];
+  }
+}
+
+void XRP::SetupDigitalOutTag(wpi::raw_uv_ostream& buf) {
+  for (auto digitalOut : m_digital_outputs) {
+    // DIO payload is 3 bytes
+    buf << static_cast<uint8_t>(3)                           // Size
+        << static_cast<uint8_t>(XRP_TAG_DIO)                 // Tag
+        << static_cast<uint8_t>(digitalOut.first)            // Channel
+        << static_cast<uint8_t>(digitalOut.second ? 1 : 0);  // Value
+  }
+}
+
+void XRP::ReadGyroTag(std::span<const uint8_t> packet) {
+  if (packet.size() < 26) {
+    return;  // size(1) + tag(1) + 6x 4byte
+  }
+
+  packet = packet.subspan(2);  // Skip past the size and tag
+  float rate_x =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[0]));
+  float rate_y =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[4]));
+  float rate_z =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[8]));
+  float angle_x =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[12]));
+  float angle_y =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[16]));
+  float angle_z =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[20]));
+
+  // Make the json object
+  wpi::json gyroJson;
+  gyroJson["type"] = "Gyro";
+  gyroJson["device"] = m_gyro_name;
+  gyroJson["data"] = {{">rate_x", rate_x},   {">rate_y", rate_y},
+                      {">rate_z", rate_z},   {">angle_x", angle_x},
+                      {">angle_y", angle_y}, {">angle_z", angle_z}};
+
+  // Update WPILib
+  m_wpilib_update_func(gyroJson);
+}
+
+void XRP::ReadAccelTag(std::span<const uint8_t> packet) {
+  if (packet.size() < 14) {
+    return;  // size(1) + tag(1) + 3x 4 byte
+  }
+
+  packet = packet.subspan(2);  // Skip past the size and tag
+  float accel_x =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[0]));
+  float accel_y =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[4]));
+  float accel_z =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[8]));
+
+  wpi::json accelJson;
+  accelJson["type"] = "Accel";
+  accelJson["device"] = "BuiltInAccel";
+  accelJson["data"] = {{">x", accel_x}, {">y", accel_y}, {">z", accel_z}};
+
+  // Update WPILib
+  m_wpilib_update_func(accelJson);
+}
+
+void XRP::ReadDIOTag(std::span<const uint8_t> packet) {
+  if (packet.size() < 4) {
+    return;  // size(1) + tag(1) + id(1) + value(1)
+  }
+
+  wpi::json dioJson;
+  dioJson["type"] = "DIO";
+  dioJson["device"] = std::to_string(packet[2]);
+  dioJson["data"] = {{"<>value", packet[3] == 1}};
+
+  m_wpilib_update_func(dioJson);
+}
+
+void XRP::ReadEncoderTag(std::span<const uint8_t> packet) {
+  if (packet.size() < 7) {
+    return;  // size(1) + tag(1) + id(1) + value(4)
+  }
+
+  uint8_t encoderId = packet[2];
+
+  packet = packet.subspan(3);  // Skip past the size and tag and ID
+  int32_t value =
+      static_cast<int32_t>(wpi::support::endian::read32be(&packet[0]));
+
+  // Look up the registered encoders
+  if (m_encoder_channel_map.count(encoderId) == 0) {
+    return;
+  }
+
+  uint8_t wpilibEncoderChannel = m_encoder_channel_map[encoderId];
+
+  wpi::json encJson;
+  encJson["type"] = "Encoder";
+  encJson["device"] = std::to_string(wpilibEncoderChannel);
+  encJson["data"] = {{">count", value}};
+
+  m_wpilib_update_func(encJson);
+}
+
+void XRP::ReadAnalogTag(std::span<const uint8_t> packet) {
+  if (packet.size() < 7) {
+    return;  // size(1) + tag(1) + id(1) + float
+  }
+
+  uint8_t analogId = packet[2];
+
+  packet = packet.subspan(3);
+  float voltage =
+      wpi::bit_cast<float>(wpi::support::endian::read32be(&packet[0]));
+
+  wpi::json analogJson;
+  analogJson["type"] = "AI";
+  analogJson["device"] = std::to_string(analogId);
+  analogJson["data"] = {{">voltage", voltage}};
+
+  m_wpilib_update_func(analogJson);
+}
diff --git a/simulation/halsim_xrp/src/main/native/cpp/main.cpp b/simulation/halsim_xrp/src/main/native/cpp/main.cpp
new file mode 100644
index 0000000..a399c59
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/cpp/main.cpp
@@ -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.
+
+#include <cstdio>
+#include <memory>
+
+#include <hal/Extensions.h>
+
+#include "HALSimXRPClient.h"
+
+#if defined(Win32) || defined(_WIN32)
+#pragma comment(lib, "Ws2_32.lib")
+#endif
+
+using namespace wpilibxrp;
+
+static std::unique_ptr<HALSimXRPClient> gClient;
+
+/*--------------------------------------------------------------------------
+** Main Entry Point. Start up the listening threads
+**------------------------------------------------------------------------*/
+extern "C" {
+#if defined(WIN32) || defined(_WIN32)
+__declspec(dllexport)
+#endif
+
+    int HALSIM_InitExtension(void) {
+  std::puts("HALSim XRP Extension Initializing");
+
+  HAL_OnShutdown(nullptr, [](void*) { gClient.reset(); });
+
+  gClient = std::make_unique<HALSimXRPClient>();
+  if (!gClient->Initialize()) {
+    return -1;
+  }
+
+  std::puts("HALSim XRP Extension Initialized");
+  return 0;
+}
+
+}  // extern "C"
diff --git a/simulation/halsim_xrp/src/main/native/include/HALSimXRP.h b/simulation/halsim_xrp/src/main/native/include/HALSimXRP.h
new file mode 100644
index 0000000..ee6d730
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/include/HALSimXRP.h
@@ -0,0 +1,72 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <span>
+#include <string>
+
+#include <HALSimBaseWebSocketConnection.h>
+#include <WSProviderContainer.h>
+#include <WSProvider_SimDevice.h>
+#include <wpi/json_fwd.h>
+#include <wpinet/uv/Async.h>
+#include <wpinet/uv/Buffer.h>
+#include <wpinet/uv/Loop.h>
+#include <wpinet/uv/Timer.h>
+#include <wpinet/uv/Udp.h>
+
+#include "XRP.h"
+
+namespace wpilibxrp {
+
+// This masquerades as a "WebSocket" so that we can reuse the
+// stuff in halsim_ws_core
+class HALSimXRP : public wpilibws::HALSimBaseWebSocketConnection,
+                  public std::enable_shared_from_this<HALSimXRP> {
+ public:
+  using LoopFunc = std::function<void()>;
+  using UvExecFunc = wpi::uv::Async<LoopFunc>;
+
+  HALSimXRP(wpi::uv::Loop& loop, wpilibws::ProviderContainer& providers,
+            wpilibws::HALSimWSProviderSimDevices& simDevicesProvider);
+  HALSimXRP(const HALSimXRP&) = delete;
+  HALSimXRP& operator=(const HALSimXRP&) = delete;
+
+  bool Initialize();
+  void Start();
+
+  void ParsePacket(std::span<const uint8_t> packet);
+  void OnNetValueChanged(const wpi::json& msg);
+  void OnSimValueChanged(const wpi::json& simData) override;
+
+  const std::string& GetTargetHost() const { return m_host; }
+  int GetTargetPort() const { return m_port; }
+  wpi::uv::Loop& GetLoop() { return m_loop; }
+
+  UvExecFunc& GetExec() { return *m_exec; }
+
+ private:
+  XRP m_xrp;
+
+  wpi::uv::Loop& m_loop;
+  std::shared_ptr<wpi::uv::Udp> m_udp_client;
+  std::shared_ptr<UvExecFunc> m_exec;
+
+  wpilibws::ProviderContainer& m_providers;
+  wpilibws::HALSimWSProviderSimDevices& m_simDevicesProvider;
+
+  std::string m_host;
+  int m_port;
+
+  void SendStateToXRP();
+  wpi::uv::SimpleBufferPool<4>& GetBufferPool();
+  std::mutex m_buffer_mutex;
+
+  struct sockaddr_in m_dest;
+};
+
+}  // namespace wpilibxrp
diff --git a/simulation/halsim_xrp/src/main/native/include/HALSimXRPClient.h b/simulation/halsim_xrp/src/main/native/include/HALSimXRPClient.h
new file mode 100644
index 0000000..d6b26ff
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/include/HALSimXRPClient.h
@@ -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.
+
+#pragma once
+
+#include <memory>
+
+#include <WSProviderContainer.h>
+#include <WSProvider_SimDevice.h>
+#include <wpinet/EventLoopRunner.h>
+
+#include "HALSimXRP.h"
+
+namespace wpilibxrp {
+
+class HALSimXRPClient {
+ public:
+  HALSimXRPClient() = default;
+  HALSimXRPClient(const HALSimXRPClient&) = delete;
+  HALSimXRPClient& operator=(const HALSimXRPClient&) = delete;
+
+  bool Initialize();
+
+  wpilibws::ProviderContainer providers;
+  wpilibws::HALSimWSProviderSimDevices simDevices{providers};
+  wpi::EventLoopRunner runner;
+  std::shared_ptr<HALSimXRP> simxrp;
+};
+
+}  // namespace wpilibxrp
diff --git a/simulation/halsim_xrp/src/main/native/include/XRP.h b/simulation/halsim_xrp/src/main/native/include/XRP.h
new file mode 100644
index 0000000..2eaf2b0
--- /dev/null
+++ b/simulation/halsim_xrp/src/main/native/include/XRP.h
@@ -0,0 +1,88 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <functional>
+#include <map>
+#include <span>
+#include <string>
+
+#include <wpi/json_fwd.h>
+#include <wpinet/raw_uv_ostream.h>
+
+#define XRP_TAG_MOTOR 0x12
+#define XRP_TAG_SERVO 0x13
+#define XRP_TAG_DIO 0x14
+#define XRP_TAG_ANALOG 0x15
+#define XRP_TAG_GYRO 0x16
+#define XRP_TAG_ACCEL 0x17
+#define XRP_TAG_ENCODER 0x18
+
+namespace wpilibxrp {
+
+using WPILibUpdateFunc = std::function<void(const wpi::json&)>;
+
+class XRP {
+ public:
+  XRP();
+
+  void SetWPILibUpdateFunc(WPILibUpdateFunc func) {
+    m_wpilib_update_func = func;
+  }
+
+  void HandleWPILibUpdate(const wpi::json& data);
+  void HandleXRPUpdate(std::span<const uint8_t> packet);
+
+  void SetupXRPSendBuffer(wpi::raw_uv_ostream& buf);
+
+ private:
+  // To XRP Methods
+  void SetupSendHeader(wpi::raw_uv_ostream& buf);
+  void SetupMotorTag(wpi::raw_uv_ostream& buf);
+  void SetupServoTag(wpi::raw_uv_ostream& buf);
+  void SetupDigitalOutTag(wpi::raw_uv_ostream& buf);
+
+  // WPILib Sim Update Handlers
+  void HandleDriverStationSimValueChanged(const wpi::json& data);
+  void HandleMotorSimValueChanged(const wpi::json& data);
+  void HandleServoSimValueChanged(const wpi::json& data);
+  void HandleDIOSimValueChanged(const wpi::json& data);
+  void HandleGyroSimValueChanged(const wpi::json& data);
+  void HandleEncoderSimValueChanged(const wpi::json& data);
+
+  // XRP Packet Update Handlers
+  void ReadGyroTag(std::span<const uint8_t> packet);
+  void ReadAccelTag(std::span<const uint8_t> packet);
+  void ReadDIOTag(std::span<const uint8_t> packet);
+  void ReadEncoderTag(std::span<const uint8_t> packet);
+  void ReadAnalogTag(std::span<const uint8_t> packet);
+
+  // Robot State
+  std::map<uint8_t, bool> m_digital_outputs;
+  std::map<uint8_t, float> m_motor_outputs;
+  std::map<uint8_t, float> m_servo_outputs;
+
+  // Might not need these
+  std::map<uint8_t, bool> m_digital_inputs;
+  std::map<uint8_t, float> m_analog_inputs;
+  std::map<uint8_t, int32_t> m_encoder_inputs;
+
+  // We need a map from XRP encoder channels (0=left, 1=right etc)
+  // to WPILib device ID
+  // Key: XRP encoder number, Value: WPILib channel
+  // If no encoders are init-ed, this map is empty
+  std::map<uint8_t, uint8_t> m_encoder_channel_map;
+
+  uint16_t m_wpilib_bound_seq = 0;
+  uint16_t m_xrp_bound_seq = 0;
+
+  bool m_robot_enabled = false;
+
+  std::string m_gyro_name;
+
+  WPILibUpdateFunc m_wpilib_update_func;
+};
+
+}  // namespace wpilibxrp