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/wpiutil/src/main/native/cpp/DataLog.cpp b/wpiutil/src/main/native/cpp/DataLog.cpp
index 7009628..d05a49e 100644
--- a/wpiutil/src/main/native/cpp/DataLog.cpp
+++ b/wpiutil/src/main/native/cpp/DataLog.cpp
@@ -26,17 +26,37 @@
 #include <random>
 #include <vector>
 
-#include "fmt/format.h"
+#include <fmt/format.h>
+
 #include "wpi/Endian.h"
 #include "wpi/Logger.h"
 #include "wpi/MathExtras.h"
+#include "wpi/SmallString.h"
 #include "wpi/fs.h"
 #include "wpi/timestamp.h"
 
 using namespace wpi::log;
 
 static constexpr size_t kBlockSize = 16 * 1024;
+static constexpr size_t kMaxBufferCount = 1024 * 1024 / kBlockSize;
+static constexpr size_t kMaxFreeCount = 256 * 1024 / kBlockSize;
 static constexpr size_t kRecordMaxHeaderSize = 17;
+static constexpr uintmax_t kMinFreeSpace = 5 * 1024 * 1024;
+
+static std::string FormatBytesSize(uintmax_t value) {
+  static constexpr uintmax_t kKiB = 1024;
+  static constexpr uintmax_t kMiB = kKiB * 1024;
+  static constexpr uintmax_t kGiB = kMiB * 1024;
+  if (value >= kGiB) {
+    return fmt::format("{:.1f} GiB", static_cast<double>(value) / kGiB);
+  } else if (value >= kMiB) {
+    return fmt::format("{:.1f} MiB", static_cast<double>(value) / kMiB);
+  } else if (value >= kKiB) {
+    return fmt::format("{:.1f} KiB", static_cast<double>(value) / kKiB);
+  } else {
+    return fmt::format("{} B", value);
+  }
+}
 
 template <typename T>
 static unsigned int WriteVarInt(uint8_t* buf, T val) {
@@ -159,7 +179,7 @@
 DataLog::~DataLog() {
   {
     std::scoped_lock lock{m_mutex};
-    m_active = false;
+    m_state = kShutdown;
     m_doFlush = true;
   }
   m_cond.notify_all();
@@ -184,12 +204,56 @@
 
 void DataLog::Pause() {
   std::scoped_lock lock{m_mutex};
-  m_paused = true;
+  m_state = kPaused;
 }
 
 void DataLog::Resume() {
   std::scoped_lock lock{m_mutex};
-  m_paused = false;
+  if (m_state == kPaused) {
+    m_state = kActive;
+  } else if (m_state == kStopped) {
+    m_state = kStart;
+  }
+}
+
+void DataLog::Stop() {
+  {
+    std::scoped_lock lock{m_mutex};
+    m_state = kStopped;
+    m_newFilename.clear();
+  }
+  m_cond.notify_all();
+}
+
+bool DataLog::HasSchema(std::string_view name) const {
+  std::scoped_lock lock{m_mutex};
+  wpi::SmallString<128> fullName{"/.schema/"};
+  fullName += name;
+  auto it = m_entries.find(fullName);
+  return it != m_entries.end();
+}
+
+void DataLog::AddSchema(std::string_view name, std::string_view type,
+                        std::span<const uint8_t> schema, int64_t timestamp) {
+  std::scoped_lock lock{m_mutex};
+  wpi::SmallString<128> fullName{"/.schema/"};
+  fullName += name;
+  auto& entryInfo = m_entries[fullName];
+  if (entryInfo.id != 0) {
+    return;  // don't add duplicates
+  }
+  entryInfo.schemaData.assign(schema.begin(), schema.end());
+  int entry = StartImpl(fullName, type, {}, timestamp);
+
+  // inline AppendRaw() without releasing lock
+  if (entry <= 0) {
+    [[unlikely]] return;  // should never happen, but check anyway
+  }
+  if (m_state != kActive && m_state != kPaused) {
+    [[unlikely]] return;
+  }
+  StartRecord(entry, timestamp, schema.size(), 0);
+  AppendImpl(schema);
 }
 
 static void WriteToFile(fs::file_t f, std::span<const uint8_t> data,
@@ -236,93 +300,201 @@
   return filename;
 }
 
-void DataLog::WriterThreadMain(std::string_view dir) {
-  std::chrono::duration<double> periodTime{m_period};
+struct DataLog::WriterThreadState {
+  explicit WriterThreadState(std::string_view dir) : dirPath{dir} {}
+  WriterThreadState(const WriterThreadState&) = delete;
+  WriterThreadState& operator=(const WriterThreadState&) = delete;
+  ~WriterThreadState() { Close(); }
 
-  std::error_code ec;
-  fs::path dirPath{dir};
-  std::string filename;
-
-  {
-    std::scoped_lock lock{m_mutex};
-    filename = std::move(m_newFilename);
-    m_newFilename.clear();
-  }
-
-  if (filename.empty()) {
-    filename = MakeRandomFilename();
-  }
-
-  // try preferred filename, or randomize it a few times, before giving up
-  fs::file_t f;
-  for (int i = 0; i < 5; ++i) {
-    // open file for append
-#ifdef _WIN32
-    // WIN32 doesn't allow combination of CreateNew and Append
-    f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
-                             fs::OF_None);
-#else
-    f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
-                             fs::OF_Append);
-#endif
-    if (ec) {
-      WPI_ERROR(m_msglog, "Could not open log file '{}': {}",
-                (dirPath / filename).string(), ec.message());
-      // try again with random filename
-      filename = MakeRandomFilename();
-    } else {
-      break;
+  void Close() {
+    if (f != fs::kInvalidFile) {
+      fs::CloseFile(f);
+      f = fs::kInvalidFile;
     }
   }
 
-  if (f == fs::kInvalidFile) {
-    WPI_ERROR(m_msglog, "Could not open log file, no log being saved");
+  void SetFilename(std::string_view fn) {
+    baseFilename = fn;
+    filename = fn;
+    path = dirPath / filename;
+    segmentCount = 1;
+  }
+
+  void IncrementFilename() {
+    fs::path basePath{baseFilename};
+    filename = fmt::format("{}.{}{}", basePath.stem().string(), ++segmentCount,
+                           basePath.extension().string());
+    path = dirPath / filename;
+  }
+
+  fs::path dirPath;
+  std::string baseFilename;
+  std::string filename;
+  fs::path path;
+  fs::file_t f = fs::kInvalidFile;
+  uintmax_t freeSpace = UINTMAX_MAX;
+  int segmentCount = 1;
+};
+
+void DataLog::StartLogFile(WriterThreadState& state) {
+  std::error_code ec;
+
+  if (state.filename.empty()) {
+    state.SetFilename(MakeRandomFilename());
+  }
+
+  // get free space
+  auto freeSpaceInfo = fs::space(state.dirPath, ec);
+  if (!ec) {
+    state.freeSpace = freeSpaceInfo.available;
   } else {
-    WPI_INFO(m_msglog, "Logging to '{}'", (dirPath / filename).string());
+    state.freeSpace = UINTMAX_MAX;
+  }
+  if (state.freeSpace < kMinFreeSpace) {
+    WPI_ERROR(m_msglog,
+              "Insufficient free space ({} available), no log being saved",
+              FormatBytesSize(state.freeSpace));
+  } else {
+    // try preferred filename, or randomize it a few times, before giving up
+    for (int i = 0; i < 5; ++i) {
+      // open file for append
+#ifdef _WIN32
+      // WIN32 doesn't allow combination of CreateNew and Append
+      state.f =
+          fs::OpenFileForWrite(state.path, ec, fs::CD_CreateNew, fs::OF_None);
+#else
+      state.f =
+          fs::OpenFileForWrite(state.path, ec, fs::CD_CreateNew, fs::OF_Append);
+#endif
+      if (ec) {
+        WPI_ERROR(m_msglog, "Could not open log file '{}': {}",
+                  state.path.string(), ec.message());
+        // try again with random filename
+        state.SetFilename(MakeRandomFilename());
+      } else {
+        break;
+      }
+    }
+
+    if (state.f == fs::kInvalidFile) {
+      WPI_ERROR(m_msglog, "Could not open log file, no log being saved");
+    } else {
+      WPI_INFO(m_msglog, "Logging to '{}' ({} free space)", state.path.string(),
+               FormatBytesSize(state.freeSpace));
+    }
   }
 
   // write header (version 1.0)
-  if (f != fs::kInvalidFile) {
+  if (state.f != fs::kInvalidFile) {
     const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
-    WriteToFile(f, header, filename, m_msglog);
+    WriteToFile(state.f, header, state.filename, m_msglog);
     uint8_t extraLen[4];
     support::endian::write32le(extraLen, m_extraHeader.size());
-    WriteToFile(f, extraLen, filename, m_msglog);
+    WriteToFile(state.f, extraLen, state.filename, m_msglog);
     if (m_extraHeader.size() > 0) {
-      WriteToFile(f,
+      WriteToFile(state.f,
                   {reinterpret_cast<const uint8_t*>(m_extraHeader.data()),
                    m_extraHeader.size()},
-                  filename, m_msglog);
+                  state.filename, m_msglog);
     }
   }
+}
 
+void DataLog::WriterThreadMain(std::string_view dir) {
+  std::chrono::duration<double> periodTime{m_period};
+
+  WriterThreadState state{dir};
+  {
+    std::scoped_lock lock{m_mutex};
+    state.SetFilename(m_newFilename);
+    m_newFilename.clear();
+  }
+  StartLogFile(state);
+
+  std::error_code ec;
   std::vector<Buffer> toWrite;
+  int freeSpaceCount = 0;
+  int checkExistCount = 0;
+  bool blocked = false;
+  uintmax_t written = 0;
 
   std::unique_lock lock{m_mutex};
-  while (m_active) {
+  while (m_state != kShutdown) {
     bool doFlush = false;
     auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
     if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
       doFlush = true;
     }
 
-    if (!m_newFilename.empty()) {
+    if (m_state == kStopped) {
+      state.Close();
+      continue;
+    }
+
+    bool doStart = false;
+
+    // if file was deleted, recreate it with the same name
+    if (++checkExistCount >= 10) {
+      checkExistCount = 0;
+      lock.unlock();
+      bool exists = fs::exists(state.path, ec);
+      lock.lock();
+      if (!ec && !exists) {
+        state.Close();
+        state.IncrementFilename();
+        WPI_INFO(m_msglog, "Log file deleted, recreating as fresh log '{}'",
+                 state.filename);
+        doStart = true;
+      }
+    }
+
+    // start new file if file exceeds 1.8 GB
+    if (written > 1800000000ull) {
+      state.Close();
+      state.IncrementFilename();
+      WPI_INFO(m_msglog, "Log file reached 1.8 GB, starting new file '{}'",
+               state.filename);
+      doStart = true;
+    }
+
+    if (m_state == kStart || doStart) {
+      lock.unlock();
+      StartLogFile(state);
+      lock.lock();
+      if (state.f != fs::kInvalidFile) {
+        // Emit start and schema data records
+        for (auto&& entryInfo : m_entries) {
+          AppendStartRecord(entryInfo.second.id, entryInfo.first(),
+                            entryInfo.second.type,
+                            m_entryIds[entryInfo.second.id].metadata, 0);
+          if (!entryInfo.second.schemaData.empty()) {
+            StartRecord(entryInfo.second.id, 0,
+                        entryInfo.second.schemaData.size(), 0);
+            AppendImpl(entryInfo.second.schemaData);
+          }
+        }
+      }
+      m_state = kActive;
+      written = 0;
+    }
+
+    if (!m_newFilename.empty() && state.f != fs::kInvalidFile) {
       auto newFilename = std::move(m_newFilename);
       m_newFilename.clear();
-      lock.unlock();
       // rename
-      if (filename != newFilename) {
-        fs::rename(dirPath / filename, dirPath / newFilename, ec);
+      if (state.filename != newFilename) {
+        lock.unlock();
+        fs::rename(state.path, state.dirPath / newFilename, ec);
+        lock.lock();
       }
       if (ec) {
         WPI_ERROR(m_msglog, "Could not rename log file from '{}' to '{}': {}",
-                  filename, newFilename, ec.message());
+                  state.filename, newFilename, ec.message());
       } else {
-        WPI_INFO(m_msglog, "Renamed log file from '{}' to '{}'", filename,
+        WPI_INFO(m_msglog, "Renamed log file from '{}' to '{}'", state.filename,
                  newFilename);
       }
-      filename = std::move(newFilename);
-      lock.lock();
+      state.SetFilename(newFilename);
     }
 
     if (doFlush || m_doFlush) {
@@ -334,34 +506,58 @@
       // swap outgoing with empty vector
       toWrite.swap(m_outgoing);
 
-      if (f != fs::kInvalidFile) {
+      if (state.f != fs::kInvalidFile && !blocked) {
         lock.unlock();
+
+        // update free space every 10 flushes (in case other things are writing)
+        if (++freeSpaceCount >= 10) {
+          freeSpaceCount = 0;
+          auto freeSpaceInfo = fs::space(state.dirPath, ec);
+          if (!ec) {
+            state.freeSpace = freeSpaceInfo.available;
+          } else {
+            state.freeSpace = UINTMAX_MAX;
+          }
+        }
+
         // write buffers to file
         for (auto&& buf : toWrite) {
-          WriteToFile(f, buf.GetData(), filename, m_msglog);
+          // stop writing when we go below the minimum free space
+          state.freeSpace -= buf.GetData().size();
+          written += buf.GetData().size();
+          if (state.freeSpace < kMinFreeSpace) {
+            [[unlikely]] WPI_ERROR(
+                m_msglog,
+                "Stopped logging due to low free space ({} available)",
+                FormatBytesSize(state.freeSpace));
+            blocked = true;
+            break;
+          }
+          WriteToFile(state.f, buf.GetData(), state.filename, m_msglog);
         }
 
         // sync to storage
 #if defined(__linux__)
-        ::fdatasync(f);
+        ::fdatasync(state.f);
 #elif defined(__APPLE__)
-        ::fsync(f);
+        ::fsync(state.f);
 #endif
         lock.lock();
+        if (blocked) {
+          [[unlikely]] m_state = kPaused;
+        }
       }
 
       // release buffers back to free list
       for (auto&& buf : toWrite) {
         buf.Clear();
-        m_free.emplace_back(std::move(buf));
+        if (m_free.size() < kMaxFreeCount) {
+          [[likely]] m_free.emplace_back(std::move(buf));
+        }
       }
       toWrite.resize(0);
     }
   }
-
-  if (f != fs::kInvalidFile) {
-    fs::CloseFile(f);
-  }
 }
 
 void DataLog::WriterThreadMain(
@@ -384,7 +580,7 @@
   std::vector<Buffer> toWrite;
 
   std::unique_lock lock{m_mutex};
-  while (m_active) {
+  while (m_state != kShutdown) {
     bool doFlush = false;
     auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
     if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
@@ -412,7 +608,9 @@
       // release buffers back to free list
       for (auto&& buf : toWrite) {
         buf.Clear();
-        m_free.emplace_back(std::move(buf));
+        if (m_free.size() < kMaxFreeCount) {
+          [[likely]] m_free.emplace_back(std::move(buf));
+        }
       }
       toWrite.resize(0);
     }
@@ -429,13 +627,18 @@
 int DataLog::Start(std::string_view name, std::string_view type,
                    std::string_view metadata, int64_t timestamp) {
   std::scoped_lock lock{m_mutex};
+  return StartImpl(name, type, metadata, timestamp);
+}
+
+int DataLog::StartImpl(std::string_view name, std::string_view type,
+                       std::string_view metadata, int64_t timestamp) {
   auto& entryInfo = m_entries[name];
   if (entryInfo.id == 0) {
     entryInfo.id = ++m_lastId;
   }
-  auto& savedCount = m_entryCounts[entryInfo.id];
-  ++savedCount;
-  if (savedCount > 1) {
+  auto& entryInfo2 = m_entryIds[entryInfo.id];
+  ++entryInfo2.count;
+  if (entryInfo2.count > 1) {
     if (entryInfo.type != type) {
       WPI_ERROR(m_msglog,
                 "type mismatch for '{}': was '{}', requested '{}'; ignoring",
@@ -445,15 +648,26 @@
     return entryInfo.id;
   }
   entryInfo.type = type;
+  entryInfo2.metadata = metadata;
+
+  if (m_state != kActive && m_state != kPaused) {
+    [[unlikely]] return entryInfo.id;
+  }
+
+  AppendStartRecord(entryInfo.id, name, type, metadata, timestamp);
+  return entryInfo.id;
+}
+
+void DataLog::AppendStartRecord(int id, std::string_view name,
+                                std::string_view type,
+                                std::string_view metadata, int64_t timestamp) {
   size_t strsize = name.size() + type.size() + metadata.size();
   uint8_t* buf = StartRecord(0, timestamp, 5 + 12 + strsize, 5);
   *buf++ = impl::kControlStart;
-  wpi::support::endian::write32le(buf, entryInfo.id);
+  wpi::support::endian::write32le(buf, id);
   AppendStringImpl(name);
   AppendStringImpl(type);
   AppendStringImpl(metadata);
-
-  return entryInfo.id;
 }
 
 void DataLog::Finish(int entry, int64_t timestamp) {
@@ -461,15 +675,18 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  auto& savedCount = m_entryCounts[entry];
-  if (savedCount == 0) {
+  auto& entryInfo2 = m_entryIds[entry];
+  if (entryInfo2.count == 0) {
     return;
   }
-  --savedCount;
-  if (savedCount != 0) {
+  --entryInfo2.count;
+  if (entryInfo2.count != 0) {
     return;
   }
-  m_entryCounts.erase(entry);
+  m_entryIds.erase(entry);
+  if (m_state != kActive && m_state != kPaused) {
+    [[unlikely]] return;
+  }
   uint8_t* buf = StartRecord(0, timestamp, 5, 5);
   *buf++ = impl::kControlFinish;
   wpi::support::endian::write32le(buf, entry);
@@ -481,6 +698,10 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
+  m_entryIds[entry].metadata = metadata;
+  if (m_state != kActive && m_state != kPaused) {
+    [[unlikely]] return;
+  }
   uint8_t* buf = StartRecord(0, timestamp, 5 + 4 + metadata.size(), 5);
   *buf++ = impl::kControlSetMetadata;
   wpi::support::endian::write32le(buf, entry);
@@ -491,6 +712,13 @@
   assert(size <= kBlockSize);
   if (m_outgoing.empty() || size > m_outgoing.back().GetRemaining()) {
     if (m_free.empty()) {
+      if (m_outgoing.size() >= kMaxBufferCount) {
+        [[unlikely]] WPI_ERROR(
+            m_msglog,
+            "outgoing buffers exceeded threshold, pausing logging--"
+            "consider flushing to disk more frequently (smaller period)");
+        m_state = kPaused;
+      }
       m_outgoing.emplace_back();
     } else {
       m_outgoing.emplace_back(std::move(m_free.back()));
@@ -531,8 +759,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   StartRecord(entry, timestamp, data.size(), 0);
   AppendImpl(data);
@@ -545,8 +773,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   size_t size = 0;
   for (auto&& chunk : data) {
@@ -563,8 +791,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, 1, 1);
   buf[0] = value ? 1 : 0;
@@ -575,8 +803,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
   wpi::support::endian::write64le(buf, value);
@@ -587,15 +815,15 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, 4, 4);
   if constexpr (wpi::support::endian::system_endianness() ==
                 wpi::support::little) {
     std::memcpy(buf, &value, 4);
   } else {
-    wpi::support::endian::write32le(buf, wpi::FloatToBits(value));
+    wpi::support::endian::write32le(buf, wpi::bit_cast<uint32_t>(value));
   }
 }
 
@@ -604,15 +832,15 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
   if constexpr (wpi::support::endian::system_endianness() ==
                 wpi::support::little) {
     std::memcpy(buf, &value, 8);
   } else {
-    wpi::support::endian::write64le(buf, wpi::DoubleToBits(value));
+    wpi::support::endian::write64le(buf, wpi::bit_cast<uint64_t>(value));
   }
 }
 
@@ -629,8 +857,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   StartRecord(entry, timestamp, arr.size(), 0);
   uint8_t* buf;
@@ -653,8 +881,8 @@
     return;
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   StartRecord(entry, timestamp, arr.size(), 0);
   uint8_t* buf;
@@ -688,8 +916,8 @@
       return;
     }
     std::scoped_lock lock{m_mutex};
-    if (m_paused) {
-      return;
+    if (m_state != kActive) {
+      [[unlikely]] return;
     }
     StartRecord(entry, timestamp, arr.size() * 8, 0);
     uint8_t* buf;
@@ -721,22 +949,22 @@
       return;
     }
     std::scoped_lock lock{m_mutex};
-    if (m_paused) {
-      return;
+    if (m_state != kActive) {
+      [[unlikely]] return;
     }
     StartRecord(entry, timestamp, arr.size() * 4, 0);
     uint8_t* buf;
     while ((arr.size() * 4) > kBlockSize) {
       buf = Reserve(kBlockSize);
       for (auto val : arr.subspan(0, kBlockSize / 4)) {
-        wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
+        wpi::support::endian::write32le(buf, wpi::bit_cast<uint32_t>(val));
         buf += 4;
       }
       arr = arr.subspan(kBlockSize / 4);
     }
     buf = Reserve(arr.size() * 4);
     for (auto val : arr) {
-      wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
+      wpi::support::endian::write32le(buf, wpi::bit_cast<uint32_t>(val));
       buf += 4;
     }
   }
@@ -754,22 +982,22 @@
       return;
     }
     std::scoped_lock lock{m_mutex};
-    if (m_paused) {
-      return;
+    if (m_state != kActive) {
+      [[unlikely]] return;
     }
     StartRecord(entry, timestamp, arr.size() * 8, 0);
     uint8_t* buf;
     while ((arr.size() * 8) > kBlockSize) {
       buf = Reserve(kBlockSize);
       for (auto val : arr.subspan(0, kBlockSize / 8)) {
-        wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
+        wpi::support::endian::write64le(buf, wpi::bit_cast<uint64_t>(val));
         buf += 8;
       }
       arr = arr.subspan(kBlockSize / 8);
     }
     buf = Reserve(arr.size() * 8);
     for (auto val : arr) {
-      wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
+      wpi::support::endian::write64le(buf, wpi::bit_cast<uint64_t>(val));
       buf += 8;
     }
   }
@@ -787,8 +1015,8 @@
     size += 4 + str.size();
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, size, 4);
   wpi::support::endian::write32le(buf, arr.size());
@@ -810,12 +1038,169 @@
     size += 4 + str.size();
   }
   std::scoped_lock lock{m_mutex};
-  if (m_paused) {
-    return;
+  if (m_state != kActive) {
+    [[unlikely]] return;
   }
   uint8_t* buf = StartRecord(entry, timestamp, size, 4);
   wpi::support::endian::write32le(buf, arr.size());
-  for (auto sv : arr) {
+  for (auto&& sv : arr) {
     AppendStringImpl(sv);
   }
 }
+
+void DataLog::AppendStringArray(int entry,
+                                std::span<const WPI_DataLog_String> arr,
+                                int64_t timestamp) {
+  if (entry <= 0) {
+    return;
+  }
+  // storage: 4-byte array length, each string prefixed by 4-byte length
+  // calculate total size
+  size_t size = 4;
+  for (auto&& str : arr) {
+    size += 4 + str.len;
+  }
+  std::scoped_lock lock{m_mutex};
+  if (m_state != kActive) {
+    [[unlikely]] return;
+  }
+  uint8_t* buf = StartRecord(entry, timestamp, size, 4);
+  wpi::support::endian::write32le(buf, arr.size());
+  for (auto&& sv : arr) {
+    AppendStringImpl(sv.str);
+  }
+}
+
+extern "C" {
+
+struct WPI_DataLog* WPI_DataLog_Create(const char* dir, const char* filename,
+                                       double period, const char* extraHeader) {
+  return reinterpret_cast<WPI_DataLog*>(
+      new DataLog{dir, filename, period, extraHeader});
+}
+
+struct WPI_DataLog* WPI_DataLog_Create_Func(
+    void (*write)(void* ptr, const uint8_t* data, size_t len), void* ptr,
+    double period, const char* extraHeader) {
+  return reinterpret_cast<WPI_DataLog*>(
+      new DataLog{[=](auto data) { write(ptr, data.data(), data.size()); },
+                  period, extraHeader});
+}
+
+void WPI_DataLog_Release(struct WPI_DataLog* datalog) {
+  delete reinterpret_cast<DataLog*>(datalog);
+}
+
+void WPI_DataLog_SetFilename(struct WPI_DataLog* datalog,
+                             const char* filename) {
+  reinterpret_cast<DataLog*>(datalog)->SetFilename(filename);
+}
+
+void WPI_DataLog_Flush(struct WPI_DataLog* datalog) {
+  reinterpret_cast<DataLog*>(datalog)->Flush();
+}
+
+void WPI_DataLog_Pause(struct WPI_DataLog* datalog) {
+  reinterpret_cast<DataLog*>(datalog)->Pause();
+}
+
+void WPI_DataLog_Resume(struct WPI_DataLog* datalog) {
+  reinterpret_cast<DataLog*>(datalog)->Resume();
+}
+
+void WPI_DataLog_Stop(struct WPI_DataLog* datalog) {
+  reinterpret_cast<DataLog*>(datalog)->Stop();
+}
+
+int WPI_DataLog_Start(struct WPI_DataLog* datalog, const char* name,
+                      const char* type, const char* metadata,
+                      int64_t timestamp) {
+  return reinterpret_cast<DataLog*>(datalog)->Start(name, type, metadata,
+                                                    timestamp);
+}
+
+void WPI_DataLog_Finish(struct WPI_DataLog* datalog, int entry,
+                        int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->Finish(entry, timestamp);
+}
+
+void WPI_DataLog_SetMetadata(struct WPI_DataLog* datalog, int entry,
+                             const char* metadata, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->SetMetadata(entry, metadata, timestamp);
+}
+
+void WPI_DataLog_AppendRaw(struct WPI_DataLog* datalog, int entry,
+                           const uint8_t* data, size_t len, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendRaw(entry, {data, len}, timestamp);
+}
+
+void WPI_DataLog_AppendBoolean(struct WPI_DataLog* datalog, int entry,
+                               int value, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendBoolean(entry, value, timestamp);
+}
+
+void WPI_DataLog_AppendInteger(struct WPI_DataLog* datalog, int entry,
+                               int64_t value, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendInteger(entry, value, timestamp);
+}
+
+void WPI_DataLog_AppendFloat(struct WPI_DataLog* datalog, int entry,
+                             float value, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendFloat(entry, value, timestamp);
+}
+
+void WPI_DataLog_AppendDouble(struct WPI_DataLog* datalog, int entry,
+                              double value, int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendDouble(entry, value, timestamp);
+}
+
+void WPI_DataLog_AppendString(struct WPI_DataLog* datalog, int entry,
+                              const char* value, size_t len,
+                              int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendString(entry, {value, len},
+                                                    timestamp);
+}
+
+void WPI_DataLog_AppendBooleanArray(struct WPI_DataLog* datalog, int entry,
+                                    const int* arr, size_t len,
+                                    int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendBooleanArray(entry, {arr, len},
+                                                          timestamp);
+}
+
+void WPI_DataLog_AppendBooleanArrayByte(struct WPI_DataLog* datalog, int entry,
+                                        const uint8_t* arr, size_t len,
+                                        int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendBooleanArray(entry, {arr, len},
+                                                          timestamp);
+}
+
+void WPI_DataLog_AppendIntegerArray(struct WPI_DataLog* datalog, int entry,
+                                    const int64_t* arr, size_t len,
+                                    int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendIntegerArray(entry, {arr, len},
+                                                          timestamp);
+}
+
+void WPI_DataLog_AppendFloatArray(struct WPI_DataLog* datalog, int entry,
+                                  const float* arr, size_t len,
+                                  int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendFloatArray(entry, {arr, len},
+                                                        timestamp);
+}
+
+void WPI_DataLog_AppendDoubleArray(struct WPI_DataLog* datalog, int entry,
+                                   const double* arr, size_t len,
+                                   int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendDoubleArray(entry, {arr, len},
+                                                         timestamp);
+}
+
+void WPI_DataLog_AppendStringArray(struct WPI_DataLog* datalog, int entry,
+                                   const WPI_DataLog_String* arr, size_t len,
+                                   int64_t timestamp) {
+  reinterpret_cast<DataLog*>(datalog)->AppendStringArray(entry, {arr, len},
+                                                         timestamp);
+}
+
+}  // extern "C"
diff --git a/wpiutil/src/main/native/cpp/DataLogReader.cpp b/wpiutil/src/main/native/cpp/DataLogReader.cpp
index 96f6689..c2e1192 100644
--- a/wpiutil/src/main/native/cpp/DataLogReader.cpp
+++ b/wpiutil/src/main/native/cpp/DataLogReader.cpp
@@ -95,7 +95,7 @@
   if (m_data.size() != 4) {
     return false;
   }
-  *value = wpi::BitsToFloat(wpi::support::endian::read32le(m_data.data()));
+  *value = wpi::bit_cast<float>(wpi::support::endian::read32le(m_data.data()));
   return true;
 }
 
@@ -103,7 +103,7 @@
   if (m_data.size() != 8) {
     return false;
   }
-  *value = wpi::BitsToDouble(wpi::support::endian::read64le(m_data.data()));
+  *value = wpi::bit_cast<double>(wpi::support::endian::read64le(m_data.data()));
   return true;
 }
 
@@ -141,7 +141,7 @@
   arr->reserve(m_data.size() / 4);
   for (size_t pos = 0; pos < m_data.size(); pos += 4) {
     arr->push_back(
-        wpi::BitsToFloat(wpi::support::endian::read32le(&m_data[pos])));
+        wpi::bit_cast<float>(wpi::support::endian::read32le(&m_data[pos])));
   }
   return true;
 }
@@ -154,7 +154,7 @@
   arr->reserve(m_data.size() / 8);
   for (size_t pos = 0; pos < m_data.size(); pos += 8) {
     arr->push_back(
-        wpi::BitsToDouble(wpi::support::endian::read64le(&m_data[pos])));
+        wpi::bit_cast<double>(wpi::support::endian::read64le(&m_data[pos])));
   }
   return true;
 }
diff --git a/wpiutil/src/main/native/cpp/fs.cpp b/wpiutil/src/main/native/cpp/fs.cpp
index fad6a66..ed68297 100644
--- a/wpiutil/src/main/native/cpp/fs.cpp
+++ b/wpiutil/src/main/native/cpp/fs.cpp
@@ -43,26 +43,6 @@
 
 #endif  // _WIN32
 
-#if defined(__APPLE__)
-#include <Availability.h>
-#endif
-#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) \
-     || (defined(__cplusplus) && __cplusplus >= 201703L)) \
-    && defined(__has_include)
-#if __has_include(<filesystem>) \
-    && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) \
-        || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) \
-    && (defined(__clang__) || !defined(__GNUC__) || __GNUC__ >= 10 \
-        || (__GNUC__ >= 9 && __GNUC_MINOR__ >= 1))
-#define GHC_USE_STD_FS
-#endif
-#endif
-#ifndef GHC_USE_STD_FS
-// #define GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE
-#define GHC_FILESYSTEM_IMPLEMENTATION
-#include "wpi/ghc/filesystem.hpp"
-#endif
-
 #include "wpi/Errno.h"
 #include "wpi/ErrorHandling.h"
 #include "wpi/WindowsError.h"
diff --git a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
index 997b90b..c78c891 100644
--- a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
+++ b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
@@ -4,6 +4,9 @@
 
 #include <jni.h>
 
+#include <fmt/format.h>
+
+#include "WPIUtilJNI.h"
 #include "edu_wpi_first_util_datalog_DataLogJNI.h"
 #include "wpi/DataLog.h"
 #include "wpi/jni_util.h"
@@ -23,6 +26,18 @@
   (JNIEnv* env, jclass, jstring dir, jstring filename, jdouble period,
    jstring extraHeader)
 {
+  if (!dir) {
+    wpi::ThrowNullPointerException(env, "dir is null");
+    return 0;
+  }
+  if (!filename) {
+    wpi::ThrowNullPointerException(env, "filename is null");
+    return 0;
+  }
+  if (!extraHeader) {
+    wpi::ThrowNullPointerException(env, "extraHeader is null");
+    return 0;
+  }
   return reinterpret_cast<jlong>(new DataLog{JStringRef{env, dir},
                                              JStringRef{env, filename}, period,
                                              JStringRef{env, extraHeader}});
@@ -38,6 +53,11 @@
   (JNIEnv* env, jclass, jlong impl, jstring filename)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!filename) {
+    wpi::ThrowNullPointerException(env, "filename is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->SetFilename(JStringRef{env, filename});
@@ -50,9 +70,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_flush
-  (JNIEnv*, jclass, jlong impl)
+  (JNIEnv* env, jclass, jlong impl)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->Flush();
@@ -65,9 +86,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_pause
-  (JNIEnv*, jclass, jlong impl)
+  (JNIEnv* env, jclass, jlong impl)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->Pause();
@@ -80,9 +102,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_resume
-  (JNIEnv*, jclass, jlong impl)
+  (JNIEnv* env, jclass, jlong impl)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->Resume();
@@ -90,6 +113,63 @@
 
 /*
  * Class:     edu_wpi_first_util_datalog_DataLogJNI
+ * Method:    stop
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_stop
+  (JNIEnv* env, jclass, jlong impl)
+{
+  if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  reinterpret_cast<DataLog*>(impl)->Stop();
+}
+
+/*
+ * Class:     edu_wpi_first_util_datalog_DataLogJNI
+ * Method:    addSchema
+ * Signature: (JLjava/lang/String;Ljava/lang/String;[BJ)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_addSchema
+  (JNIEnv* env, jclass, jlong impl, jstring name, jstring type,
+   jbyteArray schema, jlong timestamp)
+{
+  if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  reinterpret_cast<DataLog*>(impl)->AddSchema(
+      JStringRef{env, name}, JStringRef{env, type},
+      JSpan<const jbyte>{env, schema}.uarray(), timestamp);
+}
+
+/*
+ * Class:     edu_wpi_first_util_datalog_DataLogJNI
+ * Method:    addSchemaString
+ * Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_addSchemaString
+  (JNIEnv* env, jclass, jlong impl, jstring name, jstring type, jstring schema,
+   jlong timestamp)
+{
+  if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  JStringRef schemaStr{env, schema};
+  std::string_view schemaView = schemaStr.str();
+  reinterpret_cast<DataLog*>(impl)->AddSchema(
+      JStringRef{env, name}, JStringRef{env, type},
+      {reinterpret_cast<const uint8_t*>(schemaView.data()), schemaView.size()},
+      timestamp);
+}
+
+/*
+ * Class:     edu_wpi_first_util_datalog_DataLogJNI
  * Method:    start
  * Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;J)I
  */
@@ -99,6 +179,7 @@
    jstring metadata, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return 0;
   }
   return reinterpret_cast<DataLog*>(impl)->Start(
@@ -113,9 +194,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_finish
-  (JNIEnv*, jclass, jlong impl, jint entry, jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->Finish(entry, timestamp);
@@ -132,6 +214,7 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->SetMetadata(
@@ -153,21 +236,73 @@
 /*
  * Class:     edu_wpi_first_util_datalog_DataLogJNI
  * Method:    appendRaw
- * Signature: (JI[BJ)V
+ * Signature: (JI[BIIJ)V
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_appendRaw
-  (JNIEnv* env, jclass, jlong impl, jint entry, jbyteArray value,
-   jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jbyteArray value, jint start,
+   jint length, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
-  JByteArrayRef cvalue{env, value};
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
+    return;
+  }
+  if (start < 0) {
+    wpi::ThrowIndexOobException(env, "start must be >= 0");
+    return;
+  }
+  if (length < 0) {
+    wpi::ThrowIndexOobException(env, "length must be >= 0");
+    return;
+  }
+  CriticalJSpan<const jbyte> cvalue{env, value};
+  if (static_cast<unsigned int>(start + length) > cvalue.size()) {
+    wpi::ThrowIndexOobException(
+        env, "start + len must be smaller than array length");
+    return;
+  }
   reinterpret_cast<DataLog*>(impl)->AppendRaw(
-      entry,
-      {reinterpret_cast<const uint8_t*>(cvalue.array().data()), cvalue.size()},
-      timestamp);
+      entry, cvalue.uarray().subspan(start, length), timestamp);
+}
+
+/*
+ * Class:     edu_wpi_first_util_datalog_DataLogJNI
+ * Method:    appendRawBuffer
+ * Signature: (JILjava/lang/Object;IIJ)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_appendRawBuffer
+  (JNIEnv* env, jclass, jlong impl, jint entry, jobject value, jint start,
+   jint length, jlong timestamp)
+{
+  if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
+    return;
+  }
+  if (start < 0) {
+    wpi::ThrowIndexOobException(env, "start must be >= 0");
+    return;
+  }
+  if (length < 0) {
+    wpi::ThrowIndexOobException(env, "length must be >= 0");
+    return;
+  }
+  JSpan<const jbyte> cvalue{env, value, static_cast<size_t>(start + length)};
+  if (!cvalue) {
+    wpi::ThrowIllegalArgumentException(env,
+                                       "value must be a native ByteBuffer");
+    return;
+  }
+  reinterpret_cast<DataLog*>(impl)->AppendRaw(
+      entry, cvalue.uarray().subspan(start, length), timestamp);
 }
 
 /*
@@ -177,9 +312,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_appendBoolean
-  (JNIEnv*, jclass, jlong impl, jint entry, jboolean value, jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jboolean value, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendBoolean(entry, value, timestamp);
@@ -192,9 +328,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_appendInteger
-  (JNIEnv*, jclass, jlong impl, jint entry, jlong value, jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jlong value, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendInteger(entry, value, timestamp);
@@ -207,9 +344,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_appendFloat
-  (JNIEnv*, jclass, jlong impl, jint entry, jfloat value, jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jfloat value, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendFloat(entry, value, timestamp);
@@ -222,9 +360,10 @@
  */
 JNIEXPORT void JNICALL
 Java_edu_wpi_first_util_datalog_DataLogJNI_appendDouble
-  (JNIEnv*, jclass, jlong impl, jint entry, jdouble value, jlong timestamp)
+  (JNIEnv* env, jclass, jlong impl, jint entry, jdouble value, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendDouble(entry, value, timestamp);
@@ -240,6 +379,7 @@
   (JNIEnv* env, jclass, jlong impl, jint entry, jstring value, jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendString(entry, JStringRef{env, value},
@@ -257,10 +397,15 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendBooleanArray(
-      entry, JBooleanArrayRef{env, value}, timestamp);
+      entry, JSpan<const jboolean>{env, value}, timestamp);
 }
 
 /*
@@ -274,19 +419,22 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
     return;
   }
-  JLongArrayRef jarr{env, value};
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
+    return;
+  }
+  JSpan<const jlong> jarr{env, value};
   if constexpr (sizeof(jlong) == sizeof(int64_t)) {
     reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(
-        entry,
-        {reinterpret_cast<const int64_t*>(jarr.array().data()),
-         jarr.array().size()},
+        entry, {reinterpret_cast<const int64_t*>(jarr.data()), jarr.size()},
         timestamp);
   } else {
     wpi::SmallVector<int64_t, 16> arr;
     arr.reserve(jarr.size());
-    for (auto v : jarr.array()) {
+    for (auto v : jarr) {
       arr.push_back(v);
     }
     reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(entry, arr, timestamp);
@@ -304,10 +452,15 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendFloatArray(
-      entry, JFloatArrayRef{env, value}, timestamp);
+      entry, JSpan<const jfloat>{env, value}, timestamp);
 }
 
 /*
@@ -321,10 +474,15 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
     return;
   }
   reinterpret_cast<DataLog*>(impl)->AppendDoubleArray(
-      entry, JDoubleArrayRef{env, value}, timestamp);
+      entry, JSpan<const jdouble>{env, value}, timestamp);
 }
 
 /*
@@ -338,6 +496,11 @@
    jlong timestamp)
 {
   if (impl == 0) {
+    wpi::ThrowNullPointerException(env, "impl is null");
+    return;
+  }
+  if (!value) {
+    wpi::ThrowNullPointerException(env, "value is null");
     return;
   }
   size_t len = env->GetArrayLength(value);
@@ -347,6 +510,8 @@
     JLocal<jstring> elem{
         env, static_cast<jstring>(env->GetObjectArrayElement(value, i))};
     if (!elem) {
+      wpi::ThrowNullPointerException(
+          env, fmt::format("string at element {} is null", i));
       return;
     }
     arr.emplace_back(JStringRef{env, elem}.str());
diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
index 5fa4ddf..eb55fd0 100644
--- a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
+++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
@@ -2,10 +2,13 @@
 // Open Source 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 "WPIUtilJNI.h"
+
 #include <jni.h>
 
+#include <fmt/format.h>
+
 #include "edu_wpi_first_util_WPIUtilJNI.h"
-#include "fmt/format.h"
 #include "wpi/Synchronization.h"
 #include "wpi/jni_util.h"
 #include "wpi/timestamp.h"
@@ -15,7 +18,28 @@
 static bool mockTimeEnabled = false;
 static uint64_t mockNow = 0;
 
+static JException illegalArgEx;
+static JException indexOobEx;
 static JException interruptedEx;
+static JException nullPointerEx;
+
+static const JExceptionInit exceptions[] = {
+    {"java/lang/IllegalArgumentException", &illegalArgEx},
+    {"java/lang/IndexOutOfBoundsException", &indexOobEx},
+    {"java/lang/InterruptedException", &interruptedEx},
+    {"java/lang/NullPointerException", &nullPointerEx}};
+
+void wpi::ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg) {
+  illegalArgEx.Throw(env, msg);
+}
+
+void wpi::ThrowIndexOobException(JNIEnv* env, std::string_view msg) {
+  indexOobEx.Throw(env, msg);
+}
+
+void wpi::ThrowNullPointerException(JNIEnv* env, std::string_view msg) {
+  nullPointerEx.Throw(env, msg);
+}
 
 extern "C" {
 
@@ -25,9 +49,11 @@
     return JNI_ERR;
   }
 
-  interruptedEx = JException(env, "java/lang/InterruptedException");
-  if (!interruptedEx) {
-    return JNI_ERR;
+  for (auto& c : exceptions) {
+    *c.cls = JException(env, c.name);
+    if (!*c.cls) {
+      return JNI_ERR;
+    }
   }
 
   return JNI_VERSION_1_6;
@@ -39,7 +65,9 @@
     return;
   }
 
-  interruptedEx.free(env);
+  for (auto& c : exceptions) {
+    c.cls->free(env);
+  }
 }
 
 /*
@@ -51,7 +79,7 @@
 Java_edu_wpi_first_util_WPIUtilJNI_writeStderr
   (JNIEnv* env, jclass, jstring str)
 {
-  fmt::print(stderr, "{}", JStringRef{env, str});
+  fmt::print(stderr, "{}", JStringRef{env, str}.str());
 }
 
 /*
@@ -63,8 +91,12 @@
 Java_edu_wpi_first_util_WPIUtilJNI_enableMockTime
   (JNIEnv*, jclass)
 {
+#ifdef __FRC_ROBORIO__
+  fmt::print(stderr, "WPIUtil: Mocking time is not available on the Rio\n");
+#else
   mockTimeEnabled = true;
   wpi::SetNowImpl([] { return mockNow; });
+#endif
 }
 
 /*
@@ -244,11 +276,11 @@
 Java_edu_wpi_first_util_WPIUtilJNI_waitForObjects
   (JNIEnv* env, jclass, jintArray handles)
 {
-  JIntArrayRef handlesArr{env, handles};
+  JSpan<const jint> handlesArr{env, handles};
   wpi::SmallVector<WPI_Handle, 8> signaledBuf;
   signaledBuf.resize(handlesArr.size());
   std::span<const WPI_Handle> handlesArr2{
-      reinterpret_cast<const WPI_Handle*>(handlesArr.array().data()),
+      reinterpret_cast<const WPI_Handle*>(handlesArr.data()),
       handlesArr.size()};
 
   auto signaled = wpi::WaitForObjects(handlesArr2, signaledBuf);
@@ -268,11 +300,11 @@
 Java_edu_wpi_first_util_WPIUtilJNI_waitForObjectsTimeout
   (JNIEnv* env, jclass, jintArray handles, jdouble timeout)
 {
-  JIntArrayRef handlesArr{env, handles};
+  JSpan<const jint> handlesArr{env, handles};
   wpi::SmallVector<WPI_Handle, 8> signaledBuf;
   signaledBuf.resize(handlesArr.size());
   std::span<const WPI_Handle> handlesArr2{
-      reinterpret_cast<const WPI_Handle*>(handlesArr.array().data()),
+      reinterpret_cast<const WPI_Handle*>(handlesArr.data()),
       handlesArr.size()};
 
   bool timedOut;
diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.h b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.h
new file mode 100644
index 0000000..541064f
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.h
@@ -0,0 +1,17 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source 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 <jni.h>
+
+#include <string_view>
+
+namespace wpi {
+
+void ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg);
+void ThrowIndexOobException(JNIEnv* env, std::string_view msg);
+void ThrowNullPointerException(JNIEnv* env, std::string_view msg);
+
+}  // namespace wpi
diff --git a/wpiutil/src/main/native/cpp/protobuf/Protobuf.cpp b/wpiutil/src/main/native/cpp/protobuf/Protobuf.cpp
new file mode 100644
index 0000000..4f2a616
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/protobuf/Protobuf.cpp
@@ -0,0 +1,194 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "wpi/protobuf/Protobuf.h"
+
+#include <fmt/format.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/message.h>
+
+#include "wpi/SmallVector.h"
+
+using namespace wpi;
+
+using google::protobuf::Arena;
+using google::protobuf::FileDescriptor;
+using google::protobuf::FileDescriptorProto;
+
+namespace {
+class VectorOutputStream final
+    : public google::protobuf::io::ZeroCopyOutputStream {
+ public:
+  // Create a StringOutputStream which appends bytes to the given string.
+  // The string remains property of the caller, but it is mutated in arbitrary
+  // ways and MUST NOT be accessed in any way until you're done with the
+  // stream. Either be sure there's no further usage, or (safest) destroy the
+  // stream before using the contents.
+  //
+  // Hint:  If you call target->reserve(n) before creating the stream,
+  //   the first call to Next() will return at least n bytes of buffer
+  //   space.
+  explicit VectorOutputStream(std::vector<uint8_t>& target) : target_{target} {}
+  VectorOutputStream(const VectorOutputStream&) = delete;
+  ~VectorOutputStream() override = default;
+
+  VectorOutputStream& operator=(const VectorOutputStream&) = delete;
+
+  // implements ZeroCopyOutputStream ---------------------------------
+  bool Next(void** data, int* size) override;
+  void BackUp(int count) override { target_.resize(target_.size() - count); }
+  int64_t ByteCount() const override { return target_.size(); }
+
+ private:
+  static constexpr size_t kMinimumSize = 16;
+
+  std::vector<uint8_t>& target_;
+};
+
+class SmallVectorOutputStream final
+    : public google::protobuf::io::ZeroCopyOutputStream {
+ public:
+  // Create a StringOutputStream which appends bytes to the given string.
+  // The string remains property of the caller, but it is mutated in arbitrary
+  // ways and MUST NOT be accessed in any way until you're done with the
+  // stream. Either be sure there's no further usage, or (safest) destroy the
+  // stream before using the contents.
+  //
+  // Hint:  If you call target->reserve(n) before creating the stream,
+  //   the first call to Next() will return at least n bytes of buffer
+  //   space.
+  explicit SmallVectorOutputStream(wpi::SmallVectorImpl<uint8_t>& target)
+      : target_{target} {
+    target.resize(0);
+  }
+  SmallVectorOutputStream(const SmallVectorOutputStream&) = delete;
+  ~SmallVectorOutputStream() override = default;
+
+  SmallVectorOutputStream& operator=(const SmallVectorOutputStream&) = delete;
+
+  // implements ZeroCopyOutputStream ---------------------------------
+  bool Next(void** data, int* size) override;
+  void BackUp(int count) override { target_.resize(target_.size() - count); }
+  int64_t ByteCount() const override { return target_.size(); }
+
+ private:
+  static constexpr size_t kMinimumSize = 16;
+
+  wpi::SmallVectorImpl<uint8_t>& target_;
+};
+}  // namespace
+
+bool VectorOutputStream::Next(void** data, int* size) {
+  size_t old_size = target_.size();
+
+  // Grow the string.
+  size_t new_size;
+  if (old_size < target_.capacity()) {
+    // Resize to match its capacity, since we can get away
+    // without a memory allocation this way.
+    new_size = target_.capacity();
+  } else {
+    // Size has reached capacity, try to double it.
+    new_size = old_size * 2;
+  }
+  // Avoid integer overflow in returned '*size'.
+  new_size = (std::min)(new_size, old_size + (std::numeric_limits<int>::max)());
+  // Increase the size, also make sure that it is at least kMinimumSize.
+  target_.resize((std::max)(new_size, kMinimumSize));
+
+  *data = target_.data() + old_size;
+  *size = target_.size() - old_size;
+  return true;
+}
+
+bool SmallVectorOutputStream::Next(void** data, int* size) {
+  size_t old_size = target_.size();
+
+  // Grow the string.
+  size_t new_size;
+  if (old_size < target_.capacity()) {
+    // Resize to match its capacity, since we can get away
+    // without a memory allocation this way.
+    new_size = target_.capacity();
+  } else {
+    // Size has reached capacity, try to double it.
+    new_size = old_size * 2;
+  }
+  // Avoid integer overflow in returned '*size'.
+  new_size = (std::min)(new_size, old_size + (std::numeric_limits<int>::max)());
+  // Increase the size, also make sure that it is at least kMinimumSize.
+  target_.resize_for_overwrite((std::max)(new_size, kMinimumSize));
+
+  *data = target_.data() + old_size;
+  *size = target_.size() - old_size;
+  return true;
+}
+
+void detail::DeleteProtobuf(google::protobuf::Message* msg) {
+  if (msg && !msg->GetArena()) {
+    delete msg;
+  }
+}
+
+bool detail::ParseProtobuf(google::protobuf::Message* msg,
+                           std::span<const uint8_t> data) {
+  return msg->ParseFromArray(data.data(), data.size());
+}
+
+bool detail::SerializeProtobuf(wpi::SmallVectorImpl<uint8_t>& out,
+                               const google::protobuf::Message& msg) {
+  SmallVectorOutputStream stream{out};
+  return msg.SerializeToZeroCopyStream(&stream);
+}
+
+bool detail::SerializeProtobuf(std::vector<uint8_t>& out,
+                               const google::protobuf::Message& msg) {
+  VectorOutputStream stream{out};
+  return msg.SerializeToZeroCopyStream(&stream);
+}
+
+std::string detail::GetTypeString(const google::protobuf::Message& msg) {
+  return fmt::format("proto:{}", msg.GetDescriptor()->full_name());
+}
+
+static void ForEachProtobufDescriptorImpl(
+    const FileDescriptor* desc,
+    function_ref<bool(std::string_view typeString)> exists,
+    function_ref<void(std::string_view typeString,
+                      std::span<const uint8_t> schema)>
+        fn,
+    Arena* arena, FileDescriptorProto** descproto) {
+  std::string name = fmt::format("proto:{}", desc->name());
+  if (exists(name)) {
+    return;
+  }
+  for (int i = 0, ndep = desc->dependency_count(); i < ndep; ++i) {
+    ForEachProtobufDescriptorImpl(desc->dependency(i), exists, fn, arena,
+                                  descproto);
+  }
+  if (!*descproto) {
+    *descproto = Arena::CreateMessage<FileDescriptorProto>(arena);
+  }
+  (*descproto)->Clear();
+  desc->CopyTo(*descproto);
+  SmallVector<uint8_t, 128> buf;
+  detail::SerializeProtobuf(buf, **descproto);
+  fn(name, buf);
+}
+
+void detail::ForEachProtobufDescriptor(
+    const google::protobuf::Message& msg,
+    function_ref<bool(std::string_view filename)> exists,
+    function_ref<void(std::string_view filename,
+                      std::span<const uint8_t> descriptor)>
+        fn) {
+  FileDescriptorProto* descproto = nullptr;
+  ForEachProtobufDescriptorImpl(msg.GetDescriptor()->file(), exists, fn,
+                                msg.GetArena(), &descproto);
+  if (descproto && !msg.GetArena()) {
+    delete descproto;
+  }
+}
diff --git a/wpiutil/src/main/native/cpp/protobuf/ProtobufMessageDatabase.cpp b/wpiutil/src/main/native/cpp/protobuf/ProtobufMessageDatabase.cpp
new file mode 100644
index 0000000..144bd03
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/protobuf/ProtobufMessageDatabase.cpp
@@ -0,0 +1,131 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "wpi/protobuf/ProtobufMessageDatabase.h"
+
+#include <google/protobuf/descriptor.h>
+
+using namespace wpi;
+
+using google::protobuf::Arena;
+using google::protobuf::FileDescriptorProto;
+using google::protobuf::Message;
+
+bool ProtobufMessageDatabase::Add(std::string_view filename,
+                                  std::span<const uint8_t> data) {
+  auto& file = m_files[filename];
+  bool needsRebuild = false;
+  if (file.complete) {
+    file.complete = false;
+
+    m_msgs.clear();
+    m_factory.reset();
+
+    // rebuild the pool EXCEPT for this descriptor
+    m_pool = std::make_unique<google::protobuf::DescriptorPool>();
+
+    for (auto&& p : m_files) {
+      p.second.inPool = false;
+    }
+
+    needsRebuild = true;
+  }
+
+  if (!file.proto) {
+    file.proto = std::unique_ptr<FileDescriptorProto>{
+        Arena::CreateMessage<FileDescriptorProto>(nullptr)};
+  } else {
+    // replacing an existing one; remove any previously existing refs
+    for (auto&& dep : file.proto->dependency()) {
+      auto& depFile = m_files[dep];
+      std::erase(depFile.uses, filename);
+    }
+    file.proto->Clear();
+  }
+
+  // parse data
+  if (!file.proto->ParseFromArray(data.data(), data.size())) {
+    return false;
+  }
+
+  // rebuild if necessary; we do this after the parse due to dependencies
+  if (needsRebuild) {
+    for (auto&& p : m_files) {
+      if (p.second.complete && !p.second.inPool) {
+        Rebuild(p.second);
+      }
+    }
+
+    // clear messages and reset factory; Find() will recreate as needed
+    m_factory = std::make_unique<google::protobuf::DynamicMessageFactory>();
+  }
+
+  // build this one
+  Build(filename, file);
+  return true;
+}
+
+Message* ProtobufMessageDatabase::Find(std::string_view name) const {
+  // cached
+  auto& msg = m_msgs[name];
+  if (msg) {
+    return msg.get();
+  }
+
+  // need to create it
+  auto desc = m_pool->FindMessageTypeByName(std::string{name});
+  if (!desc) {
+    return nullptr;
+  }
+  msg = std::unique_ptr<Message>{m_factory->GetPrototype(desc)->New(nullptr)};
+  return msg.get();
+}
+
+void ProtobufMessageDatabase::Build(std::string_view filename,
+                                    ProtoFile& file) {
+  if (file.complete) {
+    return;
+  }
+  // are all of the dependencies complete?
+  bool complete = true;
+  for (auto&& dep : file.proto->dependency()) {
+    auto& depFile = m_files[dep];
+    if (!depFile.complete) {
+      complete = false;
+    }
+    depFile.uses.emplace_back(filename);
+  }
+  if (!complete) {
+    return;
+  }
+
+  // add to pool
+  if (!m_pool->BuildFile(*file.proto)) {
+    return;
+  }
+  file.inPool = true;
+  file.complete = true;
+
+  // recursively validate all uses
+  for (auto&& use : file.uses) {
+    Build(use, m_files[use]);
+  }
+}
+
+bool ProtobufMessageDatabase::Rebuild(ProtoFile& file) {
+  for (auto&& dep : file.proto->dependency()) {
+    auto& depFile = m_files[dep];
+    if (!depFile.inPool) {
+      if (!Rebuild(depFile)) {
+        return false;
+      }
+    }
+  }
+  if (!m_pool->BuildFile(*file.proto)) {
+    return false;
+  }
+  file.inPool = true;
+  file.complete = true;
+  return true;
+}
diff --git a/wpiutil/src/main/native/cpp/sendable/SendableRegistry.cpp b/wpiutil/src/main/native/cpp/sendable/SendableRegistry.cpp
index 2c591e9..7a1e0b0 100644
--- a/wpiutil/src/main/native/cpp/sendable/SendableRegistry.cpp
+++ b/wpiutil/src/main/native/cpp/sendable/SendableRegistry.cpp
@@ -6,7 +6,8 @@
 
 #include <memory>
 
-#include "fmt/format.h"
+#include <fmt/format.h>
+
 #include "wpi/DenseMap.h"
 #include "wpi/SmallVector.h"
 #include "wpi/UidVector.h"
diff --git a/wpiutil/src/main/native/cpp/struct/DynamicStruct.cpp b/wpiutil/src/main/native/cpp/struct/DynamicStruct.cpp
new file mode 100644
index 0000000..7ae9271
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/struct/DynamicStruct.cpp
@@ -0,0 +1,444 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "wpi/struct/DynamicStruct.h"
+
+#include <algorithm>
+
+#include <fmt/format.h>
+
+#include "wpi/Endian.h"
+#include "wpi/SmallString.h"
+#include "wpi/SmallVector.h"
+#include "wpi/raw_ostream.h"
+#include "wpi/struct/SchemaParser.h"
+
+using namespace wpi;
+
+static size_t TypeToSize(StructFieldType type) {
+  switch (type) {
+    case StructFieldType::kBool:
+    case StructFieldType::kChar:
+    case StructFieldType::kInt8:
+    case StructFieldType::kUint8:
+      return 1;
+    case StructFieldType::kInt16:
+    case StructFieldType::kUint16:
+      return 2;
+    case StructFieldType::kInt32:
+    case StructFieldType::kUint32:
+    case StructFieldType::kFloat:
+      return 4;
+    case StructFieldType::kInt64:
+    case StructFieldType::kUint64:
+    case StructFieldType::kDouble:
+      return 8;
+    default:
+      return 0;
+  }
+}
+
+static StructFieldType TypeStringToType(std::string_view str) {
+  if (str == "bool") {
+    return StructFieldType::kBool;
+  } else if (str == "char") {
+    return StructFieldType::kChar;
+  } else if (str == "int8") {
+    return StructFieldType::kInt8;
+  } else if (str == "int16") {
+    return StructFieldType::kInt16;
+  } else if (str == "int32") {
+    return StructFieldType::kInt32;
+  } else if (str == "int64") {
+    return StructFieldType::kInt64;
+  } else if (str == "uint8") {
+    return StructFieldType::kUint8;
+  } else if (str == "uint16") {
+    return StructFieldType::kUint16;
+  } else if (str == "uint32") {
+    return StructFieldType::kUint32;
+  } else if (str == "uint64") {
+    return StructFieldType::kUint64;
+  } else if (str == "float" || str == "float32") {
+    return StructFieldType::kFloat;
+  } else if (str == "double" || str == "float64") {
+    return StructFieldType::kDouble;
+  } else {
+    return StructFieldType::kStruct;
+  }
+}
+
+static inline unsigned int ToBitWidth(size_t size, unsigned int bitWidth) {
+  if (bitWidth == 0) {
+    return size * 8;
+  } else {
+    return bitWidth;
+  }
+}
+
+static inline uint64_t ToBitMask(size_t size, unsigned int bitWidth) {
+  if (size == 0) {
+    return 0;
+  } else {
+    return UINT64_MAX >> (64 - ToBitWidth(size, bitWidth));
+  }
+}
+
+StructFieldDescriptor::StructFieldDescriptor(
+    const StructDescriptor* parent, std::string_view name, StructFieldType type,
+    size_t size, size_t arraySize, unsigned int bitWidth, EnumValues enumValues,
+    const StructDescriptor* structDesc, const private_init&)
+    : m_parent{parent},
+      m_name{name},
+      m_size{size},
+      m_arraySize{arraySize},
+      m_enum{std::move(enumValues)},
+      m_struct{structDesc},
+      m_bitMask{ToBitMask(size, bitWidth)},
+      m_type{type},
+      m_bitWidth{ToBitWidth(size, bitWidth)} {}
+
+const StructFieldDescriptor* StructDescriptor::FindFieldByName(
+    std::string_view name) const {
+  auto it = m_fieldsByName.find(name);
+  if (it == m_fieldsByName.end()) {
+    return nullptr;
+  }
+  return &m_fields[it->second];
+}
+
+bool StructDescriptor::CheckCircular(
+    wpi::SmallVectorImpl<const StructDescriptor*>& stack) const {
+  stack.emplace_back(this);
+  for (auto&& ref : m_references) {
+    if (std::find(stack.begin(), stack.end(), ref) != stack.end()) {
+      [[unlikely]] return false;
+    }
+    if (!ref->CheckCircular(stack)) {
+      [[unlikely]] return false;
+    }
+  }
+  stack.pop_back();
+  return true;
+}
+
+std::string StructDescriptor::CalculateOffsets(
+    wpi::SmallVectorImpl<const StructDescriptor*>& stack) {
+  size_t offset = 0;
+  unsigned int shift = 0;
+  size_t prevBitfieldSize = 0;
+  for (auto&& field : m_fields) {
+    if (!field.IsBitField()) {
+      [[likely]] shift = 0;        // reset shift on non-bitfield element
+      offset += prevBitfieldSize;  // finish bitfield if active
+      prevBitfieldSize = 0;        // previous is now not bitfield
+      field.m_offset = offset;
+      if (field.m_struct) {
+        if (!field.m_struct->IsValid()) {
+          m_valid = false;
+          [[unlikely]] return {};
+        }
+        field.m_size = field.m_struct->m_size;
+      }
+      offset += field.m_size * field.m_arraySize;
+    } else {
+      if (field.m_type == StructFieldType::kBool && prevBitfieldSize != 0 &&
+          (shift + 1) <= (prevBitfieldSize * 8)) {
+        // bool takes on size of preceding bitfield type (if it fits)
+        field.m_size = prevBitfieldSize;
+      } else if (field.m_size != prevBitfieldSize ||
+                 (shift + field.m_bitWidth) > (field.m_size * 8)) {
+        shift = 0;
+        offset += prevBitfieldSize;
+      }
+      prevBitfieldSize = field.m_size;
+      field.m_offset = offset;
+      field.m_bitShift = shift;
+      shift += field.m_bitWidth;
+    }
+  }
+
+  // update struct size
+  m_size = offset + prevBitfieldSize;
+  m_valid = true;
+
+  // now that we're valid, referring types may be too
+  stack.emplace_back(this);
+  for (auto&& ref : m_references) {
+    if (std::find(stack.begin(), stack.end(), ref) != stack.end()) {
+      [[unlikely]] return fmt::format(
+          "internal error (inconsistent data): circular struct reference "
+          "between {} and {}",
+          m_name, ref->m_name);
+    }
+    auto err = ref->CalculateOffsets(stack);
+    if (!err.empty()) {
+      [[unlikely]] return err;
+    }
+  }
+  stack.pop_back();
+  return {};
+}
+
+const StructDescriptor* StructDescriptorDatabase::Add(std::string_view name,
+                                                      std::string_view schema,
+                                                      std::string* err) {
+  structparser::Parser parser{schema};
+  structparser::ParsedSchema parsed;
+  if (!parser.Parse(&parsed)) {
+    *err = fmt::format("parse error: {}", parser.GetError());
+    [[unlikely]] return nullptr;
+  }
+
+  // turn parsed schema into descriptors
+  auto& theStruct = m_structs[name];
+  if (!theStruct) {
+    theStruct = std::make_unique<StructDescriptor>(
+        name, StructDescriptor::private_init{});
+  }
+  theStruct->m_schema = schema;
+  theStruct->m_fields.clear();
+  theStruct->m_fields.reserve(parsed.declarations.size());
+  bool isValid = true;
+  for (auto&& decl : parsed.declarations) {
+    auto type = TypeStringToType(decl.typeString);
+    size_t size = TypeToSize(type);
+
+    // bitfield checks
+    if (decl.bitWidth != 0) {
+      // only integer or boolean types are allowed
+      if (type == StructFieldType::kChar || type == StructFieldType::kFloat ||
+          type == StructFieldType::kDouble ||
+          type == StructFieldType::kStruct) {
+        *err = fmt::format("field {}: type {} cannot be bitfield", decl.name,
+                           decl.typeString);
+        [[unlikely]] return nullptr;
+      }
+
+      // bit width cannot be larger than field size
+      if (decl.bitWidth > (size * 8)) {
+        *err = fmt::format("field {}: bit width {} exceeds type size",
+                           decl.name, decl.bitWidth);
+        [[unlikely]] return nullptr;
+      }
+
+      // bit width must be 1 for booleans
+      if (type == StructFieldType::kBool && decl.bitWidth != 1) {
+        *err = fmt::format("field {}: bit width must be 1 for bool type",
+                           decl.name);
+        [[unlikely]] return nullptr;
+      }
+
+      // cannot combine array and bitfield (shouldn't parse, but double-check)
+      if (decl.arraySize > 1) {
+        *err = fmt::format("field {}: cannot combine array and bitfield",
+                           decl.name);
+        [[unlikely]] return nullptr;
+      }
+    }
+
+    // struct handling
+    const StructDescriptor* structDesc = nullptr;
+    if (type == StructFieldType::kStruct) {
+      // recursive definitions are not allowed
+      if (decl.typeString == name) {
+        *err = fmt::format("field {}: recursive struct reference", decl.name);
+        [[unlikely]] return nullptr;
+      }
+
+      // cross-reference struct, creating a placeholder if necessary
+      auto& aStruct = m_structs[decl.typeString];
+      if (!aStruct) {
+        aStruct = std::make_unique<StructDescriptor>(
+            decl.typeString, StructDescriptor::private_init{});
+      }
+
+      // if the struct isn't valid, we can't be valid either
+      if (aStruct->IsValid()) {
+        size = aStruct->GetSize();
+      } else {
+        isValid = false;
+      }
+
+      // add to cross-references for when the struct does become valid
+      aStruct->m_references.emplace_back(theStruct.get());
+      structDesc = aStruct.get();
+    }
+
+    // create field
+    if (!theStruct->m_fieldsByName
+             .insert({decl.name, theStruct->m_fields.size()})
+             .second) {
+      *err = fmt::format("duplicate field {}", decl.name);
+      [[unlikely]] return nullptr;
+    }
+
+    theStruct->m_fields.emplace_back(theStruct.get(), decl.name, type, size,
+                                     decl.arraySize, decl.bitWidth,
+                                     std::move(decl.enumValues), structDesc,
+                                     StructFieldDescriptor::private_init{});
+  }
+
+  theStruct->m_valid = isValid;
+  if (isValid) {
+    // we have all the info needed, so calculate field offset & shift
+    wpi::SmallVector<const StructDescriptor*, 16> stack;
+    auto err2 = theStruct->CalculateOffsets(stack);
+    if (!err2.empty()) {
+      *err = std::move(err2);
+      [[unlikely]] return nullptr;
+    }
+  } else {
+    // check for circular reference
+    wpi::SmallVector<const StructDescriptor*, 16> stack;
+    if (!theStruct->CheckCircular(stack)) {
+      wpi::SmallString<128> buf;
+      wpi::raw_svector_ostream os{buf};
+      for (auto&& elem : stack) {
+        if (!buf.empty()) {
+          os << " <- ";
+        }
+        os << elem->GetName();
+      }
+      *err = fmt::format("circular struct reference: {}", os.str());
+      [[unlikely]] return nullptr;
+    }
+  }
+
+  return theStruct.get();
+}
+
+const StructDescriptor* StructDescriptorDatabase::Find(
+    std::string_view name) const {
+  auto it = m_structs.find(name);
+  if (it == m_structs.end()) {
+    return nullptr;
+  }
+  return it->second.get();
+}
+
+uint64_t DynamicStruct::GetFieldImpl(const StructFieldDescriptor* field,
+                                     size_t arrIndex) const {
+  assert(field->m_parent == m_desc);
+  assert(m_desc->IsValid());
+  assert(arrIndex < field->m_arraySize);
+  uint64_t val;
+  switch (field->m_size) {
+    case 1:
+      val = m_data[field->m_offset + arrIndex];
+      break;
+    case 2:
+      val = support::endian::read16le(&m_data[field->m_offset + arrIndex * 2]);
+      break;
+    case 4:
+      val = support::endian::read32le(&m_data[field->m_offset + arrIndex * 4]);
+      break;
+    case 8:
+      val = support::endian::read64le(&m_data[field->m_offset + arrIndex * 8]);
+      break;
+    default:
+      assert(false && "invalid field size");
+      return 0;
+  }
+  return (val >> field->m_bitShift) & field->m_bitMask;
+}
+
+void MutableDynamicStruct::SetData(std::span<const uint8_t> data) {
+  assert(data.size() >= m_desc->GetSize());
+  std::copy(data.begin(), data.begin() + m_desc->GetSize(), m_data.begin());
+}
+
+void MutableDynamicStruct::SetStringField(const StructFieldDescriptor* field,
+                                          std::string_view value) {
+  assert(field->m_type == StructFieldType::kChar);
+  assert(field->m_parent == m_desc);
+  assert(m_desc->IsValid());
+  size_t len = (std::min)(field->m_arraySize, value.size());
+  std::copy(value.begin(), value.begin() + len,
+            reinterpret_cast<char*>(&m_data[field->m_offset]));
+  std::fill(&m_data[field->m_offset + len],
+            &m_data[field->m_offset + field->m_arraySize], 0);
+}
+
+void MutableDynamicStruct::SetStructField(const StructFieldDescriptor* field,
+                                          const DynamicStruct& value,
+                                          size_t arrIndex) {
+  assert(field->m_type == StructFieldType::kStruct);
+  assert(field->m_parent == m_desc);
+  assert(m_desc->IsValid());
+  assert(value.GetDescriptor() == field->m_struct);
+  assert(value.GetDescriptor()->IsValid());
+  assert(arrIndex < field->m_arraySize);
+  auto source = value.GetData();
+  size_t len = field->m_struct->GetSize();
+  std::copy(source.begin(), source.begin() + len,
+            m_data.begin() + field->m_offset + arrIndex * len);
+}
+
+void MutableDynamicStruct::SetFieldImpl(const StructFieldDescriptor* field,
+                                        uint64_t value, size_t arrIndex) {
+  assert(field->m_parent == m_desc);
+  assert(m_desc->IsValid());
+  assert(arrIndex < field->m_arraySize);
+
+  // common case is no bit shift and no masking
+  if (!field->IsBitField()) {
+    switch (field->m_size) {
+      case 1:
+        m_data[field->m_offset + arrIndex] = value;
+        break;
+      case 2:
+        support::endian::write16le(&m_data[field->m_offset + arrIndex * 2],
+                                   value);
+        break;
+      case 4:
+        support::endian::write32le(&m_data[field->m_offset + arrIndex * 4],
+                                   value);
+        break;
+      case 8:
+        support::endian::write64le(&m_data[field->m_offset + arrIndex * 8],
+                                   value);
+        break;
+      default:
+        assert(false && "invalid field size");
+    }
+    return;
+  }
+
+  // handle bit shifting and masking into current value
+  switch (field->m_size) {
+    case 1: {
+      uint8_t* data = &m_data[field->m_offset + arrIndex];
+      *data &= ~(field->m_bitMask << field->m_bitShift);
+      *data |= (value & field->m_bitMask) << field->m_bitShift;
+      break;
+    }
+    case 2: {
+      uint8_t* data = &m_data[field->m_offset + arrIndex * 2];
+      uint16_t val = support::endian::read16le(data);
+      val &= ~(field->m_bitMask << field->m_bitShift);
+      val |= (value & field->m_bitMask) << field->m_bitShift;
+      support::endian::write16le(data, val);
+      break;
+    }
+    case 4: {
+      uint8_t* data = &m_data[field->m_offset + arrIndex * 4];
+      uint32_t val = support::endian::read32le(data);
+      val &= ~(field->m_bitMask << field->m_bitShift);
+      val |= (value & field->m_bitMask) << field->m_bitShift;
+      support::endian::write32le(data, val);
+      break;
+    }
+    case 8: {
+      uint8_t* data = &m_data[field->m_offset + arrIndex * 8];
+      uint64_t val = support::endian::read64le(data);
+      val &= ~(field->m_bitMask << field->m_bitShift);
+      val |= (value & field->m_bitMask) << field->m_bitShift;
+      support::endian::write64le(data, val);
+      break;
+    }
+    default:
+      assert(false && "invalid field size");
+  }
+}
diff --git a/wpiutil/src/main/native/cpp/struct/SchemaParser.cpp b/wpiutil/src/main/native/cpp/struct/SchemaParser.cpp
new file mode 100644
index 0000000..347019e
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/struct/SchemaParser.cpp
@@ -0,0 +1,238 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "wpi/struct/SchemaParser.h"
+
+#include <fmt/format.h>
+
+#include "wpi/StringExtras.h"
+
+using namespace wpi::structparser;
+
+std::string_view wpi::structparser::ToString(Token::Kind kind) {
+  switch (kind) {
+    case Token::kInteger:
+      return "integer";
+    case Token::kIdentifier:
+      return "identifier";
+    case Token::kLeftBracket:
+      return "'['";
+    case Token::kRightBracket:
+      return "']'";
+    case Token::kLeftBrace:
+      return "'{'";
+    case Token::kRightBrace:
+      return "'}'";
+    case Token::kColon:
+      return "':'";
+    case Token::kSemicolon:
+      return "';'";
+    case Token::kComma:
+      return "','";
+    case Token::kEquals:
+      return "'='";
+    case Token::kEndOfInput:
+      return "<EOF>";
+    default:
+      return "unknown";
+  }
+}
+
+Token Lexer::Scan() {
+  // skip whitespace
+  do {
+    Get();
+  } while (m_current == ' ' || m_current == '\t' || m_current == '\n' ||
+           m_current == '\r');
+  m_tokenStart = m_pos - 1;
+
+  switch (m_current) {
+    case '[':
+      return MakeToken(Token::kLeftBracket);
+    case ']':
+      return MakeToken(Token::kRightBracket);
+    case '{':
+      return MakeToken(Token::kLeftBrace);
+    case '}':
+      return MakeToken(Token::kRightBrace);
+    case ':':
+      return MakeToken(Token::kColon);
+    case ';':
+      return MakeToken(Token::kSemicolon);
+    case ',':
+      return MakeToken(Token::kComma);
+    case '=':
+      return MakeToken(Token::kEquals);
+    case '-':
+    case '0':
+    case '1':
+    case '2':
+    case '3':
+    case '4':
+    case '5':
+    case '6':
+    case '7':
+    case '8':
+    case '9':
+      return ScanInteger();
+    case -1:
+      return {Token::kEndOfInput, {}};
+    default:
+      if (isAlpha(m_current) || m_current == '_') {
+        [[likely]] return ScanIdentifier();
+      }
+      return MakeToken(Token::kUnknown);
+  }
+}
+
+Token Lexer::ScanInteger() {
+  do {
+    Get();
+  } while (isDigit(m_current));
+  Unget();
+  return MakeToken(Token::kInteger);
+}
+
+Token Lexer::ScanIdentifier() {
+  do {
+    Get();
+  } while (isAlnum(m_current) || m_current == '_');
+  Unget();
+  return MakeToken(Token::kIdentifier);
+}
+
+void Parser::FailExpect(Token::Kind desired) {
+  Fail(fmt::format("expected {}, got '{}'", ToString(desired), m_token.text));
+}
+
+void Parser::Fail(std::string_view msg) {
+  m_error = fmt::format("{}: {}", m_lexer.GetPosition(), msg);
+}
+
+bool Parser::Parse(ParsedSchema* out) {
+  do {
+    GetNextToken();
+    if (m_token.Is(Token::kSemicolon)) {
+      continue;
+    }
+    if (m_token.Is(Token::kEndOfInput)) {
+      break;
+    }
+    if (!ParseDeclaration(&out->declarations.emplace_back())) {
+      [[unlikely]] return false;
+    }
+  } while (m_token.kind != Token::kEndOfInput);
+  return true;
+}
+
+bool Parser::ParseDeclaration(ParsedDeclaration* out) {
+  // optional enum specification
+  if (m_token.Is(Token::kIdentifier) && m_token.text == "enum") {
+    GetNextToken();
+    if (!Expect(Token::kLeftBrace)) {
+      [[unlikely]] return false;
+    }
+    if (!ParseEnum(&out->enumValues)) {
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+  } else if (m_token.Is(Token::kLeftBrace)) {
+    if (!ParseEnum(&out->enumValues)) {
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+  }
+
+  // type name
+  if (!Expect(Token::kIdentifier)) {
+    [[unlikely]] return false;
+  }
+  out->typeString = m_token.text;
+  GetNextToken();
+
+  // identifier name
+  if (!Expect(Token::kIdentifier)) {
+    [[unlikely]] return false;
+  }
+  out->name = m_token.text;
+  GetNextToken();
+
+  // array or bit field
+  if (m_token.Is(Token::kLeftBracket)) {
+    GetNextToken();
+    if (!Expect(Token::kInteger)) {
+      [[unlikely]] return false;
+    }
+    auto val = parse_integer<uint64_t>(m_token.text, 10);
+    if (val && *val > 0) {
+      out->arraySize = *val;
+    } else {
+      Fail(fmt::format("array size '{}' is not a positive integer",
+                       m_token.text));
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+    if (!Expect(Token::kRightBracket)) {
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+  } else if (m_token.Is(Token::kColon)) {
+    GetNextToken();
+    if (!Expect(Token::kInteger)) {
+      [[unlikely]] return false;
+    }
+    auto val = parse_integer<unsigned int>(m_token.text, 10);
+    if (val && *val > 0) {
+      out->bitWidth = *val;
+    } else {
+      Fail(fmt::format("bitfield width '{}' is not a positive integer",
+                       m_token.text));
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+  }
+
+  // declaration must end with EOF or semicolon
+  if (m_token.Is(Token::kEndOfInput)) {
+    return true;
+  }
+  return Expect(Token::kSemicolon);
+}
+
+bool Parser::ParseEnum(EnumValues* out) {
+  // we start with current = '{'
+  GetNextToken();
+  while (!m_token.Is(Token::kRightBrace)) {
+    if (!Expect(Token::kIdentifier)) {
+      [[unlikely]] return false;
+    }
+    std::string name;
+    name = m_token.text;
+    GetNextToken();
+    if (!Expect(Token::kEquals)) {
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+    if (!Expect(Token::kInteger)) {
+      [[unlikely]] return false;
+    }
+    int64_t value;
+    if (auto val = parse_integer<int64_t>(m_token.text, 10)) {
+      value = *val;
+    } else {
+      Fail(fmt::format("could not parse enum value '{}'", m_token.text));
+      [[unlikely]] return false;
+    }
+    out->emplace_back(std::move(name), value);
+    GetNextToken();
+    if (m_token.Is(Token::kRightBrace)) {
+      break;
+    }
+    if (!Expect(Token::kComma)) {
+      [[unlikely]] return false;
+    }
+    GetNextToken();
+  }
+  return true;
+}
diff --git a/wpiutil/src/main/native/cpp/timestamp.cpp b/wpiutil/src/main/native/cpp/timestamp.cpp
index 521197f..c7e2fa9 100644
--- a/wpiutil/src/main/native/cpp/timestamp.cpp
+++ b/wpiutil/src/main/native/cpp/timestamp.cpp
@@ -6,6 +6,25 @@
 
 #include <atomic>
 
+#ifdef __FRC_ROBORIO__
+#include <stdint.h>
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpedantic"
+#pragma GCC diagnostic ignored "-Wignored-qualifiers"
+#include <FRC_FPGA_ChipObject/RoboRIO_FRC_ChipObject_Aliases.h>
+#include <FRC_FPGA_ChipObject/nRoboRIO_FPGANamespace/nInterfaceGlobals.h>
+#include <FRC_FPGA_ChipObject/nRoboRIO_FPGANamespace/tHMB.h>
+#include <FRC_NetworkCommunication/LoadOut.h>
+#pragma GCC diagnostic pop
+namespace fpga {
+using namespace nFPGA;
+using namespace nRoboRIO_FPGANamespace;
+}  // namespace fpga
+#include <memory>
+
+#include "dlfcn.h"
+#endif
+
 #ifdef _WIN32
 #include <windows.h>
 
@@ -15,6 +34,85 @@
 #include <chrono>
 #endif
 
+#include <cstdio>
+
+#include <fmt/format.h>
+
+#ifdef __FRC_ROBORIO__
+namespace {
+static constexpr const char hmbName[] = "HMB_0_RAM";
+static constexpr int timestampLowerOffset = 0xF0;
+static constexpr int timestampUpperOffset = 0xF1;
+static constexpr int hmbTimestampOffset = 5;  // 5 us offset
+using NiFpga_CloseHmbFunc = NiFpga_Status (*)(const NiFpga_Session session,
+                                              const char* memoryName);
+using NiFpga_OpenHmbFunc = NiFpga_Status (*)(const NiFpga_Session session,
+                                             const char* memoryName,
+                                             size_t* memorySize,
+                                             void** virtualAddress);
+struct HMBHolder {
+  ~HMBHolder() {
+    if (hmb) {
+      closeHmb(hmb->getSystemInterface()->getHandle(), hmbName);
+      dlclose(niFpga);
+    }
+  }
+  explicit operator bool() const { return hmb != nullptr; }
+  void Configure() {
+    nFPGA::nRoboRIO_FPGANamespace::g_currentTargetClass =
+        nLoadOut::getTargetClass();
+    int32_t status = 0;
+    hmb.reset(fpga::tHMB::create(&status));
+    niFpga = dlopen("libNiFpga.so", RTLD_LAZY);
+    if (!niFpga) {
+      hmb = nullptr;
+      return;
+    }
+    NiFpga_OpenHmbFunc openHmb = reinterpret_cast<NiFpga_OpenHmbFunc>(
+        dlsym(niFpga, "NiFpgaDll_OpenHmb"));
+    closeHmb = reinterpret_cast<NiFpga_CloseHmbFunc>(
+        dlsym(niFpga, "NiFpgaDll_CloseHmb"));
+    if (openHmb == nullptr || closeHmb == nullptr) {
+      closeHmb = nullptr;
+      dlclose(niFpga);
+      hmb = nullptr;
+      return;
+    }
+    size_t hmbBufferSize = 0;
+    status =
+        openHmb(hmb->getSystemInterface()->getHandle(), hmbName, &hmbBufferSize,
+                reinterpret_cast<void**>(const_cast<uint32_t**>(&hmbBuffer)));
+    if (status != 0) {
+      closeHmb = nullptr;
+      dlclose(niFpga);
+      hmb = nullptr;
+      return;
+    }
+    auto cfg = hmb->readConfig(&status);
+    cfg.Enables_Timestamp = 1;
+    hmb->writeConfig(cfg, &status);
+  }
+  void Reset() {
+    if (hmb) {
+      std::unique_ptr<fpga::tHMB> oldHmb;
+      oldHmb.swap(hmb);
+      closeHmb(oldHmb->getSystemInterface()->getHandle(), hmbName);
+      closeHmb = nullptr;
+      hmbBuffer = nullptr;
+      oldHmb.reset();
+      dlclose(niFpga);
+      niFpga = nullptr;
+    }
+  }
+  std::unique_ptr<fpga::tHMB> hmb;
+  void* niFpga = nullptr;
+  NiFpga_CloseHmbFunc closeHmb = nullptr;
+  volatile uint32_t* hmbBuffer = nullptr;
+};
+static HMBHolder hmb;
+}  // namespace
+#endif
+
 // offset in microseconds
 static uint64_t time_since_epoch() noexcept {
 #ifdef _WIN32
@@ -22,7 +120,7 @@
   uint64_t tmpres = 0;
   // 100-nanosecond intervals since January 1, 1601 (UTC)
   // which means 0.1 us
-  GetSystemTimeAsFileTime(&ft);
+  GetSystemTimePreciseAsFileTime(&ft);
   tmpres |= ft.dwHighDateTime;
   tmpres <<= 32;
   tmpres |= ft.dwLowDateTime;
@@ -88,12 +186,56 @@
 
 static std::atomic<uint64_t (*)()> now_impl{wpi::NowDefault};
 
+void wpi::impl::SetupNowRio() {
+#ifdef __FRC_ROBORIO__
+  if (!hmb) {
+    hmb.Configure();
+  }
+#endif
+}
+
+void wpi::impl::ShutdownNowRio() {
+#ifdef __FRC_ROBORIO__
+  hmb.Reset();
+#endif
+}
+
 void wpi::SetNowImpl(uint64_t (*func)(void)) {
   now_impl = func ? func : NowDefault;
 }
 
 uint64_t wpi::Now() {
+#ifdef __FRC_ROBORIO__
+  // Same code as HAL_GetFPGATime()
+  if (!hmb) {
+    std::fputs(
+        "FPGA not yet configured in wpi::Now(). Time will not be correct",
+        stderr);
+    std::fflush(stderr);
+    return 0;
+  }
+
+  asm("dmb");
+  uint64_t upper1 = hmb.hmbBuffer[timestampUpperOffset];
+  asm("dmb");
+  uint32_t lower = hmb.hmbBuffer[timestampLowerOffset];
+  asm("dmb");
+  uint64_t upper2 = hmb.hmbBuffer[timestampUpperOffset];
+
+  if (upper1 != upper2) {
+    // Rolled over between the lower call, reread lower
+    asm("dmb");
+    lower = hmb.hmbBuffer[timestampLowerOffset];
+  }
+  // 5 is added here because the time to write from the FPGA
+  // to the HMB buffer is longer then the time to read
+  // from the time register. This would cause register based
+  // timestamps to be ahead of HMB timestamps, which could
+  // be very bad.
+  return (upper2 << 32) + lower + hmbTimestampOffset;
+#else
   return (now_impl.load())();
+#endif
 }
 
 uint64_t wpi::GetSystemTime() {
@@ -102,6 +244,14 @@
 
 extern "C" {
 
+void WPI_Impl_SetupNowRio(void) {
+  return wpi::impl::SetupNowRio();
+}
+
+void WPI_Impl_ShutdownNowRio(void) {
+  return wpi::impl::ShutdownNowRio();
+}
+
 uint64_t WPI_NowDefault(void) {
   return wpi::NowDefault();
 }