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

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

git-subtree-dir: third_party/allwpilib
git-subtree-split: ce550705d7cdab117c0153a202973fc026a81274
Signed-off-by: Maxwell Henderson <mxwhenderson@gmail.com>
Change-Id: Ic8645d0551d62b411b0a816c493f0f33291896a1
diff --git a/wpiutil/CMakeLists.txt b/wpiutil/CMakeLists.txt
index a319293..a3f6f19 100644
--- a/wpiutil/CMakeLists.txt
+++ b/wpiutil/CMakeLists.txt
@@ -6,104 +6,118 @@
 include(AddTest)
 include(DownloadAndCheck)
 
-file(GLOB wpiutil_jni_src src/main/native/cpp/jni/WPIUtilJNI.cpp src/main/native/cpp/jni/DataLogJNI.cpp)
+file(
+    GLOB wpiutil_jni_src
+    src/main/native/cpp/jni/WPIUtilJNI.cpp
+    src/main/native/cpp/jni/DataLogJNI.cpp
+)
 
 # Java bindings
-if (WITH_JAVA)
-  find_package(Java REQUIRED)
-  find_package(JNI REQUIRED)
-  include(UseJava)
-  set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
+if(WITH_JAVA)
+    find_package(Java REQUIRED)
+    find_package(JNI REQUIRED)
+    include(UseJava)
+    set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
 
-  if(NOT EXISTS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/jackson-core-2.15.2.jar")
+    if(NOT EXISTS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/jackson-core-2.15.2.jar")
         set(BASE_URL "https://search.maven.org/remotecontent?filepath=")
         set(JAR_ROOT "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson")
 
         message(STATUS "Downloading Jackson jarfiles...")
 
         download_and_check("${BASE_URL}com/fasterxml/jackson/core/jackson-core/2.15.2/jackson-core-2.15.2.jar"
-            "${JAR_ROOT}/jackson-core-2.15.2.jar")
+            "${JAR_ROOT}/jackson-core-2.15.2.jar"
+        )
         download_and_check("${BASE_URL}com/fasterxml/jackson/core/jackson-databind/2.15.2/jackson-databind-2.15.2.jar"
-            "${JAR_ROOT}/jackson-databind-2.15.2.jar")
+            "${JAR_ROOT}/jackson-databind-2.15.2.jar"
+        )
         download_and_check("${BASE_URL}com/fasterxml/jackson/core/jackson-annotations/2.15.2/jackson-annotations-2.15.2.jar"
-            "${JAR_ROOT}/jackson-annotations-2.15.2.jar")
+            "${JAR_ROOT}/jackson-annotations-2.15.2.jar"
+        )
 
         message(STATUS "All files downloaded.")
     endif()
 
-  file(GLOB JACKSON_JARS
-        ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar)
+    file(GLOB JACKSON_JARS ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar)
 
-  if(NOT EXISTS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/quickbuf-runtime-1.3.2.jar")
+    if(NOT EXISTS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/quickbuf-runtime-1.3.3.jar")
         set(BASE_URL "https://search.maven.org/remotecontent?filepath=")
         set(JAR_ROOT "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf")
 
         message(STATUS "Downloading Quickbuf jarfile...")
-        file(DOWNLOAD "${BASE_URL}us/hebi/quickbuf/quickbuf-runtime/1.3.2/quickbuf-runtime-1.3.2.jar"
-            "${JAR_ROOT}/quickbuf-runtime-1.3.2.jar")
+        file(
+            DOWNLOAD
+                "${BASE_URL}us/hebi/quickbuf/quickbuf-runtime/1.3.3/quickbuf-runtime-1.3.3.jar"
+                "${JAR_ROOT}/quickbuf-runtime-1.3.3.jar"
+        )
 
         message(STATUS "Downloaded.")
-  endif()
+    endif()
 
-  file(GLOB QUICKBUF_JAR
-        ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar)
+    file(GLOB QUICKBUF_JAR ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar)
 
-  set(CMAKE_JAVA_INCLUDE_PATH wpiutil.jar ${JACKSON_JARS} ${QUICKBUF_JAR})
+    set(CMAKE_JAVA_INCLUDE_PATH wpiutil.jar ${JACKSON_JARS} ${QUICKBUF_JAR})
 
-  set(CMAKE_JNI_TARGET true)
+    set(CMAKE_JNI_TARGET true)
 
-  file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
+    file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
 
-  add_jar(wpiutil_jar ${JAVA_SOURCES} INCLUDE_JARS ${JACKSON_JARS} ${QUICKBUF_JAR} OUTPUT_NAME wpiutil GENERATE_NATIVE_HEADERS wpiutil_jni_headers)
+    add_jar(
+        wpiutil_jar
+        ${JAVA_SOURCES}
+        INCLUDE_JARS ${JACKSON_JARS} ${QUICKBUF_JAR}
+        OUTPUT_NAME wpiutil
+        GENERATE_NATIVE_HEADERS wpiutil_jni_headers
+    )
 
-  get_property(WPIUTIL_JAR_FILE TARGET wpiutil_jar PROPERTY JAR_FILE)
-  install(FILES ${WPIUTIL_JAR_FILE} DESTINATION "${java_lib_dest}")
+    install_jar(wpiutil_jar DESTINATION ${java_lib_dest})
+    install_jar_exports(TARGETS wpiutil_jar FILE wpiutil_jar.cmake DESTINATION share/wpiutil)
 
-  set_property(TARGET wpiutil_jar PROPERTY FOLDER "java")
+    add_library(wpiutiljni ${wpiutil_jni_src})
+    wpilib_target_warnings(wpiutiljni)
+    target_link_libraries(wpiutiljni PUBLIC wpiutil)
 
-  add_library(wpiutiljni ${wpiutil_jni_src})
-  wpilib_target_warnings(wpiutiljni)
-  target_link_libraries(wpiutiljni PUBLIC wpiutil)
+    set_property(TARGET wpiutiljni PROPERTY FOLDER "libraries")
 
-  set_property(TARGET wpiutiljni PROPERTY FOLDER "libraries")
+    target_link_libraries(wpiutiljni PRIVATE wpiutil_jni_headers)
+    add_dependencies(wpiutiljni wpiutil_jar)
 
-  target_link_libraries(wpiutiljni PRIVATE wpiutil_jni_headers)
-  add_dependencies(wpiutiljni wpiutil_jar)
-
-  install(TARGETS wpiutiljni EXPORT wpiutiljni)
-
+    install(TARGETS wpiutiljni EXPORT wpiutiljni)
 endif()
 
-if (WITH_JAVA_SOURCE)
-  find_package(Java REQUIRED)
-  include(UseJava)
-  file(GLOB WPIUTIL_SOURCES src/main/java/edu/wpi/first/util/*.java)
-  file(GLOB WPIUTIL_CLEANUP_SOURCES src/main/java/edu/wpi/first/util/cleanup/*.java)
-  file(GLOB WPIUTIL_CONCURRENT_SOURCES src/main/java/edu/wpi/first/util/concurrent/*.java)
-  file(GLOB WPIUTIL_DATALOG_SOURCES src/main/java/edu/wpi/first/util/datalog/*.java)
-  file(GLOB WPIUTIL_FUNCTION_SOURCES src/main/java/edu/wpi/first/util/function/*.java)
-  file(GLOB WPIUTIL_SENDABLE_SOURCES src/main/java/edu/wpi/first/util/sendable/*.java)
-  add_jar(wpiutil_src_jar
-  RESOURCES NAMESPACE "edu/wpi/first/util" ${WPIUTIL_SOURCES}
-  NAMESPACE "edu/wpi/first/util/cleanup" ${WPIUTIL_CLEANUP_SOURCES}
-  NAMESPACE "edu/wpi/first/util/concurrent" ${WPIUTIL_CONCURRENT_SOURCES}
-  NAMESPACE "edu/wpi/first/util/datalog" ${WPIUTIL_DATALOG_SOURCES}
-  NAMESPACE "edu/wpi/first/util/function" ${WPIUTIL_FUNCTION_SOURCES}
-  NAMESPACE "edu/wpi/first/util/sendable" ${WPIUTIL_SENDABLE_SOURCES}
-  OUTPUT_NAME wpiutil-sources)
+if(WITH_JAVA_SOURCE)
+    find_package(Java REQUIRED)
+    include(UseJava)
+    file(GLOB WPIUTIL_SOURCES src/main/java/edu/wpi/first/util/*.java)
+    file(GLOB WPIUTIL_CLEANUP_SOURCES src/main/java/edu/wpi/first/util/cleanup/*.java)
+    file(GLOB WPIUTIL_CONCURRENT_SOURCES src/main/java/edu/wpi/first/util/concurrent/*.java)
+    file(GLOB WPIUTIL_DATALOG_SOURCES src/main/java/edu/wpi/first/util/datalog/*.java)
+    file(GLOB WPIUTIL_FUNCTION_SOURCES src/main/java/edu/wpi/first/util/function/*.java)
+    file(GLOB WPIUTIL_SENDABLE_SOURCES src/main/java/edu/wpi/first/util/sendable/*.java)
+    add_jar(
+        wpiutil_src_jar
+        RESOURCES
+        NAMESPACE "edu/wpi/first/util" ${WPIUTIL_SOURCES}
+        NAMESPACE "edu/wpi/first/util/cleanup" ${WPIUTIL_CLEANUP_SOURCES}
+        NAMESPACE "edu/wpi/first/util/concurrent" ${WPIUTIL_CONCURRENT_SOURCES}
+        NAMESPACE "edu/wpi/first/util/datalog" ${WPIUTIL_DATALOG_SOURCES}
+        NAMESPACE "edu/wpi/first/util/function" ${WPIUTIL_FUNCTION_SOURCES}
+        NAMESPACE "edu/wpi/first/util/sendable" ${WPIUTIL_SENDABLE_SOURCES}
+        OUTPUT_NAME wpiutil-sources
+    )
 
-  get_property(WPIUTIL_SRC_JAR_FILE TARGET wpiutil_src_jar PROPERTY JAR_FILE)
-  install(FILES ${WPIUTIL_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
+    get_property(WPIUTIL_SRC_JAR_FILE TARGET wpiutil_src_jar PROPERTY JAR_FILE)
+    install(FILES ${WPIUTIL_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
 
-  set_property(TARGET wpiutil_src_jar PROPERTY FOLDER "java")
+    set_property(TARGET wpiutil_src_jar PROPERTY FOLDER "java")
 endif()
 
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 find_package(Threads REQUIRED)
 
-if (NOT MSVC AND NOT APPLE AND NOT ANDROID)
+if(NOT MSVC AND NOT APPLE AND NOT ANDROID)
     find_library(ATOMIC NAMES atomic libatomic.so.1)
-    if (ATOMIC)
+    if(ATOMIC)
         message(STATUS "Found libatomic: ${ATOMIC}")
     else()
         message(STATUS "libatomic not found. If build fails, install libatomic")
@@ -112,10 +126,13 @@
 
 generate_resources(src/main/native/resources generated/main/cpp WPI wpi wpiutil_resources_src)
 
-file(GLOB_RECURSE wpiutil_native_src src/main/native/cpp/*.cpp
-                                     src/main/native/thirdparty/json/cpp/*.cpp
-                                     src/main/native/thirdparty/llvm/cpp/*.cpp
-                                     src/main/native/thirdparty/mpack/src/*.cpp)
+file(
+    GLOB_RECURSE wpiutil_native_src
+    src/main/native/cpp/*.cpp
+    src/main/native/thirdparty/json/cpp/*.cpp
+    src/main/native/thirdparty/llvm/cpp/*.cpp
+    src/main/native/thirdparty/mpack/src/*.cpp
+)
 list(REMOVE_ITEM wpiutil_native_src ${wpiutil_jni_src})
 file(GLOB_RECURSE wpiutil_unix_src src/main/native/unix/*.cpp)
 file(GLOB_RECURSE wpiutil_linux_src src/main/native/linux/*.cpp)
@@ -131,24 +148,32 @@
 set_property(TARGET wpiutil PROPERTY FOLDER "libraries")
 
 target_compile_features(wpiutil PUBLIC cxx_std_20)
-if (MSVC)
-    target_compile_options(wpiutil PUBLIC /permissive- /Zc:preprocessor /Zc:throwingNew /MP /bigobj)
+if(MSVC)
+    target_compile_options(
+        wpiutil
+        PUBLIC /permissive- /Zc:preprocessor /Zc:throwingNew /MP /bigobj
+    )
     target_compile_definitions(wpiutil PRIVATE -D_CRT_SECURE_NO_WARNINGS)
 endif()
 wpilib_target_warnings(wpiutil)
 target_link_libraries(wpiutil protobuf::libprotobuf Threads::Threads ${CMAKE_DL_LIBS})
 
-if (ATOMIC)
+if(ATOMIC)
     target_link_libraries(wpiutil ${ATOMIC})
 endif()
 
-
-if (NOT USE_SYSTEM_FMTLIB)
+if(NOT USE_SYSTEM_FMTLIB)
     target_sources(wpiutil PRIVATE ${fmtlib_native_src})
-    install(DIRECTORY src/main/native/thirdparty/fmtlib/include/ DESTINATION "${include_dest}/wpiutil")
-    target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/fmtlib/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+    install(
+        DIRECTORY src/main/native/thirdparty/fmtlib/include/
+        DESTINATION "${include_dest}/wpiutil"
+    )
+    target_include_directories(
+        wpiutil
+        PUBLIC
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/fmtlib/include>
+            $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+    )
 else()
     find_package(fmt CONFIG REQUIRED)
     target_link_libraries(wpiutil fmt::fmt)
@@ -161,11 +186,11 @@
     endif()
 endif()
 
-if (MSVC)
+if(MSVC)
     target_sources(wpiutil PRIVATE ${wpiutil_windows_src})
-else ()
+else()
     target_sources(wpiutil PRIVATE ${wpiutil_unix_src})
-    if (APPLE)
+    if(APPLE)
         target_sources(wpiutil PRIVATE ${wpiutil_macos_src})
     else()
         target_sources(wpiutil PRIVATE ${wpiutil_linux_src})
@@ -173,47 +198,59 @@
 endif()
 
 install(DIRECTORY src/main/native/thirdparty/memory/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/memory/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/memory/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(DIRECTORY src/main/native/thirdparty/json/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/json/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/json/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(DIRECTORY src/main/native/thirdparty/llvm/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/llvm/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/llvm/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(DIRECTORY src/main/native/thirdparty/mpack/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/mpack/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/mpack/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(DIRECTORY src/main/native/thirdparty/sigslot/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/sigslot/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/thirdparty/sigslot/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/wpiutil")
-target_include_directories(wpiutil PUBLIC
-                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
-                            $<INSTALL_INTERFACE:${include_dest}/wpiutil>)
+target_include_directories(
+    wpiutil
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
+        $<INSTALL_INTERFACE:${include_dest}/wpiutil>
+)
 
 install(TARGETS wpiutil EXPORT wpiutil)
 
-if (WITH_FLAT_INSTALL)
-    set (wpiutil_config_dir ${wpilib_dest})
-else()
-    set (wpiutil_config_dir share/wpiutil)
-endif()
-
-configure_file(wpiutil-config.cmake.in ${WPILIB_BINARY_DIR}/wpiutil-config.cmake )
-install(FILES ${WPILIB_BINARY_DIR}/wpiutil-config.cmake DESTINATION ${wpiutil_config_dir})
-install(EXPORT wpiutil DESTINATION ${wpiutil_config_dir})
+configure_file(wpiutil-config.cmake.in ${WPILIB_BINARY_DIR}/wpiutil-config.cmake)
+install(FILES ${WPILIB_BINARY_DIR}/wpiutil-config.cmake DESTINATION share/wpiutil)
+install(EXPORT wpiutil DESTINATION share/wpiutil)
 
 subdir_list(wpiutil_examples "${CMAKE_CURRENT_SOURCE_DIR}/examples")
 foreach(example ${wpiutil_examples})
@@ -226,7 +263,7 @@
     endif()
 endforeach()
 
-if (WITH_TESTS)
+if(WITH_TESTS)
     file(GLOB_RECURSE wpiutil_testlib_src src/test/native/include/*.h)
     add_library(wpiutil_testlib INTERFACE ${wpiutil_test_src})
     target_include_directories(wpiutil_testlib INTERFACE src/test/native/include)
diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle
index ab13092..7e4cc16 100644
--- a/wpiutil/build.gradle
+++ b/wpiutil/build.gradle
@@ -147,12 +147,6 @@
             }
         }
     }
-
-    exeSplitSetup = {
-        if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-            nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
-        }
-    }
 }
 
 def examplesMap = [:];
@@ -252,9 +246,6 @@
                 targetBuildTypes 'debug'
                 binaries.all {
                     lib library: 'wpiutil', linkage: 'shared'
-                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                        nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
-                    }
                 }
                 sources {
                     cpp {
@@ -275,7 +266,7 @@
             if (!(it instanceof NativeBinarySpec)) return
                 if (it.component.name != 'wpiutil' && it.component.name != 'wpiutilBase') return
                 if (it.targetPlatform.name != nativeUtils.wpi.platforms.roborio) return
-                nativeUtils.useRequiredLibrary(it, 'ni_link_libraries')
+                nativeUtils.useRequiredLibrary(it, 'chipobject_headers')
         }
     }
 }
@@ -294,7 +285,7 @@
     api "com.fasterxml.jackson.core:jackson-annotations:2.15.2"
     api "com.fasterxml.jackson.core:jackson-core:2.15.2"
     api "com.fasterxml.jackson.core:jackson-databind:2.15.2"
-    api 'us.hebi.quickbuf:quickbuf-runtime:1.3.2'
+    api 'us.hebi.quickbuf:quickbuf-runtime:1.3.3'
 
     printlogImplementation sourceSets.main.output
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/CircularBuffer.java b/wpiutil/src/main/java/edu/wpi/first/util/CircularBuffer.java
index 729c8b1..ff68d4d 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/CircularBuffer.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/CircularBuffer.java
@@ -4,11 +4,13 @@
 
 package edu.wpi.first.util;
 
-import java.util.Arrays;
-
-/** This is a simple circular buffer so we don't need to "bucket brigade" copy old values. */
-public class CircularBuffer {
-  private double[] m_data;
+/**
+ * This is a simple circular buffer so we don't need to "bucket brigade" copy old values.
+ *
+ * @param <T> Buffer element type.
+ */
+public class CircularBuffer<T> {
+  private T[] m_data;
 
   // Index of element at front of buffer
   private int m_front;
@@ -19,11 +21,11 @@
   /**
    * Create a CircularBuffer with the provided size.
    *
-   * @param size The size of the circular buffer.
+   * @param size Maximum number of buffer elements.
    */
+  @SuppressWarnings("unchecked")
   public CircularBuffer(int size) {
-    m_data = new double[size];
-    Arrays.fill(m_data, 0.0);
+    m_data = (T[]) new Object[size];
   }
 
   /**
@@ -40,7 +42,7 @@
    *
    * @return value at front of buffer
    */
-  public double getFirst() {
+  public T getFirst() {
     return m_data[m_front];
   }
 
@@ -48,11 +50,14 @@
    * Get value at back of buffer.
    *
    * @return value at back of buffer
+   * @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
+   *     size())
    */
-  public double getLast() {
+  @SuppressWarnings("unchecked")
+  public T getLast() {
     // If there are no elements in the buffer, do nothing
     if (m_length == 0) {
-      return 0.0;
+      throw new IndexOutOfBoundsException("getLast() called on an empty container");
     }
 
     return m_data[(m_front + m_length - 1) % m_data.length];
@@ -64,7 +69,7 @@
    *
    * @param value The value to push.
    */
-  public void addFirst(double value) {
+  public void addFirst(T value) {
     if (m_data.length == 0) {
       return;
     }
@@ -84,7 +89,7 @@
    *
    * @param value The value to push.
    */
-  public void addLast(double value) {
+  public void addLast(T value) {
     if (m_data.length == 0) {
       return;
     }
@@ -103,14 +108,17 @@
    * Pop value at front of buffer.
    *
    * @return value at front of buffer
+   * @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
+   *     size())
    */
-  public double removeFirst() {
+  @SuppressWarnings("unchecked")
+  public T removeFirst() {
     // If there are no elements in the buffer, do nothing
     if (m_length == 0) {
-      return 0.0;
+      throw new IndexOutOfBoundsException("removeFirst() called on an empty container");
     }
 
-    double temp = m_data[m_front];
+    T temp = m_data[m_front];
     m_front = moduloInc(m_front);
     m_length--;
     return temp;
@@ -120,11 +128,14 @@
    * Pop value at back of buffer.
    *
    * @return value at back of buffer
+   * @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
+   *     size())
    */
-  public double removeLast() {
+  @SuppressWarnings("unchecked")
+  public T removeLast() {
     // If there are no elements in the buffer, do nothing
     if (m_length == 0) {
-      return 0.0;
+      throw new IndexOutOfBoundsException("removeLast() called on an empty container");
     }
 
     m_length--;
@@ -138,8 +149,9 @@
    *
    * @param size New buffer size.
    */
+  @SuppressWarnings("unchecked")
   public void resize(int size) {
-    double[] newBuffer = new double[size];
+    var newBuffer = (T[]) new Object[size];
     m_length = Math.min(m_length, size);
     for (int i = 0; i < m_length; i++) {
       newBuffer[i] = m_data[(m_front + i) % m_data.length];
@@ -150,7 +162,6 @@
 
   /** Sets internal buffer contents to zero. */
   public void clear() {
-    Arrays.fill(m_data, 0.0);
     m_front = 0;
     m_length = 0;
   }
@@ -161,23 +172,25 @@
    * @param index Index into the buffer.
    * @return Element at index starting from front of buffer.
    */
-  public double get(int index) {
+  public T get(int index) {
     return m_data[(m_front + index) % m_data.length];
   }
 
   /**
-   * Increment an index modulo the length of the m_data buffer.
+   * Increment an index modulo the length of the buffer.
    *
    * @param index Index into the buffer.
+   * @return The incremented index.
    */
   private int moduloInc(int index) {
     return (index + 1) % m_data.length;
   }
 
   /**
-   * Decrement an index modulo the length of the m_data buffer.
+   * Decrement an index modulo the length of the buffer.
    *
    * @param index Index into the buffer.
+   * @return The decremented index.
    */
   private int moduloDec(int index) {
     if (index == 0) {
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/CombinedRuntimeLoader.java b/wpiutil/src/main/java/edu/wpi/first/util/CombinedRuntimeLoader.java
index 09e739d..8fe4839 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/CombinedRuntimeLoader.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/CombinedRuntimeLoader.java
@@ -16,11 +16,17 @@
 import java.util.Map;
 import java.util.Objects;
 
+/** Loads dynamic libraries for all platforms. */
 public final class CombinedRuntimeLoader {
   private CombinedRuntimeLoader() {}
 
   private static String extractionDirectory;
 
+  /**
+   * Returns library extraction directory.
+   *
+   * @return Library extraction directory.
+   */
   public static synchronized String getExtractionDirectory() {
     return extractionDirectory;
   }
@@ -29,6 +35,12 @@
     extractionDirectory = directory;
   }
 
+  /**
+   * Sets DLL directory.
+   *
+   * @param directory Directory.
+   * @return DLL directory.
+   */
   public static native String setDllDirectory(String directory);
 
   private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) {
@@ -59,8 +71,7 @@
   @SuppressWarnings("unchecked")
   public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName)
       throws IOException {
-    TypeReference<HashMap<String, Object>> typeRef =
-        new TypeReference<HashMap<String, Object>>() {};
+    TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {};
     ObjectMapper mapper = new ObjectMapper();
     Map<String, Object> map;
     try (var stream = clazz.getResourceAsStream(resourceName)) {
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/DoubleCircularBuffer.java b/wpiutil/src/main/java/edu/wpi/first/util/DoubleCircularBuffer.java
new file mode 100644
index 0000000..548f14b
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/DoubleCircularBuffer.java
@@ -0,0 +1,191 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util;
+
+import java.util.Arrays;
+
+/** This is a simple circular buffer so we don't need to "bucket brigade" copy old values. */
+public class DoubleCircularBuffer {
+  private double[] m_data;
+
+  // Index of element at front of buffer
+  private int m_front;
+
+  // Number of elements used in buffer
+  private int m_length;
+
+  /**
+   * Create a CircularBuffer with the provided size.
+   *
+   * @param size The size of the circular buffer.
+   */
+  public DoubleCircularBuffer(int size) {
+    m_data = new double[size];
+    Arrays.fill(m_data, 0.0);
+  }
+
+  /**
+   * Returns number of elements in buffer.
+   *
+   * @return number of elements in buffer
+   */
+  public int size() {
+    return m_length;
+  }
+
+  /**
+   * Get value at front of buffer.
+   *
+   * @return value at front of buffer
+   */
+  public double getFirst() {
+    return m_data[m_front];
+  }
+
+  /**
+   * Get value at back of buffer.
+   *
+   * @return value at back of buffer
+   */
+  public double getLast() {
+    // If there are no elements in the buffer, do nothing
+    if (m_length == 0) {
+      return 0.0;
+    }
+
+    return m_data[(m_front + m_length - 1) % m_data.length];
+  }
+
+  /**
+   * Push new value onto front of the buffer. The value at the back is overwritten if the buffer is
+   * full.
+   *
+   * @param value The value to push.
+   */
+  public void addFirst(double value) {
+    if (m_data.length == 0) {
+      return;
+    }
+
+    m_front = moduloDec(m_front);
+
+    m_data[m_front] = value;
+
+    if (m_length < m_data.length) {
+      m_length++;
+    }
+  }
+
+  /**
+   * Push new value onto back of the buffer. The value at the front is overwritten if the buffer is
+   * full.
+   *
+   * @param value The value to push.
+   */
+  public void addLast(double value) {
+    if (m_data.length == 0) {
+      return;
+    }
+
+    m_data[(m_front + m_length) % m_data.length] = value;
+
+    if (m_length < m_data.length) {
+      m_length++;
+    } else {
+      // Increment front if buffer is full to maintain size
+      m_front = moduloInc(m_front);
+    }
+  }
+
+  /**
+   * Pop value at front of buffer.
+   *
+   * @return value at front of buffer
+   */
+  public double removeFirst() {
+    // If there are no elements in the buffer, do nothing
+    if (m_length == 0) {
+      return 0.0;
+    }
+
+    double temp = m_data[m_front];
+    m_front = moduloInc(m_front);
+    m_length--;
+    return temp;
+  }
+
+  /**
+   * Pop value at back of buffer.
+   *
+   * @return value at back of buffer
+   */
+  public double removeLast() {
+    // If there are no elements in the buffer, do nothing
+    if (m_length == 0) {
+      return 0.0;
+    }
+
+    m_length--;
+    return m_data[(m_front + m_length) % m_data.length];
+  }
+
+  /**
+   * Resizes internal buffer to given size.
+   *
+   * <p>A new buffer is allocated because arrays are not resizable.
+   *
+   * @param size New buffer size.
+   */
+  public void resize(int size) {
+    double[] newBuffer = new double[size];
+    m_length = Math.min(m_length, size);
+    for (int i = 0; i < m_length; i++) {
+      newBuffer[i] = m_data[(m_front + i) % m_data.length];
+    }
+    m_data = newBuffer;
+    m_front = 0;
+  }
+
+  /** Sets internal buffer contents to zero. */
+  public void clear() {
+    Arrays.fill(m_data, 0.0);
+    m_front = 0;
+    m_length = 0;
+  }
+
+  /**
+   * Get the element at the provided index relative to the start of the buffer.
+   *
+   * @param index Index into the buffer.
+   * @return Element at index starting from front of buffer.
+   */
+  public double get(int index) {
+    return m_data[(m_front + index) % m_data.length];
+  }
+
+  /**
+   * Increment an index modulo the length of the buffer.
+   *
+   * @param index Index into the buffer.
+   * @return The incremented index.
+   */
+  private int moduloInc(int index) {
+    return (index + 1) % m_data.length;
+  }
+
+  /**
+   * Decrement an index modulo the length of the buffer.
+   *
+   * @param index Index into the buffer.
+   * @return The decremented index.
+   */
+  private int moduloDec(int index) {
+    if (index == 0) {
+      return m_data.length - 1;
+    } else {
+      return index - 1;
+    }
+  }
+}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/EventVector.java b/wpiutil/src/main/java/edu/wpi/first/util/EventVector.java
index 4d2c800..60fb5d1 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/EventVector.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/EventVector.java
@@ -8,10 +8,14 @@
 import java.util.List;
 import java.util.concurrent.locks.ReentrantLock;
 
+/** A thread-safe container for handling events. */
 public class EventVector {
   private final ReentrantLock m_lock = new ReentrantLock();
   private final List<Integer> m_events = new ArrayList<>();
 
+  /** Default constructor. */
+  public EventVector() {}
+
   /**
    * Adds an event to the event vector.
    *
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/InterpolatingTreeMap.java b/wpiutil/src/main/java/edu/wpi/first/util/InterpolatingTreeMap.java
index 2c54d00..8efe917 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/InterpolatingTreeMap.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/InterpolatingTreeMap.java
@@ -10,12 +10,17 @@
  * Interpolating Tree Maps are used to get values at points that are not defined by making a guess
  * from points that are defined. This uses linear interpolation.
  *
+ * @param <K> Key type.
+ * @param <V> Value type.
  * @deprecated Use {@link edu.wpi.first.math.interpolation.InterpolatingDoubleTreeMap} instead
  */
 @Deprecated(forRemoval = true, since = "2024")
 public class InterpolatingTreeMap<K extends Number, V extends Number> {
   private final TreeMap<K, V> m_map = new TreeMap<>();
 
+  /** Default constructor. */
+  public InterpolatingTreeMap() {}
+
   /**
    * Inserts a key-value pair.
    *
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/PixelFormat.java b/wpiutil/src/main/java/edu/wpi/first/util/PixelFormat.java
new file mode 100644
index 0000000..809bfd0
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/PixelFormat.java
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util;
+
+/** Image pixel format. */
+public enum PixelFormat {
+  /** Unknown format. */
+  kUnknown(0),
+  /** Motion-JPEG (compressed image data). */
+  kMJPEG(1),
+  /** YUY 4:2:2, 16 bpp. */
+  kYUYV(2),
+  /** RGB 5-6-5, 16 bpp. */
+  kRGB565(3),
+  /** BGR 8-8-8, 24 bpp. */
+  kBGR(4),
+  /** Grayscale, 8 bpp. */
+  kGray(5),
+  /** Grayscale, 16 bpp. */
+  kY16(6),
+  /** YUV 4:2:2, 16 bpp. */
+  kUYVY(7);
+
+  private final int value;
+
+  PixelFormat(int value) {
+    this.value = value;
+  }
+
+  /**
+   * Gets the integer value of the pixel format.
+   *
+   * @return Integer value
+   */
+  public int getValue() {
+    return value;
+  }
+
+  private static final PixelFormat[] s_values = values();
+
+  /**
+   * Gets a PixelFormat enum value from its integer value.
+   *
+   * @param pixelFormat integer value
+   * @return Enum value
+   */
+  public static PixelFormat getFromInt(int pixelFormat) {
+    return s_values[pixelFormat];
+  }
+}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/RawFrame.java b/wpiutil/src/main/java/edu/wpi/first/util/RawFrame.java
new file mode 100644
index 0000000..dd074bf
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/RawFrame.java
@@ -0,0 +1,188 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Class for storing raw frame data between image read call.
+ *
+ * <p>Data is reused for each frame read, rather then reallocating every frame.
+ */
+public class RawFrame implements AutoCloseable {
+  private long m_nativeObj;
+  private ByteBuffer m_data;
+  private int m_width;
+  private int m_height;
+  private int m_stride;
+  private PixelFormat m_pixelFormat = PixelFormat.kUnknown;
+
+  /** Construct a new empty RawFrame. */
+  public RawFrame() {
+    m_nativeObj = WPIUtilJNI.allocateRawFrame();
+  }
+
+  /**
+   * Close the RawFrame, releasing native resources. Any images currently using the data will be
+   * invalidated.
+   */
+  @Override
+  public void close() {
+    WPIUtilJNI.freeRawFrame(m_nativeObj);
+    m_nativeObj = 0;
+  }
+
+  /**
+   * Called from JNI to set data in class.
+   *
+   * @param data A native ByteBuffer pointing to the frame data.
+   * @param width The width of the frame, in pixels
+   * @param height The height of the frame, in pixels
+   * @param stride The number of bytes in each row of image data
+   * @param pixelFormat The PixelFormat of the frame
+   */
+  void setDataJNI(ByteBuffer data, int width, int height, int stride, int pixelFormat) {
+    m_data = data;
+    m_width = width;
+    m_height = height;
+    m_stride = stride;
+    m_pixelFormat = PixelFormat.getFromInt(pixelFormat);
+  }
+
+  /**
+   * Called from JNI to set info in class.
+   *
+   * @param width The width of the frame, in pixels
+   * @param height The height of the frame, in pixels
+   * @param stride The number of bytes in each row of image data
+   * @param pixelFormat The PixelFormat of the frame
+   */
+  void setInfoJNI(int width, int height, int stride, int pixelFormat) {
+    m_width = width;
+    m_height = height;
+    m_stride = stride;
+    m_pixelFormat = PixelFormat.getFromInt(pixelFormat);
+  }
+
+  /**
+   * Set frame data.
+   *
+   * @param data A native ByteBuffer pointing to the frame data.
+   * @param width The width of the frame, in pixels
+   * @param height The height of the frame, in pixels
+   * @param stride The number of bytes in each row of image data
+   * @param pixelFormat The PixelFormat of the frame
+   */
+  public void setData(ByteBuffer data, int width, int height, int stride, PixelFormat pixelFormat) {
+    if (!data.isDirect()) {
+      throw new UnsupportedOperationException("ByteBuffer must be direct");
+    }
+    m_data = data;
+    m_width = width;
+    m_height = height;
+    m_stride = stride;
+    m_pixelFormat = pixelFormat;
+    WPIUtilJNI.setRawFrameData(
+        m_nativeObj, data, data.limit(), width, height, stride, pixelFormat.getValue());
+  }
+
+  /**
+   * Call to set frame information.
+   *
+   * @param width The width of the frame, in pixels
+   * @param height The height of the frame, in pixels
+   * @param stride The number of bytes in each row of image data
+   * @param pixelFormat The PixelFormat of the frame
+   */
+  public void setInfo(int width, int height, int stride, PixelFormat pixelFormat) {
+    m_width = width;
+    m_height = height;
+    m_stride = stride;
+    m_pixelFormat = pixelFormat;
+    WPIUtilJNI.setRawFrameInfo(
+        m_nativeObj,
+        m_data != null ? m_data.limit() : 0,
+        width,
+        height,
+        stride,
+        pixelFormat.getValue());
+  }
+
+  /**
+   * Get the pointer to native representation of this frame.
+   *
+   * @return The pointer to native representation of this frame.
+   */
+  public long getNativeObj() {
+    return m_nativeObj;
+  }
+
+  /**
+   * Get a ByteBuffer pointing to the frame data. This ByteBuffer is backed by the frame directly.
+   * Its lifetime is controlled by the frame. If a new frame gets read, it will overwrite the
+   * current one.
+   *
+   * @return A ByteBuffer pointing to the frame data.
+   */
+  public ByteBuffer getData() {
+    return m_data;
+  }
+
+  /**
+   * Get a long (is a uint8_t* in native code) pointing to the frame data. This pointer is backed by
+   * the frame directly. Its lifetime is controlled by the frame. If a new frame gets read, it will
+   * overwrite the current one.
+   *
+   * @return A long pointing to the frame data.
+   */
+  public long getDataPtr() {
+    return WPIUtilJNI.getRawFrameDataPtr(m_nativeObj);
+  }
+
+  /**
+   * Get the total size of the data stored in the frame, in bytes.
+   *
+   * @return The total size of the data stored in the frame.
+   */
+  public int getSize() {
+    return m_data != null ? m_data.limit() : 0;
+  }
+
+  /**
+   * Get the width of the image.
+   *
+   * @return The width of the image, in pixels.
+   */
+  public int getWidth() {
+    return m_width;
+  }
+
+  /**
+   * Get the height of the image.
+   *
+   * @return The height of the image, in pixels.
+   */
+  public int getHeight() {
+    return m_height;
+  }
+
+  /**
+   * Get the number of bytes in each row of image data.
+   *
+   * @return The image data stride, in bytes.
+   */
+  public int getStride() {
+    return m_stride;
+  }
+
+  /**
+   * Get the PixelFormat of the frame.
+   *
+   * @return The PixelFormat of the frame.
+   */
+  public PixelFormat getPixelFormat() {
+    return m_pixelFormat;
+  }
+}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/RuntimeDetector.java b/wpiutil/src/main/java/edu/wpi/first/util/RuntimeDetector.java
index 550339e..72593dc 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/RuntimeDetector.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/RuntimeDetector.java
@@ -6,6 +6,9 @@
 
 import java.io.File;
 
+/**
+ * A utility class for detecting and providing platform-specific such as OS and CPU architecture.
+ */
 public final class RuntimeDetector {
   private static String filePrefix;
   private static String fileExtension;
@@ -131,32 +134,57 @@
   }
 
   /**
-   * check if architecture is Arm64.
+   * Check if architecture is Arm64.
    *
-   * @return if architecture is Arm64
+   * @return if architecture is Arm64.
    */
   public static boolean isArm64() {
     String arch = System.getProperty("os.arch");
     return "aarch64".equals(arch) || "arm64".equals(arch);
   }
 
+  /**
+   * Check if OS is Linux.
+   *
+   * @return if OS is Linux.
+   */
   public static boolean isLinux() {
     return System.getProperty("os.name").startsWith("Linux");
   }
 
+  /**
+   * Check if OS is Windows.
+   *
+   * @return if OS is Windows.
+   */
   public static boolean isWindows() {
     return System.getProperty("os.name").startsWith("Windows");
   }
 
+  /**
+   * Check if OS is Mac.
+   *
+   * @return if OS is Mac.
+   */
   public static boolean isMac() {
     return System.getProperty("os.name").startsWith("Mac");
   }
 
+  /**
+   * Check if OS is 32bit Intel.
+   *
+   * @return if OS is 32bit Intel.
+   */
   public static boolean is32BitIntel() {
     String arch = System.getProperty("os.arch");
     return "x86".equals(arch) || "i386".equals(arch);
   }
 
+  /**
+   * Check if OS is 64bit Intel.
+   *
+   * @return if OS is 64bit Intel.
+   */
   public static boolean is64BitIntel() {
     String arch = System.getProperty("os.arch");
     return "amd64".equals(arch) || "x86_64".equals(arch);
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/RuntimeLoader.java b/wpiutil/src/main/java/edu/wpi/first/util/RuntimeLoader.java
index f24ace3..474666e 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/RuntimeLoader.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/RuntimeLoader.java
@@ -17,6 +17,11 @@
 import java.util.Locale;
 import java.util.Scanner;
 
+/**
+ * Loads a native library at runtime.
+ *
+ * @param <T> The class to load.
+ */
 public final class RuntimeLoader<T> {
   private static String defaultExtractionRoot;
 
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/WPISerializable.java b/wpiutil/src/main/java/edu/wpi/first/util/WPISerializable.java
new file mode 100644
index 0000000..200deb5
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/WPISerializable.java
@@ -0,0 +1,8 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util;
+
+/** Marker interface to indicate a class is serializable using WPI serialization methods. */
+public interface WPISerializable {}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java
index 9929b48..0bd5b21 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/WPIUtilJNI.java
@@ -5,22 +5,38 @@
 package edu.wpi.first.util;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+/** WPIUtil JNI. */
 public class WPIUtilJNI {
   static boolean libraryLoaded = false;
   static RuntimeLoader<WPIUtilJNI> loader = null;
 
+  /** Sets whether JNI should be loaded in the static block. */
   public static class Helper {
     private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
 
+    /**
+     * Returns true if the JNI should be loaded in the static block.
+     *
+     * @return True if the JNI should be loaded in the static block.
+     */
     public static boolean getExtractOnStaticLoad() {
       return extractOnStaticLoad.get();
     }
 
+    /**
+     * Sets whether the JNI should be loaded in the static block.
+     *
+     * @param load Whether the JNI should be loaded in the static block.
+     */
     public static void setExtractOnStaticLoad(boolean load) {
       extractOnStaticLoad.set(load);
     }
+
+    /** Utility class. */
+    private Helper() {}
   }
 
   static {
@@ -54,32 +70,115 @@
     libraryLoaded = true;
   }
 
+  /**
+   * Write the given string to stderr.
+   *
+   * @param str String to write.
+   */
   public static native void writeStderr(String str);
 
+  /** Enable mock time. */
   public static native void enableMockTime();
 
+  /** Disable mock time. */
   public static native void disableMockTime();
 
+  /**
+   * Set mock time.
+   *
+   * @param time The desired time in microseconds.
+   */
   public static native void setMockTime(long time);
 
+  /**
+   * Returns the time.
+   *
+   * @return The time.
+   */
   public static native long now();
 
+  /**
+   * Returns the system time.
+   *
+   * @return The system time.
+   */
   public static native long getSystemTime();
 
+  /**
+   * Creates an event. Events have binary state (signaled or not signaled) and may be either
+   * automatically reset or manually reset. Automatic-reset events go to non-signaled state when a
+   * WaitForObject is woken up by the event; manual-reset events require ResetEvent() to be called
+   * to set the event to non-signaled state; if ResetEvent() is not called, any waiter on that event
+   * will immediately wake when called.
+   *
+   * @param manualReset true for manual reset, false for automatic reset
+   * @param initialState true to make the event initially in signaled state
+   * @return Event handle
+   */
   public static native int createEvent(boolean manualReset, boolean initialState);
 
+  /**
+   * Destroys an event. Destruction wakes up any waiters.
+   *
+   * @param eventHandle event handle
+   */
   public static native void destroyEvent(int eventHandle);
 
+  /**
+   * Sets an event to signaled state.
+   *
+   * @param eventHandle event handle
+   */
   public static native void setEvent(int eventHandle);
 
+  /**
+   * Sets an event to non-signaled state.
+   *
+   * @param eventHandle event handle
+   */
   public static native void resetEvent(int eventHandle);
 
+  /**
+   * Creates a semaphore. Semaphores keep an internal counter. Releasing the semaphore increases the
+   * count. A semaphore with a non-zero count is considered signaled. When a waiter wakes up it
+   * atomically decrements the count by 1. This is generally useful in a single-supplier,
+   * multiple-consumer scenario.
+   *
+   * @param initialCount initial value for the semaphore's internal counter
+   * @param maximumCount maximum value for the samephore's internal counter
+   * @return Semaphore handle
+   */
   public static native int createSemaphore(int initialCount, int maximumCount);
 
+  /**
+   * Destroys a semaphore. Destruction wakes up any waiters.
+   *
+   * @param semHandle semaphore handle
+   */
   public static native void destroySemaphore(int semHandle);
 
+  /**
+   * Releases N counts of a semaphore.
+   *
+   * @param semHandle semaphore handle
+   * @param releaseCount amount to add to semaphore's internal counter; must be positive
+   * @return True on successful release, false on failure (e.g. release count would exceed maximum
+   *     value, or handle invalid)
+   */
   public static native boolean releaseSemaphore(int semHandle, int releaseCount);
 
+  static native long allocateRawFrame();
+
+  static native void freeRawFrame(long frame);
+
+  static native long getRawFrameDataPtr(long frame);
+
+  static native void setRawFrameData(
+      long frame, ByteBuffer data, int size, int width, int height, int stride, int pixelFormat);
+
+  static native void setRawFrameInfo(
+      long frame, int size, int width, int height, int stride, int pixelFormat);
+
   /**
    * Waits for a handle to be signaled.
    *
@@ -124,4 +223,7 @@
    */
   public static native int[] waitForObjectsTimeout(int[] handles, double timeout)
       throws InterruptedException;
+
+  /** Utility class. */
+  protected WPIUtilJNI() {}
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java
index fab8316..4b8c211 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java
@@ -14,7 +14,10 @@
 public class CleanupPool implements AutoCloseable {
   // Use a Deque instead of a Stack, as Stack's iterators go the wrong way, and docs
   // state ArrayDeque is faster anyway.
-  private final Deque<AutoCloseable> m_closers = new ArrayDeque<AutoCloseable>();
+  private final Deque<AutoCloseable> m_closers = new ArrayDeque<>();
+
+  /** Default constructor. */
+  public CleanupPool() {}
 
   /**
    * Registers an object in the object stack for cleanup.
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java
index e2bc72e..fcbcb7e 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java
@@ -9,6 +9,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+/** Attribute for telling JVM to skip object cleanup. */
 @Target(ElementType.FIELD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface SkipCleanup {}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
index 718d460..21ac596 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log array of boolean values. */
 public class BooleanArrayLogEntry extends DataLogEntry {
+  /** The data type for boolean array values. */
   public static final String kDataType = "boolean[]";
 
+  /**
+   * Constructs a boolean array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public BooleanArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a boolean array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public BooleanArrayLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a boolean array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public BooleanArrayLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a boolean array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public BooleanArrayLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
index 503b83b..c413bfa 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log boolean values. */
 public class BooleanLogEntry extends DataLogEntry {
+  /** The data type for boolean values. */
   public static final String kDataType = "boolean";
 
+  /**
+   * Constructs a boolean log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public BooleanLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a boolean log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public BooleanLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a boolean log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public BooleanLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a boolean log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public BooleanLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
index 97c629f..f48b081 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
@@ -494,6 +494,11 @@
     DataLogJNI.appendStringArray(m_impl, entry, arr, timestamp);
   }
 
+  /**
+   * Gets the JNI implementation handle.
+   *
+   * @return data log handle.
+   */
   public long getImpl() {
     return m_impl;
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogEntry.java
index 4beaff2..8502428 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogEntry.java
@@ -6,15 +6,39 @@
 
 /** Log entry base class. */
 public class DataLogEntry {
+  /**
+   * Constructs a data log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param type Data type
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   protected DataLogEntry(DataLog log, String name, String type, String metadata, long timestamp) {
     m_log = log;
     m_entry = log.start(name, type, metadata, timestamp);
   }
 
+  /**
+   * Constructs a data log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param type Data type
+   * @param metadata metadata
+   */
   protected DataLogEntry(DataLog log, String name, String type, String metadata) {
     this(log, name, type, metadata, 0);
   }
 
+  /**
+   * Constructs a data log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param type Data type
+   */
   protected DataLogEntry(DataLog log, String name, String type) {
     this(log, name, type, "");
   }
@@ -52,6 +76,9 @@
     finish(0);
   }
 
+  /** The data log instance associated with the entry. */
   protected final DataLog m_log;
+
+  /** The data log entry index. */
   protected final int m_entry;
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
index f94a86f..c764339 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
@@ -7,35 +7,145 @@
 import edu.wpi.first.util.WPIUtilJNI;
 import java.nio.ByteBuffer;
 
+/**
+ * DataLog wpiutil JNI Functions.
+ *
+ * @see "wpiutil/DataLog.h"
+ */
 public class DataLogJNI extends WPIUtilJNI {
+  /**
+   * Create a new Data Log. The log will be initially created with a temporary filename.
+   *
+   * @param dir directory to store the log
+   * @param filename filename to use; if none provided, a random filename is generated of the form
+   *     "wpilog_{}.wpilog"
+   * @param period time between automatic flushes to disk, in seconds; this is a time/storage
+   *     tradeoff
+   * @param extraHeader extra header data
+   * @return data log implementation handle
+   */
   static native long create(String dir, String filename, double period, String extraHeader);
 
+  /**
+   * Change log filename.
+   *
+   * @param impl data log implementation handle
+   * @param filename filename
+   */
   static native void setFilename(long impl, String filename);
 
+  /**
+   * Explicitly flushes the log data to disk.
+   *
+   * @param impl data log implementation handle
+   */
   static native void flush(long impl);
 
+  /**
+   * Pauses appending of data records to the log. While paused, no data records are saved (e.g.
+   * AppendX is a no-op). Has no effect on entry starts / finishes / metadata changes.
+   *
+   * @param impl data log implementation handle
+   */
   static native void pause(long impl);
 
+  /**
+   * Resumes appending of data records to the log. If called after Stop(), opens a new file (with
+   * random name if SetFilename was not called after Stop()) and appends Start records and schema
+   * data values for all previously started entries and schemas.
+   *
+   * @param impl data log implementation handle
+   */
   static native void resume(long impl);
 
+  /**
+   * Stops appending all records to the log, and closes the log file.
+   *
+   * @param impl data log implementation handle
+   */
   static native void stop(long impl);
 
+  /**
+   * Registers a data schema. Data schemas provide information for how a certain data type string
+   * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
+   * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
+   * are saved just like normal records, with the name being generated from the provided name:
+   * "/.schema/&lt;name&gt;". Duplicate calls to this function with the same name are silently
+   * ignored.
+   *
+   * @param impl data log implementation handle
+   * @param name Name (the string passed as the data type for records using this schema)
+   * @param type Type of schema (e.g. "protobuf", "struct", etc)
+   * @param schema Schema data
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void addSchema(long impl, String name, String type, byte[] schema, long timestamp);
 
   static native void addSchemaString(
       long impl, String name, String type, String schema, long timestamp);
 
+  /**
+   * Start an entry. Duplicate names are allowed (with the same type), and result in the same index
+   * being returned (Start/Finish are reference counted). A duplicate name with a different type
+   * will result in an error message being printed to the console and 0 being returned (which will
+   * be ignored by the Append functions).
+   *
+   * @param impl data log implementation handle
+   * @param name Name
+   * @param type Data type
+   * @param metadata Initial metadata (e.g. data properties)
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   * @return Entry index
+   */
   static native int start(long impl, String name, String type, String metadata, long timestamp);
 
+  /**
+   * Finish an entry.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void finish(long impl, int entry, long timestamp);
 
+  /**
+   * Updates the metadata for an entry.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index
+   * @param metadata New metadata for the entry
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void setMetadata(long impl, int entry, String metadata, long timestamp);
 
+  /**
+   * Closes the data log implementation handle.
+   *
+   * @param impl data log implementation handle
+   */
   static native void close(long impl);
 
+  /**
+   * Appends a raw record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by WPI_DataLog_Start()
+   * @param data Byte array to record
+   * @param len Length of byte array
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendRaw(
       long impl, int entry, byte[] data, int start, int len, long timestamp);
 
+  /**
+   * Appends a raw record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by WPI_DataLog_Start()
+   * @param data ByteBuffer to record
+   * @param len Length of byte array
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static void appendRaw(long impl, int entry, ByteBuffer data, int start, int len, long timestamp) {
     if (data.isDirect()) {
       if (start < 0) {
@@ -58,23 +168,106 @@
   private static native void appendRawBuffer(
       long impl, int entry, ByteBuffer data, int start, int len, long timestamp);
 
+  /**
+   * Appends a boolean record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param value Boolean value to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendBoolean(long impl, int entry, boolean value, long timestamp);
 
+  /**
+   * Appends an integer record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param value Integer value to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendInteger(long impl, int entry, long value, long timestamp);
 
+  /**
+   * Appends a float record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param value Float value to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendFloat(long impl, int entry, float value, long timestamp);
 
+  /**
+   * Appends a double record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param value Double value to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendDouble(long impl, int entry, double value, long timestamp);
 
+  /**
+   * Appends a string record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param value String value to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendString(long impl, int entry, String value, long timestamp);
 
+  /**
+   * Appends a boolean array record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param arr Boolean array to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendBooleanArray(long impl, int entry, boolean[] value, long timestamp);
 
+  /**
+   * Appends an integer array record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param arr Integer array to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendIntegerArray(long impl, int entry, long[] value, long timestamp);
 
+  /**
+   * Appends a float array record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param arr Float array to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendFloatArray(long impl, int entry, float[] value, long timestamp);
 
+  /**
+   * Appends a double array record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param arr Double array to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendDoubleArray(long impl, int entry, double[] value, long timestamp);
 
+  /**
+   * Appends a string array record to the log.
+   *
+   * @param impl data log implementation handle
+   * @param entry Entry index, as returned by Start()
+   * @param arr String array to record
+   * @param timestamp Time stamp (may be 0 to indicate now)
+   */
   static native void appendStringArray(long impl, int entry, String[] value, long timestamp);
+
+  /** Utility class. */
+  private DataLogJNI() {}
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
index 67ef8c3..485a9c8 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log array of double values. */
 public class DoubleArrayLogEntry extends DataLogEntry {
+  /** The data type for double array values. */
   public static final String kDataType = "double[]";
 
+  /**
+   * Constructs a double array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public DoubleArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a double array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public DoubleArrayLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a double array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public DoubleArrayLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a double array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public DoubleArrayLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
index f16c27e..a089df2 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log double values. */
 public class DoubleLogEntry extends DataLogEntry {
+  /** The data type for double values. */
   public static final String kDataType = "double";
 
+  /**
+   * Constructs a double log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public DoubleLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a double log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public DoubleLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a double log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public DoubleLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a double log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public DoubleLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
index 3a0b7e0..be25970 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log array of float values. */
 public class FloatArrayLogEntry extends DataLogEntry {
+  /** The data type for float array values. */
   public static final String kDataType = "float[]";
 
+  /**
+   * Constructs a float array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public FloatArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a float array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public FloatArrayLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a float array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public FloatArrayLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a float array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public FloatArrayLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
index 28adc34..28f83cb 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log float values. */
 public class FloatLogEntry extends DataLogEntry {
+  /** The data type for float values. */
   public static final String kDataType = "float";
 
+  /**
+   * Constructs a float log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public FloatLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a float log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public FloatLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a float log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public FloatLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a float log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public FloatLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
index 2cffc8d..d2f8f0e 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log array of integer values. */
 public class IntegerArrayLogEntry extends DataLogEntry {
+  /** The data type for integer array values. */
   public static final String kDataType = "int64[]";
 
+  /**
+   * Constructs a integer array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public IntegerArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a integer array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public IntegerArrayLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a integer array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public IntegerArrayLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a integer array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public IntegerArrayLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
index 142ca5d..395a208 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log integer values. */
 public class IntegerLogEntry extends DataLogEntry {
+  /** The data type for integer values. */
   public static final String kDataType = "int64";
 
+  /**
+   * Constructs a integer log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public IntegerLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a integer log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public IntegerLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a integer log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public IntegerLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a integer log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public IntegerLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
index 1db2647..9e7fa43 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
@@ -37,7 +37,7 @@
    */
   public static <T, MessageType extends ProtoMessage<?>> ProtobufLogEntry<T> create(
       DataLog log, String name, Protobuf<T, MessageType> proto, String metadata, long timestamp) {
-    return new ProtobufLogEntry<T>(log, name, proto, metadata, timestamp);
+    return new ProtobufLogEntry<>(log, name, proto, metadata, timestamp);
   }
 
   /**
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
index 972fc03..a9e3373 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
@@ -8,28 +8,74 @@
 
 /** Log raw byte array values. */
 public class RawLogEntry extends DataLogEntry {
+  /** The data type for raw values. */
   public static final String kDataType = "raw";
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param type Data type
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public RawLogEntry(DataLog log, String name, String metadata, String type, long timestamp) {
     super(log, name, type, metadata, timestamp);
   }
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param type Data type
+   */
   public RawLogEntry(DataLog log, String name, String metadata, String type) {
     this(log, name, metadata, type, 0);
   }
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public RawLogEntry(DataLog log, String name, String metadata, long timestamp) {
     this(log, name, metadata, kDataType, timestamp);
   }
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public RawLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public RawLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a raw log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public RawLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
index 37bdeb1..f0a6dde 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
@@ -6,20 +6,49 @@
 
 /** Log array of string values. */
 public class StringArrayLogEntry extends DataLogEntry {
+  /** The data type for string array values. */
   public static final String kDataType = "string[]";
 
+  /**
+   * Constructs a string array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public StringArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
     super(log, name, kDataType, metadata, timestamp);
   }
 
+  /**
+   * Constructs a string array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public StringArrayLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a string array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public StringArrayLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a string array log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public StringArrayLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
index 0722dc0..27c8aef 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
@@ -6,28 +6,74 @@
 
 /** Log string values. */
 public class StringLogEntry extends DataLogEntry {
+  /** The data type for string values. */
   public static final String kDataType = "string";
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param type Data type
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public StringLogEntry(DataLog log, String name, String metadata, String type, long timestamp) {
     super(log, name, type, metadata, timestamp);
   }
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param type Data type
+   */
   public StringLogEntry(DataLog log, String name, String metadata, String type) {
     this(log, name, metadata, type, 0);
   }
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public StringLogEntry(DataLog log, String name, String metadata, long timestamp) {
     this(log, name, metadata, kDataType, timestamp);
   }
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param metadata metadata
+   */
   public StringLogEntry(DataLog log, String name, String metadata) {
     this(log, name, metadata, 0);
   }
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   * @param timestamp entry creation timestamp (0=now)
+   */
   public StringLogEntry(DataLog log, String name, long timestamp) {
     this(log, name, "", timestamp);
   }
 
+  /**
+   * Constructs a String log entry.
+   *
+   * @param log datalog
+   * @param name name of the entry
+   */
   public StringLogEntry(DataLog log, String name) {
     this(log, name, 0);
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
index 0f6cb2e..b3a31c9 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
@@ -35,7 +35,7 @@
    */
   public static <T> StructArrayLogEntry<T> create(
       DataLog log, String name, Struct<T> struct, String metadata, long timestamp) {
-    return new StructArrayLogEntry<T>(log, name, struct, metadata, timestamp);
+    return new StructArrayLogEntry<>(log, name, struct, metadata, timestamp);
   }
 
   /**
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
index a227c32..0d09182 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
@@ -34,7 +34,7 @@
    */
   public static <T> StructLogEntry<T> create(
       DataLog log, String name, Struct<T> struct, String metadata, long timestamp) {
-    return new StructLogEntry<T>(log, name, struct, metadata, timestamp);
+    return new StructLogEntry<>(log, name, struct, metadata, timestamp);
   }
 
   /**
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufBuffer.java b/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufBuffer.java
index af3e466..1f8fdf0 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufBuffer.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufBuffer.java
@@ -26,9 +26,17 @@
     m_proto = proto;
   }
 
+  /**
+   * Creates a ProtobufBuffer for the given Protobuf object.
+   *
+   * @param <T> The type to serialize.
+   * @param <MessageType> The Protobuf message type.
+   * @param proto The Protobuf object.
+   * @return A ProtobufBuffer for the given Protobuf object.
+   */
   public static <T, MessageType extends ProtoMessage<?>> ProtobufBuffer<T, MessageType> create(
       Protobuf<T, MessageType> proto) {
-    return new ProtobufBuffer<T, MessageType>(proto);
+    return new ProtobufBuffer<>(proto);
   }
 
   /**
@@ -61,7 +69,7 @@
     m_msg.clearQuick();
     m_proto.pack(m_msg, value);
     int size = m_msg.getSerializedSize();
-    if (size < m_buf.capacity()) {
+    if (size > m_buf.capacity()) {
       m_buf = ByteBuffer.allocateDirect(size * 2);
       m_sink.setOutput(m_buf);
     }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufSerializable.java b/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufSerializable.java
new file mode 100644
index 0000000..ac75065
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/protobuf/ProtobufSerializable.java
@@ -0,0 +1,15 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util.protobuf;
+
+import edu.wpi.first.util.WPISerializable;
+
+/**
+ * Marker interface to indicate a class is serializable using Protobuf serialization.
+ *
+ * <p>While this cannot be enforced by the interface, any class implementing this interface should
+ * provide a public final static `proto` member variable.
+ */
+public interface ProtobufSerializable extends WPISerializable {}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java
index db822ce..ee93725 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java
@@ -15,10 +15,14 @@
 import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
+/** Helper class for building Sendable dashboard representations. */
 public interface SendableBuilder extends AutoCloseable {
   /** The backend kinds used for the sendable builder. */
   enum BackendKind {
+    /** Unknown. */
     kUnknown,
+
+    /** NetworkTables. */
     kNetworkTables
   }
 
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java
index 6f0ad41..025b802 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java
@@ -506,6 +506,9 @@
 
     /** Sendable builder for the sendable. */
     public SendableBuilder builder;
+
+    /** Default constructor. */
+    public CallbackData() {}
   }
 
   // As foreachLiveWindow is single threaded, cache the components it
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/BadSchemaException.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/BadSchemaException.java
index 6ff2236..9c69e1f 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/BadSchemaException.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/BadSchemaException.java
@@ -4,34 +4,70 @@
 
 package edu.wpi.first.util.struct;
 
+/** Exception thrown when encountering a bad schema. */
 public class BadSchemaException extends Exception {
+  /** The bad schema field. */
   private final String m_field;
 
-  public BadSchemaException(String s) {
-    super(s);
+  /**
+   * Constructs a BadSchemaException.
+   *
+   * @param message the detail message.
+   */
+  public BadSchemaException(String message) {
+    super(message);
     m_field = "";
   }
 
+  /**
+   * Constructs a BadSchemaException.
+   *
+   * @param message the detail message.
+   * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+   */
   public BadSchemaException(String message, Throwable cause) {
     super(message, cause);
     m_field = "";
   }
 
+  /**
+   * Constructs a BadSchemaException.
+   *
+   * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+   */
   public BadSchemaException(Throwable cause) {
     super(cause);
     m_field = "";
   }
 
-  public BadSchemaException(String field, String s) {
-    super(s);
+  /**
+   * Constructs a BadSchemaException.
+   *
+   * @param field The bad schema field.
+   * @param message the detail message.
+   */
+  public BadSchemaException(String field, String message) {
+    super(message);
     m_field = field;
   }
 
+  /**
+   * Constructs a BadSchemaException.
+   *
+   * @param field The bad schema field.
+   * @param message the detail message.
+   * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+   */
   public BadSchemaException(String field, String message, Throwable cause) {
     super(message, cause);
     m_field = field;
   }
 
+  /**
+   * Gets the name of the bad schema field.
+   *
+   * @return The name of the bad schema field, or an empty string if not applicable.
+   */
   public String getField() {
     return m_field;
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/DynamicStruct.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/DynamicStruct.java
index 165f7db..1f9fdc3 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/DynamicStruct.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/DynamicStruct.java
@@ -593,24 +593,24 @@
       case 1:
         {
           byte val = m_data.get(field.m_offset + arrIndex);
-          val &= ~(field.getBitMask() << field.m_bitShift);
-          val |= (value & field.getBitMask()) << field.m_bitShift;
+          val &= (byte) ~(field.getBitMask() << field.m_bitShift);
+          val |= (byte) ((value & field.getBitMask()) << field.m_bitShift);
           m_data.put(field.m_offset + arrIndex, val);
           break;
         }
       case 2:
         {
           short val = m_data.getShort(field.m_offset + arrIndex * 2);
-          val &= ~(field.getBitMask() << field.m_bitShift);
-          val |= (value & field.getBitMask()) << field.m_bitShift;
+          val &= (short) ~(field.getBitMask() << field.m_bitShift);
+          val |= (short) ((value & field.getBitMask()) << field.m_bitShift);
           m_data.putShort(field.m_offset + arrIndex * 2, val);
           break;
         }
       case 4:
         {
           int val = m_data.getInt(field.m_offset + arrIndex * 4);
-          val &= ~(field.getBitMask() << field.m_bitShift);
-          val |= (value & field.getBitMask()) << field.m_bitShift;
+          val &= (int) ~(field.getBitMask() << field.m_bitShift);
+          val |= (int) ((value & field.getBitMask()) << field.m_bitShift);
           m_data.putInt(field.m_offset + arrIndex * 4, val);
           break;
         }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructBuffer.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructBuffer.java
index 0e8aa18..a5d04e0 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructBuffer.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructBuffer.java
@@ -12,7 +12,7 @@
 /**
  * Reusable buffer for serialization/deserialization to/from a raw struct.
  *
- * @param <T> object type
+ * @param <T> Object type.
  */
 public final class StructBuffer<T> {
   private StructBuffer(Struct<T> struct) {
@@ -21,8 +21,15 @@
     m_struct = struct;
   }
 
+  /**
+   * Returns a StructBuffer for the given struct.
+   *
+   * @param struct A struct.
+   * @param <T> Object type.
+   * @return A StructBuffer for the given struct.
+   */
   public static <T> StructBuffer<T> create(Struct<T> struct) {
-    return new StructBuffer<T>(struct);
+    return new StructBuffer<>(struct);
   }
 
   /**
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructDescriptorDatabase.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructDescriptorDatabase.java
index be7343f..459fa9e 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructDescriptorDatabase.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructDescriptorDatabase.java
@@ -14,6 +14,9 @@
 
 /** Database of raw struct dynamic descriptors. */
 public class StructDescriptorDatabase {
+  /** Default constructor. */
+  public StructDescriptorDatabase() {}
+
   /**
    * Adds a structure schema to the database. If the struct references other structs that have not
    * yet been added, it will not be valid until those structs are also added.
@@ -33,7 +36,7 @@
     }
 
     // turn parsed schema into descriptors
-    StructDescriptor theStruct = m_structs.computeIfAbsent(name, k -> new StructDescriptor(k));
+    StructDescriptor theStruct = m_structs.computeIfAbsent(name, StructDescriptor::new);
     theStruct.m_schema = schema;
     theStruct.m_fields.clear();
     boolean isValid = true;
@@ -76,7 +79,7 @@
 
         // cross-reference struct, creating a placeholder if necessary
         StructDescriptor aStruct =
-            m_structs.computeIfAbsent(decl.typeString, k -> new StructDescriptor(k));
+            m_structs.computeIfAbsent(decl.typeString, StructDescriptor::new);
 
         // if the struct isn't valid, we can't be valid either
         if (aStruct.isValid()) {
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructFieldType.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructFieldType.java
index 28d5d8e..4b3cf17 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructFieldType.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructFieldType.java
@@ -6,29 +6,46 @@
 
 /** Known data types for raw struct dynamic fields (see StructFieldDescriptor). */
 public enum StructFieldType {
+  /** bool. */
   kBool("bool", false, false, 1),
+  /** char. */
   kChar("char", false, false, 1),
+  /** int8. */
   kInt8("int8", true, false, 1),
+  /** int16. */
   kInt16("int16", true, false, 2),
+  /** int32. */
   kInt32("int32", true, false, 4),
+  /** int64. */
   kInt64("int64", true, false, 8),
+  /** uint8. */
   kUint8("uint8", false, true, 1),
+  /** uint16. */
   kUint16("uint16", false, true, 2),
+  /** uint32. */
   kUint32("uint32", false, true, 4),
+  /** uint64. */
   kUint64("uint64", false, true, 8),
+  /** float. */
   kFloat("float", false, false, 4),
+  /** double. */
   kDouble("double", false, false, 8),
+  /** struct. */
   kStruct("struct", false, false, 0);
 
+  /** The name of the data type. */
   @SuppressWarnings("MemberName")
   public final String name;
 
+  /** Indicates if the data type is a signed integer. */
   @SuppressWarnings("MemberName")
   public final boolean isInt;
 
+  /** Indicates if the data type is an unsigned integer. */
   @SuppressWarnings("MemberName")
   public final boolean isUint;
 
+  /** The size (in bytes) of the data type. */
   @SuppressWarnings("MemberName")
   public final int size;
 
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/StructSerializable.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructSerializable.java
new file mode 100644
index 0000000..a8d1fdd
--- /dev/null
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/StructSerializable.java
@@ -0,0 +1,15 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util.struct;
+
+import edu.wpi.first.util.WPISerializable;
+
+/**
+ * Marker interface to indicate a class is serializable using Struct serialization.
+ *
+ * <p>While this cannot be enforced by the interface, any class implementing this interface should
+ * provide a public final static `struct` member variable.
+ */
+public interface StructSerializable extends WPISerializable {}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParseException.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParseException.java
index 9fa843c..5e71c79 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParseException.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParseException.java
@@ -4,24 +4,50 @@
 
 package edu.wpi.first.util.struct.parser;
 
+/** Exception for parsing errors. */
 public class ParseException extends Exception {
+  /** The parser position. */
   private final int m_pos;
 
+  /**
+   * Constructs a ParseException.
+   *
+   * @param pos The parser position.
+   * @param s Reason for parse failure.
+   */
   public ParseException(int pos, String s) {
     super(s);
     m_pos = pos;
   }
 
+  /**
+   * Constructs a ParseException.
+   *
+   * @param pos The parser position.
+   * @param message Reason for parse failure.
+   * @param cause Exception that caused the parser failure.
+   */
   public ParseException(int pos, String message, Throwable cause) {
     super(message, cause);
     m_pos = pos;
   }
 
+  /**
+   * Constructs a ParseException.
+   *
+   * @param pos The parser position.
+   * @param cause Exception that caused the parser failure.
+   */
   public ParseException(int pos, Throwable cause) {
     super(cause);
     m_pos = pos;
   }
 
+  /**
+   * Returns position in parsed string.
+   *
+   * @return Position in parsed string.
+   */
   public int getPosition() {
     return m_pos;
   }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedDeclaration.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedDeclaration.java
index 8184ae5..29743ab 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedDeclaration.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedDeclaration.java
@@ -8,18 +8,26 @@
 
 /** Raw struct schema declaration. */
 public class ParsedDeclaration {
+  /** Type string. */
   @SuppressWarnings("MemberName")
   public String typeString;
 
+  /** Name. */
   @SuppressWarnings("MemberName")
   public String name;
 
+  /** Enum values. */
   @SuppressWarnings("MemberName")
   public Map<String, Long> enumValues;
 
+  /** Array size. */
   @SuppressWarnings("MemberName")
   public int arraySize = 1;
 
+  /** Bit width. */
   @SuppressWarnings("MemberName")
   public int bitWidth;
+
+  /** Default constructor. */
+  public ParsedDeclaration() {}
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedSchema.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedSchema.java
index 2ca1753..2f7312a 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedSchema.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/ParsedSchema.java
@@ -9,6 +9,10 @@
 
 /** Raw struct schema. */
 public class ParsedSchema {
+  /** Declarations. */
   @SuppressWarnings("MemberName")
   public List<ParsedDeclaration> declarations = new ArrayList<>();
+
+  /** Default constructor. */
+  public ParsedSchema() {}
 }
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/TokenKind.java b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/TokenKind.java
index 85afa4a..fb74c6f 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/TokenKind.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/struct/parser/TokenKind.java
@@ -6,17 +6,40 @@
 
 /** A lexed raw struct schema token. */
 public enum TokenKind {
+  /** Unknown. */
   kUnknown("unknown"),
+
+  /** Integer. */
   kInteger("integer"),
+
+  /** Identifier. */
   kIdentifier("identifier"),
+
+  /** Left square bracket. */
   kLeftBracket("'['"),
+
+  /** Right square bracket. */
   kRightBracket("']'"),
+
+  /** Left curly brace. */
   kLeftBrace("'{'"),
+
+  /** Right curly brace. */
   kRightBrace("'}'"),
+
+  /** Colon. */
   kColon("':'"),
+
+  /** Semicolon. */
   kSemicolon("';'"),
+
+  /** Comma. */
   kComma("','"),
+
+  /** Equals. */
   kEquals("'='"),
+
+  /** End of input. */
   kEndOfInput("<EOF>");
 
   private final String m_name;
diff --git a/wpiutil/src/main/native/cpp/DataLog.cpp b/wpiutil/src/main/native/cpp/DataLog.cpp
index d05a49e..40dab2a 100644
--- a/wpiutil/src/main/native/cpp/DataLog.cpp
+++ b/wpiutil/src/main/native/cpp/DataLog.cpp
@@ -179,7 +179,7 @@
 DataLog::~DataLog() {
   {
     std::scoped_lock lock{m_mutex};
-    m_state = kShutdown;
+    m_shutdown = true;
     m_doFlush = true;
   }
   m_cond.notify_all();
@@ -419,7 +419,7 @@
   uintmax_t written = 0;
 
   std::unique_lock lock{m_mutex};
-  while (m_state != kShutdown) {
+  do {
     bool doFlush = false;
     auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
     if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
@@ -557,7 +557,7 @@
       }
       toWrite.resize(0);
     }
-  }
+  } while (!m_shutdown);
 }
 
 void DataLog::WriterThreadMain(
@@ -580,7 +580,7 @@
   std::vector<Buffer> toWrite;
 
   std::unique_lock lock{m_mutex};
-  while (m_state != kShutdown) {
+  do {
     bool doFlush = false;
     auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
     if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
@@ -614,7 +614,7 @@
       }
       toWrite.resize(0);
     }
-  }
+  } while (!m_shutdown);
 
   write({});  // indicate EOF
 }
@@ -743,8 +743,10 @@
     std::memcpy(buf, data.data(), kBlockSize);
     data = data.subspan(kBlockSize);
   }
-  uint8_t* buf = Reserve(data.size());
-  std::memcpy(buf, data.data(), data.size());
+  if (!data.empty()) {
+    uint8_t* buf = Reserve(data.size());
+    std::memcpy(buf, data.data(), data.size());
+  }
 }
 
 void DataLog::AppendStringImpl(std::string_view str) {
diff --git a/wpiutil/src/main/native/cpp/RawFrame.cpp b/wpiutil/src/main/native/cpp/RawFrame.cpp
new file mode 100644
index 0000000..2bc36ed
--- /dev/null
+++ b/wpiutil/src/main/native/cpp/RawFrame.cpp
@@ -0,0 +1,49 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source 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/RawFrame.h"
+
+#include <wpi/MemAlloc.h>
+
+#include <cstring>
+
+extern "C" {
+int WPI_AllocateRawFrameData(WPI_RawFrame* frame, size_t requestedSize) {
+  if (frame->capacity >= requestedSize) {
+    return 0;
+  }
+  WPI_FreeRawFrameData(frame);
+  frame->data = static_cast<uint8_t*>(wpi::safe_malloc(requestedSize));
+  frame->capacity = requestedSize;
+  frame->size = 0;
+  return 1;
+}
+
+void WPI_FreeRawFrameData(WPI_RawFrame* frame) {
+  if (frame->data) {
+    if (frame->freeFunc) {
+      frame->freeFunc(frame->freeCbData, frame->data, frame->capacity);
+    } else {
+      std::free(frame->data);
+    }
+    frame->data = nullptr;
+    frame->freeFunc = nullptr;
+    frame->freeCbData = nullptr;
+    frame->capacity = 0;
+  }
+}
+
+void WPI_SetRawFrameData(WPI_RawFrame* frame, void* data, size_t size,
+                         size_t capacity, void* cbdata,
+                         void (*freeFunc)(void* cbdata, void* data,
+                                          size_t capacity)) {
+  WPI_FreeRawFrameData(frame);
+  frame->data = static_cast<uint8_t*>(data);
+  frame->freeFunc = freeFunc;
+  frame->freeCbData = cbdata;
+  frame->capacity = capacity;
+  frame->size = size;
+}
+
+}  // extern "C"
diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
index eb55fd0..26d1c93 100644
--- a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
+++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
@@ -9,6 +9,7 @@
 #include <fmt/format.h>
 
 #include "edu_wpi_first_util_WPIUtilJNI.h"
+#include "wpi/RawFrame.h"
 #include "wpi/Synchronization.h"
 #include "wpi/jni_util.h"
 #include "wpi/timestamp.h"
@@ -317,4 +318,95 @@
   return MakeJIntArray(env, signaled);
 }
 
+/*
+ * Class:     edu_wpi_first_util_WPIUtilJNI
+ * Method:    allocateRawFrame
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_util_WPIUtilJNI_allocateRawFrame
+  (JNIEnv*, jclass)
+{
+  return reinterpret_cast<jlong>(new wpi::RawFrame);
+}
+
+/*
+ * Class:     edu_wpi_first_util_WPIUtilJNI
+ * Method:    freeRawFrame
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_WPIUtilJNI_freeRawFrame
+  (JNIEnv*, jclass, jlong frame)
+{
+  delete reinterpret_cast<wpi::RawFrame*>(frame);
+}
+
+/*
+ * Class:     edu_wpi_first_util_WPIUtilJNI
+ * Method:    getRawFrameDataPtr
+ * Signature: (J)J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_util_WPIUtilJNI_getRawFrameDataPtr
+  (JNIEnv* env, jclass, jlong frame)
+{
+  auto* f = reinterpret_cast<wpi::RawFrame*>(frame);
+  if (!f) {
+    wpi::ThrowNullPointerException(env, "frame is null");
+    return 0;
+  }
+  return reinterpret_cast<jlong>(f->data);
+}
+
+/*
+ * Class:     edu_wpi_first_util_WPIUtilJNI
+ * Method:    setRawFrameData
+ * Signature: (JLjava/lang/Object;IIIII)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_WPIUtilJNI_setRawFrameData
+  (JNIEnv* env, jclass, jlong frame, jobject data, jint size, jint width,
+   jint height, jint stride, jint pixelFormat)
+{
+  auto* f = reinterpret_cast<wpi::RawFrame*>(frame);
+  if (!f) {
+    wpi::ThrowNullPointerException(env, "frame is null");
+    return;
+  }
+  auto buf = env->GetDirectBufferAddress(data);
+  if (!buf) {
+    wpi::ThrowNullPointerException(env, "data is null");
+    return;
+  }
+  // there's no way to free a passed-in direct byte buffer
+  f->SetData(buf, size, env->GetDirectBufferCapacity(data), nullptr,
+             [](void*, void*, size_t) {});
+  f->width = width;
+  f->height = height;
+  f->stride = stride;
+  f->pixelFormat = pixelFormat;
+}
+
+/*
+ * Class:     edu_wpi_first_util_WPIUtilJNI
+ * Method:    setRawFrameInfo
+ * Signature: (JIIIII)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_util_WPIUtilJNI_setRawFrameInfo
+  (JNIEnv* env, jclass, jlong frame, jint size, jint width, jint height,
+   jint stride, jint pixelFormat)
+{
+  auto* f = reinterpret_cast<wpi::RawFrame*>(frame);
+  if (!f) {
+    wpi::ThrowNullPointerException(env, "frame is null");
+    return;
+  }
+  f->width = width;
+  f->height = height;
+  f->stride = stride;
+  f->pixelFormat = pixelFormat;
+}
+
 }  // extern "C"
diff --git a/wpiutil/src/main/native/cpp/timestamp.cpp b/wpiutil/src/main/native/cpp/timestamp.cpp
index c7e2fa9..c811964 100644
--- a/wpiutil/src/main/native/cpp/timestamp.cpp
+++ b/wpiutil/src/main/native/cpp/timestamp.cpp
@@ -5,6 +5,7 @@
 #include "wpi/timestamp.h"
 
 #include <atomic>
+#include <optional>
 
 #ifdef __FRC_ROBORIO__
 #include <stdint.h>
@@ -12,9 +13,7 @@
 #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;
@@ -50,64 +49,107 @@
                                              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();
+using NiFpga_FindRegisterFunc = NiFpga_Status (*)(NiFpga_Session session,
+                                                  const char* registerName,
+                                                  uint32_t* registerOffset);
+using NiFpga_ReadU32Func = NiFpga_Status (*)(NiFpga_Session session,
+                                             uint32_t indicator,
+                                             uint32_t* value);
+using NiFpga_WriteU32Func = NiFpga_Status (*)(NiFpga_Session session,
+                                              uint32_t control, uint32_t value);
+static void dlcloseWrapper(void* handle) {
+  dlclose(handle);
+}
+static std::atomic_flag hmbInitialized = ATOMIC_FLAG_INIT;
+static std::atomic_flag nowUseDefaultOnFailure = ATOMIC_FLAG_INIT;
+struct HMBLowLevel {
+  ~HMBLowLevel() { Reset(); }
+  bool Configure(const NiFpga_Session session) {
     int32_t status = 0;
-    hmb.reset(fpga::tHMB::create(&status));
-    niFpga = dlopen("libNiFpga.so", RTLD_LAZY);
+    niFpga.reset(dlopen("libNiFpga.so", RTLD_LAZY));
     if (!niFpga) {
-      hmb = nullptr;
-      return;
+      fmt::print(stderr, "Could not open libNiFpga.so\n");
+      return false;
     }
     NiFpga_OpenHmbFunc openHmb = reinterpret_cast<NiFpga_OpenHmbFunc>(
-        dlsym(niFpga, "NiFpgaDll_OpenHmb"));
+        dlsym(niFpga.get(), "NiFpgaDll_OpenHmb"));
     closeHmb = reinterpret_cast<NiFpga_CloseHmbFunc>(
-        dlsym(niFpga, "NiFpgaDll_CloseHmb"));
-    if (openHmb == nullptr || closeHmb == nullptr) {
+        dlsym(niFpga.get(), "NiFpgaDll_CloseHmb"));
+    NiFpga_FindRegisterFunc findRegister =
+        reinterpret_cast<NiFpga_FindRegisterFunc>(
+            dlsym(niFpga.get(), "NiFpgaDll_FindRegister"));
+    NiFpga_ReadU32Func readU32 = reinterpret_cast<NiFpga_ReadU32Func>(
+        dlsym(niFpga.get(), "NiFpgaDll_ReadU32"));
+    NiFpga_WriteU32Func writeU32 = reinterpret_cast<NiFpga_WriteU32Func>(
+        dlsym(niFpga.get(), "NiFpgaDll_WriteU32"));
+    if (openHmb == nullptr || closeHmb == nullptr || findRegister == nullptr ||
+        writeU32 == nullptr || readU32 == nullptr) {
+      fmt::print(stderr, "Could not find HMB symbols in libNiFpga.so\n");
+      niFpga = nullptr;
+      return false;
+    }
+    uint32_t hmbConfigRegister = 0;
+    status = findRegister(session, "HMB.Config", &hmbConfigRegister);
+    if (status != 0) {
+      fmt::print(stderr, "Failed to find HMB.Config register, status code {}\n",
+                 status);
       closeHmb = nullptr;
-      dlclose(niFpga);
-      hmb = nullptr;
-      return;
+      niFpga = nullptr;
+      return false;
     }
     size_t hmbBufferSize = 0;
     status =
-        openHmb(hmb->getSystemInterface()->getHandle(), hmbName, &hmbBufferSize,
+        openHmb(session, hmbName, &hmbBufferSize,
                 reinterpret_cast<void**>(const_cast<uint32_t**>(&hmbBuffer)));
     if (status != 0) {
+      fmt::print(stderr, "Failed to open HMB, status code {}\n", status);
       closeHmb = nullptr;
-      dlclose(niFpga);
-      hmb = nullptr;
-      return;
+      niFpga = nullptr;
+      return false;
     }
-    auto cfg = hmb->readConfig(&status);
+    fpga::tHMB::tConfig cfg;
+    uint32_t read = 0;
+    status = readU32(session, hmbConfigRegister, &read);
+    cfg.value = read;
     cfg.Enables_Timestamp = 1;
-    hmb->writeConfig(cfg, &status);
+    status = writeU32(session, hmbConfigRegister, cfg.value);
+    hmbSession.emplace(session);
+    hmbInitialized.test_and_set();
+    return true;
   }
   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);
+    hmbInitialized.clear();
+    std::optional<NiFpga_Session> oldSesh;
+    hmbSession.swap(oldSesh);
+    if (oldSesh.has_value()) {
+      closeHmb(oldSesh.value(), hmbName);
       niFpga = nullptr;
     }
   }
-  std::unique_ptr<fpga::tHMB> hmb;
-  void* niFpga = nullptr;
+  std::optional<NiFpga_Session> hmbSession;
   NiFpga_CloseHmbFunc closeHmb = nullptr;
   volatile uint32_t* hmbBuffer = nullptr;
+  std::unique_ptr<void, decltype(&dlcloseWrapper)> niFpga{nullptr,
+                                                          dlcloseWrapper};
+};
+struct HMBHolder {
+  void Configure(void* col, std::unique_ptr<fpga::tHMB> hmbObject) {
+    hmb = std::move(hmbObject);
+    chipObjectLibrary.reset(col);
+    if (!lowLevel.Configure(hmb->getSystemInterface()->getHandle())) {
+      hmb = nullptr;
+      chipObjectLibrary = nullptr;
+    }
+  }
+  void Reset() {
+    lowLevel.Reset();
+    hmb = nullptr;
+    chipObjectLibrary = nullptr;
+  }
+  HMBLowLevel lowLevel;
+  std::unique_ptr<fpga::tHMB> hmb;
+  std::unique_ptr<void, decltype(&dlcloseWrapper)> chipObjectLibrary{
+      nullptr, dlcloseWrapper};
 };
 static HMBHolder hmb;
 }  // namespace
@@ -186,10 +228,26 @@
 
 static std::atomic<uint64_t (*)()> now_impl{wpi::NowDefault};
 
-void wpi::impl::SetupNowRio() {
+void wpi::impl::SetupNowDefaultOnRio() {
 #ifdef __FRC_ROBORIO__
-  if (!hmb) {
-    hmb.Configure();
+  nowUseDefaultOnFailure.test_and_set();
+#endif
+}
+
+#ifdef __FRC_ROBORIO__
+template <>
+void wpi::impl::SetupNowRio(void* chipObjectLibrary,
+                            std::unique_ptr<fpga::tHMB> hmbObject) {
+  if (!hmbInitialized.test()) {
+    hmb.Configure(chipObjectLibrary, std::move(hmbObject));
+  }
+}
+#endif
+
+void wpi::impl::SetupNowRio(uint32_t session) {
+#ifdef __FRC_ROBORIO__
+  if (!hmbInitialized.test()) {
+    hmb.lowLevel.Configure(session);
   }
 #endif
 }
@@ -207,25 +265,29 @@
 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;
+  if (!hmbInitialized.test()) {
+    if (nowUseDefaultOnFailure.test()) {
+      return timestamp() - offset_val;
+    } else {
+      fmt::print(
+          stderr,
+          "FPGA not yet configured in wpi::Now(). Time will not be correct.\n");
+      std::fflush(stderr);
+      return 1;
+    }
   }
 
   asm("dmb");
-  uint64_t upper1 = hmb.hmbBuffer[timestampUpperOffset];
+  uint64_t upper1 = hmb.lowLevel.hmbBuffer[timestampUpperOffset];
   asm("dmb");
-  uint32_t lower = hmb.hmbBuffer[timestampLowerOffset];
+  uint32_t lower = hmb.lowLevel.hmbBuffer[timestampLowerOffset];
   asm("dmb");
-  uint64_t upper2 = hmb.hmbBuffer[timestampUpperOffset];
+  uint64_t upper2 = hmb.lowLevel.hmbBuffer[timestampUpperOffset];
 
   if (upper1 != upper2) {
     // Rolled over between the lower call, reread lower
     asm("dmb");
-    lower = hmb.hmbBuffer[timestampLowerOffset];
+    lower = hmb.lowLevel.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
@@ -244,8 +306,12 @@
 
 extern "C" {
 
-void WPI_Impl_SetupNowRio(void) {
-  return wpi::impl::SetupNowRio();
+void WPI_Impl_SetupNowUseDefaultOnRio(void) {
+  return wpi::impl::SetupNowDefaultOnRio();
+}
+
+void WPI_Impl_SetupNowRioWithSession(uint32_t session) {
+  return wpi::impl::SetupNowRio(session);
 }
 
 void WPI_Impl_ShutdownNowRio(void) {
diff --git a/wpiutil/src/main/native/include/wpi/Algorithm.h b/wpiutil/src/main/native/include/wpi/Algorithm.h
index 1fd2502..112bfc3 100644
--- a/wpiutil/src/main/native/include/wpi/Algorithm.h
+++ b/wpiutil/src/main/native/include/wpi/Algorithm.h
@@ -5,6 +5,8 @@
 #pragma once
 
 #include <algorithm>
+#include <cstddef>
+#include <utility>
 #include <vector>
 
 namespace wpi {
@@ -15,4 +17,19 @@
                                                 T const& item) {
   return vec.insert(std::upper_bound(vec.begin(), vec.end(), item), item);
 }
+
+/**
+ * Calls f(i, elem) for each element of elems where i is the index of the
+ * element in elems and elem is the element.
+ *
+ * @param f The callback.
+ * @param elems The elements.
+ */
+template <typename F, typename... Ts>
+constexpr void for_each(F&& f, Ts&&... elems) {
+  [&]<size_t... Is>(std::index_sequence<Is...>) {
+    (f(Is, elems), ...);
+  }(std::index_sequence_for<Ts...>{});
+}
+
 }  // namespace wpi
diff --git a/wpiutil/src/main/native/include/wpi/DataLog.h b/wpiutil/src/main/native/include/wpi/DataLog.h
index 99db964..2f79a3a 100644
--- a/wpiutil/src/main/native/include/wpi/DataLog.h
+++ b/wpiutil/src/main/native/include/wpi/DataLog.h
@@ -16,6 +16,7 @@
 #include <string>
 #include <string_view>
 #include <thread>
+#include <tuple>
 #include <utility>
 #include <vector>
 #include <version>
@@ -263,16 +264,20 @@
    * name are silently ignored.
    *
    * @tparam T struct serializable type
+   * @param info optional struct type info
    * @param timestamp Time stamp (0 to indicate now)
    */
-  template <StructSerializable T>
-  void AddStructSchema(int64_t timestamp = 0) {
+  template <typename T, typename... I>
+    requires StructSerializable<T, I...>
+  void AddStructSchema(const I&... info, int64_t timestamp = 0) {
     if (timestamp == 0) {
       timestamp = Now();
     }
-    ForEachStructSchema<T>([this, timestamp](auto typeString, auto schema) {
-      AddSchema(typeString, "structschema", schema, timestamp);
-    });
+    ForEachStructSchema<T>(
+        [this, timestamp](auto typeString, auto schema) {
+          this->AddSchema(typeString, "structschema", schema, timestamp);
+        },
+        info...);
   }
 
   /**
@@ -486,12 +491,12 @@
   mutable wpi::mutex m_mutex;
   wpi::condition_variable m_cond;
   bool m_doFlush{false};
+  bool m_shutdown{false};
   enum State {
     kStart,
     kActive,
     kPaused,
     kStopped,
-    kShutdown,
   } m_state = kActive;
   double m_period;
   std::string m_extraHeader;
@@ -946,19 +951,22 @@
 /**
  * Log raw struct serializable objects.
  */
-template <StructSerializable T>
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
 class StructLogEntry : public DataLogEntry {
-  using S = Struct<T>;
+  using S = Struct<T, I...>;
 
  public:
   StructLogEntry() = default;
-  StructLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
-      : StructLogEntry{log, name, {}, timestamp} {}
+  StructLogEntry(DataLog& log, std::string_view name, I... info,
+                 int64_t timestamp = 0)
+      : StructLogEntry{log, name, {}, std::move(info)..., timestamp} {}
   StructLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
-                 int64_t timestamp = 0) {
+                 I... info, int64_t timestamp = 0)
+      : m_info{std::move(info)...} {
     m_log = &log;
-    log.AddStructSchema<T>(timestamp);
-    m_entry = log.Start(name, S::kTypeString, metadata, timestamp);
+    log.AddStructSchema<T, I...>(info..., timestamp);
+    m_entry = log.Start(name, S::GetTypeString(info...), metadata, timestamp);
   }
 
   /**
@@ -968,31 +976,46 @@
    * @param timestamp Time stamp (may be 0 to indicate now)
    */
   void Append(const T& data, int64_t timestamp = 0) {
-    uint8_t buf[S::kSize];
-    S::Pack(buf, data);
+    if constexpr (sizeof...(I) == 0) {
+      if constexpr (wpi::is_constexpr([] { S::GetSize(); })) {
+        uint8_t buf[S::GetSize()];
+        S::Pack(buf, data);
+        m_log->AppendRaw(m_entry, buf, timestamp);
+        return;
+      }
+    }
+    wpi::SmallVector<uint8_t, 128> buf;
+    buf.resize_for_overwrite(std::apply(S::GetSize, m_info));
+    std::apply([&](const I&... info) { S::Pack(buf, data, info...); }, m_info);
     m_log->AppendRaw(m_entry, buf, timestamp);
   }
+
+ private:
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
  * Log raw struct serializable array of objects.
  */
-template <StructSerializable T>
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
 class StructArrayLogEntry : public DataLogEntry {
-  using S = Struct<T>;
+  using S = Struct<T, I...>;
 
  public:
   StructArrayLogEntry() = default;
-  StructArrayLogEntry(DataLog& log, std::string_view name,
+  StructArrayLogEntry(DataLog& log, std::string_view name, I... info,
                       int64_t timestamp = 0)
-      : StructArrayLogEntry{log, name, {}, timestamp} {}
+      : StructArrayLogEntry{log, name, {}, std::move(info)..., timestamp} {}
   StructArrayLogEntry(DataLog& log, std::string_view name,
-                      std::string_view metadata, int64_t timestamp = 0) {
+                      std::string_view metadata, I... info,
+                      int64_t timestamp = 0)
+      : m_info{std::move(info)...} {
     m_log = &log;
-    log.AddStructSchema<T>(timestamp);
-    m_entry =
-        log.Start(name, MakeStructArrayTypeString<T, std::dynamic_extent>(),
-                  metadata, timestamp);
+    log.AddStructSchema<T, I...>(info..., timestamp);
+    m_entry = log.Start(
+        name, MakeStructArrayTypeString<T, std::dynamic_extent>(info...),
+        metadata, timestamp);
   }
 
   /**
@@ -1007,9 +1030,14 @@
              std::convertible_to<std::ranges::range_value_t<U>, T>
 #endif
   void Append(U&& data, int64_t timestamp = 0) {
-    m_buf.Write(std::forward<U>(data), [&](auto bytes) {
-      m_log->AppendRaw(m_entry, bytes, timestamp);
-    });
+    std::apply(
+        [&](const I&... info) {
+          m_buf.Write(
+              std::forward<U>(data),
+              [&](auto bytes) { m_log->AppendRaw(m_entry, bytes, timestamp); },
+              info...);
+        },
+        m_info);
   }
 
   /**
@@ -1019,12 +1047,19 @@
    * @param timestamp Time stamp (may be 0 to indicate now)
    */
   void Append(std::span<const T> data, int64_t timestamp = 0) {
-    m_buf.Write(
-        data, [&](auto bytes) { m_log->AppendRaw(m_entry, bytes, timestamp); });
+    std::apply(
+        [&](const I&... info) {
+          m_buf.Write(
+              data,
+              [&](auto bytes) { m_log->AppendRaw(m_entry, bytes, timestamp); },
+              info...);
+        },
+        m_info);
   }
 
  private:
-  StructArrayBuffer<T> m_buf;
+  StructArrayBuffer<T, I...> m_buf;
+  [[no_unique_address]] std::tuple<I...> m_info;
 };
 
 /**
diff --git a/wpiutil/src/main/native/include/wpi/DataLogReader.h b/wpiutil/src/main/native/include/wpi/DataLogReader.h
index b1153e4..cb9a8cb 100644
--- a/wpiutil/src/main/native/include/wpi/DataLogReader.h
+++ b/wpiutil/src/main/native/include/wpi/DataLogReader.h
@@ -288,7 +288,7 @@
 
   pointer operator->() const { return &this->operator*(); }
 
- private:
+ protected:
   const DataLogReader* m_reader;
   size_t m_pos;
   mutable bool m_valid = false;
diff --git a/wpiutil/src/main/native/include/wpi/Demangle.h b/wpiutil/src/main/native/include/wpi/Demangle.h
index 03a7d3f..8514be3 100644
--- a/wpiutil/src/main/native/include/wpi/Demangle.h
+++ b/wpiutil/src/main/native/include/wpi/Demangle.h
@@ -7,6 +7,7 @@
 
 #include <string>
 #include <string_view>
+#include <typeinfo>
 
 namespace wpi {
 
@@ -18,6 +19,15 @@
  */
 std::string Demangle(std::string_view mangledSymbol);
 
+/**
+ * Returns the type name of an object
+ * @param type The object
+ */
+template <typename T>
+std::string GetTypeName(const T& type) {
+  return Demangle(typeid(type).name());
+}
+
 }  // namespace wpi
 
 #endif  // WPIUTIL_WPI_DEMANGLE_H_
diff --git a/wpiutil/src/main/native/include/wpi/RawFrame.h b/wpiutil/src/main/native/include/wpi/RawFrame.h
new file mode 100644
index 0000000..1345ff1
--- /dev/null
+++ b/wpiutil/src/main/native/include/wpi/RawFrame.h
@@ -0,0 +1,142 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#ifndef WPIUTIL_WPI_RAWFRAME_H_
+#define WPIUTIL_WPI_RAWFRAME_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+#include <concepts>
+#include <cstddef>
+#else
+
+#include <stddef.h>  // NOLINT
+
+#endif
+
+#ifdef WPI_RAWFRAME_JNI
+#include "jni_util.h"
+#endif
+
+// NOLINT
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Raw Frame
+ */
+typedef struct WPI_RawFrame {  // NOLINT
+  // image data
+  uint8_t* data;
+  // function to free image data (may be NULL)
+  void (*freeFunc)(void* cbdata, void* data, size_t capacity);
+  void* freeCbData;  // data passed to freeFunc
+  size_t capacity;   // data buffer capacity, in bytes
+  size_t size;       // actual size of data, in bytes
+  int pixelFormat;   // WPI_PixelFormat
+  int width;         // width of image, in pixels
+  int height;        // height of image, in pixels
+  int stride;        // size of each row of data, in bytes (may be 0)
+} WPI_RawFrame;
+
+/**
+ * Pixel formats
+ */
+enum WPI_PixelFormat {
+  WPI_PIXFMT_UNKNOWN = 0,  // unknown
+  WPI_PIXFMT_MJPEG,        // Motion-JPEG (compressed image data)
+  WPI_PIXFMT_YUYV,         // YUV 4:2:2, 16 bpp
+  WPI_PIXFMT_RGB565,       // RGB 5-6-5, 16 bpp
+  WPI_PIXFMT_BGR,          // BGR 8-8-8, 24 bpp
+  WPI_PIXFMT_GRAY,         // Grayscale, 8 bpp
+  WPI_PIXFMT_Y16,          // Grayscale, 16 bpp
+  WPI_PIXFMT_UYVY,         // YUV 4:2:2, 16 bpp
+};
+
+// Returns nonzero if the frame data was allocated/reallocated
+int WPI_AllocateRawFrameData(WPI_RawFrame* frame, size_t requestedSize);
+void WPI_FreeRawFrameData(WPI_RawFrame* frame);
+void WPI_SetRawFrameData(WPI_RawFrame* frame, void* data, size_t size,
+                         size_t capacity, void* cbdata,
+                         void (*freeFunc)(void* cbdata, void* data,
+                                          size_t capacity));
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#ifdef __cplusplus
+namespace wpi {
+struct RawFrame : public WPI_RawFrame {
+  RawFrame() {
+    data = nullptr;
+    freeFunc = nullptr;
+    freeCbData = nullptr;
+    capacity = 0;
+    size = 0;
+    pixelFormat = WPI_PIXFMT_UNKNOWN;
+    width = 0;
+    height = 0;
+  }
+  RawFrame(const RawFrame&) = delete;
+  RawFrame& operator=(const RawFrame&) = delete;
+  RawFrame(RawFrame&& rhs) noexcept : WPI_RawFrame{rhs} {
+    rhs.data = nullptr;
+    rhs.freeFunc = nullptr;
+    rhs.freeCbData = nullptr;
+    rhs.capacity = 0;
+    rhs.size = 0;
+  }
+  RawFrame& operator=(RawFrame&& rhs) noexcept {
+    *static_cast<WPI_RawFrame*>(this) = rhs;
+    rhs.data = nullptr;
+    rhs.freeFunc = nullptr;
+    rhs.freeCbData = nullptr;
+    rhs.capacity = 0;
+    rhs.size = 0;
+    return *this;
+  }
+
+  void SetData(void* data, size_t size, size_t capacity, void* cbdata,
+               void (*freeFunc)(void* cbdata, void* data, size_t capacity)) {
+    WPI_SetRawFrameData(this, data, size, capacity, cbdata, freeFunc);
+  }
+
+  // returns true if the frame data was allocated/reallocated
+  bool Reserve(size_t size) {
+    return WPI_AllocateRawFrameData(this, size) != 0;
+  }
+
+  ~RawFrame() { WPI_FreeRawFrameData(this); }
+};
+
+#ifdef WPI_RAWFRAME_JNI
+template <std::same_as<wpi::RawFrame> T>
+void SetFrameData(JNIEnv* env, jclass rawFrameCls, jobject jframe,
+                  const T& frame, bool newData) {
+  if (newData) {
+    static jmethodID setData = env->GetMethodID(rawFrameCls, "setDataJNI",
+                                                "(Ljava/nio/ByteBuffer;IIII)V");
+    env->CallVoidMethod(
+        jframe, setData, env->NewDirectByteBuffer(frame.data, frame.size),
+        static_cast<jint>(frame.width), static_cast<jint>(frame.height),
+        static_cast<jint>(frame.stride), static_cast<jint>(frame.pixelFormat));
+  } else {
+    static jmethodID setInfo =
+        env->GetMethodID(rawFrameCls, "setInfoJNI", "(IIII)V");
+    env->CallVoidMethod(jframe, setInfo, static_cast<jint>(frame.width),
+                        static_cast<jint>(frame.height),
+                        static_cast<jint>(frame.stride),
+                        static_cast<jint>(frame.pixelFormat));
+  }
+}
+#endif
+
+}  // namespace wpi
+#endif
+
+#endif  // WPIUTIL_WPI_RAWFRAME_H_
diff --git a/wpiutil/src/main/native/include/wpi/circular_buffer.h b/wpiutil/src/main/native/include/wpi/circular_buffer.h
index c54e2f5..a40a627 100644
--- a/wpiutil/src/main/native/include/wpi/circular_buffer.h
+++ b/wpiutil/src/main/native/include/wpi/circular_buffer.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <cstddef>
+#include <iterator>
 #include <vector>
 
 namespace wpi {
@@ -12,10 +13,17 @@
 /**
  * This is a simple circular buffer so we don't need to "bucket brigade" copy
  * old values.
+ *
+ * @tparam T Buffer element type.
  */
 template <class T>
 class circular_buffer {
  public:
+  /**
+   * Constructs a circular buffer.
+   *
+   * @param size Maximum number of buffer elements.
+   */
   explicit circular_buffer(size_t size) : m_data(size, T{}) {}
 
   circular_buffer(const circular_buffer&) = default;
@@ -32,7 +40,7 @@
     using reference = T&;
 
     iterator(circular_buffer* buffer, size_t index)
-        : m_buffer(buffer), m_index(index) {}
+        : m_buffer{buffer}, m_index{index} {}
 
     iterator& operator++() {
       ++m_index;
@@ -60,7 +68,7 @@
     using const_reference = const T&;
 
     const_iterator(const circular_buffer* buffer, size_t index)
-        : m_buffer(buffer), m_index(index) {}
+        : m_buffer{buffer}, m_index{index} {}
 
     const_iterator& operator++() {
       ++m_index;
@@ -268,16 +276,18 @@
   size_t m_length = 0;
 
   /**
-   * Increment an index modulo the length of the buffer.
+   * Increment an index modulo the size of the buffer.
    *
-   * @return The result of the modulo operation.
+   * @param index Index into the buffer.
+   * @return The incremented index.
    */
   size_t ModuloInc(size_t index) { return (index + 1) % m_data.size(); }
 
   /**
-   * Decrement an index modulo the length of the buffer.
+   * Decrement an index modulo the size of the buffer.
    *
-   * @return The result of the modulo operation.
+   * @param index Index into the buffer.
+   * @return The decremented index.
    */
   size_t ModuloDec(size_t index) {
     if (index == 0) {
diff --git a/wpiutil/src/main/native/include/wpi/ct_string.h b/wpiutil/src/main/native/include/wpi/ct_string.h
index 9f0ef90..2c7796b 100644
--- a/wpiutil/src/main/native/include/wpi/ct_string.h
+++ b/wpiutil/src/main/native/include/wpi/ct_string.h
@@ -31,7 +31,7 @@
 
   template <size_t M>
     requires(M <= (N + 1))
-  consteval ct_string(Char const (&s)[M]) {  // NOLINT
+  constexpr ct_string(Char const (&s)[M]) {  // NOLINT
     if constexpr (M == (N + 1)) {
       if (s[N] != Char{}) {
         throw std::logic_error{"char array not null terminated"};
@@ -50,7 +50,7 @@
     }
   }
 
-  explicit consteval ct_string(std::basic_string_view<Char, Traits> s) {
+  explicit constexpr ct_string(std::basic_string_view<Char, Traits> s) {
     // avoid dependency on <algorithm>
     // auto p = std::ranges::copy(s, chars.begin()).out;
     auto p = chars.begin();
@@ -63,6 +63,64 @@
     }
   }
 
+  constexpr bool operator==(const ct_string<Char, Traits, N>&) const = default;
+
+  constexpr bool operator==(const std::basic_string<Char, Traits>& rhs) const {
+    if (size() != rhs.size()) {
+      return false;
+    }
+
+    for (size_t i = 0; i < size(); ++i) {
+      if (chars[i] != rhs[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  constexpr bool operator==(std::basic_string_view<Char, Traits> rhs) const {
+    if (size() != rhs.size()) {
+      return false;
+    }
+
+    for (size_t i = 0; i < size(); ++i) {
+      if (chars[i] != rhs[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  template <size_t M>
+    requires(N + 1 == M)
+  constexpr bool operator==(Char const (&rhs)[M]) const {
+    for (size_t i = 0; i < M; ++i) {
+      if (chars[i] != rhs[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  constexpr bool operator==(const Char* rhs) const {
+    for (size_t i = 0; i < N + 1; ++i) {
+      if (chars[i] != rhs[i]) {
+        return false;
+      }
+
+      // If index of rhs's null terminator is less than lhs's size - 1, rhs is
+      // shorter than lhs
+      if (rhs[i] == '\0' && i < N) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
   constexpr auto size() const noexcept { return N; }
 
   constexpr auto begin() const noexcept { return chars.begin(); }
@@ -71,6 +129,11 @@
   constexpr auto data() const noexcept { return chars.data(); }
   constexpr auto c_str() const noexcept { return chars.data(); }
 
+  constexpr operator std::basic_string<Char, Traits>()  // NOLINT
+      const noexcept {
+    return std::basic_string<Char, Traits>{chars.data(), N};
+  }
+
   constexpr operator std::basic_string_view<Char, Traits>()  // NOLINT
       const noexcept {
     return std::basic_string_view<Char, Traits>{chars.data(), N};
@@ -82,13 +145,13 @@
 
 inline namespace literals {
 template <ct_string S>
-consteval auto operator""_ct_string() {
+constexpr auto operator""_ct_string() {
   return S;
 }
 }  // namespace literals
 
 template <typename Char, typename Traits, size_t N1, size_t N2>
-consteval auto operator+(ct_string<Char, Traits, N1> const& s1,
+constexpr auto operator+(ct_string<Char, Traits, N1> const& s1,
                          ct_string<Char, Traits, N2> const& s2) noexcept {
   return Concat(s1, s2);
 }
@@ -102,7 +165,7 @@
  * @return concatenated string
  */
 template <typename Char, typename Traits, size_t N1, size_t... N>
-consteval auto Concat(ct_string<Char, Traits, N1> const& s1,
+constexpr auto Concat(ct_string<Char, Traits, N1> const& s1,
                       ct_string<Char, Traits, N> const&... s) {
   // Need a dummy array to instantiate a ct_string.
   constexpr Char dummy[1] = {};
@@ -136,7 +199,7 @@
 template <intmax_t N, int Base = 10, typename Char = char,
           typename Traits = std::char_traits<Char>>
   requires(Base >= 2 && Base <= 36)
-consteval auto NumToCtString() {
+constexpr auto NumToCtString() {
   constexpr char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 
   auto buflen = [] {
diff --git a/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h b/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h
index 4115420..1913d7c 100644
--- a/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h
+++ b/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h
@@ -15,12 +15,20 @@
 
 namespace wpi {
 
+/**
+ * Helper class for building Sendable dashboard representations.
+ */
 class SendableBuilder {
  public:
   /**
    * The backend kinds used for the sendable builder.
    */
-  enum BackendKind { kUnknown, kNetworkTables };
+  enum BackendKind {
+    /// Unknown.
+    kUnknown,
+    /// NetworkTables.
+    kNetworkTables
+  };
 
   virtual ~SendableBuilder() = default;
 
diff --git a/wpiutil/src/main/native/include/wpi/static_circular_buffer.h b/wpiutil/src/main/native/include/wpi/static_circular_buffer.h
index 76498a1..0d38f25 100644
--- a/wpiutil/src/main/native/include/wpi/static_circular_buffer.h
+++ b/wpiutil/src/main/native/include/wpi/static_circular_buffer.h
@@ -6,12 +6,16 @@
 
 #include <array>
 #include <cstddef>
+#include <iterator>
 
 namespace wpi {
 
 /**
  * This is a simple circular buffer so we don't need to "bucket brigade" copy
  * old values.
+ *
+ * @tparam T Buffer element type.
+ * @tparam N Maximum number of buffer elements.
  */
 template <class T, size_t N>
 class static_circular_buffer {
@@ -27,7 +31,7 @@
     using reference = T&;
 
     iterator(static_circular_buffer* buffer, size_t index)
-        : m_buffer(buffer), m_index(index) {}
+        : m_buffer{buffer}, m_index{index} {}
 
     iterator& operator++() {
       ++m_index;
@@ -55,7 +59,7 @@
     using const_reference = const T&;
 
     const_iterator(const static_circular_buffer* buffer, size_t index)
-        : m_buffer(buffer), m_index(index) {}
+        : m_buffer{buffer}, m_index{index} {}
 
     const_iterator& operator++() {
       ++m_index;
diff --git a/wpiutil/src/main/native/include/wpi/struct/DynamicStruct.h b/wpiutil/src/main/native/include/wpi/struct/DynamicStruct.h
index b88894c..4254090 100644
--- a/wpiutil/src/main/native/include/wpi/struct/DynamicStruct.h
+++ b/wpiutil/src/main/native/include/wpi/struct/DynamicStruct.h
@@ -32,18 +32,31 @@
  * Known data types for raw struct dynamic fields (see StructFieldDescriptor).
  */
 enum class StructFieldType {
+  /// bool.
   kBool,
+  /// char.
   kChar,
+  /// int8.
   kInt8,
+  /// int16.
   kInt16,
+  /// int32.
   kInt32,
+  /// int64.
   kInt64,
+  /// uint8.
   kUint8,
+  /// uint16.
   kUint16,
+  /// uint32.
   kUint32,
+  /// uint64.
   kUint64,
+  /// float.
   kFloat,
+  /// double.
   kDouble,
+  /// struct.
   kStruct
 };
 
diff --git a/wpiutil/src/main/native/include/wpi/struct/SchemaParser.h b/wpiutil/src/main/native/include/wpi/struct/SchemaParser.h
index a8cf1a4..4e5a1df 100644
--- a/wpiutil/src/main/native/include/wpi/struct/SchemaParser.h
+++ b/wpiutil/src/main/native/include/wpi/struct/SchemaParser.h
@@ -17,18 +17,31 @@
  * A lexed raw struct schema token.
  */
 struct Token {
+  /** A lexed raw struct schema token kind. */
   enum Kind {
+    /// Unknown.
     kUnknown,
+    /// Integer.
     kInteger,
+    /// Identifier.
     kIdentifier,
+    /// Left square bracket.
     kLeftBracket,
+    /// Right square bracket.
     kRightBracket,
+    /// Left curly brace.
     kLeftBrace,
+    /// Right curly brace.
     kRightBrace,
+    /// Colon.
     kColon,
+    /// Semicolon.
     kSemicolon,
+    /// Comma.
     kComma,
+    /// Equals.
     kEquals,
+    /// End of input.
     kEndOfInput,
   };
 
diff --git a/wpiutil/src/main/native/include/wpi/struct/Struct.h b/wpiutil/src/main/native/include/wpi/struct/Struct.h
index 1336707..085b50b 100644
--- a/wpiutil/src/main/native/include/wpi/struct/Struct.h
+++ b/wpiutil/src/main/native/include/wpi/struct/Struct.h
@@ -7,6 +7,7 @@
 #include <stdint.h>
 
 #include <concepts>
+#include <memory>
 #include <span>
 #include <string>
 #include <string_view>
@@ -14,12 +15,14 @@
 #include <utility>
 #include <vector>
 
+#include <fmt/format.h>
+
 #include "wpi/Endian.h"
-#include "wpi/MathExtras.h"
 #include "wpi/bit.h"
 #include "wpi/ct_string.h"
 #include "wpi/function_ref.h"
 #include "wpi/mutex.h"
+#include "wpi/type_traits.h"
 
 namespace wpi {
 
@@ -29,8 +32,9 @@
  * StructSerializable concept.
  *
  * @tparam T type to serialize/deserialize
+ * @tparam I optional struct type info
  */
-template <typename T>
+template <typename T, typename... I>
 struct Struct {};
 
 /**
@@ -45,37 +49,47 @@
  * Implementations must define a template specialization for wpi::Struct with
  * T being the type that is being serialized/deserialized, with the following
  * static members (as enforced by this concept):
- * - std::string_view kTypeString: the type string
- * - size_t kSize: the structure size in bytes
- * - std::string_view kSchema: the struct schema
- * - T Unpack(std::span<const uint8_t, kSize>): function for deserialization
- * - void Pack(std::span<uint8_t, kSize>, T&& value): function for
+ * - std::string_view GetTypeString(): function that returns the type string
+ * - size_t GetSize(): function that returns the structure size in bytes
+ * - std::string_view GetSchema(): function that returns the struct schema
+ * - T Unpack(std::span<const uint8_t>): function for deserialization
+ * - void Pack(std::span<uint8_t>, T&& value): function for
  *   serialization
  *
+ * If possible, the GetTypeString(), GetSize(), and GetSchema() functions should
+ * be marked constexpr. GetTypeString() and GetSchema() may return types other
+ * than std::string_view, as long as the return value is convertible to
+ * std::string_view.
+ *
  * If the struct has nested structs, implementations should also meet the
  * requirements of HasNestedStruct<T>.
  */
-template <typename T>
-concept StructSerializable =
-    requires(std::span<const uint8_t> in, std::span<uint8_t> out, T&& value) {
-      typename Struct<typename std::remove_cvref_t<T>>;
-      {
-        Struct<typename std::remove_cvref_t<T>>::kTypeString
-      } -> std::convertible_to<std::string_view>;
-      {
-        Struct<typename std::remove_cvref_t<T>>::kSize
-      } -> std::convertible_to<size_t>;
-      {
-        Struct<typename std::remove_cvref_t<T>>::kSchema
-      } -> std::convertible_to<std::string_view>;
-      {
-        Struct<typename std::remove_cvref_t<T>>::Unpack(
-            in.subspan<0, Struct<typename std::remove_cvref_t<T>>::kSize>())
-      } -> std::same_as<typename std::remove_cvref_t<T>>;
-      Struct<typename std::remove_cvref_t<T>>::Pack(
-          out.subspan<0, Struct<typename std::remove_cvref_t<T>>::kSize>(),
-          std::forward<T>(value));
-    };
+template <typename T, typename... I>
+concept StructSerializable = requires(std::span<const uint8_t> in,
+                                      std::span<uint8_t> out, T&& value,
+                                      const I&... info) {
+  typename Struct<typename std::remove_cvref_t<T>,
+                  typename std::remove_cvref_t<I>...>;
+  {
+    Struct<typename std::remove_cvref_t<T>,
+           typename std::remove_cvref_t<I>...>::GetTypeString(info...)
+  } -> std::convertible_to<std::string_view>;
+  {
+    Struct<typename std::remove_cvref_t<T>,
+           typename std::remove_cvref_t<I>...>::GetSize(info...)
+  } -> std::convertible_to<size_t>;
+  {
+    Struct<typename std::remove_cvref_t<T>,
+           typename std::remove_cvref_t<I>...>::GetSchema(info...)
+  } -> std::convertible_to<std::string_view>;
+  {
+    Struct<typename std::remove_cvref_t<T>,
+           typename std::remove_cvref_t<I>...>::Unpack(in, info...)
+  } -> std::same_as<typename std::remove_cvref_t<T>>;
+  Struct<typename std::remove_cvref_t<T>,
+         typename std::remove_cvref_t<I>...>::Pack(out, std::forward<T>(value),
+                                                   info...);
+};
 
 /**
  * Specifies that a type is capable of in-place raw struct deserialization.
@@ -84,11 +98,12 @@
  * wpi::Struct<T> static member `void UnpackInto(T*, std::span<const uint8_t>)`
  * to update the pointed-to T with the contents of the span.
  */
-template <typename T>
+template <typename T, typename... I>
 concept MutableStructSerializable =
-    StructSerializable<T> && requires(T* out, std::span<const uint8_t> in) {
-      Struct<typename std::remove_cvref_t<T>>::UnpackInto(
-          out, in.subspan<0, Struct<typename std::remove_cvref_t<T>>::kSize>());
+    StructSerializable<T, I...> &&
+    requires(T* out, std::span<const uint8_t> in, const I&... info) {
+      Struct<typename std::remove_cvref_t<T>,
+             typename std::remove_cvref_t<I>...>::UnpackInto(out, in, info...);
     };
 
 /**
@@ -100,11 +115,13 @@
  * fn)` (or equivalent) and call ForEachNestedStruct<Type> on each nested struct
  * type.
  */
-template <typename T>
+template <typename T, typename... I>
 concept HasNestedStruct =
-    StructSerializable<T> &&
-    requires(function_ref<void(std::string_view, std::string_view)> fn) {
-      Struct<typename std::remove_cvref_t<T>>::ForEachNested(fn);
+    StructSerializable<T, I...> &&
+    requires(function_ref<void(std::string_view, std::string_view)> fn,
+             const I&... info) {
+      Struct<typename std::remove_cvref_t<T>,
+             typename std::remove_cvref_t<I>...>::ForEachNested(fn, info...);
     };
 
 /**
@@ -112,11 +129,14 @@
  *
  * @tparam T object type
  * @param data raw struct data
+ * @param info optional struct type info
  * @return Deserialized object
  */
-template <StructSerializable T>
-inline T UnpackStruct(std::span<const uint8_t, Struct<T>::kSize> data) {
-  return Struct<T>::Unpack(data);
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+inline T UnpackStruct(std::span<const uint8_t> data, const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  return S::Unpack(data, info...);
 }
 
 /**
@@ -126,11 +146,14 @@
  * @tparam T object type
  * @tparam Offset starting offset
  * @param data raw struct data
+ * @param info optional struct type info
  * @return Deserialized object
  */
-template <StructSerializable T, size_t Offset>
-inline T UnpackStruct(std::span<const uint8_t> data) {
-  return Struct<T>::Unpack(data.template subspan<Offset, Struct<T>::kSize>());
+template <typename T, size_t Offset, typename... I>
+  requires StructSerializable<T, I...>
+inline T UnpackStruct(std::span<const uint8_t> data, const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  return S::Unpack(data.subspan(Offset), info...);
 }
 
 /**
@@ -138,12 +161,14 @@
  *
  * @param data struct storage (mutable, output)
  * @param value object
+ * @param info optional struct type info
  */
-template <StructSerializable T>
-inline void PackStruct(
-    std::span<uint8_t, Struct<typename std::remove_cvref_t<T>>::kSize> data,
-    T&& value) {
-  Struct<typename std::remove_cvref_t<T>>::Pack(data, std::forward<T>(value));
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+inline void PackStruct(std::span<uint8_t> data, T&& value, const I&... info) {
+  using S = Struct<typename std::remove_cvref_t<T>,
+                   typename std::remove_cvref_t<I>...>;
+  S::Pack(data, std::forward<T>(value), info...);
 }
 
 /**
@@ -153,13 +178,14 @@
  * @tparam Offset starting offset
  * @param data struct storage (mutable, output)
  * @param value object
+ * @param info optional struct type info
  */
-template <size_t Offset, StructSerializable T>
-inline void PackStruct(std::span<uint8_t> data, T&& value) {
-  Struct<typename std::remove_cvref_t<T>>::Pack(
-      data.template subspan<Offset,
-                            Struct<typename std::remove_cvref_t<T>>::kSize>(),
-      std::forward<T>(value));
+template <size_t Offset, typename T, typename... I>
+  requires StructSerializable<T, I...>
+inline void PackStruct(std::span<uint8_t> data, T&& value, const I&... info) {
+  using S = Struct<typename std::remove_cvref_t<T>,
+                   typename std::remove_cvref_t<I>...>;
+  S::Pack(data.subspan(Offset), std::forward<T>(value), info...);
 }
 
 /**
@@ -167,14 +193,17 @@
  *
  * @param out object (output)
  * @param data raw struct data
+ * @param info optional struct type info
  */
-template <StructSerializable T>
-inline void UnpackStructInto(T* out,
-                             std::span<const uint8_t, Struct<T>::kSize> data) {
-  if constexpr (MutableStructSerializable<T>) {
-    Struct<T>::UnpackInto(out, data);
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+inline void UnpackStructInto(T* out, std::span<const uint8_t> data,
+                             const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  if constexpr (MutableStructSerializable<T, I...>) {
+    S::UnpackInto(out, data, info...);
   } else {
-    *out = UnpackStruct<T>(data);
+    *out = UnpackStruct<T>(data, info...);
   }
 }
 
@@ -186,14 +215,17 @@
  * @tparam Offset starting offset
  * @param out object (output)
  * @param data raw struct data
+ * @param info optional struct type info
  */
-template <size_t Offset, StructSerializable T>
-inline void UnpackStructInto(T* out, std::span<const uint8_t> data) {
-  if constexpr (MutableStructSerializable<T>) {
-    Struct<T>::UnpackInto(out,
-                          data.template subspan<Offset, Struct<T>::kSize>());
+template <size_t Offset, typename T, typename... I>
+  requires StructSerializable<T, I...>
+inline void UnpackStructInto(T* out, std::span<const uint8_t> data,
+                             const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  if constexpr (MutableStructSerializable<T, I...>) {
+    S::UnpackInto(out, data.subspan(Offset), info...);
   } else {
-    *out = UnpackStruct<T, Offset>(data);
+    *out = UnpackStruct<T, Offset>(data, info...);
   }
 }
 
@@ -201,68 +233,116 @@
  * Get the type string for a raw struct serializable type
  *
  * @tparam T serializable type
+ * @param info optional struct type info
  * @return type string
  */
-template <StructSerializable T>
-constexpr auto GetStructTypeString() {
-  return Struct<T>::kTypeString;
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+constexpr auto GetStructTypeString(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  return S::GetTypeString(info...);
 }
 
-template <StructSerializable T, size_t N>
-consteval auto MakeStructArrayTypeString() {
-  using namespace literals;
-  if constexpr (N == std::dynamic_extent) {
-    return Concat(
-        ct_string<char, std::char_traits<char>, Struct<T>::kTypeString.size()>{
-            Struct<T>::kTypeString},
-        "[]"_ct_string);
+/**
+ * Get the size for a raw struct serializable type
+ *
+ * @tparam T serializable type
+ * @param info optional struct type info
+ * @return size
+ */
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+constexpr size_t GetStructSize(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  return S::GetSize(info...);
+}
+
+template <typename T, size_t N, typename... I>
+  requires StructSerializable<T, I...>
+constexpr auto MakeStructArrayTypeString(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  if constexpr (sizeof...(I) == 0 &&
+                is_constexpr([&] { S::GetTypeString(info...); })) {
+    constexpr auto typeString = S::GetTypeString(info...);
+    using namespace literals;
+    if constexpr (N == std::dynamic_extent) {
+      return Concat(
+          ct_string<char, std::char_traits<char>, typeString.size()>{
+              typeString},
+          "[]"_ct_string);
+    } else {
+      return Concat(
+          ct_string<char, std::char_traits<char>, typeString.size()>{
+              typeString},
+          "["_ct_string, NumToCtString<N>(), "]"_ct_string);
+    }
   } else {
-    return Concat(
-        ct_string<char, std::char_traits<char>, Struct<T>::kTypeString.size()>{
-            Struct<T>::kTypeString},
-        "["_ct_string, NumToCtString<N>(), "]"_ct_string);
+    if constexpr (N == std::dynamic_extent) {
+      return fmt::format("{}[]", S::GetTypeString(info...));
+    } else {
+      return fmt::format("{}[{}]", S::GetTypeString(info...), N);
+    }
   }
 }
 
-template <StructSerializable T, size_t N>
-consteval auto MakeStructArraySchema() {
-  using namespace literals;
-  if constexpr (N == std::dynamic_extent) {
-    return Concat(
-        ct_string<char, std::char_traits<char>, Struct<T>::kSchema.size()>{
-            Struct<T>::kSchema},
-        "[]"_ct_string);
+template <typename T, size_t N, typename... I>
+  requires StructSerializable<T, I...>
+constexpr auto MakeStructArraySchema(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  if constexpr (sizeof...(I) == 0 &&
+                is_constexpr([&] { S::GetSchema(info...); })) {
+    constexpr auto schema = S::GetSchema(info...);
+    using namespace literals;
+    if constexpr (N == std::dynamic_extent) {
+      return Concat(
+          ct_string<char, std::char_traits<char>, schema.size()>{schema},
+          "[]"_ct_string);
+    } else {
+      return Concat(
+          ct_string<char, std::char_traits<char>, schema.size()>{schema},
+          "["_ct_string, NumToCtString<N>(), "]"_ct_string);
+    }
   } else {
-    return Concat(
-        ct_string<char, std::char_traits<char>, Struct<T>::kSchema.size()>{
-            Struct<T>::kSchema},
-        "["_ct_string, NumToCtString<N>(), "]"_ct_string);
+    if constexpr (N == std::dynamic_extent) {
+      return fmt::format("{}[]", S::GetSchema(info...));
+    } else {
+      return fmt::format("{}[{}]", S::GetSchema(info...), N);
+    }
   }
 }
 
-template <StructSerializable T>
-constexpr std::string_view GetStructSchema() {
-  return Struct<T>::kSchema;
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+constexpr std::string_view GetStructSchema(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  return S::GetSchema(info...);
 }
 
-template <StructSerializable T>
-constexpr std::span<const uint8_t> GetStructSchemaBytes() {
-  return {reinterpret_cast<const uint8_t*>(Struct<T>::kSchema.data()),
-          Struct<T>::kSchema.size()};
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
+constexpr std::span<const uint8_t> GetStructSchemaBytes(const I&... info) {
+  using S = Struct<T, typename std::remove_cvref_t<I>...>;
+  auto schema = S::GetSchema(info...);
+  return {reinterpret_cast<const uint8_t*>(schema.data()), schema.size()};
 }
 
-template <StructSerializable T>
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
 void ForEachStructSchema(
-    std::invocable<std::string_view, std::string_view> auto fn) {
-  if constexpr (HasNestedStruct<T>) {
-    Struct<T>::ForEachNested(fn);
+    std::invocable<std::string_view, std::string_view> auto fn,
+    const I&... info) {
+  using S = Struct<typename std::remove_cvref_t<T>,
+                   typename std::remove_cvref_t<I>...>;
+  if constexpr (HasNestedStruct<T, I...>) {
+    S::ForEachNested(fn, info...);
   }
-  fn(Struct<T>::kTypeString, Struct<T>::kSchema);
+  fn(S::GetTypeString(info...), S::GetSchema(info...));
 }
 
-template <StructSerializable T>
+template <typename T, typename... I>
+  requires StructSerializable<T, I...>
 class StructArrayBuffer {
-  using S = Struct<T>;
+  using S = Struct<T, I...>;
 
  public:
   StructArrayBuffer() = default;
@@ -281,25 +361,26 @@
       std::convertible_to<std::ranges::range_value_t<U>, T> &&
 #endif
       std::invocable<F, std::span<const uint8_t>>
-    void Write(U&& data, F&& func) {
-    if ((std::size(data) * S::kSize) < 256) {
+    void Write(U&& data, F&& func, const I&... info) {
+    auto size = S::GetSize(info...);
+    if ((std::size(data) * size) < 256) {
       // use the stack
       uint8_t buf[256];
       auto out = buf;
       for (auto&& val : data) {
-        S::Pack(std::span<uint8_t, S::kSize>{out, out + S::kSize},
-                std::forward<decltype(val)>(val));
-        out += S::kSize;
+        S::Pack(std::span<uint8_t>{std::to_address(out), size},
+                std::forward<decltype(val)>(val), info...);
+        out += size;
       }
       func(std::span<uint8_t>{buf, out});
     } else {
       std::scoped_lock lock{m_mutex};
-      m_buf.resize(std::size(data) * S::kSize);
+      m_buf.resize(std::size(data) * size);
       auto out = m_buf.begin();
       for (auto&& val : data) {
-        S::Pack(std::span<uint8_t, S::kSize>{out, out + S::kSize},
-                std::forward<decltype(val)>(val));
-        out += S::kSize;
+        S::Pack(std::span<uint8_t>{std::to_address(out), size},
+                std::forward<decltype(val)>(val), info...);
+        out += size;
       }
       func(m_buf);
     }
@@ -313,38 +394,49 @@
 /**
  * Raw struct support for fixed-size arrays of other structs.
  */
-template <StructSerializable T, size_t N>
-struct Struct<std::array<T, N>> {
-  static constexpr auto kTypeString = MakeStructArrayTypeString<T, N>();
-  static constexpr size_t kSize = N * Struct<T>::kSize;
-  static constexpr auto kSchema = MakeStructArraySchema<T, N>();
-  static std::array<T, N> Unpack(std::span<const uint8_t, kSize> data) {
+template <typename T, size_t N, typename... I>
+  requires StructSerializable<T, I...>
+struct Struct<std::array<T, N>, I...> {
+  static constexpr auto GetTypeString(const I&... info) {
+    return MakeStructArrayTypeString<T, N>(info...);
+  }
+  static constexpr size_t GetSize(const I&... info) {
+    return N * GetStructSize<T>(info...);
+  }
+  static constexpr auto GetSchema(const I&... info) {
+    return MakeStructArraySchema<T, N>(info...);
+  }
+  static std::array<T, N> Unpack(std::span<const uint8_t> data,
+                                 const I&... info) {
+    auto size = GetStructSize<T>(info...);
     std::array<T, N> result;
     for (size_t i = 0; i < N; ++i) {
-      result[i] = UnpackStruct<T, 0>(data);
-      data = data.subspan(Struct<T>::kSize);
+      result[i] = UnpackStruct<T, 0>(data, info...);
+      data = data.subspan(size);
     }
     return result;
   }
-  static void Pack(std::span<uint8_t, kSize> data,
-                   std::span<const T, N> values) {
+  static void Pack(std::span<uint8_t> data, std::span<const T, N> values,
+                   const I&... info) {
+    auto size = GetStructSize<T>(info...);
     std::span<uint8_t> unsizedData = data;
     for (auto&& val : values) {
-      PackStruct<0>(unsizedData, val);
-      unsizedData = unsizedData.subspan(Struct<T>::kSize);
+      PackStruct(unsizedData, val, info...);
+      unsizedData = unsizedData.subspan(size);
     }
   }
-  static void UnpackInto(std::array<T, N>* out,
-                         std::span<const uint8_t, kSize> data) {
-    UnpackInto(std::span{*out}, data);
+  static void UnpackInto(std::array<T, N>* out, std::span<const uint8_t> data,
+                         const I&... info) {
+    UnpackInto(std::span{*out}, data, info...);
   }
   // alternate span-based function
-  static void UnpackInto(std::span<T, N> out,
-                         std::span<const uint8_t, kSize> data) {
+  static void UnpackInto(std::span<T, N> out, std::span<const uint8_t> data,
+                         const I&... info) {
+    auto size = GetStructSize<T>(info...);
     std::span<const uint8_t> unsizedData = data;
     for (size_t i = 0; i < N; ++i) {
-      UnpackStructInto<0, T>(&out[i], unsizedData);
-      unsizedData = unsizedData.subspan(Struct<T>::kSize);
+      UnpackStructInto(&out[i], unsizedData, info...);
+      unsizedData = unsizedData.subspan(size);
     }
   }
 };
@@ -355,11 +447,11 @@
  */
 template <>
 struct Struct<bool> {
-  static constexpr std::string_view kTypeString = "struct:bool";
-  static constexpr size_t kSize = 1;
-  static constexpr std::string_view kSchema = "bool value";
-  static bool Unpack(std::span<const uint8_t, 1> data) { return data[0]; }
-  static void Pack(std::span<uint8_t, 1> data, bool value) {
+  static constexpr std::string_view GetTypeString() { return "struct:bool"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "bool value"; }
+  static bool Unpack(std::span<const uint8_t> data) { return data[0]; }
+  static void Pack(std::span<uint8_t> data, bool value) {
     data[0] = static_cast<char>(value ? 1 : 0);
   }
 };
@@ -370,13 +462,11 @@
  */
 template <>
 struct Struct<uint8_t> {
-  static constexpr std::string_view kTypeString = "struct:uint8";
-  static constexpr size_t kSize = 1;
-  static constexpr std::string_view kSchema = "uint8 value";
-  static uint8_t Unpack(std::span<const uint8_t, 1> data) { return data[0]; }
-  static void Pack(std::span<uint8_t, 1> data, uint8_t value) {
-    data[0] = value;
-  }
+  static constexpr std::string_view GetTypeString() { return "struct:uint8"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "uint8 value"; }
+  static uint8_t Unpack(std::span<const uint8_t> data) { return data[0]; }
+  static void Pack(std::span<uint8_t> data, uint8_t value) { data[0] = value; }
 };
 
 /**
@@ -385,9 +475,9 @@
  */
 template <>
 struct Struct<int8_t> {
-  static constexpr std::string_view kTypeString = "struct:int8";
-  static constexpr size_t kSize = 1;
-  static constexpr std::string_view kSchema = "int8 value";
+  static constexpr std::string_view GetTypeString() { return "struct:int8"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "int8 value"; }
   static int8_t Unpack(std::span<const uint8_t, 1> data) { return data[0]; }
   static void Pack(std::span<uint8_t, 1> data, int8_t value) {
     data[0] = value;
@@ -400,13 +490,13 @@
  */
 template <>
 struct Struct<uint16_t> {
-  static constexpr std::string_view kTypeString = "struct:uint16";
-  static constexpr size_t kSize = 2;
-  static constexpr std::string_view kSchema = "uint16 value";
-  static uint16_t Unpack(std::span<const uint8_t, 2> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:uint16"; }
+  static constexpr size_t GetSize() { return 2; }
+  static constexpr std::string_view GetSchema() { return "uint16 value"; }
+  static uint16_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read16le(data.data());
   }
-  static void Pack(std::span<uint8_t, 2> data, uint16_t value) {
+  static void Pack(std::span<uint8_t> data, uint16_t value) {
     support::endian::write16le(data.data(), value);
   }
 };
@@ -417,13 +507,13 @@
  */
 template <>
 struct Struct<int16_t> {
-  static constexpr std::string_view kTypeString = "struct:int16";
-  static constexpr size_t kSize = 2;
-  static constexpr std::string_view kSchema = "int16 value";
-  static int16_t Unpack(std::span<const uint8_t, 2> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:int16"; }
+  static constexpr size_t GetSize() { return 2; }
+  static constexpr std::string_view GetSchema() { return "int16 value"; }
+  static int16_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read16le(data.data());
   }
-  static void Pack(std::span<uint8_t, 2> data, int16_t value) {
+  static void Pack(std::span<uint8_t> data, int16_t value) {
     support::endian::write16le(data.data(), value);
   }
 };
@@ -434,13 +524,13 @@
  */
 template <>
 struct Struct<uint32_t> {
-  static constexpr std::string_view kTypeString = "struct:uint32";
-  static constexpr size_t kSize = 4;
-  static constexpr std::string_view kSchema = "uint32 value";
-  static uint32_t Unpack(std::span<const uint8_t, 4> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:uint32"; }
+  static constexpr size_t GetSize() { return 4; }
+  static constexpr std::string_view GetSchema() { return "uint32 value"; }
+  static uint32_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read32le(data.data());
   }
-  static void Pack(std::span<uint8_t, 4> data, uint32_t value) {
+  static void Pack(std::span<uint8_t> data, uint32_t value) {
     support::endian::write32le(data.data(), value);
   }
 };
@@ -451,13 +541,13 @@
  */
 template <>
 struct Struct<int32_t> {
-  static constexpr std::string_view kTypeString = "struct:int32";
-  static constexpr size_t kSize = 4;
-  static constexpr std::string_view kSchema = "int32 value";
-  static int32_t Unpack(std::span<const uint8_t, 4> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:int32"; }
+  static constexpr size_t GetSize() { return 4; }
+  static constexpr std::string_view GetSchema() { return "int32 value"; }
+  static int32_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read32le(data.data());
   }
-  static void Pack(std::span<uint8_t, 4> data, int32_t value) {
+  static void Pack(std::span<uint8_t> data, int32_t value) {
     support::endian::write32le(data.data(), value);
   }
 };
@@ -468,13 +558,13 @@
  */
 template <>
 struct Struct<uint64_t> {
-  static constexpr std::string_view kTypeString = "struct:uint64";
-  static constexpr size_t kSize = 8;
-  static constexpr std::string_view kSchema = "uint64 value";
-  static uint64_t Unpack(std::span<const uint8_t, 8> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:uint64"; }
+  static constexpr size_t GetSize() { return 8; }
+  static constexpr std::string_view GetSchema() { return "uint64 value"; }
+  static uint64_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read64le(data.data());
   }
-  static void Pack(std::span<uint8_t, 8> data, uint64_t value) {
+  static void Pack(std::span<uint8_t> data, uint64_t value) {
     support::endian::write64le(data.data(), value);
   }
 };
@@ -485,13 +575,13 @@
  */
 template <>
 struct Struct<int64_t> {
-  static constexpr std::string_view kTypeString = "struct:int64";
-  static constexpr size_t kSize = 8;
-  static constexpr std::string_view kSchema = "int64 value";
-  static int64_t Unpack(std::span<const uint8_t, 8> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:int64"; }
+  static constexpr size_t GetSize() { return 8; }
+  static constexpr std::string_view GetSchema() { return "int64 value"; }
+  static int64_t Unpack(std::span<const uint8_t> data) {
     return support::endian::read64le(data.data());
   }
-  static void Pack(std::span<uint8_t, 8> data, int64_t value) {
+  static void Pack(std::span<uint8_t> data, int64_t value) {
     support::endian::write64le(data.data(), value);
   }
 };
@@ -502,13 +592,13 @@
  */
 template <>
 struct Struct<float> {
-  static constexpr std::string_view kTypeString = "struct:float";
-  static constexpr size_t kSize = 4;
-  static constexpr std::string_view kSchema = "float value";
-  static float Unpack(std::span<const uint8_t, 4> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:float"; }
+  static constexpr size_t GetSize() { return 4; }
+  static constexpr std::string_view GetSchema() { return "float value"; }
+  static float Unpack(std::span<const uint8_t> data) {
     return bit_cast<float>(support::endian::read32le(data.data()));
   }
-  static void Pack(std::span<uint8_t, 4> data, float value) {
+  static void Pack(std::span<uint8_t> data, float value) {
     support::endian::write32le(data.data(), bit_cast<uint32_t>(value));
   }
 };
@@ -519,13 +609,13 @@
  */
 template <>
 struct Struct<double> {
-  static constexpr std::string_view kTypeString = "struct:double";
-  static constexpr size_t kSize = 8;
-  static constexpr std::string_view kSchema = "double value";
-  static double Unpack(std::span<const uint8_t, 8> data) {
+  static constexpr std::string_view GetTypeString() { return "struct:double"; }
+  static constexpr size_t GetSize() { return 8; }
+  static constexpr std::string_view GetSchema() { return "double value"; }
+  static double Unpack(std::span<const uint8_t> data) {
     return bit_cast<double>(support::endian::read64le(data.data()));
   }
-  static void Pack(std::span<uint8_t, 8> data, double value) {
+  static void Pack(std::span<uint8_t> data, double value) {
     support::endian::write64le(data.data(), bit_cast<uint64_t>(value));
   }
 };
diff --git a/wpiutil/src/main/native/include/wpi/timestamp.h b/wpiutil/src/main/native/include/wpi/timestamp.h
index c232481..4d61055 100644
--- a/wpiutil/src/main/native/include/wpi/timestamp.h
+++ b/wpiutil/src/main/native/include/wpi/timestamp.h
@@ -8,15 +8,13 @@
 #include <stdint.h>
 
 #ifdef __cplusplus
-extern "C" {
+#include <memory>  // NOLINT
+
 #endif
 
-/**
- * Initialize the on-Rio Now() implementation to use the FPGA timestamp.
- * No effect on non-Rio platforms. This is called by HAL_Initialize() and
- * thus should generally not be called by user code.
- */
-void WPI_Impl_SetupNowRio(void);
+#ifdef __cplusplus
+extern "C" {
+#endif
 
 /**
  * De-initialize the on-Rio Now() implementation. No effect on non-Rio
@@ -63,11 +61,31 @@
 
 namespace impl {
 /**
+ * Initialize the on-Rio Now() implementation to use the desktop timestamp.
+ * No effect on non-Rio platforms. This should only be used for testing
+ * purposes if the HAL is not available.
+ */
+void SetupNowDefaultOnRio();
+
+/**
  * Initialize the on-Rio Now() implementation to use the FPGA timestamp.
  * No effect on non-Rio platforms. This is called by HAL_Initialize() and
  * thus should generally not be called by user code.
  */
-void SetupNowRio();
+#ifdef __FRC_ROBORIO__
+template <typename T>
+void SetupNowRio(void* chipObjectLibrary, std::unique_ptr<T> hmbObject);
+#else
+template <typename T>
+inline void SetupNowRio(void*, std::unique_ptr<T>) {}
+#endif
+
+/**
+ * Initialize the on-Rio Now() implementation to use the FPGA timestamp.
+ * No effect on non-Rio platforms. This take an FPGA session that has
+ * already been initialized, and is used from LabVIEW.
+ */
+void SetupNowRio(uint32_t session);
 
 /**
  * De-initialize the on-Rio Now() implementation. No effect on non-Rio
diff --git a/wpiutil/src/main/native/thirdparty/fmtlib/include/fmt/core.h b/wpiutil/src/main/native/thirdparty/fmtlib/include/fmt/core.h
index 1fe1388..915d895 100644
--- a/wpiutil/src/main/native/thirdparty/fmtlib/include/fmt/core.h
+++ b/wpiutil/src/main/native/thirdparty/fmtlib/include/fmt/core.h
@@ -1435,7 +1435,7 @@
   // Only map owning types because mapping views can be unsafe.
   template <typename T, typename U = format_as_t<T>,
             FMT_ENABLE_IF(std::is_arithmetic<U>::value)>
-  FMT_CONSTEXPR FMT_INLINE auto map(const T& val) -> decltype(this->map(U())) {
+  FMT_CONSTEXPR FMT_INLINE auto map(const T& val) -> decltype(map(U())) {
     return map(format_as(val));
   }
 
@@ -1459,13 +1459,13 @@
                           !is_string<U>::value && !is_char<U>::value &&
                           !is_named_arg<U>::value &&
                           !std::is_arithmetic<format_as_t<U>>::value)>
-  FMT_CONSTEXPR FMT_INLINE auto map(T& val) -> decltype(this->do_map(val)) {
+  FMT_CONSTEXPR FMT_INLINE auto map(T& val) -> decltype(do_map(val)) {
     return do_map(val);
   }
 
   template <typename T, FMT_ENABLE_IF(is_named_arg<T>::value)>
   FMT_CONSTEXPR FMT_INLINE auto map(const T& named_arg)
-      -> decltype(this->map(named_arg.value)) {
+      -> decltype(map(named_arg.value)) {
     return map(named_arg.value);
   }
 
diff --git a/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp b/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp
index 72cdf09..3ee53a2 100644
--- a/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp
+++ b/wpiutil/src/main/native/thirdparty/llvm/cpp/llvm/MemoryBuffer.cpp
@@ -230,6 +230,7 @@
     fs::file_t f, std::string_view bufferName, std::error_code& ec) {
   constexpr size_t ChunkSize = 4096 * 4;
   SmallVector<uint8_t, ChunkSize> buffer;
+  uint64_t size = 0;
 #ifdef _WIN32
   DWORD readBytes;
 #else
@@ -237,23 +238,23 @@
 #endif
   // Read into Buffer until we hit EOF.
   do {
-    size_t prevSize = buffer.size();
-    buffer.resize_for_overwrite(prevSize + ChunkSize);
+    buffer.resize_for_overwrite(size + ChunkSize);
 #ifdef _WIN32
-    if (!ReadFile(f, buffer.begin() + prevSize, ChunkSize, &readBytes,
-                  nullptr)) {
+    if (!ReadFile(f, buffer.begin() + size, ChunkSize, &readBytes, nullptr)) {
       ec = mapWindowsError(GetLastError());
       return nullptr;
     }
 #else
-    readBytes = sys::RetryAfterSignal(-1, ::read, f, buffer.begin() + prevSize,
+    readBytes = sys::RetryAfterSignal(-1, ::read, f, buffer.begin() + size,
                                       ChunkSize);
     if (readBytes == -1) {
       ec = std::error_code(errno, std::generic_category());
       return nullptr;
     }
 #endif
+    size += readBytes;
   } while (readBytes != 0);
+  buffer.truncate(size);
 
   return GetMemBufferCopyImpl(buffer, bufferName, ec);
 }
@@ -370,7 +371,7 @@
 
       // If this not a file or a block device (e.g. it's a named pipe
       // or character device), we can't mmap it, so error out.
-      if (status.st_mode != S_IFREG && status.st_mode != S_IFBLK) {
+      if (!S_ISREG(status.st_mode) && !S_ISBLK(status.st_mode)) {
         ec = make_error_code(errc::invalid_argument);
         return nullptr;
       }
@@ -433,7 +434,7 @@
       // If this not a file or a block device (e.g. it's a named pipe
       // or character device), we can't trust the size. Create the memory
       // buffer by copying off the stream.
-      if (status.st_mode != S_IFREG && status.st_mode != S_IFBLK) {
+      if (!S_ISREG(status.st_mode) && !S_ISBLK(status.st_mode)) {
         return GetMemoryBufferForStream(f, filename, ec);
       }
 
diff --git a/wpiutil/src/main/native/thirdparty/llvm/include/wpi/MemoryBuffer.h b/wpiutil/src/main/native/thirdparty/llvm/include/wpi/MemoryBuffer.h
index 7907c07..b5eaea4 100644
--- a/wpiutil/src/main/native/thirdparty/llvm/include/wpi/MemoryBuffer.h
+++ b/wpiutil/src/main/native/thirdparty/llvm/include/wpi/MemoryBuffer.h
@@ -62,6 +62,11 @@
 
   std::span<const uint8_t> GetBuffer() const { return {begin(), end()}; }
 
+  std::span<const char> GetCharBuffer() const {
+    return {reinterpret_cast<const char*>(begin()),
+            reinterpret_cast<const char*>(end())};
+  }
+
   /// Return an identifier for this buffer, typically the filename it was read
   /// from.
   virtual std::string_view GetBufferIdentifier() const {
@@ -145,6 +150,10 @@
   uint8_t* begin() { return const_cast<uint8_t*>(MemoryBuffer::begin()); }
   uint8_t* end() { return const_cast<uint8_t*>(MemoryBuffer::end()); }
   std::span<uint8_t> GetBuffer() { return {begin(), end()}; }
+  std::span<char> GetCharBuffer() const {
+    return {reinterpret_cast<char*>(const_cast<uint8_t*>(begin())),
+            reinterpret_cast<char*>(const_cast<uint8_t*>(end()))};
+  }
 
   static std::unique_ptr<WritableMemoryBuffer> GetFile(
       std::string_view filename, std::error_code& ec, int64_t fileSize = -1);
@@ -196,6 +205,10 @@
   uint8_t* begin() { return const_cast<uint8_t*>(MemoryBuffer::begin()); }
   uint8_t* end() { return const_cast<uint8_t*>(MemoryBuffer::end()); }
   std::span<uint8_t> GetBuffer() { return {begin(), end()}; }
+  std::span<char> GetCharBuffer() const {
+    return {reinterpret_cast<char*>(const_cast<uint8_t*>(begin())),
+            reinterpret_cast<char*>(const_cast<uint8_t*>(end()))};
+  }
 
   static std::unique_ptr<WriteThroughMemoryBuffer> GetFile(
       std::string_view filename, std::error_code& ec, int64_t fileSize = -1);
diff --git a/wpiutil/src/main/native/thirdparty/llvm/include/wpi/type_traits.h b/wpiutil/src/main/native/thirdparty/llvm/include/wpi/type_traits.h
index d74fd0f..5166f2a 100644
--- a/wpiutil/src/main/native/thirdparty/llvm/include/wpi/type_traits.h
+++ b/wpiutil/src/main/native/thirdparty/llvm/include/wpi/type_traits.h
@@ -76,6 +76,11 @@
 
 } // end namespace detail
 
+// https://stackoverflow.com/questions/55288555/c-check-if-statement-can-be-evaluated-constexpr
+template<class Lambda, int=(Lambda{}(), 0)>
+constexpr bool is_constexpr(Lambda) { return true; }
+constexpr bool is_constexpr(...) { return false; }
+
 } // end namespace wpi
 
 #endif // WPIUTIL_WPI_TYPE_TRAITS_H
diff --git a/wpiutil/src/main/native/thirdparty/protobuf/include/google/protobuf/io/coded_stream.h b/wpiutil/src/main/native/thirdparty/protobuf/include/google/protobuf/io/coded_stream.h
index 6c0dd4a..a102cec 100644
--- a/wpiutil/src/main/native/thirdparty/protobuf/include/google/protobuf/io/coded_stream.h
+++ b/wpiutil/src/main/native/thirdparty/protobuf/include/google/protobuf/io/coded_stream.h
@@ -681,7 +681,14 @@
     if (PROTOBUF_PREDICT_FALSE(end_ - ptr < static_cast<int>(size))) {
       return WriteRawFallback(data, size, ptr);
     }
+#if __GNUC__ >= 13
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wstringop-overflow="
+#endif  // __GNUC__ >= 13
     std::memcpy(ptr, data, size);
+#if __GNUC__ >= 13
+#pragma GCC diagnostic pop
+#endif  // __GNUC__ >= 13
     return ptr + size;
   }
   // Writes the buffer specified by data, size to the stream. Possibly by
diff --git a/wpiutil/src/main/native/thirdparty/protobuf/src/unknown_field_set.cpp b/wpiutil/src/main/native/thirdparty/protobuf/src/unknown_field_set.cpp
index 74c358e..c058735 100644
--- a/wpiutil/src/main/native/thirdparty/protobuf/src/unknown_field_set.cpp
+++ b/wpiutil/src/main/native/thirdparty/protobuf/src/unknown_field_set.cpp
@@ -96,9 +96,16 @@
   if (fields_.empty()) {
     fields_ = std::move(other->fields_);
   } else {
+#if __GNUC__ >= 13
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wstringop-overflow="
+#endif  // __GNUC__ >= 13
     fields_.insert(fields_.end(),
                    std::make_move_iterator(other->fields_.begin()),
                    std::make_move_iterator(other->fields_.end()));
+#if __GNUC__ >= 13
+#pragma GCC diagnostic pop
+#endif  // __GNUC__ >= 13
   }
   other->fields_.clear();
 }
diff --git a/wpiutil/src/printlog/java/printlog/PrintLog.java b/wpiutil/src/printlog/java/printlog/PrintLog.java
index e9339b6..b3d972b 100644
--- a/wpiutil/src/printlog/java/printlog/PrintLog.java
+++ b/wpiutil/src/printlog/java/printlog/PrintLog.java
@@ -10,9 +10,9 @@
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.InputMismatchException;
+import java.util.List;
 import java.util.Map;
 
 public final class PrintLog {
@@ -133,11 +133,11 @@
           } else if ("boolean".equals(entry.type)) {
             System.out.println("  " + record.getBoolean());
           } else if ("double[]".equals(entry.type)) {
-            System.out.println("  " + Arrays.asList(record.getDoubleArray()));
+            System.out.println("  " + List.of(record.getDoubleArray()));
           } else if ("int64[]".equals(entry.type)) {
-            System.out.println("  " + Arrays.asList(record.getIntegerArray()));
+            System.out.println("  " + List.of(record.getIntegerArray()));
           } else if ("string[]".equals(entry.type)) {
-            System.out.println("  " + Arrays.asList(record.getStringArray()));
+            System.out.println("  " + List.of(record.getStringArray()));
           }
         } catch (InputMismatchException ex) {
           System.out.println("  invalid");
diff --git a/wpiutil/src/test/java/edu/wpi/first/util/CircularBufferTest.java b/wpiutil/src/test/java/edu/wpi/first/util/CircularBufferTest.java
index 7972754..a040ba5 100644
--- a/wpiutil/src/test/java/edu/wpi/first/util/CircularBufferTest.java
+++ b/wpiutil/src/test/java/edu/wpi/first/util/CircularBufferTest.java
@@ -21,7 +21,7 @@
 
   @Test
   void addFirstTest() {
-    CircularBuffer queue = new CircularBuffer(8);
+    var queue = new CircularBuffer<Double>(8);
 
     for (double value : m_values) {
       queue.addFirst(value);
@@ -34,7 +34,7 @@
 
   @Test
   void addLastTest() {
-    CircularBuffer queue = new CircularBuffer(8);
+    var queue = new CircularBuffer<Double>(8);
 
     for (double value : m_values) {
       queue.addLast(value);
@@ -47,7 +47,7 @@
 
   @Test
   void pushPopTest() {
-    CircularBuffer queue = new CircularBuffer(3);
+    var queue = new CircularBuffer<Double>(3);
 
     // Insert three elements into the buffer
     queue.addLast(1.0);
@@ -91,22 +91,20 @@
 
   @Test
   void resetTest() {
-    CircularBuffer queue = new CircularBuffer(5);
+    var queue = new CircularBuffer<Double>(5);
 
     for (int i = 0; i < 6; i++) {
-      queue.addLast(i);
+      queue.addLast((double) i);
     }
 
     queue.clear();
 
-    for (int i = 0; i < 5; i++) {
-      assertEquals(0.0, queue.get(i), 0.00005);
-    }
+    assertEquals(0, queue.size());
   }
 
   @Test
   void resizeTest() {
-    CircularBuffer queue = new CircularBuffer(5);
+    var queue = new CircularBuffer<Double>(5);
 
     /* Buffer contains {1, 2, 3, _, _}
      *                  ^ front
diff --git a/wpiutil/src/test/java/edu/wpi/first/util/DoubleCircularBufferTest.java b/wpiutil/src/test/java/edu/wpi/first/util/DoubleCircularBufferTest.java
new file mode 100644
index 0000000..58a39f4
--- /dev/null
+++ b/wpiutil/src/test/java/edu/wpi/first/util/DoubleCircularBufferTest.java
@@ -0,0 +1,213 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class DoubleCircularBufferTest {
+  private final double[] m_values = {
+    751.848, 766.366, 342.657, 234.252, 716.126, 132.344, 445.697, 22.727, 421.125, 799.913
+  };
+  private final double[] m_addFirstOut = {
+    799.913, 421.125, 22.727, 445.697, 132.344, 716.126, 234.252, 342.657
+  };
+  private final double[] m_addLastOut = {
+    342.657, 234.252, 716.126, 132.344, 445.697, 22.727, 421.125, 799.913
+  };
+
+  @Test
+  void addFirstTest() {
+    var queue = new DoubleCircularBuffer(8);
+
+    for (double value : m_values) {
+      queue.addFirst(value);
+    }
+
+    for (int i = 0; i < m_addFirstOut.length; i++) {
+      assertEquals(m_addFirstOut[i], queue.get(i), 0.00005);
+    }
+  }
+
+  @Test
+  void addLastTest() {
+    var queue = new DoubleCircularBuffer(8);
+
+    for (double value : m_values) {
+      queue.addLast(value);
+    }
+
+    for (int i = 0; i < m_addLastOut.length; i++) {
+      assertEquals(m_addLastOut[i], queue.get(i), 0.00005);
+    }
+  }
+
+  @Test
+  void pushPopTest() {
+    var queue = new DoubleCircularBuffer(3);
+
+    // Insert three elements into the buffer
+    queue.addLast(1.0);
+    queue.addLast(2.0);
+    queue.addLast(3.0);
+
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+    assertEquals(3.0, queue.get(2), 0.00005);
+
+    /*
+     * The buffer is full now, so pushing subsequent elements will overwrite the
+     * front-most elements.
+     */
+
+    queue.addLast(4.0); // Overwrite 1 with 4
+
+    // The buffer now contains 2, 3, and 4
+    assertEquals(2.0, queue.get(0), 0.00005);
+    assertEquals(3.0, queue.get(1), 0.00005);
+    assertEquals(4.0, queue.get(2), 0.00005);
+
+    queue.addLast(5.0); // Overwrite 2 with 5
+
+    // The buffer now contains 3, 4, and 5
+    assertEquals(3.0, queue.get(0), 0.00005);
+    assertEquals(4.0, queue.get(1), 0.00005);
+    assertEquals(5.0, queue.get(2), 0.00005);
+
+    assertEquals(5.0, queue.removeLast(), 0.00005); // 5 is removed
+
+    // The buffer now contains 3 and 4
+    assertEquals(3.0, queue.get(0), 0.00005);
+    assertEquals(4.0, queue.get(1), 0.00005);
+
+    assertEquals(3.0, queue.removeFirst(), 0.00005); // 3 is removed
+
+    // Leaving only one element with value == 4
+    assertEquals(4.0, queue.get(0), 0.00005);
+  }
+
+  @Test
+  void resetTest() {
+    var queue = new DoubleCircularBuffer(5);
+
+    for (int i = 0; i < 6; i++) {
+      queue.addLast(i);
+    }
+
+    queue.clear();
+
+    for (int i = 0; i < 5; i++) {
+      assertEquals(0.0, queue.get(i), 0.00005);
+    }
+  }
+
+  @Test
+  void resizeTest() {
+    var queue = new DoubleCircularBuffer(5);
+
+    /* Buffer contains {1, 2, 3, _, _}
+     *                  ^ front
+     */
+    queue.addLast(1.0);
+    queue.addLast(2.0);
+    queue.addLast(3.0);
+
+    queue.resize(2);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.resize(5);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.clear();
+
+    /* Buffer contains {_, 1, 2, 3, _}
+     *                     ^ front
+     */
+    queue.addLast(0.0);
+    queue.addLast(1.0);
+    queue.addLast(2.0);
+    queue.addLast(3.0);
+    queue.removeFirst();
+
+    queue.resize(2);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.resize(5);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.clear();
+
+    /* Buffer contains {_, _, 1, 2, 3}
+     *                        ^ front
+     */
+    queue.addLast(0.0);
+    queue.addLast(0.0);
+    queue.addLast(1.0);
+    queue.addLast(2.0);
+    queue.addLast(3.0);
+    queue.removeFirst();
+    queue.removeFirst();
+
+    queue.resize(2);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.resize(5);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.clear();
+
+    /* Buffer contains {3, _, _, 1, 2}
+     *                           ^ front
+     */
+    queue.addLast(3.0);
+    queue.addFirst(2.0);
+    queue.addFirst(1.0);
+
+    queue.resize(2);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.resize(5);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.clear();
+
+    /* Buffer contains {2, 3, _, _, 1}
+     *                              ^ front
+     */
+    queue.addLast(2.0);
+    queue.addLast(3.0);
+    queue.addFirst(1.0);
+
+    queue.resize(2);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    queue.resize(5);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+
+    // Test addLast() after resize
+    queue.addLast(3.0);
+    assertEquals(1.0, queue.get(0), 0.00005);
+    assertEquals(2.0, queue.get(1), 0.00005);
+    assertEquals(3.0, queue.get(2), 0.00005);
+
+    // Test addFirst() after resize
+    queue.addFirst(4.0);
+    assertEquals(4.0, queue.get(0), 0.00005);
+    assertEquals(1.0, queue.get(1), 0.00005);
+    assertEquals(2.0, queue.get(2), 0.00005);
+    assertEquals(3.0, queue.get(3), 0.00005);
+  }
+}
diff --git a/wpiutil/src/test/java/edu/wpi/first/util/struct/parser/ParserTest.java b/wpiutil/src/test/java/edu/wpi/first/util/struct/parser/ParserTest.java
index 52ba8da..b985b2a 100644
--- a/wpiutil/src/test/java/edu/wpi/first/util/struct/parser/ParserTest.java
+++ b/wpiutil/src/test/java/edu/wpi/first/util/struct/parser/ParserTest.java
@@ -15,21 +15,21 @@
   @Test
   void testEmpty() {
     Parser p = new Parser("");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertTrue(schema.declarations.isEmpty());
   }
 
   @Test
   void testEmptySemicolon() {
     Parser p = new Parser(";");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertTrue(schema.declarations.isEmpty());
   }
 
   @Test
   void testSimple() {
     Parser p = new Parser("int32 a");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -40,14 +40,14 @@
   @Test
   void testSimpleTrailingSemi() {
     Parser p = new Parser("int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
   }
 
   @Test
   void testArray() {
     Parser p = new Parser("int32 a[2]");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -58,14 +58,14 @@
   @Test
   void testArrayTrailingSemi() {
     Parser p = new Parser("int32 a[2];");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
   }
 
   @Test
   void testBitfield() {
     Parser p = new Parser("int32 a:2");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -76,14 +76,14 @@
   @Test
   void testBitfieldTrailingSemi() {
     Parser p = new Parser("int32 a:2;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
   }
 
   @Test
   void testEnumKeyword() {
     Parser p = new Parser("enum {x=1} int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -95,7 +95,7 @@
   @Test
   void testEnumNoKeyword() {
     Parser p = new Parser("{x=1} int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -107,7 +107,7 @@
   @Test
   void testEnumNoValues() {
     Parser p = new Parser("{} int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -118,7 +118,7 @@
   @Test
   void testEnumMultipleValues() {
     Parser p = new Parser("{x=1,y=-2} int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -131,7 +131,7 @@
   @Test
   void testEnumTrailingComma() {
     Parser p = new Parser("{x=1,y=2,} int32 a;");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 1);
     var decl = schema.declarations.get(0);
     assertEquals(decl.typeString, "int32");
@@ -144,7 +144,7 @@
   @Test
   void testMultipleNoTrailingSemi() {
     Parser p = new Parser("int32 a; int16 b");
-    ParsedSchema schema = assertDoesNotThrow(() -> p.parse());
+    ParsedSchema schema = assertDoesNotThrow(p::parse);
     assertEquals(schema.declarations.size(), 2);
     assertEquals(schema.declarations.get(0).typeString, "int32");
     assertEquals(schema.declarations.get(0).name, "a");
@@ -155,58 +155,55 @@
   @Test
   void testErrBitfieldArray() {
     Parser p = new Parser("int32 a[1]:2");
-    assertThrows(ParseException.class, () -> p.parse(), "10: expected ';', got ':'");
+    assertThrows(ParseException.class, p::parse, "10: expected ';', got ':'");
   }
 
   @Test
   void testErrNoArrayValue() {
     Parser p = new Parser("int32 a[]");
-    assertThrows(ParseException.class, () -> p.parse(), "8: expected integer, got ']'");
+    assertThrows(ParseException.class, p::parse, "8: expected integer, got ']'");
   }
 
   @Test
   void testErrNoBitfieldValue() {
     Parser p = new Parser("int32 a:");
-    assertThrows(ParseException.class, () -> p.parse(), "8: expected integer, got ''");
+    assertThrows(ParseException.class, p::parse, "8: expected integer, got ''");
   }
 
   @Test
   void testErrNoNameArray() {
     Parser p = new Parser("int32 [2]");
-    assertThrows(ParseException.class, () -> p.parse(), "6: expected identifier, got '['");
+    assertThrows(ParseException.class, p::parse, "6: expected identifier, got '['");
   }
 
   @Test
   void testErrNoNameBitField() {
     Parser p = new Parser("int32 :2");
-    assertThrows(ParseException.class, () -> p.parse(), "6: expected identifier, got ':'");
+    assertThrows(ParseException.class, p::parse, "6: expected identifier, got ':'");
   }
 
   @Test
   void testNegativeBitField() {
     Parser p = new Parser("int32 a:-1");
     assertThrows(
-        ParseException.class, () -> p.parse(), "8: bitfield width '-1' is not a positive integer");
+        ParseException.class, p::parse, "8: bitfield width '-1' is not a positive integer");
   }
 
   @Test
   void testNegativeArraySize() {
     Parser p = new Parser("int32 a[-1]");
-    assertThrows(
-        ParseException.class, () -> p.parse(), "8: array size '-1' is not a positive integer");
+    assertThrows(ParseException.class, p::parse, "8: array size '-1' is not a positive integer");
   }
 
   @Test
   void testZeroBitField() {
     Parser p = new Parser("int32 a:0");
-    assertThrows(
-        ParseException.class, () -> p.parse(), "8: bitfield width '0' is not a positive integer");
+    assertThrows(ParseException.class, p::parse, "8: bitfield width '0' is not a positive integer");
   }
 
   @Test
   void testZeroArraySize() {
     Parser p = new Parser("int32 a[0]");
-    assertThrows(
-        ParseException.class, () -> p.parse(), "8: array size '0' is not a positive integer");
+    assertThrows(ParseException.class, p::parse, "8: array size '0' is not a positive integer");
   }
 }
diff --git a/wpiutil/src/test/native/cpp/ArrayTest.cpp b/wpiutil/src/test/native/cpp/ArrayTest.cpp
index f70b201..d3a12a4 100644
--- a/wpiutil/src/test/native/cpp/ArrayTest.cpp
+++ b/wpiutil/src/test/native/cpp/ArrayTest.cpp
@@ -16,20 +16,17 @@
 }  // namespace
 
 TEST(ArrayTest, CopyableTypeCompiles) {
-  constexpr wpi::array<int, 3> arr1{1, 2, 3};
-  static_cast<void>(arr1);
+  [[maybe_unused]] constexpr wpi::array<int, 3> arr1{1, 2, 3};
 
   // Test deduction guide
-  constexpr wpi::array arr2{1, 2, 3};
-  static_cast<void>(arr2);
+  [[maybe_unused]] constexpr wpi::array arr2{1, 2, 3};
 }
 
 TEST(ArrayTest, MoveOnlyTypeCompiles) {
-  constexpr wpi::array<MoveOnlyType, 3> arr1{MoveOnlyType{}, MoveOnlyType{},
-                                             MoveOnlyType{}};
-  static_cast<void>(arr1);
+  [[maybe_unused]] constexpr wpi::array<MoveOnlyType, 3> arr1{
+      MoveOnlyType{}, MoveOnlyType{}, MoveOnlyType{}};
 
   // Test deduction guide
-  constexpr wpi::array arr2{MoveOnlyType{}, MoveOnlyType{}, MoveOnlyType{}};
-  static_cast<void>(arr2);
+  [[maybe_unused]] constexpr wpi::array arr2{MoveOnlyType{}, MoveOnlyType{},
+                                             MoveOnlyType{}};
 }
diff --git a/wpiutil/src/test/native/cpp/DataLogTest.cpp b/wpiutil/src/test/native/cpp/DataLogTest.cpp
new file mode 100644
index 0000000..d06ad33
--- /dev/null
+++ b/wpiutil/src/test/native/cpp/DataLogTest.cpp
@@ -0,0 +1,249 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source 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 <array>
+
+#include <gtest/gtest.h>
+
+#include "wpi/DataLog.h"
+
+namespace {
+struct ThingA {
+  int x = 0;
+};
+
+struct ThingB {
+  int x = 0;
+};
+
+struct ThingC {
+  int x = 0;
+};
+
+struct Info1 {
+  int info = 0;
+};
+
+struct Info2 {
+  int info = 0;
+};
+}  // namespace
+
+template <>
+struct wpi::Struct<ThingA> {
+  static constexpr std::string_view GetTypeString() { return "struct:ThingA"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "uint8 value"; }
+  static ThingA Unpack(std::span<const uint8_t> data) {
+    return ThingA{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingA& value) {
+    data[0] = value.x;
+  }
+};
+
+template <>
+struct wpi::Struct<ThingB, Info1> {
+  static constexpr std::string_view GetTypeString(const Info1&) {
+    return "struct:ThingB";
+  }
+  static constexpr size_t GetSize(const Info1&) { return 1; }
+  static constexpr std::string_view GetSchema(const Info1&) {
+    return "uint8 value";
+  }
+  static ThingB Unpack(std::span<const uint8_t> data, const Info1&) {
+    return ThingB{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingB& value, const Info1&) {
+    data[0] = value.x;
+  }
+};
+
+template <>
+struct wpi::Struct<ThingC> {
+  static constexpr std::string_view GetTypeString() { return "struct:ThingC"; }
+  static constexpr size_t GetSize() { return 1; }
+  static constexpr std::string_view GetSchema() { return "uint8 value"; }
+  static ThingC Unpack(std::span<const uint8_t> data) {
+    return ThingC{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingC& value) {
+    data[0] = value.x;
+  }
+};
+
+template <>
+struct wpi::Struct<ThingC, Info1> {
+  static constexpr std::string_view GetTypeString(const Info1&) {
+    return "struct:ThingC";
+  }
+  static constexpr size_t GetSize(const Info1&) { return 1; }
+  static constexpr std::string_view GetSchema(const Info1&) {
+    return "uint8 value";
+  }
+  static ThingC Unpack(std::span<const uint8_t> data, const Info1&) {
+    return ThingC{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingC& value, const Info1&) {
+    data[0] = value.x;
+  }
+};
+
+template <>
+struct wpi::Struct<ThingC, Info2> {
+  static constexpr std::string_view GetTypeString(const Info2&) {
+    return "struct:ThingC";
+  }
+  static constexpr size_t GetSize(const Info2&) { return 1; }
+  static constexpr std::string_view GetSchema(const Info2&) {
+    return "uint8 value";
+  }
+  static ThingC Unpack(std::span<const uint8_t> data, const Info2&) {
+    return ThingC{.x = data[0]};
+  }
+  static void Pack(std::span<uint8_t> data, const ThingC& value, const Info2&) {
+    data[0] = value.x;
+  }
+};
+
+static_assert(wpi::StructSerializable<ThingA>);
+static_assert(!wpi::StructSerializable<ThingA, Info1>);
+
+static_assert(!wpi::StructSerializable<ThingB>);
+static_assert(wpi::StructSerializable<ThingB, Info1>);
+static_assert(!wpi::StructSerializable<ThingB, Info2>);
+
+static_assert(wpi::StructSerializable<ThingC>);
+static_assert(wpi::StructSerializable<ThingC, Info1>);
+static_assert(wpi::StructSerializable<ThingC, Info2>);
+
+TEST(DataLogTest, SimpleInt) {
+  std::vector<uint8_t> data;
+  {
+    wpi::log::DataLog log{
+        [&](auto out) { data.insert(data.end(), out.begin(), out.end()); }};
+    int entry = log.Start("test", "int64");
+    log.AppendInteger(entry, 1, 0);
+  }
+  ASSERT_EQ(data.size(), 66u);
+}
+
+TEST(DataLogTest, StructA) {
+  wpi::log::DataLog log{[](auto) {}};
+  [[maybe_unused]] wpi::log::StructLogEntry<ThingA> entry0;
+  wpi::log::StructLogEntry<ThingA> entry{log, "a", 5};
+  entry.Append(ThingA{});
+  entry.Append(ThingA{}, 7);
+}
+
+TEST(DataLogTest, StructArrayA) {
+  wpi::log::DataLog log{[](auto) {}};
+  [[maybe_unused]] wpi::log::StructArrayLogEntry<ThingA> entry0;
+  wpi::log::StructArrayLogEntry<ThingA> entry{log, "a", 5};
+  entry.Append({{ThingA{}, ThingA{}}});
+  entry.Append({{ThingA{}, ThingA{}}}, 7);
+}
+
+TEST(DataLogTest, StructFixedArrayA) {
+  wpi::log::DataLog log{[](auto) {}};
+  [[maybe_unused]] wpi::log::StructArrayLogEntry<std::array<ThingA, 2>> entry0;
+  wpi::log::StructLogEntry<std::array<ThingA, 2>> entry{log, "a", 5};
+  std::array<ThingA, 2> arr;
+  entry.Append(arr);
+  entry.Append(arr, 7);
+}
+
+TEST(DataLogTest, StructB) {
+  wpi::log::DataLog log{[](auto) {}};
+  Info1 info;
+  [[maybe_unused]] wpi::log::StructLogEntry<ThingB, Info1> entry0;
+  wpi::log::StructLogEntry<ThingB, Info1> entry{log, "b", info, 5};
+  entry.Append(ThingB{});
+  entry.Append(ThingB{}, 7);
+}
+
+TEST(DataLogTest, StructArrayB) {
+  wpi::log::DataLog log{[](auto) {}};
+  Info1 info;
+  [[maybe_unused]] wpi::log::StructArrayLogEntry<ThingB, Info1> entry0;
+  wpi::log::StructArrayLogEntry<ThingB, Info1> entry{log, "a", info, 5};
+  entry.Append({{ThingB{}, ThingB{}}});
+  entry.Append({{ThingB{}, ThingB{}}}, 7);
+}
+
+TEST(DataLogTest, StructFixedArrayB) {
+  wpi::log::DataLog log{[](auto) {}};
+  Info1 info;
+  wpi::log::StructLogEntry<std::array<ThingB, 2>, Info1> entry{log, "a", info,
+                                                               5};
+  std::array<ThingB, 2> arr;
+  entry.Append(arr);
+  entry.Append(arr, 7);
+}
+
+TEST(DataLogTest, StructC) {
+  wpi::log::DataLog log{[](auto) {}};
+  {
+    wpi::log::StructLogEntry<ThingC> entry{log, "c", 5};
+    entry.Append(ThingC{});
+    entry.Append(ThingC{}, 7);
+  }
+  {
+    Info1 info;
+    wpi::log::StructLogEntry<ThingC, Info1> entry{log, "c1", info, 5};
+    entry.Append(ThingC{});
+    entry.Append(ThingC{}, 7);
+  }
+  {
+    Info2 info;
+    wpi::log::StructLogEntry<ThingC, Info2> entry{log, "c2", info, 5};
+    entry.Append(ThingC{});
+    entry.Append(ThingC{}, 7);
+  }
+}
+
+TEST(DataLogTest, StructArrayC) {
+  wpi::log::DataLog log{[](auto) {}};
+  {
+    wpi::log::StructArrayLogEntry<ThingC> entry{log, "c", 5};
+    entry.Append({{ThingC{}, ThingC{}}});
+    entry.Append({{ThingC{}, ThingC{}}}, 7);
+  }
+  {
+    Info1 info;
+    wpi::log::StructArrayLogEntry<ThingC, Info1> entry{log, "c1", info, 5};
+    entry.Append({{ThingC{}, ThingC{}}});
+    entry.Append({{ThingC{}, ThingC{}}}, 7);
+  }
+  {
+    Info2 info;
+    wpi::log::StructArrayLogEntry<ThingC, Info2> entry{log, "c2", info, 5};
+    entry.Append({{ThingC{}, ThingC{}}});
+    entry.Append({{ThingC{}, ThingC{}}}, 7);
+  }
+}
+
+TEST(DataLogTest, StructFixedArrayC) {
+  wpi::log::DataLog log{[](auto) {}};
+  std::array<ThingC, 2> arr;
+  {
+    wpi::log::StructLogEntry<std::array<ThingC, 2>> entry{log, "c", 5};
+    entry.Append(arr);
+    entry.Append(arr, 7);
+  }
+  {
+    Info1 info;
+    wpi::log::StructLogEntry<std::array<ThingC, 2>, Info1> entry{log, "c1",
+                                                                 info, 5};
+    entry.Append(arr);
+    entry.Append(arr, 7);
+  }
+  {
+    Info2 info;
+    wpi::log::StructLogEntry<std::array<ThingC, 2>, Info2> entry{log, "c2",
+                                                                 info, 5};
+    entry.Append(arr);
+    entry.Append(arr, 7);
+  }
+}
diff --git a/wpiutil/wpiutil-config.cmake.in b/wpiutil/wpiutil-config.cmake.in
index 3f696c8..3926ecb 100644
--- a/wpiutil/wpiutil-config.cmake.in
+++ b/wpiutil/wpiutil-config.cmake.in
@@ -2,6 +2,7 @@
 @FILENAME_DEP_REPLACE@
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 find_dependency(Threads)
+find_dependency(Protobuf)
 @FMTLIB_SYSTEM_REPLACE@
 
 if(@USE_SYSTEM_FMTLIB@)
@@ -10,3 +11,6 @@
 
 @FILENAME_DEP_REPLACE@
 include(${SELF_DIR}/wpiutil.cmake)
+if(@WITH_JAVA@)
+    include(${SELF_DIR}/wpiutil_jar.cmake)
+endif()