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

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

Change-Id: Ia1dd90fe42c6cd5df281b8a5b710e136f54355f4
git-subtree-dir: third_party/allwpilib
git-subtree-split: f1a82828fed8950f9a3f1586c44327027627a0c8
Signed-off-by: James Kuszmaul <jabukuszmaul+collab@gmail.com>
diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp
index a20ff8b..0b3473c 100644
--- a/glass/src/app/native/cpp/main.cpp
+++ b/glass/src/app/native/cpp/main.cpp
@@ -8,7 +8,9 @@
 #include <fmt/format.h>
 #include <imgui.h>
 #include <ntcore_cpp.h>
+#include <wpi/StringExtras.h>
 #include <wpigui.h>
+#include <wpigui_openurl.h>
 
 #include "glass/Context.h"
 #include "glass/MainMenuBar.h"
@@ -52,6 +54,8 @@
 static bool gKeyEdit = false;
 static int* gEnterKey;
 static void (*gPrevKeyCallback)(GLFWwindow*, int, int, int, int);
+static bool gNetworkTablesDebugLog = false;
+static unsigned int gPrevMode = NT_NET_MODE_NONE;
 
 static void RemapEnterKeyCallback(GLFWwindow* window, int key, int scancode,
                                   int action, int mods) {
@@ -69,27 +73,46 @@
   }
 }
 
+/**
+ * Generates the proper title bar title based on current instance state and
+ * event.
+ */
+static std::string MakeTitle(NT_Inst inst, nt::Event event) {
+  auto mode = nt::GetNetworkMode(inst);
+  if (mode & NT_NET_MODE_SERVER) {
+    auto numClients = nt::GetConnections(inst).size();
+    return fmt::format("Glass - {} Client{} Connected", numClients,
+                       (numClients == 1 ? "" : "s"));
+  } else if (mode & NT_NET_MODE_CLIENT3 || mode & NT_NET_MODE_CLIENT4) {
+    if (event.Is(NT_EVENT_CONNECTED)) {
+      return fmt::format("Glass - Connected ({})",
+                         event.GetConnectionInfo()->remote_ip);
+    }
+  }
+  return "Glass - DISCONNECTED";
+}
+
 static void NtInitialize() {
   auto inst = nt::GetDefaultInstance();
   auto poller = nt::CreateListenerPoller(inst);
-  nt::AddPolledListener(
-      poller, inst,
-      NT_EVENT_CONNECTION | NT_EVENT_IMMEDIATE | NT_EVENT_LOGMESSAGE);
-  gui::AddEarlyExecute([poller] {
+  nt::AddPolledListener(poller, inst, NT_EVENT_CONNECTION | NT_EVENT_IMMEDIATE);
+  nt::AddPolledLogger(poller, 0, 100);
+  gui::AddEarlyExecute([inst, poller] {
     auto win = gui::GetSystemWindow();
     if (!win) {
       return;
     }
+    bool updateTitle = false;
+    nt::Event connectionEvent;
+    if (nt::GetNetworkMode(inst) != gPrevMode) {
+      gPrevMode = nt::GetNetworkMode(inst);
+      updateTitle = true;
+    }
+
     for (auto&& event : nt::ReadListenerQueue(poller)) {
-      if (auto connInfo = event.GetConnectionInfo()) {
-        // update window title when connection status changes
-        if ((event.flags & NT_EVENT_CONNECTED) != 0) {
-          glfwSetWindowTitle(
-              win, fmt::format("Glass - Connected ({})", connInfo->remote_ip)
-                       .c_str());
-        } else {
-          glfwSetWindowTitle(win, "Glass - DISCONNECTED");
-        }
+      if (event.Is(NT_EVENT_CONNECTION)) {
+        updateTitle = true;
+        connectionEvent = event;
       } else if (auto msg = event.GetLogMessage()) {
         const char* level = "";
         if (msg->level >= NT_LOG_CRITICAL) {
@@ -98,11 +121,17 @@
           level = "ERROR: ";
         } else if (msg->level >= NT_LOG_WARNING) {
           level = "WARNING: ";
+        } else if (msg->level < NT_LOG_INFO && !gNetworkTablesDebugLog) {
+          continue;
         }
         gNetworkTablesLog.Append(fmt::format(
             "{}{} ({}:{})\n", level, msg->message, msg->filename, msg->line));
       }
     }
+
+    if (updateTitle) {
+      glfwSetWindowTitle(win, MakeTitle(inst, connectionEvent).c_str());
+    }
   });
 
   gNetworkTablesLogWindow = std::make_unique<glass::Window>(
@@ -232,6 +261,8 @@
       if (gNetworkTablesLogWindow) {
         gNetworkTablesLogWindow->DisplayMenuItem("NetworkTables Log");
       }
+      ImGui::MenuItem("NetworkTables Debug Logging", nullptr,
+                      &gNetworkTablesDebugLog);
       ImGui::Separator();
       gNtProvider->DisplayMenu();
       ImGui::EndMenu();
@@ -252,6 +283,15 @@
       }
       ImGui::EndMenu();
     }
+
+    if (ImGui::BeginMenu("Docs")) {
+      if (ImGui::MenuItem("Online documentation")) {
+        wpi::gui::OpenURL(
+            "https://docs.wpilib.org/en/stable/docs/software/dashboards/"
+            "glass/");
+      }
+      ImGui::EndMenu();
+    }
   });
 
   gui::AddLateExecute([] {
@@ -265,6 +305,8 @@
       ImGui::Text("v%s", GetWPILibVersion());
       ImGui::Separator();
       ImGui::Text("Save location: %s", glass::GetStorageDir().c_str());
+      ImGui::Text("%.3f ms/frame (%.1f FPS)",
+                  1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
       if (ImGui::Button("Close")) {
         ImGui::CloseCurrentPopup();
       }
@@ -286,11 +328,13 @@
       char nameBuf[32];
       const char* name = glfwGetKeyName(*gEnterKey, 0);
       if (!name) {
-        std::snprintf(nameBuf, sizeof(nameBuf), "%d", *gEnterKey);
+        wpi::format_to_n_c_str(nameBuf, sizeof(nameBuf), "{}", *gEnterKey);
+
         name = nameBuf;
       }
-      std::snprintf(editLabel, sizeof(editLabel), "%s###edit",
-                    gKeyEdit ? "(press key)" : name);
+      wpi::format_to_n_c_str(editLabel, sizeof(editLabel), "{}###edit",
+                             gKeyEdit ? "(press key)" : name);
+
       if (ImGui::SmallButton(editLabel)) {
         gKeyEdit = true;
       }
diff --git a/glass/src/lib/native/cpp/Context.cpp b/glass/src/lib/native/cpp/Context.cpp
index a55cf82..e09a86e 100644
--- a/glass/src/lib/native/cpp/Context.cpp
+++ b/glass/src/lib/native/cpp/Context.cpp
@@ -6,18 +6,16 @@
 
 #include <algorithm>
 #include <cinttypes>
-#include <cstdio>
 #include <filesystem>
 
 #include <fmt/format.h>
 #include <imgui.h>
 #include <imgui_internal.h>
 #include <imgui_stdlib.h>
+#include <wpi/MemoryBuffer.h>
 #include <wpi/StringExtras.h>
 #include <wpi/fs.h>
 #include <wpi/json.h>
-#include <wpi/json_serializer.h>
-#include <wpi/raw_istream.h>
 #include <wpi/raw_ostream.h>
 #include <wpi/timestamp.h>
 #include <wpigui.h>
@@ -131,14 +129,17 @@
 
 static bool LoadWindowStorageImpl(const std::string& filename) {
   std::error_code ec;
-  wpi::raw_fd_istream is{filename, ec};
-  if (ec) {
+  std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
+      wpi::MemoryBuffer::GetFile(filename, ec);
+  if (fileBuffer == nullptr || ec) {
     ImGui::LogText("error opening %s: %s", filename.c_str(),
                    ec.message().c_str());
     return false;
   } else {
     try {
-      return JsonToWindow(wpi::json::parse(is), filename.c_str());
+      return JsonToWindow(
+          wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
+          filename.c_str());
     } catch (wpi::json::parse_error& e) {
       ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
       return false;
@@ -149,8 +150,9 @@
 static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
                                 std::string_view rootName) {
   std::error_code ec;
-  wpi::raw_fd_istream is{filename, ec};
-  if (ec) {
+  std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
+      wpi::MemoryBuffer::GetFile(filename, ec);
+  if (fileBuffer == nullptr || ec) {
     ImGui::LogText("error opening %s: %s", filename.c_str(),
                    ec.message().c_str());
     return false;
@@ -162,7 +164,9 @@
       createdStorage = true;
     }
     try {
-      storage->FromJson(wpi::json::parse(is), filename.c_str());
+      storage->FromJson(
+          wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
+          filename.c_str());
     } catch (wpi::json::parse_error& e) {
       ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
       if (createdStorage) {
@@ -533,7 +537,8 @@
 
 void glass::PushID(int int_id) {
   char buf[16];
-  std::snprintf(buf, sizeof(buf), "%d", int_id);
+  wpi::format_to_n_c_str(buf, sizeof(buf), "{}", int_id);
+
   PushStorageStack(buf);
   ImGui::PushID(int_id);
 }
diff --git a/glass/src/lib/native/cpp/MainMenuBar.cpp b/glass/src/lib/native/cpp/MainMenuBar.cpp
index 879f664..b426df4 100644
--- a/glass/src/lib/native/cpp/MainMenuBar.cpp
+++ b/glass/src/lib/native/cpp/MainMenuBar.cpp
@@ -4,9 +4,8 @@
 
 #include "glass/MainMenuBar.h"
 
-#include <cstdio>
-
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 #include <wpigui.h>
 
 #include "glass/Context.h"
@@ -52,11 +51,11 @@
 
 #if 0
   char str[64];
-  std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)",
-                1000.0f / ImGui::GetIO().Framerate,
-                ImGui::GetIO().Framerate);
-  ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x -
-                  10);
+  wpi::format_to_n_c_str(str, sizeof(str), "{:.3f} ms/frame ({:.1f} FPS)",
+                         1000.0f / ImGui::GetIO().Framerate,
+                         ImGui::GetIO().Framerate);
+
+  ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x - 10);
   ImGui::Text("%s", str);
 #endif
   ImGui::EndMainMenuBar();
diff --git a/glass/src/lib/native/cpp/Storage.cpp b/glass/src/lib/native/cpp/Storage.cpp
index add6203..6cab443 100644
--- a/glass/src/lib/native/cpp/Storage.cpp
+++ b/glass/src/lib/native/cpp/Storage.cpp
@@ -4,7 +4,7 @@
 
 #include "glass/Storage.h"
 
-#include <type_traits>
+#include <concepts>
 
 #include <imgui.h>
 #include <wpi/StringExtras.h>
@@ -14,7 +14,7 @@
 
 template <typename To>
 bool ConvertFromString(To* out, std::string_view str) {
-  if constexpr (std::is_same_v<To, bool>) {
+  if constexpr (std::same_as<To, bool>) {
     if (str == "true") {
       *out = true;
     } else if (str == "false") {
@@ -24,7 +24,7 @@
     } else {
       return false;
     }
-  } else if constexpr (std::is_floating_point_v<To>) {
+  } else if constexpr (std::floating_point<To>) {
     if (auto val = wpi::parse_float<To>(str)) {
       *out = val.value();
     } else {
@@ -95,10 +95,14 @@
 template <typename From, typename To>
 static void ConvertArray(std::vector<To>** outPtr, std::vector<From>** inPtr) {
   if (*inPtr) {
-    std::vector<To>* tmp;
-    tmp = new std::vector<To>{(*inPtr)->begin(), (*inPtr)->end()};
-    delete *inPtr;
-    *outPtr = tmp;
+    if (*outPtr) {
+      (*outPtr)->assign((*inPtr)->begin(), (*inPtr)->end());
+    } else {
+      std::vector<To>* tmp;
+      tmp = new std::vector<To>{(*inPtr)->begin(), (*inPtr)->end()};
+      delete *inPtr;
+      *outPtr = tmp;
+    }
   } else {
     *outPtr = nullptr;
   }
@@ -300,7 +304,7 @@
     childPtr = std::make_unique<Value>();
   }
   if (childPtr->type != Value::kChild) {
-    childPtr->type = Value::kChild;
+    childPtr->Reset(Value::kChild);
     childPtr->child = new Storage;
   }
   return *childPtr->child;
@@ -630,22 +634,46 @@
         value.stringVal = value.stringDefault;
         break;
       case Value::kIntArray:
-        *value.intArray = *value.intArrayDefault;
+        if (value.intArrayDefault) {
+          *value.intArray = *value.intArrayDefault;
+        } else {
+          value.intArray->clear();
+        }
         break;
       case Value::kInt64Array:
-        *value.int64Array = *value.int64ArrayDefault;
+        if (value.int64ArrayDefault) {
+          *value.int64Array = *value.int64ArrayDefault;
+        } else {
+          value.int64Array->clear();
+        }
         break;
       case Value::kBoolArray:
-        *value.boolArray = *value.boolArrayDefault;
+        if (value.boolArrayDefault) {
+          *value.boolArray = *value.boolArrayDefault;
+        } else {
+          value.boolArray->clear();
+        }
         break;
       case Value::kFloatArray:
-        *value.floatArray = *value.floatArrayDefault;
+        if (value.floatArrayDefault) {
+          *value.floatArray = *value.floatArrayDefault;
+        } else {
+          value.floatArray->clear();
+        }
         break;
       case Value::kDoubleArray:
-        *value.doubleArray = *value.doubleArrayDefault;
+        if (value.doubleArrayDefault) {
+          *value.doubleArray = *value.doubleArrayDefault;
+        } else {
+          value.doubleArray->clear();
+        }
         break;
       case Value::kStringArray:
-        *value.stringArray = *value.stringArrayDefault;
+        if (value.stringArrayDefault) {
+          *value.stringArray = *value.stringArrayDefault;
+        } else {
+          value.stringArray->clear();
+        }
         break;
       case Value::kChild:
         value.child->Clear();
diff --git a/glass/src/lib/native/cpp/Window.cpp b/glass/src/lib/native/cpp/Window.cpp
index f43c0ee..32c7a21 100644
--- a/glass/src/lib/native/cpp/Window.cpp
+++ b/glass/src/lib/native/cpp/Window.cpp
@@ -56,9 +56,12 @@
   }
 
   char label[128];
-  std::snprintf(label, sizeof(label), "%s###%s",
-                m_name.empty() ? m_defaultName.c_str() : m_name.c_str(),
-                m_id.c_str());
+  if (m_name.empty()) {
+    wpi::format_to_n_c_str(label, sizeof(label), "{}###{}", m_defaultName,
+                           m_id);
+  } else {
+    wpi::format_to_n_c_str(label, sizeof(label), "{}###{}", m_name, m_id);
+  }
 
   if (Begin(label, &m_visible, m_flags)) {
     if (m_renamePopupEnabled || m_view->HasSettings()) {
diff --git a/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp
index be06a71..259539f 100644
--- a/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp
+++ b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp
@@ -4,6 +4,8 @@
 
 #include "glass/hardware/AnalogGyro.h"
 
+#include <wpi/StringExtras.h>
+
 #include "glass/DataSource.h"
 #include "glass/other/DeviceTree.h"
 
@@ -11,7 +13,8 @@
 
 void glass::DisplayAnalogGyroDevice(AnalogGyroModel* model, int index) {
   char name[32];
-  std::snprintf(name, sizeof(name), "AnalogGyro[%d]", index);
+  wpi::format_to_n_c_str(name, sizeof(name), "AnalogGyro[{}]", index);
+
   if (BeginDevice(name)) {
     // angle
     if (auto angleData = model->GetAngleData()) {
diff --git a/glass/src/lib/native/cpp/hardware/AnalogInput.cpp b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp
index af22511..a2051cf 100644
--- a/glass/src/lib/native/cpp/hardware/AnalogInput.cpp
+++ b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp
@@ -5,6 +5,7 @@
 #include "glass/hardware/AnalogInput.h"
 
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -22,9 +23,9 @@
   std::string& name = GetStorage().GetString("name");
   char label[128];
   if (!name.empty()) {
-    std::snprintf(label, sizeof(label), "%s [%d]###name", name.c_str(), index);
+    wpi::format_to_n_c_str(label, sizeof(label), "{} [{}]###name", name, index);
   } else {
-    std::snprintf(label, sizeof(label), "In[%d]###name", index);
+    wpi::format_to_n_c_str(label, sizeof(label), "In[{}]###name", index);
   }
 
   if (model->IsGyro()) {
diff --git a/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp
index 174e013..2436dd2 100644
--- a/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp
+++ b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp
@@ -4,6 +4,8 @@
 
 #include "glass/hardware/AnalogOutput.h"
 
+#include <wpi/StringExtras.h>
+
 #include "glass/Context.h"
 #include "glass/DataSource.h"
 #include "glass/Storage.h"
@@ -30,9 +32,9 @@
       std::string& name = GetStorage().GetString("name");
       char label[128];
       if (!name.empty()) {
-        std::snprintf(label, sizeof(label), "%s [%d]###name", name.c_str(), i);
+        wpi::format_to_n_c_str(label, sizeof(label), "{} [{}]###name", name, i);
       } else {
-        std::snprintf(label, sizeof(label), "Out[%d]###name", i);
+        wpi::format_to_n_c_str(label, sizeof(label), "Out[{}]###name", i);
       }
 
       double value = analogOutData->GetValue();
diff --git a/glass/src/lib/native/cpp/hardware/Encoder.cpp b/glass/src/lib/native/cpp/hardware/Encoder.cpp
index 7032636..b359274 100644
--- a/glass/src/lib/native/cpp/hardware/Encoder.cpp
+++ b/glass/src/lib/native/cpp/hardware/Encoder.cpp
@@ -6,6 +6,7 @@
 
 #include <fmt/format.h>
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -70,10 +71,11 @@
   std::string& name = GetStorage().GetString("name");
   char label[128];
   if (!name.empty()) {
-    std::snprintf(label, sizeof(label), "%s [%d,%d]###header", name.c_str(),
-                  chA, chB);
+    wpi::format_to_n_c_str(label, sizeof(label), "{} [{},{}]###header", name,
+                           chA, chB);
   } else {
-    std::snprintf(label, sizeof(label), "Encoder[%d,%d]###header", chA, chB);
+    wpi::format_to_n_c_str(label, sizeof(label), "Encoder[{},{}]###header", chA,
+                           chB);
   }
 
   // header
diff --git a/glass/src/lib/native/cpp/hardware/Gyro.cpp b/glass/src/lib/native/cpp/hardware/Gyro.cpp
index 607b251..8ba5d5e 100644
--- a/glass/src/lib/native/cpp/hardware/Gyro.cpp
+++ b/glass/src/lib/native/cpp/hardware/Gyro.cpp
@@ -5,12 +5,13 @@
 #include "glass/hardware/Gyro.h"
 
 #include <cmath>
+#include <numbers>
 
 #define IMGUI_DEFINE_MATH_OPERATORS
 
 #include <imgui.h>
 #include <imgui_internal.h>
-#include <numbers>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -65,7 +66,8 @@
                   color, 1.2f);
     if (major) {
       char txt[16];
-      std::snprintf(txt, sizeof(txt), "%d°", i);
+      wpi::format_to_n_c_str(txt, sizeof(txt), "{}°", i);
+
       draw->AddText(
           center + (direction * radius * 1.25) - ImGui::CalcTextSize(txt) * 0.5,
           primaryColor, txt, nullptr);
diff --git a/glass/src/lib/native/cpp/hardware/PCM.cpp b/glass/src/lib/native/cpp/hardware/PCM.cpp
index d260bda..6238fd9 100644
--- a/glass/src/lib/native/cpp/hardware/PCM.cpp
+++ b/glass/src/lib/native/cpp/hardware/PCM.cpp
@@ -9,6 +9,7 @@
 
 #include <imgui.h>
 #include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -46,10 +47,10 @@
   std::string& name = GetStorage().GetString("name");
   char label[128];
   if (!name.empty()) {
-    std::snprintf(label, sizeof(label), "%s [%d]###header", name.c_str(),
-                  index);
+    wpi::format_to_n_c_str(label, sizeof(label), "{} [{}]###header", name,
+                           index);
   } else {
-    std::snprintf(label, sizeof(label), "PCM[%d]###header", index);
+    wpi::format_to_n_c_str(label, sizeof(label), "PCM[{}]###header", index);
   }
 
   // header
@@ -111,7 +112,8 @@
 void glass::DisplayCompressorDevice(CompressorModel* model, int index,
                                     bool outputsEnabled) {
   char name[32];
-  std::snprintf(name, sizeof(name), "Compressor[%d]", index);
+  wpi::format_to_n_c_str(name, sizeof(name), "Compressor[{}]", index);
+
   if (BeginDevice(name)) {
     // output enabled
     if (auto runningData = model->GetRunningData()) {
diff --git a/glass/src/lib/native/cpp/hardware/PWM.cpp b/glass/src/lib/native/cpp/hardware/PWM.cpp
index 0200ac6..f719a2b 100644
--- a/glass/src/lib/native/cpp/hardware/PWM.cpp
+++ b/glass/src/lib/native/cpp/hardware/PWM.cpp
@@ -5,6 +5,7 @@
 #include "glass/hardware/PWM.h"
 
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -22,9 +23,9 @@
   std::string& name = GetStorage().GetString("name");
   char label[128];
   if (!name.empty()) {
-    std::snprintf(label, sizeof(label), "%s [%d]###name", name.c_str(), index);
+    wpi::format_to_n_c_str(label, sizeof(label), "{} [{}]###name", name, index);
   } else {
-    std::snprintf(label, sizeof(label), "PWM[%d]###name", index);
+    wpi::format_to_n_c_str(label, sizeof(label), "PWM[{}]###name", index);
   }
 
   int led = model->GetAddressableLED();
diff --git a/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp b/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp
index f1de461..90aea0e 100644
--- a/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp
+++ b/glass/src/lib/native/cpp/hardware/PowerDistribution.cpp
@@ -5,9 +5,9 @@
 #include "glass/hardware/PowerDistribution.h"
 
 #include <algorithm>
-#include <cstdio>
 
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -36,7 +36,8 @@
 
 void glass::DisplayPowerDistribution(PowerDistributionModel* model, int index) {
   char name[128];
-  std::snprintf(name, sizeof(name), "PowerDistribution[%d]", index);
+  wpi::format_to_n_c_str(name, sizeof(name), "PowerDistribution[{}]", index);
+
   if (CollapsingHeader(name)) {
     // temperature
     if (auto tempData = model->GetTemperatureData()) {
diff --git a/glass/src/lib/native/cpp/other/DeviceTree.cpp b/glass/src/lib/native/cpp/other/DeviceTree.cpp
index cfce8c4..b242c07 100644
--- a/glass/src/lib/native/cpp/other/DeviceTree.cpp
+++ b/glass/src/lib/native/cpp/other/DeviceTree.cpp
@@ -7,6 +7,7 @@
 #include <cinttypes>
 
 #include <imgui.h>
+#include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/ContextInternal.h"
@@ -53,8 +54,11 @@
   // build label
   std::string& name = GetStorage().GetString("name");
   char label[128];
-  std::snprintf(label, sizeof(label), "%s###header",
-                name.empty() ? id : name.c_str());
+  if (name.empty()) {
+    wpi::format_to_n_c_str(label, sizeof(label), "{}###header", id);
+  } else {
+    wpi::format_to_n_c_str(label, sizeof(label), "{}###header", name);
+  }
 
   bool open = CollapsingHeader(label, flags);
   PopupEditName("header", &name);
diff --git a/glass/src/lib/native/cpp/other/FMS.cpp b/glass/src/lib/native/cpp/other/FMS.cpp
index fbd504e..67c3f8c 100644
--- a/glass/src/lib/native/cpp/other/FMS.cpp
+++ b/glass/src/lib/native/cpp/other/FMS.cpp
@@ -11,8 +11,8 @@
 
 using namespace glass;
 
-static const char* stations[] = {"Red 1",  "Red 2",  "Red 3",
-                                 "Blue 1", "Blue 2", "Blue 3"};
+static const char* stations[] = {"Invalid", "Red 1",  "Red 2", "Red 3",
+                                 "Blue 1",  "Blue 2", "Blue 3"};
 
 void glass::DisplayFMS(FMSModel* model) {
   if (!model->Exists() || model->IsReadOnly()) {
@@ -41,7 +41,7 @@
   if (auto data = model->GetAllianceStationIdData()) {
     int val = data->GetValue();
     ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
-    if (ImGui::Combo("Alliance Station", &val, stations, 6)) {
+    if (ImGui::Combo("Alliance Station", &val, stations, 7)) {
       model->SetAllianceStationId(val);
     }
     data->EmitDrag();
diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp
index 66e90b1..a99da09 100644
--- a/glass/src/lib/native/cpp/other/Field2D.cpp
+++ b/glass/src/lib/native/cpp/other/Field2D.cpp
@@ -11,6 +11,7 @@
 #include <string_view>
 #include <utility>
 
+#include <fields/fields.h>
 #include <fmt/format.h>
 #include <frc/geometry/Pose2d.h>
 #include <frc/geometry/Rotation2d.h>
@@ -23,12 +24,12 @@
 #include <portable-file-dialogs.h>
 #include <units/angle.h>
 #include <units/length.h>
+#include <wpi/MemoryBuffer.h>
 #include <wpi/SmallString.h>
 #include <wpi/StringExtras.h>
 #include <wpi/StringMap.h>
 #include <wpi/fs.h>
 #include <wpi/json.h>
-#include <wpi/raw_istream.h>
 #include <wpigui.h>
 
 #include "glass/Context.h"
@@ -237,10 +238,12 @@
  private:
   void Reset();
   bool LoadImageImpl(const std::string& fn);
-  void LoadJson(std::string_view jsonfile);
+  bool LoadJson(std::span<const char> is, std::string_view filename);
+  void LoadJsonFile(std::string_view jsonfile);
 
   std::unique_ptr<pfd::open_file> m_fileOpener;
 
+  std::string& m_builtin;
   std::string& m_filename;
   gui::Texture m_texture;
 
@@ -340,7 +343,8 @@
 }
 
 FieldInfo::FieldInfo(Storage& storage)
-    : m_filename{storage.GetString("image")},
+    : m_builtin{storage.GetString("builtin")},
+      m_filename{storage.GetString("image")},
       m_width{storage.GetFloat("width", kDefaultWidth.to<float>())},
       m_height{storage.GetFloat("height", kDefaultHeight.to<float>())},
       m_top{storage.GetInt("top", 0)},
@@ -349,7 +353,25 @@
       m_right{storage.GetInt("right", -1)} {}
 
 void FieldInfo::DisplaySettings() {
-  if (ImGui::Button("Choose image...")) {
+  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
+  if (ImGui::BeginCombo("Image",
+                        m_builtin.empty() ? "Custom" : m_builtin.c_str())) {
+    if (ImGui::Selectable("Custom", m_builtin.empty())) {
+      Reset();
+    }
+    for (auto&& field : fields::GetFields()) {
+      bool selected = field.name == m_builtin;
+      if (ImGui::Selectable(field.name, selected)) {
+        Reset();
+        m_builtin = field.name;
+      }
+      if (selected) {
+        ImGui::SetItemDefaultFocus();
+      }
+    }
+    ImGui::EndCombo();
+  }
+  if (m_builtin.empty() && ImGui::Button("Load image...")) {
     m_fileOpener = std::make_unique<pfd::open_file>(
         "Choose field image", "",
         std::vector<std::string>{"Image File",
@@ -370,6 +392,7 @@
 
 void FieldInfo::Reset() {
   m_texture = gui::Texture{};
+  m_builtin.clear();
   m_filename.clear();
   m_imageWidth = 0;
   m_imageHeight = 0;
@@ -384,7 +407,7 @@
     auto result = m_fileOpener->result();
     if (!result.empty()) {
       if (wpi::ends_with(result[0], ".json")) {
-        LoadJson(result[0]);
+        LoadJsonFile(result[0]);
       } else {
         LoadImageImpl(result[0].c_str());
         m_top = 0;
@@ -395,33 +418,46 @@
     }
     m_fileOpener.reset();
   }
-  if (!m_texture && !m_filename.empty()) {
-    if (!LoadImageImpl(m_filename)) {
-      m_filename.clear();
+  if (!m_texture) {
+    if (!m_builtin.empty()) {
+      for (auto&& field : fields::GetFields()) {
+        if (field.name == m_builtin) {
+          auto jsonstr = field.getJson();
+          auto imagedata = field.getImage();
+          auto texture = gui::Texture::CreateFromImage(
+              reinterpret_cast<const unsigned char*>(imagedata.data()),
+              imagedata.size());
+          if (texture && LoadJson({jsonstr.data(), jsonstr.size()}, {})) {
+            m_texture = std::move(texture);
+            m_imageWidth = m_texture.GetWidth();
+            m_imageHeight = m_texture.GetHeight();
+          } else {
+            m_builtin.clear();
+          }
+        }
+      }
+    } else if (!m_filename.empty()) {
+      if (!LoadImageImpl(m_filename)) {
+        m_filename.clear();
+      }
     }
   }
 }
 
-void FieldInfo::LoadJson(std::string_view jsonfile) {
-  std::error_code ec;
-  wpi::raw_fd_istream f(jsonfile, ec);
-  if (ec) {
-    std::fputs("GUI: could not open field JSON file\n", stderr);
-    return;
-  }
-
+bool FieldInfo::LoadJson(std::span<const char> is, std::string_view filename) {
   // parse file
   wpi::json j;
   try {
-    j = wpi::json::parse(f);
+    j = wpi::json::parse(is);
   } catch (const wpi::json::parse_error& e) {
     fmt::print(stderr, "GUI: JSON: could not parse: {}\n", e.what());
+    return false;
   }
 
   // top level must be an object
   if (!j.is_object()) {
     std::fputs("GUI: JSON: does not contain a top object\n", stderr);
-    return;
+    return false;
   }
 
   // image filename
@@ -430,7 +466,7 @@
     image = j.at("field-image").get<std::string>();
   } catch (const wpi::json::exception& e) {
     fmt::print(stderr, "GUI: JSON: could not read field-image: {}\n", e.what());
-    return;
+    return false;
   }
 
   // corners
@@ -443,7 +479,7 @@
   } catch (const wpi::json::exception& e) {
     fmt::print(stderr, "GUI: JSON: could not read field-corners: {}\n",
                e.what());
-    return;
+    return false;
   }
 
   // size
@@ -454,7 +490,7 @@
     height = j.at("field-size").at(1).get<float>();
   } catch (const wpi::json::exception& e) {
     fmt::print(stderr, "GUI: JSON: could not read field-size: {}\n", e.what());
-    return;
+    return false;
   }
 
   // units for size
@@ -463,7 +499,7 @@
     unit = j.at("field-unit").get<std::string>();
   } catch (const wpi::json::exception& e) {
     fmt::print(stderr, "GUI: JSON: could not read field-unit: {}\n", e.what());
-    return;
+    return false;
   }
 
   // convert size units to meters
@@ -472,22 +508,38 @@
     height = units::convert<units::feet, units::meters>(height);
   }
 
-  // the image filename is relative to the json file
-  auto pathname = fs::path{jsonfile}.replace_filename(image).string();
+  if (!filename.empty()) {
+    // the image filename is relative to the json file
+    auto pathname = fs::path{filename}.replace_filename(image).string();
 
-  // load field image
-  if (!LoadImageImpl(pathname.c_str())) {
-    return;
+    // load field image
+    if (!LoadImageImpl(pathname.c_str())) {
+      return false;
+    }
+    m_filename = pathname;
   }
 
   // save to field info
-  m_filename = pathname;
   m_top = top;
   m_left = left;
   m_bottom = bottom;
   m_right = right;
   m_width = width;
   m_height = height;
+  return true;
+}
+
+void FieldInfo::LoadJsonFile(std::string_view jsonfile) {
+  std::error_code ec;
+  std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
+      wpi::MemoryBuffer::GetFile(jsonfile, ec);
+  if (fileBuffer == nullptr || ec) {
+    std::fputs("GUI: could not open field JSON file\n", stderr);
+    return;
+  }
+  LoadJson(
+      {reinterpret_cast<const char*>(fileBuffer->begin()), fileBuffer->size()},
+      jsonfile);
 }
 
 bool FieldInfo::LoadImageImpl(const std::string& fn) {
diff --git a/glass/src/lib/native/cpp/other/Plot.cpp b/glass/src/lib/native/cpp/other/Plot.cpp
index 13d7c96..0b61709 100644
--- a/glass/src/lib/native/cpp/other/Plot.cpp
+++ b/glass/src/lib/native/cpp/other/Plot.cpp
@@ -8,7 +8,6 @@
 
 #include <algorithm>
 #include <atomic>
-#include <cstdio>
 #include <cstring>
 #include <memory>
 #include <string>
@@ -16,6 +15,7 @@
 #include <vector>
 
 #include <fmt/format.h>
+#include <wpi/StringExtras.h>
 
 #if defined(__GNUC__)
 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
@@ -135,6 +135,8 @@
     }
   }
 
+  void SetColor(const ImVec4& color) { m_backgroundColor.SetColor(color); }
+
  private:
   void EmitSettingsLimits(int axis);
   void DragDropAccept(PlotView& view, size_t i, int yAxis);
@@ -143,6 +145,9 @@
 
   std::string& m_name;
   bool& m_visible;
+  static constexpr float kDefaultBackgroundColor[4] = {0.0, 0.0, 0.0,
+                                                       IMPLOT_AUTO};
+  ColorSetting m_backgroundColor;
   bool& m_showPause;
   bool& m_lockPrevX;
   bool& m_legend;
@@ -316,8 +321,8 @@
   CheckSource();
 
   char label[128];
-  std::snprintf(label, sizeof(label), "%s###name%d_%d", GetName(),
-                static_cast<int>(i), static_cast<int>(plotIndex));
+  wpi::format_to_n_c_str(label, sizeof(label), "{}###name{}_{}", GetName(),
+                         static_cast<int>(i), static_cast<int>(plotIndex));
 
   int size = m_size;
   int offset = m_offset;
@@ -484,6 +489,8 @@
     : m_seriesStorage{storage.GetChildArray("series")},
       m_name{storage.GetString("name")},
       m_visible{storage.GetBool("visible", true)},
+      m_backgroundColor{
+          storage.GetFloatArray("backgroundColor", kDefaultBackgroundColor)},
       m_showPause{storage.GetBool("showPause", true)},
       m_lockPrevX{storage.GetBool("lockPrevX", false)},
       m_legend{storage.GetBool("legend", true)},
@@ -573,13 +580,19 @@
   }
 
   char label[128];
-  std::snprintf(label, sizeof(label), "%s###plot%d", m_name.c_str(),
-                static_cast<int>(i));
+  wpi::format_to_n_c_str(label, sizeof(label), "{}###plot{}", m_name,
+                         static_cast<int>(i));
+
   ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) |
                           (m_crosshairs ? ImPlotFlags_Crosshairs : 0) |
                           (m_mousePosition ? 0 : ImPlotFlags_NoMouseText);
 
   if (ImPlot::BeginPlot(label, ImVec2(-1, m_height), plotFlags)) {
+    if (m_backgroundColor.GetColorFloat()[3] == IMPLOT_AUTO) {
+      SetColor(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
+    }
+    ImPlot::PushStyleColor(ImPlotCol_PlotBg, m_backgroundColor.GetColor());
+
     // setup legend
     if (m_legend) {
       ImPlotLegendFlags legendFlags =
@@ -656,6 +669,8 @@
     m_xaxisRange = ImPlot::GetPlotLimits().X;
 
     ImPlotPlot* plot = ImPlot::GetCurrentPlot();
+
+    ImPlot::PopStyleColor();
     ImPlot::EndPlot();
 
     // copy plot settings back to storage
@@ -715,6 +730,12 @@
   ImGui::Text("Edit plot name:");
   ImGui::InputText("##editname", &m_name);
   ImGui::Checkbox("Visible", &m_visible);
+  m_backgroundColor.ColorEdit3("Background color",
+                               ImGuiColorEditFlags_NoInputs);
+  ImGui::SameLine();
+  if (ImGui::Button("Default")) {
+    SetColor(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
+  }
   ImGui::Checkbox("Show Pause Button", &m_showPause);
   if (i != 0) {
     ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
@@ -917,14 +938,15 @@
 
     char name[64];
     if (!plot->GetName().empty()) {
-      std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
+      wpi::format_to_n_c_str(name, sizeof(name), "{}", plot->GetName().c_str());
     } else {
-      std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
+      wpi::format_to_n_c_str(name, sizeof(name), "Plot {}",
+                             static_cast<int>(i));
     }
 
     char label[90];
-    std::snprintf(label, sizeof(label), "%s###header%d", name,
-                  static_cast<int>(i));
+    wpi::format_to_n_c_str(label, sizeof(label), "{}###header{}", name,
+                           static_cast<int>(i));
 
     bool open = ImGui::CollapsingHeader(label);
 
@@ -993,7 +1015,8 @@
     char id[32];
     size_t numWindows = m_windows.size();
     for (size_t i = 0; i <= numWindows; ++i) {
-      std::snprintf(id, sizeof(id), "Plot <%d>", static_cast<int>(i));
+      wpi::format_to_n_c_str(id, sizeof(id), "Plot <{}>", static_cast<int>(i));
+
       bool match = false;
       for (size_t j = 0; j < numWindows; ++j) {
         if (m_windows[j]->GetId() == id) {
diff --git a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
index 191634e..dede4a0 100644
--- a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
+++ b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
@@ -4,9 +4,8 @@
 
 #include "glass/support/ExtraGuiWidgets.h"
 
-#include <imgui.h>
-
 #define IMGUI_DEFINE_MATH_OPERATORS
+#include <imgui.h>
 #include <imgui_internal.h>
 
 #include "glass/DataSource.h"
diff --git a/glass/src/lib/native/cpp/support/NameSetting.cpp b/glass/src/lib/native/cpp/support/NameSetting.cpp
index 1dc1d20..cfc7ab3 100644
--- a/glass/src/lib/native/cpp/support/NameSetting.cpp
+++ b/glass/src/lib/native/cpp/support/NameSetting.cpp
@@ -4,9 +4,6 @@
 
 #include "glass/support/NameSetting.h"
 
-#include <cstdio>
-#include <cstring>
-
 #include <imgui_internal.h>
 #include <imgui_stdlib.h>
 #include <wpi/StringExtras.h>
@@ -16,75 +13,80 @@
 void NameSetting::GetName(char* buf, size_t size,
                           const char* defaultName) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s", m_name.c_str());
+    wpi::format_to_n_c_str(buf, size, "{}", m_name);
   } else {
-    std::snprintf(buf, size, "%s", defaultName);
+    wpi::format_to_n_c_str(buf, size, "{}", defaultName);
   }
 }
 
 void NameSetting::GetName(char* buf, size_t size, const char* defaultName,
                           int index) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s [%d]", m_name.c_str(), index);
+    wpi::format_to_n_c_str(buf, size, "{} [{}]", m_name, index);
   } else {
-    std::snprintf(buf, size, "%s[%d]", defaultName, index);
+    wpi::format_to_n_c_str(buf, size, "{}[{}]", defaultName, index);
   }
 }
 
 void NameSetting::GetName(char* buf, size_t size, const char* defaultName,
                           int index, int index2) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s [%d,%d]", m_name.c_str(), index, index2);
+    wpi::format_to_n_c_str(buf, size, "{} [{},{}]", m_name, index, index2);
   } else {
-    std::snprintf(buf, size, "%s[%d,%d]", defaultName, index, index2);
+    wpi::format_to_n_c_str(buf, size, "{}[{},{}]", defaultName, index, index2);
   }
 }
 
 void NameSetting::GetLabel(char* buf, size_t size,
                            const char* defaultName) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s###Name%s", m_name.c_str(), defaultName);
+    wpi::format_to_n_c_str(buf, size, "{}###Name{}", m_name, defaultName);
   } else {
-    std::snprintf(buf, size, "%s###Name%s", defaultName, defaultName);
+    wpi::format_to_n_c_str(buf, size, "{}###Name{}", defaultName, defaultName);
   }
 }
 
 void NameSetting::GetLabel(char* buf, size_t size, const char* defaultName,
                            int index) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s [%d]###Name%d", m_name.c_str(), index, index);
+    wpi::format_to_n_c_str(buf, size, "{} [{}]###Name{}", m_name, index, index);
   } else {
-    std::snprintf(buf, size, "%s[%d]###Name%d", defaultName, index, index);
+    wpi::format_to_n_c_str(buf, size, "{}[{}]###Name{}", defaultName, index,
+                           index);
   }
 }
 
 void NameSetting::GetLabel(char* buf, size_t size, const char* defaultName,
                            int index, int index2) const {
   if (!m_name.empty()) {
-    std::snprintf(buf, size, "%s [%d,%d]###Name%d", m_name.c_str(), index,
-                  index2, index);
+    wpi::format_to_n_c_str(buf, size, "{} [{},{}]###Name{}", m_name, index,
+                           index2, index);
   } else {
-    std::snprintf(buf, size, "%s[%d,%d]###Name%d", defaultName, index, index2,
-                  index);
+    wpi::format_to_n_c_str(buf, size, "{}[{},{}]###Name{}", defaultName, index,
+                           index2, index);
   }
 }
 
 void NameSetting::PushEditNameId(int index) {
   char id[64];
-  std::snprintf(id, sizeof(id), "Name%d", index);
+  wpi::format_to_n_c_str(id, sizeof(id), "Name{}", index);
+
   ImGui::PushID(id);
 }
 
 void NameSetting::PushEditNameId(const char* name) {
   char id[128];
-  std::snprintf(id, sizeof(id), "Name%s", name);
+  wpi::format_to_n_c_str(id, sizeof(id), "Name{}", name);
+
   ImGui::PushID(id);
 }
 
 bool NameSetting::PopupEditName(int index) {
   bool rv = false;
+
   char id[64];
-  std::snprintf(id, sizeof(id), "Name%d", index);
+  wpi::format_to_n_c_str(id, sizeof(id), "Name{}", index);
+
   if (ImGui::BeginPopupContextItem(id)) {
     ImGui::Text("Edit name:");
     if (InputTextName("##edit")) {
@@ -101,8 +103,10 @@
 
 bool NameSetting::PopupEditName(const char* name) {
   bool rv = false;
+
   char id[128];
-  std::snprintf(id, sizeof(id), "Name%s", name);
+  wpi::format_to_n_c_str(id, sizeof(id), "Name{}", name);
+
   if (ImGui::BeginPopupContextItem(id)) {
     ImGui::Text("Edit name:");
     if (InputTextName("##edit")) {
diff --git a/glass/src/lib/native/include/glass/Context.h b/glass/src/lib/native/include/glass/Context.h
index e8dada3..f343d33 100644
--- a/glass/src/lib/native/include/glass/Context.h
+++ b/glass/src/lib/native/include/glass/Context.h
@@ -4,6 +4,8 @@
 
 #pragma once
 
+#include <stdint.h>
+
 #include <functional>
 #include <string>
 #include <string_view>
diff --git a/glass/src/lib/native/include/glass/Storage.h b/glass/src/lib/native/include/glass/Storage.h
index 7ebfa6d..bdb2b3d 100644
--- a/glass/src/lib/native/include/glass/Storage.h
+++ b/glass/src/lib/native/include/glass/Storage.h
@@ -16,10 +16,7 @@
 
 #include <wpi/StringMap.h>
 #include <wpi/iterator_range.h>
-
-namespace wpi {
-class json;
-}  // namespace wpi
+#include <wpi/json_fwd.h>
 
 namespace glass {
 
diff --git a/glass/src/lib/native/include/glass/Window.h b/glass/src/lib/native/include/glass/Window.h
index 0a37f9a..62b369c 100644
--- a/glass/src/lib/native/include/glass/Window.h
+++ b/glass/src/lib/native/include/glass/Window.h
@@ -9,6 +9,7 @@
 #include <string_view>
 #include <utility>
 
+#define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui.h>
 
 #include "glass/View.h"
diff --git a/glass/src/lib/native/include/glass/other/Field2D.h b/glass/src/lib/native/include/glass/other/Field2D.h
index 9c9f72a..2b0f9a8 100644
--- a/glass/src/lib/native/include/glass/other/Field2D.h
+++ b/glass/src/lib/native/include/glass/other/Field2D.h
@@ -10,6 +10,8 @@
 #include <frc/geometry/Pose2d.h>
 #include <frc/geometry/Rotation2d.h>
 #include <frc/geometry/Translation2d.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui.h>
 #include <wpi/function_ref.h>
 
diff --git a/glass/src/lib/native/include/glass/other/Mechanism2D.h b/glass/src/lib/native/include/glass/other/Mechanism2D.h
index ab5ccdc..440fed3 100644
--- a/glass/src/lib/native/include/glass/other/Mechanism2D.h
+++ b/glass/src/lib/native/include/glass/other/Mechanism2D.h
@@ -6,6 +6,8 @@
 
 #include <frc/geometry/Rotation2d.h>
 #include <frc/geometry/Translation2d.h>
+
+#define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui.h>
 #include <wpi/function_ref.h>
 
diff --git a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
index 6788434..d56f342 100644
--- a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
+++ b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui.h>
 
 namespace glass {
diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp
index d368359..57469a2 100644
--- a/glass/src/libnt/native/cpp/NetworkTables.cpp
+++ b/glass/src/libnt/native/cpp/NetworkTables.cpp
@@ -5,7 +5,7 @@
 #include "glass/networktables/NetworkTables.h"
 
 #include <cinttypes>
-#include <cstdio>
+#include <concepts>
 #include <cstring>
 #include <initializer_list>
 #include <memory>
@@ -14,7 +14,10 @@
 #include <vector>
 
 #include <fmt/format.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/message.h>
 #include <imgui.h>
+#include <imgui_stdlib.h>
 #include <networktables/NetworkTableInstance.h>
 #include <networktables/NetworkTableValue.h>
 #include <ntcore_c.h>
@@ -58,36 +61,6 @@
   }
 }
 
-static std::string BooleanArrayToString(std::span<const int> in) {
-  std::string rv;
-  wpi::raw_string_ostream os{rv};
-  os << '[';
-  bool first = true;
-  for (auto v : in) {
-    if (!first) {
-      os << ',';
-    }
-    first = false;
-    if (v) {
-      os << "true";
-    } else {
-      os << "false";
-    }
-  }
-  os << ']';
-  return rv;
-}
-
-static std::string IntegerArrayToString(std::span<const int64_t> in) {
-  return fmt::format("[{:d}]", fmt::join(in, ","));
-}
-
-template <typename T>
-static std::string FloatArrayToString(std::span<const T> in) {
-  static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>);
-  return fmt::format("[{:.6f}]", fmt::join(in, ","));
-}
-
 static std::string StringArrayToString(std::span<const std::string> in) {
   std::string rv;
   wpi::raw_string_ostream os{rv};
@@ -143,36 +116,37 @@
   }
 }
 
-static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
+static void UpdateMsgpackValueSource(NetworkTablesModel& model,
+                                     NetworkTablesModel::ValueSource* out,
                                      mpack_reader_t& r, std::string_view name,
                                      int64_t time) {
   mpack_tag_t tag = mpack_read_tag(&r);
   switch (mpack_tag_type(&tag)) {
     case mpack::mpack_type_bool:
-      out->UpdateFromValue(
-          nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time), name, "");
+      out->value = nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case mpack::mpack_type_int:
-      out->UpdateFromValue(
-          nt::Value::MakeInteger(mpack_tag_int_value(&tag), time), name, "");
+      out->value = nt::Value::MakeInteger(mpack_tag_int_value(&tag), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case mpack::mpack_type_uint:
-      out->UpdateFromValue(
-          nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time), name, "");
+      out->value = nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case mpack::mpack_type_float:
-      out->UpdateFromValue(
-          nt::Value::MakeFloat(mpack_tag_float_value(&tag), time), name, "");
+      out->value = nt::Value::MakeFloat(mpack_tag_float_value(&tag), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case mpack::mpack_type_double:
-      out->UpdateFromValue(
-          nt::Value::MakeDouble(mpack_tag_double_value(&tag), time), name, "");
+      out->value = nt::Value::MakeDouble(mpack_tag_double_value(&tag), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case mpack::mpack_type_str: {
       std::string str;
       mpack_read_str(&r, &tag, &str);
-      out->UpdateFromValue(nt::Value::MakeString(std::move(str), time), name,
-                           "");
+      out->value = nt::Value::MakeString(std::move(str), time);
+      out->UpdateFromValue(model, name, "");
       break;
     }
     case mpack::mpack_type_bin:
@@ -193,7 +167,8 @@
           child.path = fmt::format("{}{}", name, child.name);
         }
         ++i;
-        UpdateMsgpackValueSource(&child, r, child.path, time);  // recurse
+        UpdateMsgpackValueSource(model, &child, r, child.path,
+                                 time);  // recurse
       }
       mpack_done_array(&r);
       break;
@@ -215,7 +190,7 @@
           auto it = elems.find(key);
           if (it != elems.end()) {
             auto& child = out->valueChildren[it->second];
-            UpdateMsgpackValueSource(&child, r, child.path, time);
+            UpdateMsgpackValueSource(model, &child, r, child.path, time);
             elems.erase(it);
           } else {
             added = true;
@@ -223,7 +198,7 @@
             auto& child = out->valueChildren.back();
             child.name = std::move(key);
             child.path = fmt::format("{}/{}", name, child.name);
-            UpdateMsgpackValueSource(&child, r, child.path, time);
+            UpdateMsgpackValueSource(model, &child, r, child.path, time);
           }
         }
       }
@@ -248,7 +223,318 @@
   }
 }
 
-static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
+static void UpdateStructValueSource(NetworkTablesModel& model,
+                                    NetworkTablesModel::ValueSource* out,
+                                    const wpi::DynamicStruct& s,
+                                    std::string_view name, int64_t time) {
+  auto desc = s.GetDescriptor();
+  out->typeStr = "struct:" + desc->GetName();
+  auto& fields = desc->GetFields();
+  if (!out->valueChildrenMap || fields.size() != out->valueChildren.size()) {
+    out->valueChildren.clear();
+    out->valueChildrenMap = true;
+    out->valueChildren.reserve(fields.size());
+    for (auto&& field : fields) {
+      out->valueChildren.emplace_back();
+      auto& child = out->valueChildren.back();
+      child.name = field.GetName();
+      child.path = fmt::format("{}/{}", name, child.name);
+    }
+  }
+  auto outIt = out->valueChildren.begin();
+  for (auto&& field : fields) {
+    auto& child = *outIt++;
+    switch (field.GetType()) {
+      case wpi::StructFieldType::kBool:
+        if (field.IsArray()) {
+          std::vector<int> v;
+          v.reserve(field.GetArraySize());
+          for (size_t i = 0; i < field.GetArraySize(); ++i) {
+            v.emplace_back(s.GetBoolField(&field, i));
+          }
+          child.value = nt::Value::MakeBooleanArray(std::move(v), time);
+        } else {
+          child.value = nt::Value::MakeBoolean(s.GetBoolField(&field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case wpi::StructFieldType::kChar:
+        child.value = nt::Value::MakeString(s.GetStringField(&field), time);
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case wpi::StructFieldType::kInt8:
+      case wpi::StructFieldType::kInt16:
+      case wpi::StructFieldType::kInt32:
+      case wpi::StructFieldType::kInt64:
+      case wpi::StructFieldType::kUint8:
+      case wpi::StructFieldType::kUint16:
+      case wpi::StructFieldType::kUint32:
+      case wpi::StructFieldType::kUint64: {
+        bool isUint = field.IsUint();
+        if (field.IsArray()) {
+          std::vector<int64_t> v;
+          v.reserve(field.GetArraySize());
+          for (size_t i = 0; i < field.GetArraySize(); ++i) {
+            if (isUint) {
+              v.emplace_back(s.GetUintField(&field, i));
+            } else {
+              v.emplace_back(s.GetIntField(&field, i));
+            }
+          }
+          child.value = nt::Value::MakeIntegerArray(std::move(v), time);
+        } else {
+          if (isUint) {
+            child.value = nt::Value::MakeInteger(s.GetUintField(&field), time);
+          } else {
+            child.value = nt::Value::MakeInteger(s.GetIntField(&field), time);
+          }
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      }
+      case wpi::StructFieldType::kFloat:
+        if (field.IsArray()) {
+          std::vector<float> v;
+          v.reserve(field.GetArraySize());
+          for (size_t i = 0; i < field.GetArraySize(); ++i) {
+            v.emplace_back(s.GetFloatField(&field, i));
+          }
+          child.value = nt::Value::MakeFloatArray(std::move(v), time);
+        } else {
+          child.value = nt::Value::MakeFloat(s.GetFloatField(&field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case wpi::StructFieldType::kDouble:
+        if (field.IsArray()) {
+          std::vector<double> v;
+          v.reserve(field.GetArraySize());
+          for (size_t i = 0; i < field.GetArraySize(); ++i) {
+            v.emplace_back(s.GetDoubleField(&field, i));
+          }
+          child.value = nt::Value::MakeDoubleArray(std::move(v), time);
+        } else {
+          child.value = nt::Value::MakeDouble(s.GetDoubleField(&field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case wpi::StructFieldType::kStruct:
+        if (field.IsArray()) {
+          if (child.valueChildrenMap) {
+            child.valueChildren.clear();
+            child.valueChildrenMap = false;
+          }
+          child.valueChildren.resize(field.GetArraySize());
+          unsigned int i = 0;
+          for (auto&& child2 : child.valueChildren) {
+            if (child2.name.empty()) {
+              child2.name = fmt::format("[{}]", i);
+              child2.path = fmt::format("{}{}", name, child.name);
+            }
+            UpdateStructValueSource(model, &child2, s.GetStructField(&field, i),
+                                    child2.path, time);  // recurse
+            ++i;
+          }
+        } else {
+          UpdateStructValueSource(model, &child, s.GetStructField(&field),
+                                  child.path, time);  // recurse
+        }
+        break;
+    }
+  }
+}
+
+static void UpdateProtobufValueSource(NetworkTablesModel& model,
+                                      NetworkTablesModel::ValueSource* out,
+                                      const google::protobuf::Message& msg,
+                                      std::string_view name, int64_t time) {
+  auto desc = msg.GetDescriptor();
+  out->typeStr = "proto:" + desc->full_name();
+  if (!out->valueChildrenMap ||
+      desc->field_count() != static_cast<int>(out->valueChildren.size())) {
+    out->valueChildren.clear();
+    out->valueChildrenMap = true;
+    out->valueChildren.reserve(desc->field_count());
+    for (int i = 0, end = desc->field_count(); i < end; ++i) {
+      out->valueChildren.emplace_back();
+      auto& child = out->valueChildren.back();
+      child.name = desc->field(i)->name();
+      child.path = fmt::format("{}/{}", name, child.name);
+    }
+  }
+  auto refl = msg.GetReflection();
+  auto outIt = out->valueChildren.begin();
+  for (int fieldNum = 0, end = desc->field_count(); fieldNum < end;
+       ++fieldNum) {
+    auto field = desc->field(fieldNum);
+    auto& child = *outIt++;
+    switch (field->cpp_type()) {
+      case google::protobuf::FieldDescriptor::CPPTYPE_BOOL:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<int> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedBool(msg, field, i));
+          }
+          child.value = nt::Value::MakeBooleanArray(std::move(v), time);
+        } else {
+          child.value = nt::Value::MakeBoolean(refl->GetBool(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_STRING:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<std::string> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedString(msg, field, i));
+          }
+          child.value = nt::Value::MakeStringArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeString(refl->GetString(msg, field), time);
+          child.UpdateFromValue(model, child.path, "");
+        }
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_INT32:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<int64_t> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedInt32(msg, field, i));
+          }
+          child.value = nt::Value::MakeIntegerArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeInteger(refl->GetInt32(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_INT64:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<int64_t> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedInt64(msg, field, i));
+          }
+          child.value = nt::Value::MakeIntegerArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeInteger(refl->GetInt64(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_UINT32:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<int64_t> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedUInt32(msg, field, i));
+          }
+          child.value = nt::Value::MakeIntegerArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeInteger(refl->GetUInt32(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_UINT64:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<int64_t> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedUInt64(msg, field, i));
+          }
+          child.value = nt::Value::MakeIntegerArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeInteger(refl->GetUInt64(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<float> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedFloat(msg, field, i));
+          }
+          child.value = nt::Value::MakeFloatArray(std::move(v), time);
+        } else {
+          child.value = nt::Value::MakeFloat(refl->GetFloat(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<double> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedDouble(msg, field, i));
+          }
+          child.value = nt::Value::MakeDoubleArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeDouble(refl->GetDouble(msg, field), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_ENUM:
+        if (field->is_repeated()) {
+          size_t size = refl->FieldSize(msg, field);
+          std::vector<std::string> v;
+          v.reserve(size);
+          for (size_t i = 0; i < size; ++i) {
+            v.emplace_back(refl->GetRepeatedEnum(msg, field, i)->name());
+          }
+          child.value = nt::Value::MakeStringArray(std::move(v), time);
+        } else {
+          child.value =
+              nt::Value::MakeString(refl->GetEnum(msg, field)->name(), time);
+        }
+        child.UpdateFromValue(model, child.path, "");
+        break;
+      case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE:
+        if (field->is_repeated()) {
+          if (child.valueChildrenMap) {
+            child.valueChildren.clear();
+            child.valueChildrenMap = false;
+          }
+          size_t size = refl->FieldSize(msg, field);
+          child.valueChildren.resize(size);
+          unsigned int i = 0;
+          for (auto&& child2 : child.valueChildren) {
+            if (child2.name.empty()) {
+              child2.name = fmt::format("[{}]", i);
+              child2.path = fmt::format("{}{}", name, child.name);
+            }
+            UpdateProtobufValueSource(model, &child2,
+                                      refl->GetRepeatedMessage(msg, field, i),
+                                      child2.path, time);  // recurse
+            ++i;
+          }
+        } else {
+          UpdateProtobufValueSource(
+              model, &child,
+              refl->GetMessage(msg, field,
+                               model.GetProtobufDatabase().GetMessageFactory()),
+              child.path, time);  // recurse
+        }
+        break;
+    }
+  }
+}
+
+static void UpdateJsonValueSource(NetworkTablesModel& model,
+                                  NetworkTablesModel::ValueSource* out,
                                   const wpi::json& j, std::string_view name,
                                   int64_t time) {
   switch (j.type()) {
@@ -266,7 +552,7 @@
         auto it = elems.find(kv.key());
         if (it != elems.end()) {
           auto& child = out->valueChildren[it->second];
-          UpdateJsonValueSource(&child, kv.value(), child.path, time);
+          UpdateJsonValueSource(model, &child, kv.value(), child.path, time);
           elems.erase(it);
         } else {
           added = true;
@@ -274,7 +560,7 @@
           auto& child = out->valueChildren.back();
           child.name = kv.key();
           child.path = fmt::format("{}/{}", name, child.name);
-          UpdateJsonValueSource(&child, kv.value(), child.path, time);
+          UpdateJsonValueSource(model, &child, kv.value(), child.path, time);
         }
       }
       // erase unmatched keys
@@ -302,31 +588,30 @@
           child.name = fmt::format("[{}]", i);
           child.path = fmt::format("{}{}", name, child.name);
         }
-        ++i;
-        UpdateJsonValueSource(&child, j[i], child.path, time);  // recurse
+        // recurse
+        UpdateJsonValueSource(model, &child, j[i++], child.path, time);
       }
       break;
     }
     case wpi::json::value_t::string:
-      out->UpdateFromValue(
-          nt::Value::MakeString(j.get_ref<const std::string&>(), time), name,
-          "");
+      out->value = nt::Value::MakeString(j.get_ref<const std::string&>(), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case wpi::json::value_t::boolean:
-      out->UpdateFromValue(nt::Value::MakeBoolean(j.get<bool>(), time), name,
-                           "");
+      out->value = nt::Value::MakeBoolean(j.get<bool>(), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case wpi::json::value_t::number_integer:
-      out->UpdateFromValue(nt::Value::MakeInteger(j.get<int64_t>(), time), name,
-                           "");
+      out->value = nt::Value::MakeInteger(j.get<int64_t>(), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case wpi::json::value_t::number_unsigned:
-      out->UpdateFromValue(nt::Value::MakeInteger(j.get<uint64_t>(), time),
-                           name, "");
+      out->value = nt::Value::MakeInteger(j.get<uint64_t>(), time);
+      out->UpdateFromValue(model, name, "");
       break;
     case wpi::json::value_t::number_float:
-      out->UpdateFromValue(nt::Value::MakeDouble(j.get<double>(), time), name,
-                           "");
+      out->value = nt::Value::MakeDouble(j.get<double>(), time);
+      out->UpdateFromValue(model, name, "");
       break;
     default:
       out->value = {};
@@ -334,81 +619,161 @@
   }
 }
 
+void NetworkTablesModel::ValueSource::UpdateDiscreteSource(
+    std::string_view name, double value, int64_t time, bool digital) {
+  valueChildren.clear();
+  if (!source) {
+    source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
+  }
+  source->SetValue(value, time);
+  source->SetDigital(digital);
+}
+
+template <typename T, typename MakeValue>
+void NetworkTablesModel::ValueSource::UpdateDiscreteArray(
+    std::string_view name, std::span<const T> arr, int64_t time,
+    MakeValue makeValue, bool digital) {
+  if (valueChildrenMap) {
+    valueChildren.clear();
+    valueChildrenMap = false;
+  }
+  valueChildren.resize(arr.size());
+  unsigned int i = 0;
+  for (auto&& child : valueChildren) {
+    if (child.name.empty()) {
+      child.name = fmt::format("[{}]", i);
+      child.path = fmt::format("{}{}", name, child.name);
+    }
+    child.value = makeValue(arr[i], time);
+    child.UpdateDiscreteSource(child.path, arr[i], time, digital);
+    ++i;
+  }
+}
+
 void NetworkTablesModel::ValueSource::UpdateFromValue(
-    nt::Value&& v, std::string_view name, std::string_view typeStr) {
-  value = v;
+    NetworkTablesModel& model, std::string_view name,
+    std::string_view typeStr) {
   switch (value.type()) {
     case NT_BOOLEAN:
-      valueChildren.clear();
-      if (!source) {
-        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
-      }
-      source->SetValue(value.GetBoolean() ? 1 : 0, value.last_change());
-      source->SetDigital(true);
+      UpdateDiscreteSource(name, value.GetBoolean() ? 1 : 0, value.time(),
+                           true);
       break;
     case NT_INTEGER:
-      valueChildren.clear();
-      if (!source) {
-        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
-      }
-      source->SetValue(value.GetInteger(), value.last_change());
-      source->SetDigital(false);
+      UpdateDiscreteSource(name, value.GetInteger(), value.time());
       break;
     case NT_FLOAT:
-      valueChildren.clear();
-      if (!source) {
-        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
-      }
-      source->SetValue(value.GetFloat(), value.last_change());
-      source->SetDigital(false);
+      UpdateDiscreteSource(name, value.GetFloat(), value.time());
       break;
     case NT_DOUBLE:
-      valueChildren.clear();
-      if (!source) {
-        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
-      }
-      source->SetValue(value.GetDouble(), value.last_change());
-      source->SetDigital(false);
+      UpdateDiscreteSource(name, value.GetDouble(), value.time());
       break;
     case NT_BOOLEAN_ARRAY:
-      valueChildren.clear();
-      valueStr = BooleanArrayToString(value.GetBooleanArray());
+      UpdateDiscreteArray(name, value.GetBooleanArray(), value.time(),
+                          nt::Value::MakeBoolean, true);
       break;
     case NT_INTEGER_ARRAY:
-      valueChildren.clear();
-      valueStr = IntegerArrayToString(value.GetIntegerArray());
+      UpdateDiscreteArray(name, value.GetIntegerArray(), value.time(),
+                          nt::Value::MakeInteger);
       break;
     case NT_FLOAT_ARRAY:
-      valueChildren.clear();
-      valueStr = FloatArrayToString(value.GetFloatArray());
+      UpdateDiscreteArray(name, value.GetFloatArray(), value.time(),
+                          nt::Value::MakeFloat);
       break;
     case NT_DOUBLE_ARRAY:
-      valueChildren.clear();
-      valueStr = FloatArrayToString(value.GetDoubleArray());
+      UpdateDiscreteArray(name, value.GetDoubleArray(), value.time(),
+                          nt::Value::MakeDouble);
       break;
-    case NT_STRING_ARRAY:
-      valueChildren.clear();
-      valueStr = StringArrayToString(value.GetStringArray());
+    case NT_STRING_ARRAY: {
+      auto arr = value.GetStringArray();
+      if (valueChildrenMap) {
+        valueChildren.clear();
+        valueChildrenMap = false;
+      }
+      valueChildren.resize(arr.size());
+      unsigned int i = 0;
+      for (auto&& child : valueChildren) {
+        if (child.name.empty()) {
+          child.name = fmt::format("[{}]", i);
+          child.path = fmt::format("{}{}", name, child.name);
+        }
+        child.value = nt::Value::MakeString(arr[i++], value.time());
+        child.UpdateFromValue(model, child.path, "");
+      }
       break;
+    }
     case NT_STRING:
       if (typeStr == "json") {
         try {
-          UpdateJsonValueSource(this, wpi::json::parse(value.GetString()), name,
+          UpdateJsonValueSource(model, this,
+                                wpi::json::parse(value.GetString()), name,
                                 value.last_change());
         } catch (wpi::json::exception&) {
           // ignore
         }
       } else {
         valueChildren.clear();
+        valueStr.clear();
+        wpi::raw_string_ostream os{valueStr};
+        os << '"';
+        os.write_escaped(value.GetString());
+        os << '"';
       }
       break;
     case NT_RAW:
       if (typeStr == "msgpack") {
         mpack_reader_t r;
         mpack_reader_init_data(&r, value.GetRaw());
-        UpdateMsgpackValueSource(this, r, name, value.last_change());
-
+        UpdateMsgpackValueSource(model, this, r, name, value.last_change());
         mpack_reader_destroy(&r);
+      } else if (wpi::starts_with(typeStr, "struct:")) {
+        auto structName = wpi::drop_front(typeStr, 7);
+        bool isArray = structName.ends_with("[]");
+        if (isArray) {
+          structName = wpi::drop_back(structName, 2);
+        }
+        auto desc = model.m_structDb.Find(structName);
+        if (desc && desc->IsValid()) {
+          if (isArray) {
+            // array of struct at top level
+            if (valueChildrenMap) {
+              valueChildren.clear();
+              valueChildrenMap = false;
+            }
+            auto raw = value.GetRaw();
+            valueChildren.resize(raw.size() / desc->GetSize());
+            unsigned int i = 0;
+            for (auto&& child : valueChildren) {
+              if (child.name.empty()) {
+                child.name = fmt::format("[{}]", i);
+                child.path = fmt::format("{}{}", name, child.name);
+              }
+              wpi::DynamicStruct s{desc, raw};
+              UpdateStructValueSource(model, &child, s, child.path,
+                                      value.last_change());
+              ++i;
+              raw = wpi::drop_front(raw, desc->GetSize());
+            }
+          } else {
+            wpi::DynamicStruct s{desc, value.GetRaw()};
+            UpdateStructValueSource(model, this, s, name, value.last_change());
+          }
+        } else {
+          valueChildren.clear();
+        }
+      } else if (wpi::starts_with(typeStr, "proto:")) {
+        auto msg = model.m_protoDb.Find(wpi::drop_front(typeStr, 6));
+        if (msg) {
+          msg->Clear();
+          auto raw = value.GetRaw();
+          if (msg->ParseFromArray(raw.data(), raw.size())) {
+            UpdateProtobufValueSource(model, this, *msg, name,
+                                      value.last_change());
+          } else {
+            valueChildren.clear();
+          }
+        } else {
+          valueChildren.clear();
+        }
       } else {
         valueChildren.clear();
       }
@@ -472,8 +837,8 @@
     } else if (auto valueData = event.GetValueEventData()) {
       auto& entry = m_entries[valueData->topic];
       if (entry) {
-        entry->UpdateFromValue(std::move(valueData->value), entry->info.name,
-                               entry->info.type_str);
+        entry->value = std::move(valueData->value);
+        entry->UpdateFromValue(*this);
         if (wpi::starts_with(entry->info.name, '$') && entry->value.IsRaw() &&
             entry->info.type_str == "msgpack") {
           // meta topic handling
@@ -498,6 +863,50 @@
               it->second.UpdateSubscribers(entry->value.GetRaw());
             }
           }
+        } else if (entry->value.IsRaw() &&
+                   wpi::starts_with(entry->info.name, "/.schema/struct:") &&
+                   entry->info.type_str == "structschema") {
+          // struct schema handling
+          auto typeStr = wpi::drop_front(entry->info.name, 16);
+          std::string_view schema{
+              reinterpret_cast<const char*>(entry->value.GetRaw().data()),
+              entry->value.GetRaw().size()};
+          std::string err;
+          auto desc = m_structDb.Add(typeStr, schema, &err);
+          if (!desc) {
+            fmt::print("could not decode struct '{}' schema '{}': {}\n",
+                       entry->info.name, schema, err);
+          } else if (desc->IsValid()) {
+            // loop over all entries with this type and update
+            for (auto&& entryPair : m_entries) {
+              auto ts = entryPair.second->info.type_str;
+              if (!wpi::starts_with(ts, "struct:")) {
+                continue;
+              }
+              ts = wpi::drop_front(ts, 7);
+              if (ts == typeStr || (wpi::ends_with(ts, "[]") &&
+                                    wpi::drop_back(ts, 2) == typeStr)) {
+                entryPair.second->UpdateFromValue(*this);
+              }
+            }
+          }
+        } else if (entry->value.IsRaw() &&
+                   wpi::starts_with(entry->info.name, "/.schema/proto:") &&
+                   entry->info.type_str == "proto:FileDescriptorProto") {
+          // protobuf descriptor handling
+          auto filename = wpi::drop_front(entry->info.name, 15);
+          if (!m_protoDb.Add(filename, entry->value.GetRaw())) {
+            fmt::print("could not decode protobuf '{}' filename '{}'\n",
+                       entry->info.name, filename);
+          } else {
+            // loop over all protobuf entries and update (conservatively)
+            for (auto&& entryPair : m_entries) {
+              auto& ts = entryPair.second->info.type_str;
+              if (wpi::starts_with(ts, "proto:")) {
+                entryPair.second->UpdateFromValue(*this);
+              }
+            }
+          }
         }
       }
     }
@@ -662,153 +1071,89 @@
   m_clients = std::move(newClients);
 }
 
-static bool StringToBooleanArray(std::string_view in, std::vector<int>* out) {
-  in = wpi::trim(in);
-  if (in.empty()) {
-    return false;
-  }
-  if (in.front() == '[') {
-    in.remove_prefix(1);
-  }
-  if (in.back() == ']') {
-    in.remove_suffix(1);
-  }
-  in = wpi::trim(in);
-
-  wpi::SmallVector<std::string_view, 16> inSplit;
-
-  wpi::split(in, inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    val = wpi::trim(val);
-    if (wpi::equals_lower(val, "true")) {
-      out->emplace_back(1);
-    } else if (wpi::equals_lower(val, "false")) {
-      out->emplace_back(0);
-    } else {
-      fmt::print(stderr,
-                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return false;
+static bool GetHeadingTypeString(std::string_view* ts) {
+  if (wpi::starts_with(*ts, "proto:")) {
+    *ts = wpi::drop_front(*ts, 6);
+    auto lastdot = ts->rfind('.');
+    if (lastdot != std::string_view::npos) {
+      *ts = wpi::substr(*ts, lastdot + 1);
     }
+    if (wpi::starts_with(*ts, "Protobuf")) {
+      *ts = wpi::drop_front(*ts, 8);
+    }
+    return true;
+  } else if (wpi::starts_with(*ts, "struct:")) {
+    *ts = wpi::drop_front(*ts, 7);
+    return true;
   }
-
-  return true;
+  return false;
 }
 
-static bool StringToIntegerArray(std::string_view in,
-                                 std::vector<int64_t>* out) {
-  in = wpi::trim(in);
-  if (in.empty()) {
-    return false;
+static const char* GetShortTypeString(std::string_view ts) {
+  if (wpi::starts_with(ts, "proto:")) {
+    return "protobuf";
+  } else if (wpi::starts_with(ts, "struct:")) {
+    return "struct";
+  } else {
+    return ts.data();
   }
-  if (in.front() == '[') {
-    in.remove_prefix(1);
-  }
-  if (in.back() == ']') {
-    in.remove_suffix(1);
-  }
-  in = wpi::trim(in);
-
-  wpi::SmallVector<std::string_view, 16> inSplit;
-
-  wpi::split(in, inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    if (auto num = wpi::parse_integer<int64_t>(wpi::trim(val), 0)) {
-      out->emplace_back(num.value());
-    } else {
-      fmt::print(stderr,
-                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return false;
-    }
-  }
-
-  return true;
 }
 
-template <typename T>
-static bool StringToFloatArray(std::string_view in, std::vector<T>* out) {
-  static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>);
-  in = wpi::trim(in);
-  if (in.empty()) {
-    return false;
+static const char* GetTypeString(NT_Type type, const char* overrideTypeStr) {
+  if (overrideTypeStr) {
+    return GetShortTypeString(overrideTypeStr);
   }
-  if (in.front() == '[') {
-    in.remove_prefix(1);
+  switch (type) {
+    case NT_BOOLEAN:
+      return "boolean";
+    case NT_INTEGER:
+      return "int";
+    case NT_FLOAT:
+      return "float";
+    case NT_DOUBLE:
+      return "double";
+    case NT_STRING:
+      return "string";
+    case NT_BOOLEAN_ARRAY:
+      return "boolean[]";
+    case NT_INTEGER_ARRAY:
+      return "int[]";
+    case NT_FLOAT_ARRAY:
+      return "float[]";
+    case NT_DOUBLE_ARRAY:
+      return "double[]";
+    case NT_STRING_ARRAY:
+      return "string[]";
+    case NT_RAW:
+      return "raw";
+    case NT_RPC:
+      return "rpc";
+    default:
+      return "other";
   }
-  if (in.back() == ']') {
-    in.remove_suffix(1);
-  }
-  in = wpi::trim(in);
-
-  wpi::SmallVector<std::string_view, 16> inSplit;
-
-  wpi::split(in, inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    if (auto num = wpi::parse_float<T>(wpi::trim(val))) {
-      out->emplace_back(num.value());
-    } else {
-      fmt::print(stderr,
-                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return false;
-    }
-  }
-
-  return true;
-}
-
-static bool StringToStringArray(std::string_view in,
-                                std::vector<std::string>* out) {
-  in = wpi::trim(in);
-  if (in.empty()) {
-    return false;
-  }
-  if (in.front() == '[') {
-    in.remove_prefix(1);
-  }
-  if (in.back() == ']') {
-    in.remove_suffix(1);
-  }
-  in = wpi::trim(in);
-
-  wpi::SmallVector<std::string_view, 16> inSplit;
-  wpi::SmallString<32> buf;
-
-  wpi::split(in, inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    val = wpi::trim(val);
-    if (val.empty()) {
-      continue;
-    }
-    if (val.front() != '"' || val.back() != '"') {
-      fmt::print(stderr,
-                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return false;
-    }
-    val.remove_prefix(1);
-    val.remove_suffix(1);
-    out->emplace_back(wpi::UnescapeCString(val, buf).first);
-  }
-
-  return true;
 }
 
 static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry,
-                                   const char* typeStr,
+                                   const char* overrideTypeStr,
                                    NetworkTablesFlags flags) {
   auto& val = entry.value;
   if (!val) {
     return;
   }
 
+  const char* typeStr = GetTypeString(val.type(), overrideTypeStr);
+  ImGui::SetNextItemWidth(
+      -1 * (ImGui::CalcTextSize(typeStr).x + ImGui::GetStyle().FramePadding.x));
+
   switch (val.type()) {
     case NT_BOOLEAN:
-      ImGui::LabelText(typeStr ? typeStr : "boolean", "%s",
-                       val.GetBoolean() ? "true" : "false");
+      ImGui::LabelText(typeStr, "%s", val.GetBoolean() ? "true" : "false");
       break;
     case NT_INTEGER:
-      ImGui::LabelText(typeStr ? typeStr : "int", "%" PRId64, val.GetInteger());
+      ImGui::LabelText(typeStr, "%" PRId64, val.GetInteger());
       break;
     case NT_FLOAT:
-      ImGui::LabelText(typeStr ? typeStr : "double", "%.6f", val.GetFloat());
+      ImGui::LabelText(typeStr, "%.6f", val.GetFloat());
       break;
     case NT_DOUBLE: {
       unsigned char precision = (flags & NetworkTablesFlags_Precision) >>
@@ -817,8 +1162,7 @@
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
 #endif
-      ImGui::LabelText(typeStr ? typeStr : "double",
-                       fmt::format("%.{}f", precision).c_str(),
+      ImGui::LabelText(typeStr, fmt::format("%.{}f", precision).c_str(),
                        val.GetDouble());
 #ifdef __GNUC__
 #pragma GCC diagnostic pop
@@ -826,36 +1170,30 @@
       break;
     }
     case NT_STRING: {
-      // GetString() comes from a std::string, so it's null terminated
-      ImGui::LabelText(typeStr ? typeStr : "string", "%s",
-                       val.GetString().data());
+      ImGui::LabelText(typeStr, "%s", entry.valueStr.c_str());
       break;
     }
     case NT_BOOLEAN_ARRAY:
-      ImGui::LabelText(typeStr ? typeStr : "boolean[]", "%s",
-                       entry.valueStr.c_str());
-      break;
     case NT_INTEGER_ARRAY:
-      ImGui::LabelText(typeStr ? typeStr : "int[]", "%s",
-                       entry.valueStr.c_str());
-      break;
     case NT_FLOAT_ARRAY:
-      ImGui::LabelText(typeStr ? typeStr : "float[]", "%s",
-                       entry.valueStr.c_str());
-      break;
     case NT_DOUBLE_ARRAY:
-      ImGui::LabelText(typeStr ? typeStr : "double[]", "%s",
-                       entry.valueStr.c_str());
-      break;
     case NT_STRING_ARRAY:
-      ImGui::LabelText(typeStr ? typeStr : "string[]", "%s",
-                       entry.valueStr.c_str());
+      ImGui::LabelText(typeStr, "[]");
       break;
-    case NT_RAW:
-      ImGui::LabelText(typeStr ? typeStr : "raw", "[...]");
+    case NT_RAW: {
+      ImGui::LabelText(typeStr, val.GetRaw().empty() ? "[]" : "[...]");
+      if (ImGui::IsItemHovered()) {
+        ImGui::BeginTooltip();
+        if (overrideTypeStr) {
+          ImGui::TextUnformatted(overrideTypeStr);
+        }
+        ImGui::Text("%u bytes", static_cast<unsigned int>(val.GetRaw().size()));
+        ImGui::EndTooltip();
+      }
       break;
+    }
     default:
-      ImGui::LabelText(typeStr ? typeStr : "other", "?");
+      ImGui::LabelText(typeStr, "?");
       break;
   }
 }
@@ -870,21 +1208,165 @@
   return textBuffer;
 }
 
-static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry,
+namespace {
+class ArrayEditor {
+ public:
+  virtual ~ArrayEditor() = default;
+  virtual bool Emit() = 0;
+};
+
+template <int NTType, typename T>
+class ArrayEditorImpl final : public ArrayEditor {
+ public:
+  ArrayEditorImpl(NetworkTablesModel& model, std::string name,
+                  NetworkTablesFlags flags, std::span<const T> value)
+      : m_model{model},
+        m_name{std::move(name)},
+        m_flags{flags},
+        m_arr{value.begin(), value.end()} {}
+
+  bool Emit() final;
+
+ private:
+  NetworkTablesModel& m_model;
+  std::string m_name;
+  NetworkTablesFlags m_flags;
+  std::vector<T> m_arr;
+};
+
+template <int NTType, typename T>
+bool ArrayEditorImpl<NTType, T>::Emit() {
+  if (ImGui::BeginTable(
+          "arrayvalues", 1,
+          ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit |
+              ImGuiTableFlags_RowBg,
+          ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 16))) {
+    ImGui::TableSetupScrollFreeze(0, 1);  // Make top row always visible
+    int toAdd = -1;
+    int toRemove = -1;
+    ImGuiListClipper clipper;
+    clipper.Begin(m_arr.size());
+    while (clipper.Step()) {
+      for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::PushID(row);
+        char label[16];
+        wpi::format_to_n_c_str(label, sizeof(label), "[{}]", row);
+        if constexpr (NTType == NT_BOOLEAN_ARRAY) {
+          static const char* boolOptions[] = {"false", "true"};
+          ImGui::Combo(label, &m_arr[row], boolOptions, 2);
+        } else if constexpr (NTType == NT_FLOAT_ARRAY) {
+          ImGui::InputFloat(label, &m_arr[row], 0, 0, "%.6f");
+        } else if constexpr (NTType == NT_DOUBLE_ARRAY) {
+          unsigned char precision = (m_flags & NetworkTablesFlags_Precision) >>
+                                    kNetworkTablesFlags_PrecisionBitShift;
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+          ImGui::InputDouble(label, &m_arr[row], 0, 0,
+                             fmt::format("%.{}f", precision).c_str());
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+        } else if constexpr (NTType == NT_INTEGER_ARRAY) {
+          ImGui::InputScalar(label, ImGuiDataType_S64, &m_arr[row]);
+        } else if constexpr (NTType == NT_STRING_ARRAY) {
+          ImGui::InputText(label, &m_arr[row]);
+        }
+        ImGui::SameLine();
+        if (ImGui::SmallButton("+")) {
+          toAdd = row;
+        }
+        ImGui::SameLine();
+        if (ImGui::SmallButton("-")) {
+          toRemove = row;
+        }
+        ImGui::PopID();
+      }
+    }
+    if (toAdd != -1) {
+      m_arr.emplace(m_arr.begin() + toAdd);
+    } else if (toRemove != -1) {
+      m_arr.erase(m_arr.begin() + toRemove);
+    }
+    ImGui::EndTable();
+  }
+  if (ImGui::Button("Add to end")) {
+    m_arr.emplace_back();
+  }
+  ImGui::SameLine();
+  if (ImGui::Button("Cancel")) {
+    return true;
+  }
+  ImGui::SameLine();
+  if (ImGui::Button("Apply")) {
+    auto* entry = m_model.GetEntry(m_name);
+    if (!entry) {
+      entry = m_model.AddEntry(
+          nt::GetTopic(m_model.GetInstance().GetHandle(), m_name));
+    }
+    if constexpr (NTType == NT_BOOLEAN_ARRAY) {
+      if (entry->publisher == 0) {
+        entry->publisher =
+            nt::Publish(entry->info.topic, NT_BOOLEAN_ARRAY, "boolean[]");
+      }
+      nt::SetBooleanArray(entry->publisher, m_arr);
+    } else if constexpr (NTType == NT_FLOAT_ARRAY) {
+      if (entry->publisher == 0) {
+        entry->publisher =
+            nt::Publish(entry->info.topic, NT_FLOAT_ARRAY, "float[]");
+      }
+      nt::SetFloatArray(entry->publisher, m_arr);
+    } else if constexpr (NTType == NT_DOUBLE_ARRAY) {
+      if (entry->publisher == 0) {
+        entry->publisher =
+            nt::Publish(entry->info.topic, NT_DOUBLE_ARRAY, "double[]");
+      }
+      nt::SetDoubleArray(entry->publisher, m_arr);
+    } else if constexpr (NTType == NT_INTEGER_ARRAY) {
+      if (entry->publisher == 0) {
+        entry->publisher =
+            nt::Publish(entry->info.topic, NT_INTEGER_ARRAY, "int[]");
+      }
+      nt::SetIntegerArray(entry->publisher, m_arr);
+    } else if constexpr (NTType == NT_STRING_ARRAY) {
+      if (entry->publisher == 0) {
+        entry->publisher =
+            nt::Publish(entry->info.topic, NT_STRING_ARRAY, "string[]");
+      }
+      nt::SetStringArray(entry->publisher, m_arr);
+    }
+    return true;
+  }
+  return false;
+}
+}  // namespace
+
+static ImGuiID gArrayEditorID;
+static std::unique_ptr<ArrayEditor> gArrayEditor;
+
+static void EmitEntryValueEditable(NetworkTablesModel* model,
+                                   NetworkTablesModel::Entry& entry,
                                    NetworkTablesFlags flags) {
   auto& val = entry.value;
   if (!val) {
     return;
   }
 
-  const char* typeStr =
-      entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str();
+  const char* typeStr = GetTypeString(
+      val.type(),
+      entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str());
+  ImGui::SetNextItemWidth(
+      -1 * (ImGui::CalcTextSize(typeStr).x + ImGui::GetStyle().FramePadding.x));
+
   ImGui::PushID(entry.info.name.c_str());
   switch (val.type()) {
     case NT_BOOLEAN: {
       static const char* boolOptions[] = {"false", "true"};
       int v = val.GetBoolean() ? 1 : 0;
-      if (ImGui::Combo(typeStr ? typeStr : "boolean", &v, boolOptions, 2)) {
+      if (ImGui::Combo(typeStr, &v, boolOptions, 2)) {
         if (entry.publisher == 0) {
           entry.publisher =
               nt::Publish(entry.info.topic, NT_BOOLEAN, "boolean");
@@ -895,9 +1377,8 @@
     }
     case NT_INTEGER: {
       int64_t v = val.GetInteger();
-      if (ImGui::InputScalar(typeStr ? typeStr : "int", ImGuiDataType_S64, &v,
-                             nullptr, nullptr, nullptr,
-                             ImGuiInputTextFlags_EnterReturnsTrue)) {
+      if (ImGui::InputScalar(typeStr, ImGuiDataType_S64, &v, nullptr, nullptr,
+                             nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) {
         if (entry.publisher == 0) {
           entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int");
         }
@@ -907,7 +1388,7 @@
     }
     case NT_FLOAT: {
       float v = val.GetFloat();
-      if (ImGui::InputFloat(typeStr ? typeStr : "float", &v, 0, 0, "%.6f",
+      if (ImGui::InputFloat(typeStr, &v, 0, 0, "%.6f",
                             ImGuiInputTextFlags_EnterReturnsTrue)) {
         if (entry.publisher == 0) {
           entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float");
@@ -924,7 +1405,7 @@
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
 #endif
-      if (ImGui::InputDouble(typeStr ? typeStr : "double", &v, 0, 0,
+      if (ImGui::InputDouble(typeStr, &v, 0, 0,
                              fmt::format("%.{}f", precision).c_str(),
                              ImGuiInputTextFlags_EnterReturnsTrue)) {
         if (entry.publisher == 0) {
@@ -938,100 +1419,101 @@
       break;
     }
     case NT_STRING: {
-      char* v = GetTextBuffer(val.GetString());
-      if (ImGui::InputText(typeStr ? typeStr : "string", v, kTextBufferSize,
-                           ImGuiInputTextFlags_EnterReturnsTrue)) {
-        if (entry.publisher == 0) {
-          entry.publisher = nt::Publish(entry.info.topic, NT_STRING, "string");
-        }
-        nt::SetString(entry.publisher, v);
-      }
-      break;
-    }
-    case NT_BOOLEAN_ARRAY: {
       char* v = GetTextBuffer(entry.valueStr);
-      if (ImGui::InputText(typeStr ? typeStr : "boolean[]", v, kTextBufferSize,
+      if (ImGui::InputText(typeStr, v, kTextBufferSize,
                            ImGuiInputTextFlags_EnterReturnsTrue)) {
-        std::vector<int> outv;
-        if (StringToBooleanArray(v, &outv)) {
+        if (v[0] == '"') {
           if (entry.publisher == 0) {
             entry.publisher =
-                nt::Publish(entry.info.topic, NT_BOOLEAN_ARRAY, "boolean[]");
+                nt::Publish(entry.info.topic, NT_STRING, "string");
           }
-          nt::SetBooleanArray(entry.publisher, outv);
+          wpi::SmallString<128> buf;
+          nt::SetString(entry.publisher,
+                        wpi::UnescapeCString(v + 1, buf).first);
         }
       }
       break;
     }
-    case NT_INTEGER_ARRAY: {
-      char* v = GetTextBuffer(entry.valueStr);
-      if (ImGui::InputText(typeStr ? typeStr : "int[]", v, kTextBufferSize,
-                           ImGuiInputTextFlags_EnterReturnsTrue)) {
-        std::vector<int64_t> outv;
-        if (StringToIntegerArray(v, &outv)) {
-          if (entry.publisher == 0) {
-            entry.publisher =
-                nt::Publish(entry.info.topic, NT_INTEGER_ARRAY, "int[]");
-          }
-          nt::SetIntegerArray(entry.publisher, outv);
+    case NT_BOOLEAN_ARRAY:
+      ImGui::LabelText(typeStr, "[]");
+      if (ImGui::BeginPopupContextItem("boolean[]")) {
+        if (ImGui::Selectable("Edit Array")) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_BOOLEAN_ARRAY, int>>(
+                  *model, entry.info.name, flags,
+                  entry.value.GetBooleanArray());
+          ImGui::OpenPopup(gArrayEditorID);
         }
+        ImGui::EndPopup();
+      }
+      break;
+    case NT_INTEGER_ARRAY:
+      ImGui::LabelText(typeStr, "[]");
+      if (ImGui::BeginPopupContextItem("int[]")) {
+        if (ImGui::Selectable("Edit Array")) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_INTEGER_ARRAY, int64_t>>(
+                  *model, entry.info.name, flags,
+                  entry.value.GetIntegerArray());
+          ImGui::OpenPopup(gArrayEditorID);
+        }
+        ImGui::EndPopup();
+      }
+      break;
+    case NT_FLOAT_ARRAY:
+      ImGui::LabelText(typeStr, "[]");
+      if (ImGui::BeginPopupContextItem("float[]")) {
+        if (ImGui::Selectable("Edit Array")) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_FLOAT_ARRAY, float>>(
+                  *model, entry.info.name, flags, entry.value.GetFloatArray());
+          ImGui::OpenPopup(gArrayEditorID);
+        }
+        ImGui::EndPopup();
+      }
+      break;
+    case NT_DOUBLE_ARRAY:
+      ImGui::LabelText(typeStr, "[]");
+      if (ImGui::BeginPopupContextItem("double[]")) {
+        if (ImGui::Selectable("Edit Array")) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_DOUBLE_ARRAY, double>>(
+                  *model, entry.info.name, flags, entry.value.GetDoubleArray());
+          ImGui::OpenPopup(gArrayEditorID);
+        }
+        ImGui::EndPopup();
+      }
+      break;
+    case NT_STRING_ARRAY:
+      ImGui::LabelText(typeStr, "[]");
+      if (ImGui::BeginPopupContextItem("string[]")) {
+        if (ImGui::Selectable("Edit Array")) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_STRING_ARRAY, std::string>>(
+                  *model, entry.info.name, flags, entry.value.GetStringArray());
+          ImGui::OpenPopup(gArrayEditorID);
+        }
+        ImGui::EndPopup();
+        break;
+      }
+      break;
+    case NT_RAW: {
+      ImGui::LabelText(typeStr, val.GetRaw().empty() ? "[]" : "[...]");
+      if (ImGui::IsItemHovered()) {
+        ImGui::BeginTooltip();
+        if (!entry.info.type_str.empty()) {
+          ImGui::TextUnformatted(entry.info.type_str.c_str());
+        }
+        ImGui::Text("%u bytes", static_cast<unsigned int>(val.GetRaw().size()));
+        ImGui::EndTooltip();
       }
       break;
     }
-    case NT_FLOAT_ARRAY: {
-      char* v = GetTextBuffer(entry.valueStr);
-      if (ImGui::InputText(typeStr ? typeStr : "float[]", v, kTextBufferSize,
-                           ImGuiInputTextFlags_EnterReturnsTrue)) {
-        std::vector<float> outv;
-        if (StringToFloatArray(v, &outv)) {
-          if (entry.publisher == 0) {
-            entry.publisher =
-                nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "float[]");
-          }
-          nt::SetFloatArray(entry.publisher, outv);
-        }
-      }
-      break;
-    }
-    case NT_DOUBLE_ARRAY: {
-      char* v = GetTextBuffer(entry.valueStr);
-      if (ImGui::InputText(typeStr ? typeStr : "double[]", v, kTextBufferSize,
-                           ImGuiInputTextFlags_EnterReturnsTrue)) {
-        std::vector<double> outv;
-        if (StringToFloatArray(v, &outv)) {
-          if (entry.publisher == 0) {
-            entry.publisher =
-                nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "double[]");
-          }
-          nt::SetDoubleArray(entry.publisher, outv);
-        }
-      }
-      break;
-    }
-    case NT_STRING_ARRAY: {
-      char* v = GetTextBuffer(entry.valueStr);
-      if (ImGui::InputText(typeStr ? typeStr : "string[]", v, kTextBufferSize,
-                           ImGuiInputTextFlags_EnterReturnsTrue)) {
-        std::vector<std::string> outv;
-        if (StringToStringArray(v, &outv)) {
-          if (entry.publisher == 0) {
-            entry.publisher =
-                nt::Publish(entry.info.topic, NT_STRING_ARRAY, "string[]");
-          }
-          nt::SetStringArray(entry.publisher, outv);
-        }
-      }
-      break;
-    }
-    case NT_RAW:
-      ImGui::LabelText(typeStr ? typeStr : "raw",
-                       val.GetRaw().empty() ? "[]" : "[...]");
-      break;
     case NT_RPC:
-      ImGui::LabelText(typeStr ? typeStr : "rpc", "[...]");
+      ImGui::LabelText(typeStr, "[...]");
       break;
     default:
-      ImGui::LabelText(typeStr ? typeStr : "other", "?");
+      ImGui::LabelText(typeStr, "?");
       break;
   }
   ImGui::PopID();
@@ -1045,58 +1527,97 @@
         model->AddEntry(nt::GetTopic(model->GetInstance().GetHandle(), path));
     if (entry->publisher == 0) {
       entry->publisher = nt::Publish(entry->info.topic, type, typeStr);
+      // publish a default value so it's editable
+      switch (type) {
+        case NT_BOOLEAN:
+          nt::SetDefaultBoolean(entry->publisher, false);
+          break;
+        case NT_INTEGER:
+          nt::SetDefaultInteger(entry->publisher, 0);
+          break;
+        case NT_FLOAT:
+          nt::SetDefaultFloat(entry->publisher, 0.0);
+          break;
+        case NT_DOUBLE:
+          nt::SetDefaultDouble(entry->publisher, 0.0);
+          break;
+        case NT_STRING:
+          nt::SetDefaultString(entry->publisher, "");
+          break;
+        case NT_BOOLEAN_ARRAY:
+          nt::SetDefaultBooleanArray(entry->publisher, {});
+          break;
+        case NT_INTEGER_ARRAY:
+          nt::SetDefaultIntegerArray(entry->publisher, {});
+          break;
+        case NT_FLOAT_ARRAY:
+          nt::SetDefaultFloatArray(entry->publisher, {});
+          break;
+        case NT_DOUBLE_ARRAY:
+          nt::SetDefaultDoubleArray(entry->publisher, {});
+          break;
+        case NT_STRING_ARRAY:
+          nt::SetDefaultStringArray(entry->publisher, {});
+          break;
+        default:
+          break;
+      }
     }
   }
 }
 
+void glass::DisplayNetworkTablesAddMenu(NetworkTablesModel* model,
+                                        std::string_view path,
+                                        NetworkTablesFlags flags) {
+  static char nameBuffer[kTextBufferSize];
+
+  if (ImGui::BeginMenu("Add new...")) {
+    if (ImGui::IsWindowAppearing()) {
+      nameBuffer[0] = '\0';
+    }
+
+    ImGui::InputTextWithHint("New item name", "example", nameBuffer,
+                             kTextBufferSize);
+    std::string fullNewPath;
+    if (path == "/") {
+      path = "";
+    }
+    fullNewPath = fmt::format("{}/{}", path, nameBuffer);
+
+    ImGui::Text("Adding: %s", fullNewPath.c_str());
+    ImGui::Separator();
+    auto entry = model->GetEntry(fullNewPath);
+    bool exists = entry && entry->info.type != NT_Type::NT_UNASSIGNED;
+    bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys ||
+                    nameBuffer[0] != '\0') &&
+                   !exists;
+
+    CreateTopicMenuItem(model, fullNewPath, NT_STRING, "string", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_INTEGER, "int", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_FLOAT, "float", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE, "double", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN, "boolean", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_STRING_ARRAY, "string[]",
+                        enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_INTEGER_ARRAY, "int[]", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_FLOAT_ARRAY, "float[]", enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE_ARRAY, "double[]",
+                        enabled);
+    CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN_ARRAY, "boolean[]",
+                        enabled);
+
+    ImGui::EndMenu();
+  }
+}
+
 static void EmitParentContextMenu(NetworkTablesModel* model,
                                   const std::string& path,
                                   NetworkTablesFlags flags) {
-  static char nameBuffer[kTextBufferSize];
   if (ImGui::BeginPopupContextItem(path.c_str())) {
     ImGui::Text("%s", path.c_str());
     ImGui::Separator();
 
-    if (ImGui::BeginMenu("Add new...")) {
-      if (ImGui::IsWindowAppearing()) {
-        nameBuffer[0] = '\0';
-      }
-
-      ImGui::InputTextWithHint("New item name", "example", nameBuffer,
-                               kTextBufferSize);
-      std::string fullNewPath;
-      if (path == "/") {
-        fullNewPath = path + nameBuffer;
-      } else {
-        fullNewPath = fmt::format("{}/{}", path, nameBuffer);
-      }
-
-      ImGui::Text("Adding: %s", fullNewPath.c_str());
-      ImGui::Separator();
-      auto entry = model->GetEntry(fullNewPath);
-      bool exists = entry && entry->info.type != NT_Type::NT_UNASSIGNED;
-      bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys ||
-                      nameBuffer[0] != '\0') &&
-                     !exists;
-
-      CreateTopicMenuItem(model, fullNewPath, NT_STRING, "string", enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_INTEGER, "int", enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_FLOAT, "float", enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE, "double", enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN, "boolean", enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_STRING_ARRAY, "string[]",
-                          enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_INTEGER_ARRAY, "int[]",
-                          enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_FLOAT_ARRAY, "float[]",
-                          enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE_ARRAY, "double[]",
-                          enabled);
-      CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN_ARRAY, "boolean[]",
-                          enabled);
-
-      ImGui::EndMenu();
-    }
+    DisplayNetworkTablesAddMenu(model, path, flags);
 
     ImGui::EndPopup();
   }
@@ -1123,13 +1644,28 @@
     ImGui::TableNextRow();
     ImGui::TableNextColumn();
     EmitValueName(child.source.get(), child.name.c_str(), child.path.c_str());
+
     ImGui::TableNextColumn();
     if (!child.valueChildren.empty()) {
-      char label[64];
-      std::snprintf(label, sizeof(label),
-                    child.valueChildrenMap ? "{...}##v_%s" : "[...]##v_%s",
-                    child.name.c_str());
-      if (TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth)) {
+      auto pos = ImGui::GetCursorPos();
+      char label[128];
+      std::string_view ts = child.typeStr;
+      bool havePopup = GetHeadingTypeString(&ts);
+      wpi::format_to_n_c_str(label, sizeof(label), "{}##v_{}", ts.data(),
+                             child.name.c_str());
+      bool valueChildrenOpen =
+          TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth);
+      if (havePopup) {
+        if (ImGui::IsItemHovered()) {
+          ImGui::BeginTooltip();
+          ImGui::TextUnformatted(child.typeStr.c_str());
+          ImGui::EndTooltip();
+        }
+      }
+      // make it look like a normal label w/type
+      ImGui::SetCursorPos(pos);
+      ImGui::LabelText(child.valueChildrenMap ? "{...}" : "[...]", "%s", "");
+      if (valueChildrenOpen) {
         EmitValueTree(child.valueChildren, flags);
         TreePop();
       }
@@ -1154,23 +1690,68 @@
   ImGui::TableNextColumn();
   if (!entry.valueChildren.empty()) {
     auto pos = ImGui::GetCursorPos();
-    char label[64];
-    std::snprintf(label, sizeof(label),
-                  entry.valueChildrenMap ? "{...}##v_%s" : "[...]##v_%s",
-                  entry.info.name.c_str());
+    char label[128];
+    std::string_view ts = entry.info.type_str;
+    bool havePopup = GetHeadingTypeString(&ts);
+    wpi::format_to_n_c_str(label, sizeof(label), "{}##v_{}", ts.data(),
+                           entry.info.name.c_str());
     valueChildrenOpen =
         TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth |
                               ImGuiTreeNodeFlags_AllowItemOverlap);
+    if (havePopup) {
+      if (ImGui::IsItemHovered()) {
+        ImGui::BeginTooltip();
+        ImGui::TextUnformatted(entry.info.type_str.c_str());
+        ImGui::EndTooltip();
+      }
+    }
     // make it look like a normal label w/type
+    const char* typeStr = GetTypeString(
+        NT_RAW,
+        entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str());
     ImGui::SetCursorPos(pos);
-    ImGui::LabelText(entry.info.type_str.c_str(), "%s", "");
+    ImGui::SetNextItemWidth(-1 * (ImGui::CalcTextSize(typeStr).x +
+                                  ImGui::GetStyle().FramePadding.x));
+    ImGui::LabelText(typeStr, "%s", "");
+    if ((entry.value.IsBooleanArray() || entry.value.IsFloatArray() ||
+         entry.value.IsDoubleArray() || entry.value.IsIntegerArray() ||
+         entry.value.IsStringArray()) &&
+        ImGui::BeginPopupContextItem(label)) {
+      if (ImGui::Selectable("Edit Array")) {
+        if (entry.value.IsBooleanArray()) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_BOOLEAN_ARRAY, int>>(
+                  *model, entry.info.name, flags,
+                  entry.value.GetBooleanArray());
+        } else if (entry.value.IsFloatArray()) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_FLOAT_ARRAY, float>>(
+                  *model, entry.info.name, flags, entry.value.GetFloatArray());
+        } else if (entry.value.IsDoubleArray()) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_DOUBLE_ARRAY, double>>(
+                  *model, entry.info.name, flags, entry.value.GetDoubleArray());
+        } else if (entry.value.IsIntegerArray()) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_INTEGER_ARRAY, int64_t>>(
+                  *model, entry.info.name, flags,
+                  entry.value.GetIntegerArray());
+        } else if (entry.value.IsStringArray()) {
+          gArrayEditor =
+              std::make_unique<ArrayEditorImpl<NT_STRING_ARRAY, std::string>>(
+                  *model, entry.info.name, flags, entry.value.GetStringArray());
+        }
+        ImGui::OpenPopup(gArrayEditorID);
+      }
+      ImGui::EndPopup();
+    }
   } else if (flags & NetworkTablesFlags_ReadOnly) {
     EmitEntryValueReadonly(
         entry,
         entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(),
         flags);
   } else {
-    EmitEntryValueEditable(entry, flags);
+    EmitEntryValueEditable(model, entry, flags);
   }
 
   if (flags & NetworkTablesFlags_ShowProperties) {
@@ -1280,7 +1861,6 @@
   }
   ImGui::TableHeadersRow();
 
-  // EmitParentContextMenu(model, "/", flags);
   if (flags & NetworkTablesFlags_TreeView) {
     switch (category) {
       case ShowPersistent:
@@ -1417,6 +1997,16 @@
 
 void glass::DisplayNetworkTables(NetworkTablesModel* model,
                                  NetworkTablesFlags flags) {
+  gArrayEditorID = ImGui::GetID("Array Editor");
+  if (ImGui::BeginPopupModal("Array Editor", nullptr,
+                             ImGuiWindowFlags_AlwaysAutoResize)) {
+    if (!gArrayEditor || gArrayEditor->Emit()) {
+      ImGui::CloseCurrentPopup();
+      gArrayEditor.release();
+    }
+    ImGui::EndPopup();
+  }
+
   if (flags & NetworkTablesFlags_CombinedView) {
     DisplayTable(model, model->GetTreeRoot(), flags, ShowAll);
   } else {
@@ -1511,6 +2101,7 @@
 
 void NetworkTablesView::Settings() {
   m_flags.DisplayMenu();
+  DisplayNetworkTablesAddMenu(m_model, {}, m_flags.GetFlags());
 }
 
 bool NetworkTablesView::HasSettings() {
diff --git a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
index fd6bd52..33c4f02 100644
--- a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
+++ b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
@@ -141,23 +141,45 @@
     case 1:
     case 2: {
       ImGui::InputText("Team/IP", &m_serverTeam);
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("Team number or IP/MDNS address of server");
+      }
       int* port = m_mode.GetValue() == 1 ? &m_port4 : &m_port3;
       if (ImGui::InputInt("Port", port)) {
         LimitPortRange(port);
       }
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("TCP Port - leave this at the default");
+      }
       ImGui::SameLine();
       if (ImGui::SmallButton("Default")) {
         *port = m_mode.GetValue() == 1 ? NT_DEFAULT_PORT4 : NT_DEFAULT_PORT3;
       }
       ImGui::InputText("Network Identity", &m_clientName);
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip(
+            "Arbitrary name used to identify clients on the network. Must not "
+            "be blank.");
+      }
       ImGui::Checkbox("Get Address from DS", &m_dsClient);
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("Attempt to fetch server IP from Driver Station");
+      }
       break;
     }
     case 3:
       ImGui::InputText("Listen Address", &m_listenAddress);
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip(
+            "Address for server to listen on. Leave blank to listen on all "
+            "interfaces.");
+      }
       if (ImGui::InputInt("NT3 port", &m_port3)) {
         LimitPortRange(&m_port3);
       }
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("TCP Port for NT3. Leave at default if unsure.");
+      }
       ImGui::SameLine();
       if (ImGui::SmallButton("Default##default3")) {
         m_port3 = NT_DEFAULT_PORT3;
@@ -165,11 +187,17 @@
       if (ImGui::InputInt("NT4 port", &m_port4)) {
         LimitPortRange(&m_port4);
       }
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("TCP Port for NT4. Leave at default if unsure.");
+      }
       ImGui::SameLine();
       if (ImGui::SmallButton("Default##default4")) {
         m_port4 = NT_DEFAULT_PORT4;
       }
       ImGui::InputText("Persistent Filename", &m_persistentFilename);
+      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) {
+        ImGui::SetTooltip("File for storage of persistent entries");
+      }
       break;
     default:
       break;
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
index a7aa514..8416cf9 100644
--- a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
@@ -18,6 +18,8 @@
 #include <ntcore_cpp.h>
 #include <wpi/DenseMap.h>
 #include <wpi/json.h>
+#include <wpi/protobuf/ProtobufMessageDatabase.h>
+#include <wpi/struct/DynamicStruct.h>
 
 #include "glass/Model.h"
 #include "glass/View.h"
@@ -31,7 +33,7 @@
   struct EntryValueTreeNode;
 
   struct ValueSource {
-    void UpdateFromValue(nt::Value&& v, std::string_view name,
+    void UpdateFromValue(NetworkTablesModel& model, std::string_view name,
                          std::string_view typeStr);
 
     /** The latest value. */
@@ -40,6 +42,9 @@
     /** String representation of the value (for arrays / complex values). */
     std::string valueStr;
 
+    /** Data type */
+    std::string typeStr;
+
     /** Data source (for numeric values). */
     std::unique_ptr<DataSource> source;
 
@@ -48,6 +53,15 @@
 
     /** Whether or not the children represent a map */
     bool valueChildrenMap = false;
+
+   private:
+    void UpdateDiscreteSource(std::string_view name, double value, int64_t time,
+                              bool digital = false);
+
+    template <typename T, typename MakeValue>
+    void UpdateDiscreteArray(std::string_view name, std::span<const T> arr,
+                             int64_t time, MakeValue makeValue,
+                             bool digital = false);
   };
 
   struct EntryValueTreeNode : public ValueSource {
@@ -64,6 +78,10 @@
     Entry& operator=(const Entry&) = delete;
     ~Entry();
 
+    void UpdateFromValue(NetworkTablesModel& model) {
+      ValueSource::UpdateFromValue(model, info.name, info.type_str);
+    }
+
     void UpdateTopic(nt::Event&& event) {
       if (std::holds_alternative<nt::TopicInfo>(event.data)) {
         UpdateInfo(std::get<nt::TopicInfo>(std::move(event.data)));
@@ -149,6 +167,9 @@
   Entry* GetEntry(std::string_view name);
   Entry* AddEntry(NT_Topic topic);
 
+  wpi::StructDescriptorDatabase& GetStructDatabase() { return m_structDb; }
+  wpi::ProtobufMessageDatabase& GetProtobufDatabase() { return m_protoDb; }
+
  private:
   void RebuildTree();
   void RebuildTreeImpl(std::vector<TreeNode>* tree, int category);
@@ -168,6 +189,9 @@
 
   std::map<std::string, Client, std::less<>> m_clients;
   Client m_server;
+
+  wpi::StructDescriptorDatabase m_structDb;
+  wpi::ProtobufMessageDatabase m_protoDb;
 };
 
 using NetworkTablesFlags = int;
@@ -194,6 +218,10 @@
     NetworkTablesModel* model,
     NetworkTablesFlags flags = NetworkTablesFlags_Default);
 
+void DisplayNetworkTablesAddMenu(
+    NetworkTablesModel* model, std::string_view path = {},
+    NetworkTablesFlags flags = NetworkTablesFlags_Default);
+
 class NetworkTablesFlagsSettings {
  public:
   explicit NetworkTablesFlagsSettings(