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

83f1860047 [wpilib] Add/update documentation to PneumaticBase and subclasses (NFC) (#4881)
9872e676d8 [commands] Make Subsystem destructor virtual (#4892)
25db20e49d [hal] Fix segfault in various HAL functions (#4891)
b0c6724eed [glass] Add hamburger menu icon to titlebars (#4874)
f0fa8205ac Add missing compiler flags and fix warnings (#4889)
42fc4cb6bc [wpiutil] SafeThread: Provide start/stop hooks (#4880)
cc166c98d2 [templates] Add Command-based skeleton template (#4861)
3f51f10ad3 [build] Update to 2023v3 image (#4886)
1562eae74a [ntcore] Refactor meta-topic decoding from glass (#4809)
b632b288a3 Fix usages of std::min and std::max to be windows safe (#4887)
c11bd2720f [wpilibc] Add internal function to reset Shuffleboard instance (#4884)
f1151d375f [ntcore] Add method to get server time offset (#4847)
fe1b62647f [hal,wpilib] Update documentation for getComments (NFC) (#4879)
c49a45abbd [build] Fix examples linking in incorrect jni library (#4873)
bc3d01a721 [build] Add platform check to doxygen plugin (#4862)
bc473240ae Add Jetbrains Fleet folder to .gitignore (#4872)
2121bd5fb8 [wpimath] Remove RKF45 (#4870)
835f8470d6 [build] Fix roborio cross-compiler on arm hosts (#4864)
6cfe5de00d [ntcore] Don't deadlock server on early destroy (#4863)
2ac41f3edc [hal, wpilib] Add RobotController.getComments()  (#4463)
26bdbf3d41 Java optimization and formatting fixes (#4857)
92149efa11 Spelling and grammar cleanups (#4849)
176fddeb4c [commands] Add functions to HID classes to allow use of axes as BooleanEvents/Triggers (#4762)
87a34af367 [templates] Add bindings to command-based template (#4838)
4534e75787 [examples] Remove redundant MotorControl example (#4837)
1cbebaa2f7 [commands] Remove final semicolon from test macro definition (#4859)
6efb9ee405 [commands] Add constructor for SwerveControllerCommand that takes a HolonomicDriveController (#4785)
1e7fcd5637 [cscore] Change run loop functions to not be mac specific (#4854)
1f940e2b60 [apriltag] Add C++ wrappers, rewrite Java/JNI to match (#4842)
a6d127aedf [build] Add missing task dependency in wpilibjExamples (#4852)
b893b3d6d3 [cscore] Add support for USB cameras on macOS (#4846)
1696a490fa [glass] Add support for alternate NT ports (#4848)
40a22d69bc [glass] Add support for alternate NT ports (#4848)
e84dbfede0 [wpilib] GenericHID: Add rumble both option (#4843)
8aa9dbfa90 [examples] Link apriltag package in examples build.gradle (#4845)
eda2fa8a17 [build] Update Spotless (#4840)
d20594db0d Fix typos (#4839)
dd8ecfdd54 [commands] Fix typo in waitUntil docs (NFC) (#4841)
17ceebfff4 [apriltag] Clean up apriltag JNI (#4823)
8b74ab389d [examples] RapidReactCommandBot: Fix array indices (#4833)
1aad3489c2 [sim] Implement PD total current and power (#4830)
2744991771 [wpimath] Fix docs in SwerveModulePosition (#4825)
ffbf6a1fa2 [commands] Disable regularly failing unit test (#4824)
fbabd0ef15 [commands] Enhance Command Sendable implementations (#4822)
7713f68772 [hal] Use atomic rather then mutex for DS Data updates (#4787)
701995d6cc [examples] Update Command-based starter project (#4778)
bf7068ac27 [wpilibc] Add missing PPS implementation for C++ (#4821)
aae0f52ca6 [ntcore] NetworkTable: fix visibility of get/set value (#4820)
ee02fb7ba7 [hal] Add support for Pulse-Per-Second signal (#4819)
518916ba02 [wpilib] Fix DS mode thread event being manual reset accidentally (#4818)
3997c6635b [hal] Update to new image, use new TCP notify callback and new duty cycle API (#4774)
cc8675a4e5 [examples] Add comment on how to view elevator sim (NFC) (#4482)
fb2c170b6e [ntcore] Simplify local startup (#4803)
7ba8a9ee1f [wpimath] ProfiledPIDController: Add to SendableRegistry (#4656)
c569d8e523 [wpilib] Joystick.getMagnitude(): use hypot() function (#4816)
2a5e89fa97 [apriltag] Improve description of pose coordinates (NFC) (#4810)
cc003c6c38 [apriltag] Fix AprilTagFieldLayout JSON name (#4814)
5522916123 [commands] CommandXBoxController bumper documentation fix (NFC) (#4815)
967b30de3a [glass] Fix NT view UpdateClients() bug (#4808)
3270d4fc86 [wpimath] Rewrite pose estimator docs (#4807)
be39678447 [apriltag] Add test to ensure apriltagjni loads (#4805)
61c75deb2a [commands] Test no-op behavior of scheduling a scheduled command (#4806)
a865f48e96 [ntcore] Pass pub/sub options as a unified PubSubOptions struct (#4794)
f66a667321 [commands] Fix incorrect Trigger docs (NFC) (#4792)
f8d4e9866e [ntcore] Clean up ntcore_test.h (#4804)
7e84ea891f [wpimath] Fix ComputerVisionUtil transform example in parameter docs (NFC) (#4800)
da3ec1be10 [wpimath] Change terminology for ArmFeedforward gravity gain (NFC) (#4791)
944dd7265d [wpilibc] Add C++ Notifier error handling, update java notifier error message (#4795)
6948cea67a [wpiutil] Fix MemoryBuffer initialization (#4797)
a31459bce6 [wpiutil] Fix UnescapeCString overflow when inputSize < 2 (#4796)
4a0ad6b48c [wpimath] Rotation2d: Add reference to angleModulus in docs (NFC) (#4786)
e6552d272e [ntcore] Remove table multi-subscriber (#4789)
bde383f763 [hal] Replace const char* with std::string_view in Driver Station sim functions (#4532)
5a52b51443 [hal] Add RobotController.getSerialNumber() (#4783)
69a66ec5ec [wpilib] Fix multiple motor safety issues (#4784)
989c9fb29a [wpimath] Revert Rotation2D change that limits angles (#4781)
0f5b08ec69 [wpigui] Update imgui to 1.89.1+ (#4780)
fba191099c [examples] AddressableLED: Add unit test (#4779)
b390cad095 [wpilibj] Consistently use ErrorMessages.requireNonNullParam (#4776)
b9772214d9 [wpilib] Sendable: Don't call setter for getter changes
342c375a71 [ntcore] Add subscriber option to exclude single publisher
b0e4053087 [ntcore] Use int for options instead of double
f3e666b7bb [cscore] Convert YUYV and UYVY directly to grayscale (#4777)
b300518bd1 [hal] Add CAN Stream API to Java through JNI bindings (#4193)
be27171236 [wpilibj] Shuffleboard: Check for null sendable (#4772)
4bbdbdfb48 [commands] Move GroupedCommands to CommandScheduler (#4728)
f18fd41ac3 [wpimath] Remove broken and obsoleted ComputerVisionUtil functions (#4775)
2d0faecf4f [glass] DataSource: Add spinlock to protect value (#4771)
348bd107fc [hal] Add CANManufacturer for The Thrifty Bot (#4773)
3149dc64b8 [examples] HatchbotInlined: Use Subsystem factories (#4765)
8618dd4160 [glass, wpilib] Replace remaining references to Speed Controller with Motor Controller (#4769)
72e21a1ed1 [apriltag] Use wpilibsuite fork of apriltag (#4764)
eab0d929e6 [commands] CommandGenericHID POV methods: Fix docs (NFC) (#4760)
6789869663 [wpilib] Call set(0) rather than disable for stopMotor (#4763)
c9dea2968d [cscore] Emit warning that USB Camera isn't supported on OSX (#4766)
8f402645f5 [commands] Fix PIDSubsystem setSetpoint behavior (#4759)
f24ad1d715 [build] Upgrade to googletest 1.12.1 (#4752)
ff88756864 [wpimath] Add new DCMotor functions for alternative calculations and reduction calculation (#4749)
f58873db8e [wpimath] Remove extra terms in matrix for pose estimator docs (#4756)
37e969b41a [wpimath] Add constructors to pose estimators with default standard deviations (#4754)
13cdc29382 [ci] Rename comment command from "/wpiformat" to "/format" (#4755)
6e23985ae6 [examples] Add main include directory to test builds (#4751)
66bb0ffb2c [examples] Add unit testing infrastructure (#4646)
74cc86c4c5 [wpimath] Make transform tests use pose/transform equality operators (#4675)
e22d8cc343 [wpimath] Use Odometry for internal state in Pose Estimation (#4668)
68dba92630 [ci] Update mac and windows builds to Java 17 (#4750)
23bfc2d9ab [sim] Remove unmaintained Gazebo support (#4736)
1f1461e254 [wpilib] Add method to enable/disable LiveWindow in test mode (#4678)
eae68fc165 [wpimath] Add tolerance for Rotation3d rotation matrix special orthogonality (#4744)
4c4545fb4b [apriltag] Suppress warning (#4743)
16ffaa754d [docs] Generate docs for apriltag subproject (#4745)
5e74ff26d8 [apriltag, build] Update native utils, add apriltag impl and JNI (#4733)
53875419a1 [hal] Allow overriding stderr printing by HAL_SendError (#4742)
aa6499e920 [ntcore] Fix special topic multi-subscriber handling (#4740)
df70351107 [build] Fix cmake install of thirdparty includes (#4741)
e9bd50ff9b [glass] NT view: clear meta-topic info on disconnect (#4732)
9b319fd56b [ntcore] Add sub option for local vs remote changes (#4731)
18d28ec5e3 [ntcore] Remove duplicate value checking from ClientImpl
bdfb625211 [ntcore] Send duplicate values to network if necessary
21003e34eb [commands] Update Subsystem factories and example to return CommandBase (#4729)
70080457d5 [commands] Refactor ProxyScheduleCommand, SelectCommand into ProxyCommand (#4534)
e82cd5147b [wpilib] Tweak Color HSV formula and use in AddressableLED (#4724)
ec124bb662 [commands] Allow unsetting a subsystem's default command (#4621)
2b2aa8eef7 [examples] Update all examples to use NWU coordinate conventions (#4725)
cb38bacfe8 [commands] Revert to original Trigger implementation (#4673)
15561338d5 [commands] Remove one more default command isFinished check (#4727)
ca35a2e097 Add simgui files to .gitignore (#4726)
20dbae0cee [examples] Renovate command-based examples (#4409)
1a59737f40 [commands] Add convenience factories (#4460)
42b6d4e3f7 Use defaulted comparison operators in C++ (#4723)
135c13958f [wpigui] Add FontAwesome (#4713)
ffbfc61532 [ntcore] Add NetworkTable table-specific listeners (#4640)
8958b2a4da [commands] Add property tests for command compositions (#4715)
e4ac09077c [wpilib] Add link to MotorSafety article (#4720)
f40de0c120 [commands] Add C++ factory templates (#4686)
51fa3e851f [build] cmake: Use FetchContent instead of ExternalProject (#4714)
1da84b2255 [wpigui] Reload fonts to scale rather than preloading (#4712)
e43e2fbc84 [wpiutil] StringExtras: Add UnescapeCString (#4707)
5804d8fa84 [ntcore] Server: Properly handle multiple subscribers (#4717)
169ef5fabf [glass] Update NT view for topicsOnly and sendAll changes (#4718)
148759ef54 [examples] CANPDP: Expand properties shown (#4687)
58ed112b51 [commands] RepeatCommand: restart on following iteration (#4706)
dd1da77d20 [readme] Fix broken CI badge (#4710)
7cda85df20 [build] Check Gradle plugin repo last to fix CI (#4711)
7ed9b13277 [build] Bump version plugin to fix null tag (#4705)
6b4f26225d [apriltag] Fix pluralization of apriltag artifacts (#4671)
b2d2924b72 [cscore] Add Y16 image support (#4702)
34ec89c041 [wpilibc] Shuffleboard SimpleWidget: Return pointer instead of reference (#4703)
e15200068d [ci] Disable HW testbench runs (#4704)
d5200db6cd [wpimath] Rename HolonomicDriveController.calculate params (#4683)
2ee3d86de4 [wpimath] Clarify Rotation3d roll-pitch-yaw direction (#4699)
9f0a8b930f [cscore] Use MFVideoFormat_L8 for Gray on Windows (#4701)
2bca43779e [cscore] Add UYVY image support (#4700)
4307d0ee8b [glass] Plot: allow for more than 11 plots (#4685)
3fe8d355a1 [examples] StateSpaceDifferentialDriveSimulation: Use encoder reversed constants (#4682)
b44034dadc [ntcore] Allow duplicate client IDs on server (#4676)
52d2c53888 [commands] Rename Java factory wait() to waitSeconds() (#4684)
76e918f71e [build] Fix JNI artifacts linking to incorrect libraries (#4680)
0bee875aff [commands] Change C++ CommandPtr to use CommandBase (#4677)
98e922313b [glass] Don't check IsConnected for NT widgets (#4674)
9a36373b8f [apriltag] Switch 2022 apriltag layout length and width values (#4670)
cf8faa9e67 [wpilib] Update values on controllable sendables (#4667)
5ec067c1f8 [ntcore] Implement keep duplicates pub/sub flag (#4666)
e962fd2916 [ntcore] Allow numeric-compatible value sets (#4620)
88bd67e7de [ci] Update clang repositories to jammy (#4665)
902e8686d3 [wpimath] Rework odometry APIs to improve feature parity (#4645)
e2d49181da Update to native utils 2023.8.0 (#4664)
149bac55b1 [cscore] Add Arducam OV9281 exposure quirk (#4663)
88f7a3ccb9 [wpimath] Fix Pose relativeTo documentation (#4661)
8acce443f0 [examples] Fix swerve examples to use getDistance for turning encoder (#4652)
295a1f8f3b [ntcore] Fix WaitForListenerQueue (#4662)
388e7a4265 [ntcore] Provide mechanism to reset internals of NT instance (#4653)
13aceea8dc [apriltag] Fix FieldDimensions argument order (#4659)
c203f3f0a9 [apriltag] Fix documentation for AprilTagFieldLayout (#4657)
f54d495c90 Fix non initialized hal functionality during motor safety init (#4658)
e6392a1570 [cmd] Change factories return type to CommandBase (#4655)
53904e7cf4 [apriltag] Split AprilTag functionality to a separate library (#4578)
2e88a496c2 [wpimath] Add support for swerve joystick normalization (#4516)
ce4c45df13 [wpimath] Rework function signatures for Pose Estimation / Odometry (#4642)
0401597d3b [readme] Add wpinet to MavenArtifacts.md (#4651)
2e5f9e45bb [wpimath] Remove encoder reset comments on Swerve, Mecanum Odometry and Pose Estimation (#4643)
e4b5795fc7 [docs] Disable Doxygen for memory to fix search (#4636)
03d0ea188c [build] cmake: Add missing wpinet to installed config file (#4637)
3082bd236b [build] Move version file to its own source set (#4638)
b7ca860417 [build] Use build cache for sign step (#4635)
64838e6367 [commands] Remove unsafe default command isFinished check (#4411)
1269d2b901 [myRobot] Disable spotbugs (#4565)
14d8506b72 [wpimath] Fix units docs for LinearSystemId::IdentifyDrivetrainSystem() (#4600)
d1d458db2b [wpimath] Constrain Rotation2d range to -pi to pi (#4611)
f656e99245 [readme] Add links to development build documentation (#4481)
6dd937cef7 [commands] Fix Trigger API docs (NFC) (#4599)
49047c85b9 [commands] Report error on C++ CommandPtr use-after-move (#4575)
d07267fed1 [ci] Upgrade containers to Ubuntu 22.04 and remove libclang installation (#4633)
b53ce1d3f0 [build, wpiutil] Switch macos to universal binaries (#4628)
5a320c326b [upstream_util, wpiutil] Refactor python scripts (#4614)
c4e526d315 [glass] Fix NT Mechanism2D (#4626)
d122e4254f [ci] Run spotlessApply after wpiformat in comment command (#4623)
5a1e7ea036 [wpilibj] FieldObject2d: Add null check to close() (#4619)
179f569113 [ntcore] Notify locally on SetDefault (#4617)
b0f6dc199d [wpilibc] ShuffleboardComponent.WithProperties: Update type (#4615)
7836f661cd [wpimath] Add missing open curly brace to units/base.h (#4613)
dbcc1de37f [wpimath] Add DifferentialDriveFeedforward classes which wrap LinearPlantInversionFeedforward (#4598)
93890c528b [wpimath] Add additional angular acceleration units (#4610)
3d8d5936f9 [wpimath] Add macro for disabling units fmt support (#4609)
2b04159dec [wpimath] Update units/base.h license header (#4608)
2764004fad [wpinet] Fix incorrect jni definitions (#4605)
85f1bb8f2b [wpiutil] Reenable jni check task (#4606)
231ae2c353 [glass] Plot: Fix Y-axis not being saved (#4594)
e92b6dd5f9 [wpilib] Fix AprilTagFieldLayout JSON property name typos (#4597)
2a8e0e1cc8 Update all dependencies that use grgit (#4596)
7d06e517e9 [commands] Move SelectCommand factory impl to header (#4581)
323524fed6 [wpimath] Remove deprecated units/units.h header (#4572)
d426873ed1 [commands] Add missing PS4 triangle methods (#4576)
5be5869b2f [apriltags] Use map as internal data model (#4577)
b1b4c1e9e7 [wpimath] Fix Pose3d transformBy rotation type (#4545)
a4054d702f [commands] Allow composing two triggers directly (#4580)
0190301e09 [wpilibc] Explicitly mark EventLoop as non-copyable/non-movable (#4579)
9d1ce6a6d9 [ntcore] Catch file open error when saving preferences (#4571)
5005e2ca04 [ntcore] Change Java event mask to EnumSet (#4564)
fa44a07938 [upstream-utils][mpack] Add upstream util for mpack (#4500)
4ba16db645 [ntcore] Various fixes and cleanups (#4544)
837415abfd [hal] Fix joysticks either crashing or returning 0 (#4570)
2c20fd0d09 [wpilib] SingleJointedArmSim: Check angle equals limit on wouldHit (#4567)
64a7136e08 [wpimath] SwerveDrivePoseEstimator: Restore comment about encoder reset (#4569)
b2b473b24a [wpilib] Add AprilTag and AprilTagFieldLayout (#4421)
7aab8fa93a [build] Update to Native Utils 2023.6.0 (#4563)
12c2851856 [commands] WrapperCommand: inherit from CommandBase (#4561)
0da169dd84 [wpimath] Remove template argument from ElevatorFeedforward (#4554)
2416827c25 [wpimath] Fix docs for pose estimator local measurement models (#4558)
1177a3522e [wpilib] Fix Xbox/PS4 POV sim for port number constructors (#4548)
102344e27a [commands] HID classes: Add missing methods, tweak return types (#4557)
1831ef3e19 [wpilib] Fix Shuffleboard SuppliedValueWidget (#4559)
a9606ce870 [wpilib] Fix Xbox/PS4 POV sim (#4546)
6c80d5eab3 [wpimath] Remove unused SymbolExports.h include from units/base.h (#4541)
b114006543 [ntcore] Unify listeners (#4536)
32fbfb7da6 [build] cmake: Install ntcore generated include files (#4540)
02465920fb [build] Update native utils to 2023.4.0 (#4539)
3a5a376465 [wpimath] Increase constexpr support in geometry data types (#4231)
1c3c86e9f1 [ntcore] Cache GetEntry(name) values (#4531)
dcda09f90a [command] Rename trigger methods (#4210)
66157397c1 [wpilib] Make drive classes follow NWU axes convention (#4079)
9e22ffbebf [ntcore] Fix null deref in NT3 client (#4530)
648ab6115c [wpigui,dlt,glass,ov] Support arm in GUI tools (#4527)
8bc3b04f5b [wpimath] Make ComputerVisionUtil use 3D geometry classes (#4528)
cfb84a6083 [wpilibc] Don't hang waiting for NT server to start (#4524)
02c47726e1 [wpimath] Remove unused odometry instance from DifferentialDrivePoseEstimator test (#4522)
b2a0093294 [ci] Revert upgrade of github-pages-deploy-action (#4521)
2a98d6b5d7 [wpimath] PIDController: Add getters for position & velocity tolerances (#4458)
9f36301dc8 [ci] Write wpiformat patch to job summary (#4519)
901fc555f4 [wpimath] Position Delta Odometry for Mecanum (#4514)
4170ec6107 [wpimath] Position Delta Odometry for Swerve (#4493)
fe400f68c5 [docs] Add wpinet to docs build (#4517)
794669b346 [ntcore] Revamp listeners (#4511)
dcfa85a5d5 [ci] Build sanitizers with clang-14 (#4518)
15ad855f1d [ntcore] Add UnitTopic<T> (C++ only) (#4497)
11244a49d9 [wpilib] Add IsConnected function to all gyros (#4465)
1d2e8eb153 [build] Update myRobot deployment (#4515)
ad53fb19b4 [hal] Use new HMB api for addressable LED (#4479)
ba850bac3b [hal] Add more shutdown checks and motor safety shutdown (#4510)
023a5989f8 [ntcore] Fix typo in NetworkServer client connect message (#4512)
c970011ccc [docs] Add Doxygen aliases used by Foonathan memory (#4509)
07a43c3d9a [readme] Document clang-format version and /wpiformat (#4503)
a05b212b04 [ci] Revert changes to wpiformat task from #4501 (#4508)
09faf31b67 [commands] Replace Command HID inheritance with delegation (#4470)
9e1f9c1133 [commands] Add command factories (#4476)
f19d2b9b84 [ci] Add NUMBER environment variable to comment command commit script (#4507)
a28f93863c [ci] Push comment command commit directly to PR (#4506)
c9f61669b8 [ci] Fix comment command commit push (#4505)
dcce5ad3b3 [ci] Update github-script API usage (#4504)
6836e5923d [wpilibc] Restore get duty cycle scale factor (#4502)
335188c652 [dlt] Add deselect/select all buttons to download view (#4499)
60a29dcb99 [glass] Field2D: Add "hidden" option for objects (#4498)
b55d5b3034 [ci] Update deprecated github actions (#4501)
10ed4b3969 [ntcore] Various NT4 fixes (#4474)
4a401b89d7 [hal, wpilib] New DS thread model and implementation (#3787)
c195b4fc46 [wpimath] Clean up PoseEstimator nominal dt docs (#4496)
8f2e34c6a3 [build] Remove wpilib prefix from CMake flat install (#4492)
150d692df7 [wpimath] Remove unused private PoseEstimator function (#4495)
3e5bfff1b5 [wpimath] FromFieldRelativeSpeeds: Add ChassisSpeeds overload (#4494)
9c7e66a27d [commands] C++: Add CommandPtr overload for SetDefaultCommand (#4488)
0ca274866b [build] Fix CMake system library opt-ins (#4487)
dc037f8d41 [commands] Remove EndlessCommand (#4483)
16cdc741cf [wpimath] Add Pose3d(Pose2d) constructor (#4485)
9d5055176d [build] cmake: Allow disabling ntcore build (#4486)
d1e66e1296 [build] Compile all java code with inline string concatenation (#4490)
1fc098e696 Enable log macros to work with no args (#4475)
878cc8defb [wpilib] LiveWindow: Add enableAllTelemetry() (#4480)
8153911160 [build] Fix MSVC runtime archiver to grab default runtime (#4478)
fbdc810887 Upgrade to C++20 (#4239)
396143004c [ntcore] Add ntcoreffi binary (#4471)
1f45732700 [build] Update to 2023.2.4 native-utils and new dependencies (#4473)
574cb41c18 [ntcore] Various fixes (#4469)
d9d6c425e7 [build] Force Java 11 source compatibility (#4472)
58b6484dbe Switch away from NI interrupt manager to custom implementation (#3705)
ca43fe2798 [wpimath] Use Units conversions in ComputerVisionUtil docs (NFC) (#4464)
87a64ccedc [hal] Convert DutyCycle Raw output to be a high time measurement (#4466)
89a3d00297 [commands] Add FinallyDo and HandleInterrupt decorators (#4412)
1497665f96 [commands] Add C++ versions of Java-only decorators (#4457)
27b173374e [wpimath] Add minLinearAccel parameter to DifferentialDriveAccelerationLimiter (#4422)
2a13dba8ac [wpilib] TrajectoryUtil: Fix ambiguous documentation (NFC) (#4461)
77301b126c [ntcore] NetworkTables 4 (#3217)
90cfa00115 [build] cmake: Fix libssh include directory order (#4459)
5cf961edb9 [commands] Refactor lambda-based commands to inherit FunctionalCommand (#4451)
b2276e47de [wpimath] Enable continuous angle input for HolonomicDriveController (#4453)
893b46139a [fieldImages] Add utilities to simplify loading of fields (#4456)
60e29627c0 [commands] C++ unique_ptr migration (#4319)
3b81cf6c35 [wpilib] Improve Color.toString (#4450)
5c067d30a0 [wpinet] WebSocket: Add SendFrames() (#4445)
ceaf493811 [wpiutil] MakeJByteArray: Use span<uint8> instead of string_view (#4446)
10e04e2b13 [examples] FrisbeeBot: Fix reference capture (#4449)
726f67c64b [build] Add exeSplitSetup (#4444)
c7b7624c1c [wpiutil] Add MessagePack utility functions (#4448)
d600529ec0 [wpinet] uv::Async: Add UnsafeSend() (#4447)
b53b3526a2 [wpimath] Add CoordinateSystem conversion for Transform3d (#4443)
38bb23eb18 [wpimath] Add scalar multiply and divide operators to all geometry classes (#4438)
3937ff8221 [wpilib] Remove deprecated Controller class (#4440)
abbfe244b5 [wpilib] Improve Color FromHSV (#4439)
4ddb8aa0dd [sim] Provide function that resets all simulation data (#4016)
a791470de7 Clean up Java warning suppressions (#4433)
17f504f548 [hal,wpilib] Fix SPI Mode Setting (#4434)
773198537c [wpiutil] Add wpi::scope_exit (#4432)
5ac658c8f0 [wpiutil] Logger: Conditionalize around WPI_LOG (#4431)
8767e4a941 [wpiutil] DataLog: Fix SetMetadata output (#4430)
8c4af073f4 [wpiutil] Synchronization: shutdown race protection (#4429)
c79f38584a [build] Fix Java integration tests (#4428)
36c08dd97c [build] Fix cmake install of fmtlib (#4426)
69b7b3dd7d [ci] Remove the Windows cmake job (#4425)
738c75fed8 [readme] Fix formatting/linting link (#4423)
4eb1d03fb3 [wpimath] Document C++ LinearFilter exception (#4417)
ba4ec6c967 [build] Fix clang-tidy false positive on Linux (#4406)
97836f0e55 [commands] Fix ProfiledPIDSubsystem setGoal behavior (#4414)
fdfb85f695 [wpimath] Remove Java LQR constructor that takes a controller gain matrix (#4419)
ab1baf4832 [wpimath] Add rotation matrix constructor to Rotation3d (#4413)
9730032866 [wpimath] Document LQR and KalmanFilter exceptions (#4418)
5b656eecf6 [wpimath] Fix HTML5 entity (#4420)
9ae38eaa7c [commands] Add owning overload to ProxyScheduleCommand (#4405)
cb33bd71df [commands] deprecate withInterrupt decorator (#4407)
d9b4e7b8bf [commands] Revert "Change grouping decorator impl to flatten nested group structures (#3335)" (#4402)
0389bf5214 [hal] REVPH: Improve handling of disconnected CAN Bus (#4169)
4267fa08d1 [wpilibc] ADIS IMUs: Fix memory leak (#4170)
65c8fbd452 [wpilib] MotorControllerGroup: Override setVoltage (#4403)
f36162fddc [wpimath] Improve Discretization internal docs (#4400)
5149f7d894 [wpimath] Add two-vector Rotation3d constructor (#4398)
20b5bed1cb [wpimath] Clean up Java Quaternion class (#4399)
f18dd1905d [build] Include all thirdparty sources in distribution (#4397)
aa9d7f1cdc [wpiutil] Import foonathan memory (#4306)
2742662254 [ci] Remove a couple of obsolete clang-tidy checks (#4396)
a5df391166 [hal, wpilib] Fix up DIO pulse API (#4387)
59e6706b75 [glass] Turn on docking by default
8461bb1e03 [glass] Add support for saving docking info
b873e208b4 [wpigui] Add support for imgui config flags
873e72df8c [build] Update imgui to 1.88 docking branch
c8bd6fc5b4 [ci] Fix comment-command (take 2) (#4395)
fed68b83b4 [ci] Fix comment-command action not running runners (#4393)
0ef8a4e1df [wpimath] Support formatting more Eigen types (#4391)
c393b3b367 [build] Update to native utils 2023.1.0 and Gradle 7.5.1 (#4392)
b5a17f762c [wpimath] Add direction to slew rate limiter (#4377)
fafc81ed1a [wpiutil] Upgrade to fmt 9.1.0 (#4389)
cc56bdc787 [wpiutil] SafeThread: Add Synchronization object variant (#4382)
4254438d8d [commands] Mark command group lifecycle methods as final (#4385)
97c15af238 [wpimath] LinearSystemId: Fix docs, move C++ impls out of header (#4388)
d22ff8a158 [wpiutil] Add JNI access to C++ stderr (#4381)
fdb5a2791f [wpiutil] jni_util: Add Mac-friendly MakeJLongArray/JArrayRef (#4383)
c3a93fb995 [commands] Revamp Interruptible (#4192)
f2a8d38d2a [commands] Rename Command.repeat to repeatedly (#4379)
9e24c6eac0 [wpiutil] Logger: paren-protect instance usage in macro (#4384)
fe4d12ce22 [wpimath] Add LTV controller derivations and make enums private (#4380)
eb08486039 [build] Fix MacOS binary rpath generation (#4376)
ccf83c634a [build] Use native-utils platform names instead of raw strings (#4375)
3fd69749e7 [docs] Upgrade to doxygen 1.9.4 (#4370)
594df5fc08 [wpinet] uv/util.h: Pull in ws2_32.lib on Windows for ntohs (#4371)
539070820d [ci] Enable asan for wpinet and wpiutil (#4369)
564a56d99b [wpinet] Fix memory leak in WorkerThreadTest (#4368)
5adf50d93c [upstream_utils] Refactor upstream_utils scripts (#4367)
d80e8039d7 [wpiutil] Suppress fmtlib clang-tidy warning in C++20 consteval contexts (#4364)
0e6d67b23b [upstream_utils] Remove yapf format disable comment (#4366)
be5270697a [build] Suppress enum-enum deprecation warning in OpenCV (#4365)
8d28851263 Add Rosetta install command to build requirements (#4363)
3d2115c93e [wpinet] include-what-you-use in MulticastTest (#4360)
91002ae3cc [wpimath] Upgrade to Drake 1.6.0 (#4361)
148c18e658 [wpinet] Upgrade to libuv 1.44.2 (#4362)
a2a5c926b6 Fix clang-tidy warnings (#4359)
ea6b1d8449 [wpiutil] Remove unused ManagedStatic class (#4358)
ac9be78e27 Use stricter C++ type conversions (#4357)
151dabb2af [wpiutil] Upgrade to fmt 9.0.0 (#4337)
340465c929 [ci] Upgrade to clang-format and clang-tidy 14 (NFC) (#4347)
d45bcddd15 [examples] Add comments to StateSpaceDifferentialDrive (#4341)
0e0786331a Update LLVM libraries to 14.0.6 (#4350)
c5db23f296 [wpimath] Add Eigen sparse matrix and iterative solver support (#4349)
44abc8dfa6 [upstream_utils] Remove git version from upstream patches (#4351)
3fdb2f767d [wpimath] Add comments with Ramsete equations (#4348)
0485f05da9 [wpilibjExamples] Upgrade jacoco to match allwpilib (#4346)
0a5eb65231 [wpinet] Handle empty txt block for mdns announcer (#4072)
19ffebaf3e [wpilib] Add reference to I2C Lockup to API Docs (NFC) (#4340)
ce1a90d639 [hal] Replace SerialHelper "goto done" with continue (#4342)
d25af48797 [ci] Make upstream_utils CI fail on untracked files (#4339)
ebb836dacb [examples] Fix negations in event loop examples (#4334)
d83e202f00 [upstream_utils] Update paths in update_fmt.py (#4338)
3ccf806064 [wpimath] Remove redundant LinearFilter.finiteDifference() argument (#4335)
6f1e01f8bd [wpimath] Document example of online filtering for LinearFilter.finiteDifference() (#4336)
1023c34b1c [readme] Update location of ni-libraries (#4333)
faa29d596c [wpilib] Improve Notifier docs (NFC) (#4326)
add00a96ed [wpimath] Improve DifferentialDriveAccelerationLimiter docs (NFC) (#4323)
82fac41244 [wpimath] Better document trackwidth parameters (NFC) (#4324)
5eb44e22a9 Format Python scripts with black (NFC) (#4325)
2e09fa7325 [build] Fix mpack cmake (#4322)
fe3c24b1ee [command] Add ignoringDisable decorator (#4305)
aa221597bc [build] Add M1 builds, change arm name, update to 2023 deps (#4315)
579a8ee229 [ci] Use one worker for Windows release Gradle build (#4318)
5105c5eab6 [wpilibj] Change "final" to "exit" in the IterativeRobotBase JavaDoc (NFC) (#4317)
787fe6e7a5 [wpiutil] Separate third party libraries (#4190)
6671f8d099 [wpigui] Update portable file dialogs (#4316)
9ac9b69aa2 [command] Reorder Scheduler operations (#4261)
e61028cb18 [build] halsim_gui: Add wpinet dependency (#4313)
661d23eaf5 [glass] Add precision setting for NetworkTable view (#4311)
666040e3e5 [hal] Throw exceptions for invalid sizes in I2C and SPI JNI (#4312)
aebc272449 [build] Upgrade to spotbugs Gradle plugin 5.0.8 (#4310)
fd884581e4 [wpilib] Add BooleanEvent/Trigger factories on HID classes (#4247)
9b1bf5c7f1 [wpimath] Move Drake and Eigen to thirdparty folders (#4307)
c9e620a920 [wpilibc] Change EventLoop data structure to vector (#4304)
41d40dd62f [wpinet] Fix libuv unused variable warning on Mac (#4299)
30f5b68264 [wpinet] Fix JNI loading error (#4295)
f7b3f4b90e [examples] Getting Started: Change Joystick to XboxController (#4194)
a99c11c14c [wpimath] Replace UKF implementation with square root form (#4168)
45b7fc445b [wpilib] Add EventLoop (#4104)
16a4888c52 [wpilib] Default off LiveWindow telemetry (#4301)
17752f1337 [ci] Split debug and release Windows builds (#4277)
abb45a68db [commands] Remove custom test wrappers (#4296)
1280a54ef3 [upstream_utils]: Make work with Python 3.8 (#4298)
f2d243fa68 [build] Change defaults for Java lints (#4300)
a4787130f4 Update using development build to work with 2023 gradlerio (#4294)
af7985e46c [wpiutil] Use invoke_result_t instead of result_of in future.h (#4293)
e9d1b5c2d0 [hal] Remove deprecated SimDevice functions (#4209)
45b598d236 [wpilibj] Add toString() methods to Color and Color8Bit (#4286)
fc37265da5 [wpimath] Add angle measurement convention to ArmFeedforward docs (NFC) (#4285)
a4ec13eb0e [wpilibjexamples] Remove unnecessary voltage desaturation
2fa52007af [wpilibc] Use GetBatteryVoltage() in MotorController::SetVoltage
d9f9cd1140 [wpimath] Reset prev_time on pose estimator reset (#4283)
8b6df88783 [wpilibj] Tachometer.getFrequency(): Fix bug (#4281)
345cff08c0 [wpiutil] Make wpi::array constexpr (#4278)
57428112ac [wpimath] Upgrade to Drake v1.3.0 (#4279)
a18d4ff154 [build] Fix tools not being copied when built with -Ponly* (#4276)
d1cd07b9f3 [wpigui] Add OpenURL (#4273)
e67f8e917a [glass] Use glfwSetKeyCallback for Enter key remap (#4275)
be2fedfe50 [wpimath] Add stdexcept include for std::invalid_argument (IWYU) (#4274)
7ad2be172e [build] Update native-utils to 2023.0.1 (#4272)
abc605c9c9 [ci] Update workflows to 20.04 base image (#4271)
3e94805220 [wpiutil] Reduce llvm collections patches (#4268)
db2e1d170e [upstream_utils] Document how to update thirdparty libraries (#4253)
96ebdcaf16 [wpimath] Remove unused Eigen AutoDiff module (#4267)
553b2a3b12 [upstream_utils] Fix stackwalker (#4265)
3e13ef42eb [wpilibc] Add missing std::array #include (include-what-you-use) (#4266)
d651a1fcec Fix internal deprecation warnings (#4257)
b193b318c1 [commands] Add unless() decorator (#4244)
ef3714223b [commands] Remove docs reference to obsolete interrupted() method (NFC) (#4262)
3d8dbbbac3 [readme] Add quickstart (#4225)
013efdde25 [wpinet] Wrap a number of newer libuv features (#4260)
816aa4e465 [wpilib] Add Pneumatics sim classes (#4033)
046c2c8972 [wpilibc] Rename SpeedControllerGroupTest.cpp (#4258)
d80e9cdf64 [upstream_utils] Use shallow clones for thirdparty repos (#4255)
7576136b4a [upstream_utils] Make update_llvm.py executable (#4254)
c3b223ce60 [wpiutil] Vendor llvm and update to 13.0.0 (#4224)
5aa67f56e6 [wpimath] Clean up math comments (#4252)
fff4d1f44e [wpimath] Extend Eigen warning suppression to GCC 12 (#4251)
0d9956273c [wpimath] Add CoordinateSystem.convert() translation and rotation overloads (#4227)
3fada4e0b4 [wpinet] Update to libuv 1.44.1 (#4232)
65b23ac45e [wpilibc] Fix return value of DriverStation::GetJoystickAxisType() (#4230)
4ac34c0141 [upstream_utils] Cleanup update_libuv.py (#4249)
8bd614bb1e [upstream_utils] Use "git am" instead of "git apply" for patches (#4248)
4253d6d5f0 [upstream_utils] Apply "git am" patches individually (#4250)
6a4752dcdc Fix GCC 12.1 warning false positives (#4246)
5876b40f08 [wpimath] Memoize CoordinateSystem and CoordinateAxis statics (#4241)
5983434a70 [cameraserver] Replace IterativeRobot in comment sample code with TimedRobot (#4238)
a3d44a1e69 [wpimath] Add Translation2d.getAngle() (#4217)
d364bbd5a7 [upstream_utils] Give vendor update scripts execute permissions (#4226)
f341e1b2be [wpimath] Document standard coordinate systems better (NFC) (#4228)
9af389b200 [wpinet] AddrToName: Initialize name (#4229)
2ae4adf2d7 [ci] Add wpiformat command to PRs (#4223)
178b2a1e88 Contributing.md: Correct version of clang-format used (#4222)
18db343cdc [wpiutil, wpinet] Vendor libuv, stack walker (#4219)
f0c821282a [build] Use artifactory mirror (#4220)
d673ead481 [wpinet] Move network portions of wpiutil into new wpinet library (#4077)
b33715db15 [wpimath] Add CoordinateSystem class (#4214)
99424ad562 [sim] Allow creating a PWMSim object from a PWMMotorController (#4039)
dc6f641fd2 [wpimath] PIDController: Reset position and velocity error when reset() is called. (#4064)
f20a20f3f1 [wpimath] Add 3D geometry classes (#4175)
708a4bc3bc [wpimath] Conserve previously calculated swerve module angles when updating states for stationary ChassisSpeeds (#4208)
ef7ed21a9d [wpimath] Improve accuracy of ComputerVisionUtil.calculateDistanceToTarget() (#4215)
b1abf455c1 [wpimath] LTVUnicycleController: Use LUT, provide default hyperparameters (#4213)
d5456cf278 [wpimath] LTVDifferentialDriveController: Remove unused variable (#4212)
99343d40ba [command] Remove old command-based framework (#4211)
ee03a7ad3b Remove most 2022 deprecations (#4205)
ce1a7d698a [wpimath] Refactor WheelVoltages inner class to a separate file (#4203)
87bf70fa8e [wpimath] Add LTV controllers (#4094)
ebd2a303bf [wpimath] Remove deprecated MakeMatrix() function (#4202)
e28776d361 [wpimath] LinearSystemLoop: Add extern templates for common cases
dac1429aa9 [wpimath] LQR: Use extern template instead of Impl class
e767605e94 [wpimath] Add typedefs for common types
97c493241f [wpimath] UnscentedKalmanFilter: Move implementation out-of-line
8ea90d8bc9 [wpimath] ExtendedKalmanFilter: Move implementation out-of-line
ae7b1851ec [wpimath] KalmanFilter: Use extern template instead of Impl class
e3d62c22d3 [wpimath] Add extern templates for common cases
7200c4951d [wpiutil] SymbolExports: Add WPILIB_IMPORTS for dllimport
84056c9347 [wpiutil] SymbolExports: Add EXPORT_TEMPLATE_DECLARE/DEFINE
09cf6eeecb [wpimath] ApplyDeadband: add a scale param (#3865)
03230fc842 [build,ci] Enable artifactory build cache (#4200)
63cf3aaa3f [examples] Don't square ArcadeDrive inputs in auto (#4201)
18ff694f02 [wpimath] Add Rotation2d.fromRadians factory (#4178)
4f79ceedd9 [wpilibc] Add missing #include (#4198)
f7ca72fb41 [command] Rename PerpetualCommand to EndlessCommand (#4177)
a06b3f0307 [hal] Correct documentation on updateNotifierAlarm (#4156)
d926dd1610 [wpimath] Fix pose estimator performance (#4111)
51bc893bc5 [wpiutil] CircularBuffer: Change Java package-private methods to public (#4181)
fbe761f7f6 [build] Increase Gradle JVM heap size (#4172)
5ebe911933 [wpimath] Add DifferentialDriveAccelerationLimiter (#4091)
3919250da2 [wpilibj] Remove finalizers (#4158)
b3aee28388 [commands] Allow BooleanSupplier for Trigger operations (#4103)
9d20ab3024 [wpilib] Allow disabling ElevatorSim gravity (#4145)
aaa69f6717 [ci] Remove 32-bit Windows builds (#4078)
355a11a414 Update Java linters and fix new PMD errors (#4157)
ffc69d406c [examples] Reduce suggested acceleration in Ramsete example (#4171)
922d50079a [wpimath] Units: fix comment in degreesToRotations (NFC) (#4159)
dd163b62ae [wpimath] Rotation2d: Add factory method that uses rotations (#4166)
bd80e220b9 [ci] Upgrade CMake actions (#4161)
aef4b16d4c [wpimath] Remove unnecessary NOLINT in LinearPlantInversionFeedforward (NFC) (#4155)
975171609e [wpilib] Compressor: Rename enabled to isEnabled (#4147)
5bf46a9093 [wpimath] Add ComputerVisionUtil (#4124)
f27a1f9bfb [commands] Fix JoystickButton.getAsBoolean (#4131)
1b26e2d5da [commands] Add RepeatCommand (#4009)
88222daa3d [hal] Fix misspelling in AnalogInput/Output docs (NFC) (#4153)
81c5b41ce1 [wpilibj] Document MechanismLigament2d angle unit (NFC) (#4142)
9650e6733e [wpiutil] DataLog: Document finish and thread safety (NFC) (#4140)
c8905ec29a [wpimath] Remove ImplicitModelFollower dt argument (#4119)
b4620f01f9 [wpimath] Fix Rotation2d interpolation in Java (#4125)
2e462a19d3 [wpimath] Constexprify units unary operators (#4138)
069f932e59 [build] Fix gl3w cmake build (#4139)
126e3de91a [wpilibc] Remove unused SetPriority() call from Ultrasonic (#4123)
ba0dccaae4 [wpimath] Fix reference to Rotation2d.fromRadians() (#4118)
e1b6e5f212 [wpilib] Improve MotorSafety documentation (NFC) (#4120)
8d79dc8738 [wpimath] Add ImplicitModelFollower (#4056)
78108c2aba [wpimath] Fix PIDController having incorrect error after calling SetSetpoint() (#4070)
cdafc723fb [examples] Remove unused LinearPlantInversionFeedforward includes (#4069)
0d70884dce [wpimath] Add InterpolatedTreeMap (#4073)
765efa325e [wpimath] Remove redundant column index from vectors (#4116)
89ffcbbe41 [wpimath] Update TrapezoidProfile class name in comment (NFC) (#4107)
95ae23b0e7 [wpimath] Improve EKF numerical stability (#4093)
d5cb6fed67 [wpimath] Support zero cost entries in MakeCostMatrix() (#4100)
d0fef18378 [wpimath] Remove redundant `this.` from ExtendedKalmanFilter.java (#4115)
d640c0f41f [wpimath] Fix pose estimator local measurement standard deviation docs (NFC) (#4113)
a2fa5e3ff7 [wpilibc] BatterySim: Provide non-initializer list versions of Calculate (#4076)
a3eea9958e [hal] Add link to FRC CAN Spec (NFC) (#4086)
db27331d7b [wpilib] Update DifferentialDrive docs (NFC) (#4085)
fdfb31f164 [dlt] Export boolean[] values (#4082)
f93c3331b3 [wpigui] disable changing directory when initializing on MacOS (#4092)
ab7ac4fbb9 [build] Fix various warnings in cmake builds (#4081)
bc39a1a293 [wpilibc] Fix moved pneumatics objects not destructing properly (#4068)
2668130e70 [wpimath] Remove SwerveDrivePoseEstimator encoder reset warning (#4066)
d27ed3722b [ci] Set actions workflow concurrency (#4060)
dae18308c9 [wpimath] Minor fixes to Rotation2d docs (NFC) (#4055)
d66555e42f [datalogtool] Add datalogtool
9f52d8a3b1 [wpilib] DriverStation: Add DataLog support for modes and joystick data
757ea91932 [wpilib] Add DataLogManager
02a804f1c5 [ntcore] Add DataLog support
9b500df0d9 [wpiutil] Add high speed data logging
5a89575b3a [wpiutil] Import customized LLVM MemoryBuffer
b8c4d7527b [wpiutil] Add MappedFileRegion
ac5d46cfa7 [wpilibc] Fix ProfiledPID SetTolerance default velocity value (#4054)
bc9e96e86f [wpilib] Absolute Encoder API and behavior fixes (#4052)
f88c435dd0 [hal] Add mechanism to cancel all periodic callbacks (#4049)

Change-Id: I49aa5b08abbefc7a045e99e19d48ce2cd8fc4d1b
git-subtree-dir: third_party/allwpilib
git-subtree-split: 83f1860047c86aa3330fcb41caf3b2047e074804
Signed-off-by: James Kuszmaul <jabukuszmaul+collab@gmail.com>
diff --git a/glass/.styleguide b/glass/.styleguide
index d555d5a..c2a291a 100644
--- a/glass/.styleguide
+++ b/glass/.styleguide
@@ -24,6 +24,7 @@
   ^fmt/
   ^frc/
   ^imgui
+  ^networktables/
   ^ntcore
   ^wpi/
   ^wpigui
diff --git a/glass/build.gradle b/glass/build.gradle
index cd77f45..47b943c 100644
--- a/glass/build.gradle
+++ b/glass/build.gradle
@@ -1,6 +1,6 @@
 import org.gradle.internal.os.OperatingSystem
 
-if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) {
+if (!project.hasProperty('onlylinuxathena')) {
 
     description = "A different kind of dashboard"
 
@@ -24,6 +24,8 @@
     def wpilibVersionFileInput = file("src/app/generate/WPILibVersion.cpp.in")
     def wpilibVersionFileOutput = file("$buildDir/generated/app/cpp/WPILibVersion.cpp")
 
+    apply from: "${rootDir}/shared/imgui.gradle"
+
     task generateCppVersion() {
         description = 'Generates the wpilib version class'
         group = 'WPILib'
@@ -69,19 +71,6 @@
 
     nativeUtils.exportsConfigs {
         glass {
-            x86ExcludeSymbols = [
-                '_CT??_R0?AV_System_error',
-                '_CT??_R0?AVexception',
-                '_CT??_R0?AVfailure',
-                '_CT??_R0?AVruntime_error',
-                '_CT??_R0?AVsystem_error',
-                '_CTA5?AVfailure',
-                '_TI5?AVfailure',
-                '_CT??_R0?AVout_of_range',
-                '_CTA3?AVout_of_range',
-                '_TI3?AVout_of_range',
-                '_CT??_R0?AVbad_cast'
-            ]
             x64ExcludeSymbols = [
                 '_CT??_R0?AV_System_error',
                 '_CT??_R0?AVexception',
@@ -113,7 +102,7 @@
                     }
                 }
                 binaries.all {
-                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) {
+                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
                         it.buildable = false
                         return
                     }
@@ -124,7 +113,7 @@
                     lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
                     lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
                     lib project: ':wpigui', library: 'wpigui', linkage: 'static'
-                    nativeUtils.useRequiredLibrary(it, 'imgui_static')
+                    nativeUtils.useRequiredLibrary(it, 'imgui')
                 }
                 appendDebugPathToBinaries(binaries)
             }
@@ -141,7 +130,7 @@
                     }
                 }
                 binaries.all {
-                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) {
+                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
                         it.buildable = false
                         return
                     }
@@ -150,11 +139,12 @@
                         return
                     }
                     lib library: nativeName, linkage: 'static'
-                    lib project: ':ntcore', library: 'ntcore', linkage: 'shared'
+                    project(':ntcore').addNtcoreDependency(it, 'shared')
+                    lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
                     lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
                     lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
                     lib project: ':wpigui', library: 'wpigui', linkage: 'static'
-                    nativeUtils.useRequiredLibrary(it, 'imgui_static')
+                    nativeUtils.useRequiredLibrary(it, 'imgui')
                 }
                 appendDebugPathToBinaries(binaries)
             }
@@ -181,19 +171,20 @@
                     }
                 }
                 binaries.all {
-                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) {
+                    if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
                         it.buildable = false
                         return
                     }
                     lib project: ':cscore', library: 'cscore', linkage: 'static'
                     lib library: 'glassnt', linkage: 'static'
                     lib library: nativeName, linkage: 'static'
-                    lib project: ':ntcore', library: 'ntcore', linkage: 'static'
+                    project(':ntcore').addNtcoreDependency(it, 'static')
+                    lib project: ':wpinet', library: 'wpinet', linkage: 'static'
                     lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
                     lib project: ':wpimath', library: 'wpimath', linkage: 'static'
                     lib project: ':wpigui', library: 'wpigui', linkage: 'static'
                     nativeUtils.useRequiredLibrary(it, 'opencv_static')
-                    nativeUtils.useRequiredLibrary(it, 'imgui_static')
+                    nativeUtils.useRequiredLibrary(it, 'imgui')
                     if (it.targetPlatform.operatingSystem.isWindows()) {
                         it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib'
                         it.linker.args << '/DELAYLOAD:MF.dll' << '/DELAYLOAD:MFReadWrite.dll' << '/DELAYLOAD:MFPlat.dll' << '/delay:nobind'
@@ -201,6 +192,9 @@
                         it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore'
                     } else {
                         it.linker.args << '-lX11'
+                        if (it.targetPlatform.name.startsWith('linuxarm')) {
+                            it.linker.args << '-lGL'
+                        }
                     }
                 }
             }
diff --git a/glass/publish.gradle b/glass/publish.gradle
index f2e3da7..5e7cbaf 100644
--- a/glass/publish.gradle
+++ b/glass/publish.gradle
@@ -64,7 +64,8 @@
     tasks {
         // Create the run task.
         $.components.glassApp.binaries.each { bin ->
-            if (bin.buildable && bin.name.toLowerCase().contains("debug")) {
+            if (bin.buildable && bin.name.toLowerCase().contains("debug") && nativeUtils.isNativeDesktopPlatform(bin.targetPlatform)) {
+
                 Task run = project.tasks.create("run", Exec) {
                     commandLine bin.tasks.install.runScriptFile.get().asFile.toString()
                 }
@@ -77,20 +78,22 @@
         $.components.each { component ->
             component.binaries.each { binary ->
                 if (binary in NativeExecutableBinarySpec && binary.component.name.contains("glassApp")) {
-                    if (binary.buildable && binary.name.contains("Release")) {
+                    if (binary.buildable && (binary.name.contains('Release') || binary.name.contains('release'))) {
                         // We are now in the binary that we want.
                         // This is the default application path for the ZIP task.
                         def applicationPath = binary.executable.file
                         def icon = file("$project.projectDir/src/app/native/mac/glass.icns")
 
                         // Create the macOS bundle.
-                        def bundleTask = project.tasks.create("bundleGlassOsxApp", Copy) {
+                        def bundleTask = project.tasks.create("bundleGlassOsxApp" + binary.targetPlatform.architecture.name, Copy) {
                             description("Creates a macOS application bundle for Glass")
                             from(file("$project.projectDir/Info.plist"))
-                            into(file("$project.buildDir/outputs/bundles/Glass.app/Contents"))
+                            into(file("$project.buildDir/outputs/bundles/$binary.targetPlatform.architecture.name/Glass.app/Contents"))
                             into("MacOS") { with copySpec { from binary.executable.file } }
                             into("Resources") { with copySpec { from icon } }
 
+                            inputs.property "HasDeveloperId", project.hasProperty("developerID")
+
                             doLast {
                                 if (project.hasProperty("developerID")) {
                                     // Get path to binary.
@@ -102,7 +105,7 @@
                                             "codesign --force --strict --deep " +
                                             "--timestamp --options=runtime " +
                                             "--verbose -s ${project.findProperty("developerID")} " +
-                                            "$project.buildDir/outputs/bundles/Glass.app/"
+                                            "$project.buildDir/outputs/bundles/$binary.targetPlatform.architecture.name/Glass.app/"
                                         ]
                                         commandLine args
                                     }
@@ -112,12 +115,12 @@
 
                         // Reset the application path if we are creating a bundle.
                         if (binary.targetPlatform.operatingSystem.isMacOsX()) {
-                            applicationPath = file("$project.buildDir/outputs/bundles")
+                            applicationPath = file("$project.buildDir/outputs/bundles/$binary.targetPlatform.architecture.name")
                             project.build.dependsOn bundleTask
                         }
 
                         // Create the ZIP.
-                        def task = project.tasks.create("copyGlassExecutable", Zip) {
+                        def task = project.tasks.create("copyGlassExecutable" + binary.targetPlatform.architecture.name, Zip) {
                             description("Copies the Glass executable to the outputs directory.")
                             destinationDirectory = outputsFolder
 
diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp
index b1569b8..a20ff8b 100644
--- a/glass/src/app/native/cpp/main.cpp
+++ b/glass/src/app/native/cpp/main.cpp
@@ -42,6 +42,7 @@
 static std::unique_ptr<glass::NetworkTablesSettings> gNetworkTablesSettings;
 static glass::LogData gNetworkTablesLog;
 static std::unique_ptr<glass::Window> gNetworkTablesWindow;
+static std::unique_ptr<glass::Window> gNetworkTablesInfoWindow;
 static std::unique_ptr<glass::Window> gNetworkTablesSettingsWindow;
 static std::unique_ptr<glass::Window> gNetworkTablesLogWindow;
 
@@ -49,48 +50,61 @@
 static bool gAbout = false;
 static bool gSetEnterKey = false;
 static bool gKeyEdit = false;
+static int* gEnterKey;
+static void (*gPrevKeyCallback)(GLFWwindow*, int, int, int, int);
+
+static void RemapEnterKeyCallback(GLFWwindow* window, int key, int scancode,
+                                  int action, int mods) {
+  if (action == GLFW_PRESS || action == GLFW_RELEASE) {
+    if (gKeyEdit) {
+      *gEnterKey = key;
+      gKeyEdit = false;
+    } else if (*gEnterKey == key) {
+      key = GLFW_KEY_ENTER;
+    }
+  }
+
+  if (gPrevKeyCallback) {
+    gPrevKeyCallback(window, key, scancode, action, mods);
+  }
+}
 
 static void NtInitialize() {
-  // update window title when connection status changes
   auto inst = nt::GetDefaultInstance();
-  auto poller = nt::CreateConnectionListenerPoller(inst);
-  nt::AddPolledConnectionListener(poller, true);
+  auto poller = nt::CreateListenerPoller(inst);
+  nt::AddPolledListener(
+      poller, inst,
+      NT_EVENT_CONNECTION | NT_EVENT_IMMEDIATE | NT_EVENT_LOGMESSAGE);
   gui::AddEarlyExecute([poller] {
     auto win = gui::GetSystemWindow();
     if (!win) {
       return;
     }
-    bool timedOut;
-    for (auto&& event : nt::PollConnectionListener(poller, 0, &timedOut)) {
-      if (event.connected) {
-        glfwSetWindowTitle(
-            win, fmt::format("Glass - Connected ({})", event.conn.remote_ip)
-                     .c_str());
-      } else {
-        glfwSetWindowTitle(win, "Glass - DISCONNECTED");
+    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");
+        }
+      } else if (auto msg = event.GetLogMessage()) {
+        const char* level = "";
+        if (msg->level >= NT_LOG_CRITICAL) {
+          level = "CRITICAL: ";
+        } else if (msg->level >= NT_LOG_ERROR) {
+          level = "ERROR: ";
+        } else if (msg->level >= NT_LOG_WARNING) {
+          level = "WARNING: ";
+        }
+        gNetworkTablesLog.Append(fmt::format(
+            "{}{} ({}:{})\n", level, msg->message, msg->filename, msg->line));
       }
     }
   });
 
-  // handle NetworkTables log messages
-  auto logPoller = nt::CreateLoggerPoller(inst);
-  nt::AddPolledLogger(logPoller, NT_LOG_INFO, 100);
-  gui::AddEarlyExecute([logPoller] {
-    bool timedOut;
-    for (auto&& msg : nt::PollLogger(logPoller, 0, &timedOut)) {
-      const char* level = "";
-      if (msg.level >= NT_LOG_CRITICAL) {
-        level = "CRITICAL: ";
-      } else if (msg.level >= NT_LOG_ERROR) {
-        level = "ERROR: ";
-      } else if (msg.level >= NT_LOG_WARNING) {
-        level = "WARNING: ";
-      }
-      gNetworkTablesLog.Append(fmt::format("{}{} ({}:{})\n", level, msg.message,
-                                           msg.filename, msg.line));
-    }
-  });
-
   gNetworkTablesLogWindow = std::make_unique<glass::Window>(
       glass::GetStorageRoot().GetChild("NetworkTables Log"),
       "NetworkTables Log", glass::Window::kHide);
@@ -114,9 +128,21 @@
   gNetworkTablesWindow->DisableRenamePopup();
   gui::AddLateExecute([] { gNetworkTablesWindow->Display(); });
 
+  // NetworkTables info window
+  gNetworkTablesInfoWindow = std::make_unique<glass::Window>(
+      glass::GetStorageRoot().GetChild("NetworkTables Info"),
+      "NetworkTables Info");
+  gNetworkTablesInfoWindow->SetView(glass::MakeFunctionView(
+      [&] { glass::DisplayNetworkTablesInfo(gNetworkTablesModel.get()); }));
+  gNetworkTablesInfoWindow->SetDefaultPos(250, 130);
+  gNetworkTablesInfoWindow->SetDefaultSize(750, 145);
+  gNetworkTablesInfoWindow->SetDefaultVisibility(glass::Window::kHide);
+  gNetworkTablesInfoWindow->DisableRenamePopup();
+  gui::AddLateExecute([] { gNetworkTablesInfoWindow->Display(); });
+
   // NetworkTables settings window
   gNetworkTablesSettings = std::make_unique<glass::NetworkTablesSettings>(
-      glass::GetStorageRoot().GetChild("NetworkTables Settings"));
+      "glass", glass::GetStorageRoot().GetChild("NetworkTables Settings"));
   gui::AddEarlyExecute([] { gNetworkTablesSettings->Update(); });
 
   gNetworkTablesSettingsWindow = std::make_unique<glass::Window>(
@@ -161,6 +187,11 @@
   gui::AddIcon(glass::GetResource_glass_256_png());
   gui::AddIcon(glass::GetResource_glass_512_png());
 
+  gui::AddEarlyExecute(
+      [] { ImGui::DockSpaceOverViewport(ImGui::GetMainViewport()); });
+
+  gui::AddInit([] { ImGui::GetIO().ConfigDockingWithShift = true; });
+
   gPlotProvider = std::make_unique<glass::PlotProvider>(
       glass::GetStorageRoot().GetChild("Plots"));
   gNtProvider = std::make_unique<glass::NetworkTablesProvider>(
@@ -195,6 +226,9 @@
       if (gNetworkTablesWindow) {
         gNetworkTablesWindow->DisplayMenuItem("NetworkTables View");
       }
+      if (gNetworkTablesInfoWindow) {
+        gNetworkTablesInfoWindow->DisplayMenuItem("NetworkTables Info");
+      }
       if (gNetworkTablesLogWindow) {
         gNetworkTablesLogWindow->DisplayMenuItem("NetworkTables Log");
       }
@@ -237,11 +271,6 @@
       ImGui::EndPopup();
     }
 
-    int& enterKey = glass::GetStorageRoot().GetInt("enterKey", GLFW_KEY_ENTER);
-
-    ImGuiIO& io = ImGui::GetIO();
-    io.KeyMap[ImGuiKey_Enter] = enterKey;
-
     if (gSetEnterKey) {
       ImGui::OpenPopup("Set Enter Key");
       gSetEnterKey = false;
@@ -251,24 +280,13 @@
       ImGui::Text("This is useful to edit values without the DS disabling");
       ImGui::Separator();
 
-      if (gKeyEdit) {
-        for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); ++i) {
-          if (io.KeysDown[i]) {
-            // remove all other uses
-            enterKey = i;
-            gKeyEdit = false;
-            break;
-          }
-        }
-      }
-
       ImGui::Text("Key:");
       ImGui::SameLine();
       char editLabel[40];
       char nameBuf[32];
-      const char* name = glfwGetKeyName(enterKey, 0);
+      const char* name = glfwGetKeyName(*gEnterKey, 0);
       if (!name) {
-        std::snprintf(nameBuf, sizeof(nameBuf), "%d", enterKey);
+        std::snprintf(nameBuf, sizeof(nameBuf), "%d", *gEnterKey);
         name = nameBuf;
       }
       std::snprintf(editLabel, sizeof(editLabel), "%s###edit",
@@ -278,7 +296,7 @@
       }
       ImGui::SameLine();
       if (ImGui::SmallButton("Reset")) {
-        enterKey = GLFW_KEY_ENTER;
+        *gEnterKey = GLFW_KEY_ENTER;
       }
 
       if (ImGui::Button("Close")) {
@@ -289,7 +307,12 @@
     }
   });
 
-  gui::Initialize("Glass - DISCONNECTED", 1024, 768);
+  gui::Initialize("Glass - DISCONNECTED", 1024, 768,
+                  ImGuiConfigFlags_DockingEnable);
+  gEnterKey = &glass::GetStorageRoot().GetInt("enterKey", GLFW_KEY_ENTER);
+  if (auto win = gui::GetSystemWindow()) {
+    gPrevKeyCallback = glfwSetKeyCallback(win, RemapEnterKeyCallback);
+  }
   gui::Main();
 
   gNetworkTablesSettingsWindow.reset();
diff --git a/glass/src/lib/native/cpp/Context.cpp b/glass/src/lib/native/cpp/Context.cpp
index 1de4af8..a55cf82 100644
--- a/glass/src/lib/native/cpp/Context.cpp
+++ b/glass/src/lib/native/cpp/Context.cpp
@@ -69,6 +69,9 @@
   wpi::raw_string_ostream ini{iniStr};
 
   for (auto&& jsection : jfile.items()) {
+    if (jsection.key() == "Docking") {
+      continue;
+    }
     if (!jsection.value().is_object()) {
       ImGui::LogText("%s section %s is not object", filename,
                      jsection.key().c_str());
@@ -95,6 +98,31 @@
       ini << '\n';
     }
   }
+
+  // emit Docking section last
+  auto docking = jfile.find("Docking");
+  if (docking != jfile.end()) {
+    for (auto&& jsubsection : docking->items()) {
+      if (!jsubsection.value().is_array()) {
+        ImGui::LogText("%s section %s subsection %s is not array", filename,
+                       "Docking", jsubsection.key().c_str());
+        return false;
+      }
+      ini << "[Docking][" << jsubsection.key() << "]\n";
+      for (auto&& jv : jsubsection.value()) {
+        try {
+          auto& value = jv.get_ref<const std::string&>();
+          ini << value << "\n";
+        } catch (wpi::json::exception&) {
+          ImGui::LogText("%s section %s subsection %s value is not string",
+                         filename, "Docking", jsubsection.key().c_str());
+          return false;
+        }
+      }
+      ini << '\n';
+    }
+  }
+
   ini.flush();
 
   ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
@@ -204,7 +232,11 @@
       }
       curSection = &jsection[subsection];
       if (curSection->is_null()) {
-        *curSection = wpi::json::object();
+        if (section == "Docking") {
+          *curSection = wpi::json::array();
+        } else {
+          *curSection = wpi::json::object();
+        }
       }
     } else {
       // value
@@ -212,7 +244,11 @@
         continue;  // shouldn't happen, but just in case
       }
       auto [name, value] = wpi::split(line, '=');
-      (*curSection)[name] = value;
+      if (curSection->is_object()) {
+        (*curSection)[name] = value;
+      } else if (curSection->is_array()) {
+        curSection->emplace_back(line);
+      }
     }
   }
 
@@ -510,15 +546,24 @@
 bool glass::PopupEditName(const char* label, std::string* name) {
   bool rv = false;
   if (ImGui::BeginPopupContextItem(label)) {
-    ImGui::Text("Edit name:");
-    if (ImGui::InputText("##editname", name)) {
-      rv = true;
-    }
-    if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
-        ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
-      ImGui::CloseCurrentPopup();
-    }
+    rv = ItemEditName(name);
+
     ImGui::EndPopup();
   }
   return rv;
 }
+
+bool glass::ItemEditName(std::string* name) {
+  bool rv = false;
+
+  ImGui::Text("Edit name:");
+  if (ImGui::InputText("##editname", name)) {
+    rv = true;
+  }
+  if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
+      ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
+    ImGui::CloseCurrentPopup();
+  }
+
+  return rv;
+}
diff --git a/glass/src/lib/native/cpp/Storage.cpp b/glass/src/lib/native/cpp/Storage.cpp
index 28af20b..add6203 100644
--- a/glass/src/lib/native/cpp/Storage.cpp
+++ b/glass/src/lib/native/cpp/Storage.cpp
@@ -254,7 +254,7 @@
   }                                                                            \
                                                                                \
   std::vector<ArrCType>& Storage::Get##CapsName##Array(                        \
-      std::string_view key, wpi::span<const ArrCType> defaultVal) {            \
+      std::string_view key, std::span<const ArrCType> defaultVal) {            \
     auto& valuePtr = m_values[key];                                            \
     bool setValue = false;                                                     \
     if (!valuePtr) {                                                           \
diff --git a/glass/src/lib/native/cpp/View.cpp b/glass/src/lib/native/cpp/View.cpp
index e01c4df..3f28200 100644
--- a/glass/src/lib/native/cpp/View.cpp
+++ b/glass/src/lib/native/cpp/View.cpp
@@ -25,3 +25,9 @@
 }
 
 void View::Hidden() {}
+
+void View::Settings() {}
+
+bool View::HasSettings() {
+  return false;
+}
diff --git a/glass/src/lib/native/cpp/Window.cpp b/glass/src/lib/native/cpp/Window.cpp
index 6296fab..f43c0ee 100644
--- a/glass/src/lib/native/cpp/Window.cpp
+++ b/glass/src/lib/native/cpp/Window.cpp
@@ -4,11 +4,13 @@
 
 #include "glass/Window.h"
 
+#include <imgui.h>
 #include <imgui_internal.h>
 #include <wpi/StringExtras.h>
 
 #include "glass/Context.h"
 #include "glass/Storage.h"
+#include "glass/support/ExtraGuiWidgets.h"
 
 using namespace glass;
 
@@ -59,9 +61,57 @@
                 m_id.c_str());
 
   if (Begin(label, &m_visible, m_flags)) {
-    if (m_renamePopupEnabled) {
-      PopupEditName(nullptr, &m_name);
+    if (m_renamePopupEnabled || m_view->HasSettings()) {
+      bool isClicked = (ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
+                        ImGui::IsItemHovered());
+      ImGuiWindow* window = ImGui::GetCurrentWindow();
+
+      bool settingsButtonClicked = false;
+      // Not docked, and window has just enough for the circles not to be
+      // touching
+      if (!ImGui::IsWindowDocked() &&
+          ImGui::GetWindowWidth() > (ImGui::GetFontSize() + 2) * 3 +
+                                        ImGui::GetStyle().FramePadding.x * 2) {
+        const ImGuiItemFlags itemFlagsRestore =
+            ImGui::GetCurrentContext()->CurrentItemFlags;
+
+        ImGui::GetCurrentContext()->CurrentItemFlags |=
+            ImGuiItemFlags_NoNavDefaultFocus;
+        window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
+
+        // Allow to draw outside of normal window
+        ImGui::PushClipRect(window->OuterRectClipped.Min,
+                            window->OuterRectClipped.Max, false);
+
+        const ImRect titleBarRect = ImGui::GetCurrentWindow()->TitleBarRect();
+        const ImVec2 position = {titleBarRect.Max.x -
+                                     (ImGui::GetStyle().FramePadding.x * 3) -
+                                     (ImGui::GetFontSize() * 2),
+                                 titleBarRect.Min.y};
+        settingsButtonClicked =
+            HamburgerButton(ImGui::GetID("#SETTINGS"), position);
+
+        ImGui::PopClipRect();
+
+        ImGui::GetCurrentContext()->CurrentItemFlags = itemFlagsRestore;
+      }
+      if (settingsButtonClicked || isClicked) {
+        ImGui::OpenPopup(window->ID);
+      }
+
+      if (ImGui::BeginPopupEx(window->ID,
+                              ImGuiWindowFlags_AlwaysAutoResize |
+                                  ImGuiWindowFlags_NoTitleBar |
+                                  ImGuiWindowFlags_NoSavedSettings)) {
+        if (m_renamePopupEnabled) {
+          ItemEditName(&m_name);
+        }
+        m_view->Settings();
+
+        ImGui::EndPopup();
+      }
     }
+
     m_view->Display();
   } else {
     m_view->Hidden();
diff --git a/glass/src/lib/native/cpp/hardware/Gyro.cpp b/glass/src/lib/native/cpp/hardware/Gyro.cpp
index 36a3525..607b251 100644
--- a/glass/src/lib/native/cpp/hardware/Gyro.cpp
+++ b/glass/src/lib/native/cpp/hardware/Gyro.cpp
@@ -10,7 +10,7 @@
 
 #include <imgui.h>
 #include <imgui_internal.h>
-#include <wpi/numbers>
+#include <numbers>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -54,7 +54,7 @@
 
   // Draw the spokes at every 5 degrees and a "major" spoke every 45 degrees.
   for (int i = -175; i <= 180; i += 5) {
-    double radians = i * 2 * wpi::numbers::pi / 360.0;
+    double radians = i * 2 * std::numbers::pi / 360.0;
     ImVec2 direction(std::sin(radians), -std::cos(radians));
 
     bool major = i % 45 == 0;
@@ -74,7 +74,7 @@
 
   draw->AddCircleFilled(center, radius * 0.075, secondaryColor, 50);
 
-  double radians = value * 2 * wpi::numbers::pi / 360.0;
+  double radians = value * 2 * std::numbers::pi / 360.0;
   draw->AddLine(
       center - ImVec2(1, 0),
       center + ImVec2(std::sin(radians), -std::cos(radians)) * radius * 0.95f,
diff --git a/glass/src/lib/native/cpp/hardware/SpeedController.cpp b/glass/src/lib/native/cpp/hardware/MotorController.cpp
similarity index 89%
rename from glass/src/lib/native/cpp/hardware/SpeedController.cpp
rename to glass/src/lib/native/cpp/hardware/MotorController.cpp
index b278401..96181b2 100644
--- a/glass/src/lib/native/cpp/hardware/SpeedController.cpp
+++ b/glass/src/lib/native/cpp/hardware/MotorController.cpp
@@ -2,7 +2,7 @@
 // Open Source Software; you can modify and/or share it under the terms of
 // the WPILib BSD license file in the root directory of this project.
 
-#include "glass/hardware/SpeedController.h"
+#include "glass/hardware/MotorController.h"
 
 #include <imgui.h>
 #include <imgui_internal.h>
@@ -12,13 +12,13 @@
 
 using namespace glass;
 
-void glass::DisplaySpeedController(SpeedControllerModel* m) {
+void glass::DisplayMotorController(MotorControllerModel* m) {
   // Get duty cycle data from the model and do not display anything if the data
   // is null.
   auto dc = m->GetPercentData();
   if (!dc || !m->Exists()) {
     ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-    ImGui::Text("Unknown SpeedController");
+    ImGui::Text("Unknown MotorController");
     ImGui::PopStyleColor();
     return;
   }
diff --git a/glass/src/lib/native/cpp/other/Drive.cpp b/glass/src/lib/native/cpp/other/Drive.cpp
index a73c6de..d54b2da 100644
--- a/glass/src/lib/native/cpp/other/Drive.cpp
+++ b/glass/src/lib/native/cpp/other/Drive.cpp
@@ -11,7 +11,7 @@
 
 #include <imgui.h>
 #include <imgui_internal.h>
-#include <wpi/numbers>
+#include <numbers>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
@@ -55,11 +55,11 @@
     draw->AddTriangleFilled(
         arrowPos,
         arrowPos + ImRotate(ImVec2(0.0f, 7.5f),
-                            std::cos(angle + wpi::numbers::pi / 4),
-                            std::sin(angle + wpi::numbers::pi / 4)),
+                            std::cos(angle + std::numbers::pi / 4),
+                            std::sin(angle + std::numbers::pi / 4)),
         arrowPos + ImRotate(ImVec2(0.0f, 7.5f),
-                            std::cos(angle - wpi::numbers::pi / 4),
-                            std::sin(angle - wpi::numbers::pi / 4)),
+                            std::cos(angle - std::numbers::pi / 4),
+                            std::sin(angle - std::numbers::pi / 4)),
         color);
   };
 
@@ -88,30 +88,30 @@
   if (rotation != 0) {
     float radius = 60.0f;
     double a1 = 0.0;
-    double a2 = wpi::numbers::pi / 2 * rotation;
+    double a2 = std::numbers::pi / 2 * rotation;
 
     // PathArcTo requires a_min <= a_max, and rotation can be negative
     if (a1 > a2) {
       draw->PathArcTo(center, radius, a2, a1, 20);
       draw->PathStroke(color, false);
-      draw->PathArcTo(center, radius, a2 + wpi::numbers::pi,
-                      a1 + wpi::numbers::pi, 20);
+      draw->PathArcTo(center, radius, a2 + std::numbers::pi,
+                      a1 + std::numbers::pi, 20);
       draw->PathStroke(color, false);
     } else {
       draw->PathArcTo(center, radius, a1, a2, 20);
       draw->PathStroke(color, false);
-      draw->PathArcTo(center, radius, a1 + wpi::numbers::pi,
-                      a2 + wpi::numbers::pi, 20);
+      draw->PathArcTo(center, radius, a1 + std::numbers::pi,
+                      a2 + std::numbers::pi, 20);
       draw->PathStroke(color, false);
     }
 
-    double adder = rotation < 0 ? wpi::numbers::pi : 0;
+    double adder = rotation < 0 ? std::numbers::pi : 0;
 
     auto arrowPos =
         center + ImVec2(radius * -std::cos(a2), radius * -std::sin(a2));
     drawArrow(arrowPos, a2 + adder);
 
-    a2 += wpi::numbers::pi;
+    a2 += std::numbers::pi;
     arrowPos = center + ImVec2(radius * -std::cos(a2), radius * -std::sin(a2));
     drawArrow(arrowPos, a2 + adder);
   }
diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp
index c3ef860..66e90b1 100644
--- a/glass/src/lib/native/cpp/other/Field2D.cpp
+++ b/glass/src/lib/native/cpp/other/Field2D.cpp
@@ -93,7 +93,7 @@
 
   SelectedTargetInfo* GetTarget() { return &m_target; }
   FieldObjectModel* GetInsertModel() { return m_insertModel; }
-  wpi::span<const frc::Pose2d> GetInsertPoses() const { return m_insertPoses; }
+  std::span<const frc::Pose2d> GetInsertPoses() const { return m_insertPoses; }
 
   void Display(Field2DModel* model, const FieldFrameData& ffd);
 
@@ -113,7 +113,7 @@
 struct DisplayOptions {
   explicit DisplayOptions(const gui::Texture& texture) : texture{texture} {}
 
-  enum Style { kBoxImage = 0, kLine, kLineClosed, kTrack };
+  enum Style { kBoxImage = 0, kLine, kLineClosed, kTrack, kHidden };
 
   static constexpr Style kDefaultStyle = kBoxImage;
   static constexpr float kDefaultWeight = 4.0f;
@@ -189,7 +189,7 @@
 
   DisplayOptions GetDisplayOptions() const;
   void DisplaySettings();
-  void DrawLine(ImDrawList* drawList, wpi::span<const ImVec2> points) const;
+  void DrawLine(ImDrawList* drawList, std::span<const ImVec2> points) const;
 
   void LoadImage();
   const gui::Texture& GetTexture() const { return m_texture; }
@@ -547,7 +547,7 @@
                                 DisplayOptions::kDefaultLength.to<float>())},
       m_style{storage.GetString("style"),
               DisplayOptions::kDefaultStyle,
-              {"Box/Image", "Line", "Line (Closed)", "Track"}},
+              {"Box/Image", "Line", "Line (Closed)", "Track", "Hidden"}},
       m_weight{storage.GetFloat("weight", DisplayOptions::kDefaultWeight)},
       m_color{
           storage.GetFloatArray("color", DisplayOptions::kDefaultColorFloat)},
@@ -617,7 +617,7 @@
 }
 
 void ObjectInfo::DrawLine(ImDrawList* drawList,
-                          wpi::span<const ImVec2> points) const {
+                          std::span<const ImVec2> points) const {
   if (points.empty()) {
     return;
   }
@@ -840,6 +840,8 @@
       left->emplace_back(m_corners[4]);
       right->emplace_back(m_corners[5]);
       break;
+    case DisplayOptions::kHidden:
+      break;
   }
 
   if (m_displayOptions.arrows) {
@@ -1215,10 +1217,14 @@
 }
 
 void Field2DView::Display() {
-  if (ImGui::BeginPopupContextItem()) {
-    DisplayField2DSettings(m_model);
-    ImGui::EndPopup();
-  }
   DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() -
                               ImGui::GetWindowContentRegionMin());
 }
+
+void Field2DView::Settings() {
+  DisplayField2DSettings(m_model);
+}
+
+bool Field2DView::HasSettings() {
+  return true;
+}
diff --git a/glass/src/lib/native/cpp/other/Log.cpp b/glass/src/lib/native/cpp/other/Log.cpp
index 9a1d2c5..accf024 100644
--- a/glass/src/lib/native/cpp/other/Log.cpp
+++ b/glass/src/lib/native/cpp/other/Log.cpp
@@ -62,18 +62,21 @@
 }
 
 void LogView::Display() {
-  if (ImGui::BeginPopupContextItem()) {
-    ImGui::Checkbox("Auto-scroll", &m_autoScroll);
-    if (ImGui::Selectable("Clear")) {
-      m_data->Clear();
-    }
-    const auto& buf = m_data->GetBuffer();
-    if (ImGui::Selectable("Copy to Clipboard", false,
-                          buf.empty() ? ImGuiSelectableFlags_Disabled : 0)) {
-      ImGui::SetClipboardText(buf.c_str());
-    }
-    ImGui::EndPopup();
-  }
-
   DisplayLog(m_data, m_autoScroll);
 }
+
+void LogView::Settings() {
+  ImGui::Checkbox("Auto-scroll", &m_autoScroll);
+  if (ImGui::Selectable("Clear")) {
+    m_data->Clear();
+  }
+  const auto& buf = m_data->GetBuffer();
+  if (ImGui::Selectable("Copy to Clipboard", false,
+                        buf.empty() ? ImGuiSelectableFlags_Disabled : 0)) {
+    ImGui::SetClipboardText(buf.c_str());
+  }
+}
+
+bool LogView::HasSettings() {
+  return true;
+}
diff --git a/glass/src/lib/native/cpp/other/Mechanism2D.cpp b/glass/src/lib/native/cpp/other/Mechanism2D.cpp
index aa801a7..722585b 100644
--- a/glass/src/lib/native/cpp/other/Mechanism2D.cpp
+++ b/glass/src/lib/native/cpp/other/Mechanism2D.cpp
@@ -249,10 +249,14 @@
 }
 
 void Mechanism2DView::Display() {
-  if (ImGui::BeginPopupContextItem()) {
-    DisplayMechanism2DSettings(m_model);
-    ImGui::EndPopup();
-  }
   DisplayMechanism2D(m_model, ImGui::GetWindowContentRegionMax() -
                                   ImGui::GetWindowContentRegionMin());
 }
+
+void Mechanism2DView::Settings() {
+  DisplayMechanism2DSettings(m_model);
+}
+
+bool Mechanism2DView::HasSettings() {
+  return true;
+}
diff --git a/glass/src/lib/native/cpp/other/Plot.cpp b/glass/src/lib/native/cpp/other/Plot.cpp
index bff55b7..13d7c96 100644
--- a/glass/src/lib/native/cpp/other/Plot.cpp
+++ b/glass/src/lib/native/cpp/other/Plot.cpp
@@ -53,11 +53,12 @@
 };
 
 class PlotSeries {
-  explicit PlotSeries(Storage& storage, int yAxis = 0);
+  explicit PlotSeries(Storage& storage);
 
  public:
   PlotSeries(Storage& storage, std::string_view id);
-  PlotSeries(Storage& storage, DataSource* source, int yAxis = 0);
+  PlotSeries(Storage& storage, DataSource* source);
+  PlotSeries(Storage& storage, DataSource* source, int yAxis);
 
   const std::string& GetId() const { return m_id; }
 
@@ -83,7 +84,7 @@
     return m_digital.GetValue() == kDigital ||
            (m_digital.GetValue() == kAuto && m_source && m_source->IsDigital());
   }
-  void AppendValue(double value, uint64_t time);
+  void AppendValue(double value, int64_t time);
 
   // source linkage
   DataSource* m_source = nullptr;
@@ -149,7 +150,6 @@
   bool& m_legendHorizontal;
   int& m_legendLocation;
   bool& m_crosshairs;
-  bool& m_antialiased;
   bool& m_mousePosition;
   bool& m_yAxis2;
   bool& m_yAxis3;
@@ -182,6 +182,8 @@
   PlotView(PlotProvider* provider, Storage& storage);
 
   void Display() override;
+  void Settings() override;
+  bool HasSettings() override;
 
   void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex);
 
@@ -196,7 +198,7 @@
 
 }  // namespace
 
-PlotSeries::PlotSeries(Storage& storage, int yAxis)
+PlotSeries::PlotSeries(Storage& storage)
     : m_id{storage.GetString("id")},
       m_name{storage.GetString("name")},
       m_yAxis{storage.GetInt("yAxis", 0)},
@@ -209,12 +211,10 @@
       m_digital{
           storage.GetString("digital"), kAuto, {"Auto", "Digital", "Analog"}},
       m_digitalBitHeight{storage.GetInt("digitalBitHeight", 8)},
-      m_digitalBitGap{storage.GetInt("digitalBitGap", 4)} {
-  m_yAxis = yAxis;
-}
+      m_digitalBitGap{storage.GetInt("digitalBitGap", 4)} {}
 
 PlotSeries::PlotSeries(Storage& storage, std::string_view id)
-    : PlotSeries{storage, 0} {
+    : PlotSeries{storage} {
   m_id = id;
   if (DataSource* source = DataSource::Find(id)) {
     SetSource(source);
@@ -223,12 +223,17 @@
   CheckSource();
 }
 
-PlotSeries::PlotSeries(Storage& storage, DataSource* source, int yAxis)
-    : PlotSeries{storage, yAxis} {
+PlotSeries::PlotSeries(Storage& storage, DataSource* source)
+    : PlotSeries{storage} {
   SetSource(source);
   m_id = source->GetId();
 }
 
+PlotSeries::PlotSeries(Storage& storage, DataSource* source, int yAxis)
+    : PlotSeries{storage, source} {
+  m_yAxis = yAxis;
+}
+
 void PlotSeries::CheckSource() {
   if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
     m_source = nullptr;
@@ -249,10 +254,10 @@
   AppendValue(source->GetValue(), 0);
 
   m_newValueConn = source->valueChanged.connect_connection(
-      [this](double value, uint64_t time) { AppendValue(value, time); });
+      [this](double value, int64_t time) { AppendValue(value, time); });
 }
 
-void PlotSeries::AppendValue(double value, uint64_t timeUs) {
+void PlotSeries::AppendValue(double value, int64_t timeUs) {
   double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
   if (IsDigital()) {
     if (m_size < kMaxSize) {
@@ -327,7 +332,7 @@
     int offset;
   };
   GetterData getterData = {now, GetZeroTime() * 1.0e-6, m_data, size, offset};
-  auto getter = [](void* data, int idx) {
+  auto getter = [](int idx, void* data) {
     auto d = static_cast<GetterData*>(data);
     if (idx == d->size) {
       return ImPlotPoint{
@@ -487,7 +492,6 @@
       m_legendLocation{
           storage.GetInt("legendLocation", ImPlotLocation_NorthWest)},
       m_crosshairs{storage.GetBool("crosshairs", false)},
-      m_antialiased{storage.GetBool("antialiased", false)},
       m_mousePosition{storage.GetBool("mousePosition", true)},
       m_yAxis2{storage.GetBool("yaxis2", false)},
       m_yAxis3{storage.GetBool("yaxis3", false)},
@@ -573,7 +577,6 @@
                 static_cast<int>(i));
   ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) |
                           (m_crosshairs ? ImPlotFlags_Crosshairs : 0) |
-                          (m_antialiased ? ImPlotFlags_AntiAliased : 0) |
                           (m_mousePosition ? 0 : ImPlotFlags_NoMouseText);
 
   if (ImPlot::BeginPlot(label, ImVec2(-1, m_height), plotFlags)) {
@@ -608,7 +611,6 @@
           (m_axis[i].lockMin ? ImPlotAxisFlags_LockMin : 0) |
           (m_axis[i].lockMax ? ImPlotAxisFlags_LockMax : 0) |
           (m_axis[i].autoFit ? ImPlotAxisFlags_AutoFit : 0) |
-          (m_axis[i].logScale ? ImPlotAxisFlags_AutoFit : 0) |
           (m_axis[i].invert ? ImPlotAxisFlags_Invert : 0) |
           (m_axis[i].opposite ? ImPlotAxisFlags_Opposite : 0) |
           (m_axis[i].gridLines ? 0 : ImPlotAxisFlags_NoGridLines) |
@@ -620,6 +622,9 @@
       ImPlot::SetupAxisLimits(
           ImAxis_Y1 + i, m_axis[i].min, m_axis[i].max,
           m_axis[i].apply ? ImGuiCond_Always : ImGuiCond_Once);
+      ImPlot::SetupAxisScale(ImAxis_Y1 + i, m_axis[i].logScale
+                                                ? ImPlotScale_Log10
+                                                : ImPlotScale_Linear);
       m_axis[i].apply = false;
     }
 
@@ -656,7 +661,6 @@
     // copy plot settings back to storage
     m_legend = (plot->Flags & ImPlotFlags_NoLegend) == 0;
     m_crosshairs = (plot->Flags & ImPlotFlags_Crosshairs) != 0;
-    m_antialiased = (plot->Flags & ImPlotFlags_AntiAliased) != 0;
     m_legendOutside =
         (plot->Items.Legend.Flags & ImPlotLegendFlags_Outside) != 0;
     m_legendHorizontal =
@@ -671,12 +675,12 @@
       m_axis[i].lockMin = (flags & ImPlotAxisFlags_LockMin) != 0;
       m_axis[i].lockMax = (flags & ImPlotAxisFlags_LockMax) != 0;
       m_axis[i].autoFit = (flags & ImPlotAxisFlags_AutoFit) != 0;
-      m_axis[i].logScale = (flags & ImPlotAxisFlags_LogScale) != 0;
       m_axis[i].invert = (flags & ImPlotAxisFlags_Invert) != 0;
       m_axis[i].opposite = (flags & ImPlotAxisFlags_Opposite) != 0;
       m_axis[i].gridLines = (flags & ImPlotAxisFlags_NoGridLines) == 0;
       m_axis[i].tickMarks = (flags & ImPlotAxisFlags_NoTickMarks) == 0;
       m_axis[i].tickLabels = (flags & ImPlotAxisFlags_NoTickLabels) == 0;
+      m_axis[i].logScale = plot->Axes[ImAxis_Y1 + i].Scale == ImPlotScale_Log10;
     }
   }
 }
@@ -765,71 +769,6 @@
 }
 
 void PlotView::Display() {
-  if (ImGui::BeginPopupContextItem()) {
-    if (ImGui::Button("Add plot")) {
-      m_plotsStorage.emplace_back(std::make_unique<Storage>());
-      m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
-    }
-
-    for (size_t i = 0; i < m_plots.size(); ++i) {
-      auto& plot = m_plots[i];
-      ImGui::PushID(i);
-
-      char name[64];
-      if (!plot->GetName().empty()) {
-        std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
-      } else {
-        std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
-      }
-
-      char label[90];
-      std::snprintf(label, sizeof(label), "%s###header%d", name,
-                    static_cast<int>(i));
-
-      bool open = ImGui::CollapsingHeader(label);
-
-      // DND source and target for Plot
-      if (ImGui::BeginDragDropSource()) {
-        PlotSeriesRef ref = {this, i, 0};
-        ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
-        ImGui::TextUnformatted(name);
-        ImGui::EndDragDropSource();
-      }
-      plot->DragDropTarget(*this, i, false);
-
-      if (open) {
-        if (ImGui::Button("Move Up")) {
-          if (i > 0) {
-            std::swap(m_plotsStorage[i - 1], m_plotsStorage[i]);
-            std::swap(m_plots[i - 1], plot);
-          }
-        }
-
-        ImGui::SameLine();
-        if (ImGui::Button("Move Down")) {
-          if (i < (m_plots.size() - 1)) {
-            std::swap(m_plotsStorage[i], m_plotsStorage[i + 1]);
-            std::swap(plot, m_plots[i + 1]);
-          }
-        }
-
-        ImGui::SameLine();
-        if (ImGui::Button("Delete")) {
-          m_plotsStorage.erase(m_plotsStorage.begin() + i);
-          m_plots.erase(m_plots.begin() + i);
-          ImGui::PopID();
-          continue;
-        }
-
-        plot->EmitSettings(i);
-      }
-
-      ImGui::PopID();
-    }
-
-    ImGui::EndPopup();
-  }
-
   if (m_plots.empty()) {
     if (ImGui::Button("Add plot")) {
       m_plotsStorage.emplace_back(std::make_unique<Storage>());
@@ -966,6 +905,73 @@
   });
 }
 
+void PlotView::Settings() {
+  if (ImGui::Button("Add plot")) {
+    m_plotsStorage.emplace_back(std::make_unique<Storage>());
+    m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
+  }
+
+  for (size_t i = 0; i < m_plots.size(); ++i) {
+    auto& plot = m_plots[i];
+    ImGui::PushID(i);
+
+    char name[64];
+    if (!plot->GetName().empty()) {
+      std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
+    } else {
+      std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
+    }
+
+    char label[90];
+    std::snprintf(label, sizeof(label), "%s###header%d", name,
+                  static_cast<int>(i));
+
+    bool open = ImGui::CollapsingHeader(label);
+
+    // DND source and target for Plot
+    if (ImGui::BeginDragDropSource()) {
+      PlotSeriesRef ref = {this, i, 0};
+      ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
+      ImGui::TextUnformatted(name);
+      ImGui::EndDragDropSource();
+    }
+    plot->DragDropTarget(*this, i, false);
+
+    if (open) {
+      if (ImGui::Button("Move Up")) {
+        if (i > 0) {
+          std::swap(m_plotsStorage[i - 1], m_plotsStorage[i]);
+          std::swap(m_plots[i - 1], plot);
+        }
+      }
+
+      ImGui::SameLine();
+      if (ImGui::Button("Move Down")) {
+        if (i < (m_plots.size() - 1)) {
+          std::swap(m_plotsStorage[i], m_plotsStorage[i + 1]);
+          std::swap(plot, m_plots[i + 1]);
+        }
+      }
+
+      ImGui::SameLine();
+      if (ImGui::Button("Delete")) {
+        m_plotsStorage.erase(m_plotsStorage.begin() + i);
+        m_plots.erase(m_plots.begin() + i);
+        ImGui::PopID();
+        continue;
+      }
+
+      plot->EmitSettings(i);
+    }
+
+    ImGui::PopID();
+  }
+}
+
+bool PlotView::HasSettings() {
+  return true;
+}
+
 PlotProvider::~PlotProvider() = default;
 
 void PlotProvider::DisplayMenu() {
@@ -989,7 +995,7 @@
     for (size_t i = 0; i <= numWindows; ++i) {
       std::snprintf(id, sizeof(id), "Plot <%d>", static_cast<int>(i));
       bool match = false;
-      for (size_t j = i; j < numWindows; ++j) {
+      for (size_t j = 0; j < numWindows; ++j) {
         if (m_windows[j]->GetId() == id) {
           match = true;
           break;
diff --git a/glass/src/lib/native/cpp/support/EnumSetting.cpp b/glass/src/lib/native/cpp/support/EnumSetting.cpp
index 848b588..b863b70 100644
--- a/glass/src/lib/native/cpp/support/EnumSetting.cpp
+++ b/glass/src/lib/native/cpp/support/EnumSetting.cpp
@@ -10,16 +10,13 @@
 
 EnumSetting::EnumSetting(std::string& str, int defaultValue,
                          std::initializer_list<const char*> choices)
-    : m_str{str}, m_choices{choices}, m_value{defaultValue} {
-  // override default value if str is one of the choices
-  int i = 0;
-  for (auto choice : choices) {
-    if (str == choice) {
-      m_value = i;
-      break;
-    }
-    ++i;
+    : m_str{str}, m_choices{choices}, m_defaultValue{defaultValue} {}
+
+int EnumSetting::GetValue() const {
+  if (m_value == -1) {
+    UpdateValue();
   }
+  return m_value;
 }
 
 void EnumSetting::SetValue(int value) {
@@ -29,6 +26,9 @@
 
 bool EnumSetting::Combo(const char* label, int numOptions,
                         int popup_max_height_in_items) {
+  if (m_value == -1) {
+    UpdateValue();
+  }
   if (ImGui::Combo(
           label, &m_value, m_choices.data(),
           numOptions < 0 ? m_choices.size() : static_cast<size_t>(numOptions),
@@ -38,3 +38,17 @@
   }
   return false;
 }
+
+void EnumSetting::UpdateValue() const {
+  // override default value if str is one of the choices
+  int i = 0;
+  for (auto choice : m_choices) {
+    if (m_str == choice) {
+      m_value = i;
+      return;
+    }
+    ++i;
+  }
+  // no match, default it
+  m_value = m_defaultValue;
+}
diff --git a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
index 56f4e77..191634e 100644
--- a/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
+++ b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp
@@ -4,6 +4,8 @@
 
 #include "glass/support/ExtraGuiWidgets.h"
 
+#include <imgui.h>
+
 #define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui_internal.h>
 
@@ -174,4 +176,46 @@
   return rv;
 }
 
+bool HamburgerButton(const ImGuiID id, const ImVec2 position) {
+  const ImGuiStyle& style = ImGui::GetStyle();
+
+  ImGuiWindow* window = ImGui::GetCurrentWindow();
+
+  // Frame padding on both sides, then one character in the middle
+  const ImRect bb{
+      position, position + ImVec2(ImGui::GetFontSize(), ImGui::GetFontSize()) +
+                    style.FramePadding * 2.0f};
+
+  ImGui::ItemAdd(bb, id);
+
+  bool hovered, held;
+  bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
+
+  const ImU32 bgCol =
+      ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
+  const ImVec2 center = bb.GetCenter();
+  if (hovered) {
+    window->DrawList->AddCircleFilled(
+        center, ImMax(2.0f, ImGui::GetFontSize() * 0.5f + 1.0f), bgCol, 12);
+  }
+
+  const ImU32 fgCol = ImGui::GetColorU32(ImGuiCol_Text);
+
+  const float halfLineWidth = ImGui::GetFontSize() * 0.5 * 0.7071;
+  const float halfTotalHeight = halfLineWidth * 0.875;
+  ImVec2 lineStart = {center.x - halfLineWidth, center.y - halfTotalHeight};
+  ImVec2 lineEnd = {center.x + halfLineWidth, center.y - halfTotalHeight};
+
+  ImVec2 increment = {0.0, halfTotalHeight};
+
+  for (int i = 0; i < 3; i++) {
+    window->DrawList->AddLine(lineStart, lineEnd, fgCol);
+
+    lineStart += increment;
+    lineEnd += increment;
+  }
+
+  return pressed;
+}
+
 }  // namespace glass
diff --git a/glass/src/lib/native/include/glass/Context.h b/glass/src/lib/native/include/glass/Context.h
index 14ade2c..e8dada3 100644
--- a/glass/src/lib/native/include/glass/Context.h
+++ b/glass/src/lib/native/include/glass/Context.h
@@ -201,4 +201,6 @@
 
 bool PopupEditName(const char* label, std::string* name);
 
+bool ItemEditName(std::string* name);
+
 }  // namespace glass
diff --git a/glass/src/lib/native/include/glass/DataSource.h b/glass/src/lib/native/include/glass/DataSource.h
index 5eebb3c..7e0c673 100644
--- a/glass/src/lib/native/include/glass/DataSource.h
+++ b/glass/src/lib/native/include/glass/DataSource.h
@@ -6,7 +6,6 @@
 
 #include <stdint.h>
 
-#include <atomic>
 #include <string>
 #include <string_view>
 
@@ -38,11 +37,22 @@
   void SetDigital(bool digital) { m_digital = digital; }
   bool IsDigital() const { return m_digital; }
 
-  void SetValue(double value, uint64_t time = 0) {
+  void SetValue(double value, int64_t time = 0) {
+    std::scoped_lock lock{m_valueMutex};
     m_value = value;
+    m_valueTime = time;
     valueChanged(value, time);
   }
-  double GetValue() const { return m_value; }
+
+  double GetValue() const {
+    std::scoped_lock lock{m_valueMutex};
+    return m_value;
+  }
+
+  int64_t GetValueTime() const {
+    std::scoped_lock lock{m_valueMutex};
+    return m_valueTime;
+  }
 
   // drag source helpers
   void LabelText(const char* label, const char* fmt, ...) const IM_FMTARGS(3);
@@ -59,7 +69,7 @@
                 ImGuiInputTextFlags flags = 0) const;
   void EmitDrag(ImGuiDragDropFlags flags = 0) const;
 
-  wpi::sig::SignalBase<wpi::spinlock, double, uint64_t> valueChanged;
+  wpi::sig::SignalBase<wpi::spinlock, double, int64_t> valueChanged;
 
   static DataSource* Find(std::string_view id);
 
@@ -69,7 +79,9 @@
   std::string m_id;
   std::string& m_name;
   bool m_digital = false;
-  std::atomic<double> m_value = 0;
+  mutable wpi::spinlock m_valueMutex;
+  double m_value = 0;
+  int64_t m_valueTime = 0;
 };
 
 }  // namespace glass
diff --git a/glass/src/lib/native/include/glass/Storage.h b/glass/src/lib/native/include/glass/Storage.h
index 004b8b4..7ebfa6d 100644
--- a/glass/src/lib/native/include/glass/Storage.h
+++ b/glass/src/lib/native/include/glass/Storage.h
@@ -8,6 +8,7 @@
 
 #include <functional>
 #include <memory>
+#include <span>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -15,7 +16,6 @@
 
 #include <wpi/StringMap.h>
 #include <wpi/iterator_range.h>
-#include <wpi/span.h>
 
 namespace wpi {
 class json;
@@ -137,17 +137,17 @@
                          std::string_view defaultVal = {});
 
   std::vector<int>& GetIntArray(std::string_view key,
-                                wpi::span<const int> defaultVal = {});
+                                std::span<const int> defaultVal = {});
   std::vector<int64_t>& GetInt64Array(std::string_view key,
-                                      wpi::span<const int64_t> defaultVal = {});
+                                      std::span<const int64_t> defaultVal = {});
   std::vector<int>& GetBoolArray(std::string_view key,
-                                 wpi::span<const int> defaultVal = {});
+                                 std::span<const int> defaultVal = {});
   std::vector<float>& GetFloatArray(std::string_view key,
-                                    wpi::span<const float> defaultVal = {});
+                                    std::span<const float> defaultVal = {});
   std::vector<double>& GetDoubleArray(std::string_view key,
-                                      wpi::span<const double> defaultVal = {});
+                                      std::span<const double> defaultVal = {});
   std::vector<std::string>& GetStringArray(
-      std::string_view key, wpi::span<const std::string> defaultVal = {});
+      std::string_view key, std::span<const std::string> defaultVal = {});
   std::vector<std::unique_ptr<Storage>>& GetChildArray(std::string_view key);
 
   Value* FindValue(std::string_view key);
diff --git a/glass/src/lib/native/include/glass/View.h b/glass/src/lib/native/include/glass/View.h
index 886c29e..f52ebc6 100644
--- a/glass/src/lib/native/include/glass/View.h
+++ b/glass/src/lib/native/include/glass/View.h
@@ -35,6 +35,17 @@
    * ImGui::Begin() returns false).
    */
   virtual void Hidden();
+
+  /**
+   * Called from within ImGui::BeginContextPopupItem() and ImGui::EndPopup().
+   * Used to display the settings for the view
+   */
+  virtual void Settings();
+
+  /**
+   * If the view has settings and if the result of Settings should be displayed.
+   */
+  virtual bool HasSettings();
 };
 
 /**
diff --git a/glass/src/lib/native/include/glass/hardware/LEDDisplay.h b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h
index ddd3c27..3aee6ae 100644
--- a/glass/src/lib/native/include/glass/hardware/LEDDisplay.h
+++ b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h
@@ -4,8 +4,9 @@
 
 #pragma once
 
+#include <span>
+
 #include <wpi/function_ref.h>
-#include <wpi/span.h>
 
 #include "glass/Model.h"
 
@@ -27,7 +28,7 @@
 
   virtual bool IsRunning() = 0;
 
-  virtual wpi::span<const Data> GetData(wpi::SmallVectorImpl<Data>& buf) = 0;
+  virtual std::span<const Data> GetData(wpi::SmallVectorImpl<Data>& buf) = 0;
 };
 
 class LEDDisplaysModel : public glass::Model {
diff --git a/glass/src/lib/native/include/glass/hardware/SpeedController.h b/glass/src/lib/native/include/glass/hardware/MotorController.h
similarity index 83%
rename from glass/src/lib/native/include/glass/hardware/SpeedController.h
rename to glass/src/lib/native/include/glass/hardware/MotorController.h
index 033f27d..5fc831f 100644
--- a/glass/src/lib/native/include/glass/hardware/SpeedController.h
+++ b/glass/src/lib/native/include/glass/hardware/MotorController.h
@@ -8,12 +8,12 @@
 
 namespace glass {
 class DataSource;
-class SpeedControllerModel : public Model {
+class MotorControllerModel : public Model {
  public:
   virtual const char* GetName() const = 0;
   virtual const char* GetSimDevice() const = 0;
   virtual DataSource* GetPercentData() = 0;
   virtual void SetPercent(double value) = 0;
 };
-void DisplaySpeedController(SpeedControllerModel* m);
+void DisplayMotorController(MotorControllerModel* m);
 }  // namespace glass
diff --git a/glass/src/lib/native/include/glass/other/FMS.h b/glass/src/lib/native/include/glass/other/FMS.h
index a920f96..5970e93 100644
--- a/glass/src/lib/native/include/glass/other/FMS.h
+++ b/glass/src/lib/native/include/glass/other/FMS.h
@@ -38,7 +38,7 @@
   virtual void SetEnabled(bool val) = 0;
   virtual void SetTest(bool val) = 0;
   virtual void SetAutonomous(bool val) = 0;
-  virtual void SetGameSpecificMessage(const char* val) = 0;
+  virtual void SetGameSpecificMessage(std::string_view val) = 0;
 };
 
 /**
diff --git a/glass/src/lib/native/include/glass/other/Field2D.h b/glass/src/lib/native/include/glass/other/Field2D.h
index 9399876..9c9f72a 100644
--- a/glass/src/lib/native/include/glass/other/Field2D.h
+++ b/glass/src/lib/native/include/glass/other/Field2D.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <span>
 #include <string_view>
 
 #include <frc/geometry/Pose2d.h>
@@ -11,7 +12,6 @@
 #include <frc/geometry/Translation2d.h>
 #include <imgui.h>
 #include <wpi/function_ref.h>
-#include <wpi/span.h>
 
 #include "glass/Model.h"
 #include "glass/View.h"
@@ -22,8 +22,8 @@
  public:
   virtual const char* GetName() const = 0;
 
-  virtual wpi::span<const frc::Pose2d> GetPoses() = 0;
-  virtual void SetPoses(wpi::span<const frc::Pose2d> poses) = 0;
+  virtual std::span<const frc::Pose2d> GetPoses() = 0;
+  virtual void SetPoses(std::span<const frc::Pose2d> poses) = 0;
   virtual void SetPose(size_t i, frc::Pose2d pose) = 0;
   virtual void SetPosition(size_t i, frc::Translation2d pos) = 0;
   virtual void SetRotation(size_t i, frc::Rotation2d rot) = 0;
@@ -46,6 +46,8 @@
   explicit Field2DView(Field2DModel* model) : m_model{model} {}
 
   void Display() override;
+  void Settings() override;
+  bool HasSettings() override;
 
  private:
   Field2DModel* m_model;
diff --git a/glass/src/lib/native/include/glass/other/Log.h b/glass/src/lib/native/include/glass/other/Log.h
index 3d9c59b..f054e66 100644
--- a/glass/src/lib/native/include/glass/other/Log.h
+++ b/glass/src/lib/native/include/glass/other/Log.h
@@ -35,6 +35,8 @@
   explicit LogView(LogData* data) : m_data{data} {}
 
   void Display() override;
+  void Settings() override;
+  bool HasSettings() override;
 
  private:
   LogData* m_data;
diff --git a/glass/src/lib/native/include/glass/other/Mechanism2D.h b/glass/src/lib/native/include/glass/other/Mechanism2D.h
index 7617e6f..ab5ccdc 100644
--- a/glass/src/lib/native/include/glass/other/Mechanism2D.h
+++ b/glass/src/lib/native/include/glass/other/Mechanism2D.h
@@ -55,6 +55,8 @@
   explicit Mechanism2DView(Mechanism2DModel* model) : m_model{model} {}
 
   void Display() override;
+  void Settings() override;
+  bool HasSettings() override;
 
  private:
   Mechanism2DModel* m_model;
diff --git a/glass/src/lib/native/include/glass/other/StringChooser.h b/glass/src/lib/native/include/glass/other/StringChooser.h
index 77f9ac2..066c444 100644
--- a/glass/src/lib/native/include/glass/other/StringChooser.h
+++ b/glass/src/lib/native/include/glass/other/StringChooser.h
@@ -8,8 +8,6 @@
 #include <string_view>
 #include <vector>
 
-#include <wpi/span.h>
-
 #include "glass/Model.h"
 
 namespace glass {
@@ -21,10 +19,7 @@
   virtual const std::string& GetActive() = 0;
   virtual const std::vector<std::string>& GetOptions() = 0;
 
-  virtual void SetDefault(std::string_view val) = 0;
   virtual void SetSelected(std::string_view val) = 0;
-  virtual void SetActive(std::string_view val) = 0;
-  virtual void SetOptions(wpi::span<const std::string> val) = 0;
 };
 
 void DisplayStringChooser(StringChooserModel* model);
diff --git a/glass/src/lib/native/include/glass/support/EnumSetting.h b/glass/src/lib/native/include/glass/support/EnumSetting.h
index c4f0c26..eaf308f 100644
--- a/glass/src/lib/native/include/glass/support/EnumSetting.h
+++ b/glass/src/lib/native/include/glass/support/EnumSetting.h
@@ -15,7 +15,7 @@
   EnumSetting(std::string& str, int defaultValue,
               std::initializer_list<const char*> choices);
 
-  int GetValue() const { return m_value; }
+  int GetValue() const;
   void SetValue(int value);
 
   // updates internal value, returns true on change
@@ -23,9 +23,12 @@
              int popup_max_height_in_items = -1);
 
  private:
+  void UpdateValue() const;
+
   std::string& m_str;
   wpi::SmallVector<const char*, 8> m_choices;
-  int m_value;
+  int m_defaultValue;
+  mutable int m_value = -1;
 };
 
 }  // namespace glass
diff --git a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
index 3a35335..6788434 100644
--- a/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
+++ b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h
@@ -93,4 +93,9 @@
  */
 bool HeaderDeleteButton(const char* label);
 
+/**
+ * Settings button similar to ImGui::CloseButton.
+ */
+bool HamburgerButton(const ImGuiID id, const ImVec2 position);
+
 }  // namespace glass
diff --git a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
index ccc6412..26a5740 100644
--- a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
+++ b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
@@ -10,49 +10,38 @@
 using namespace glass;
 
 NTCommandSchedulerModel::NTCommandSchedulerModel(std::string_view path)
-    : NTCommandSchedulerModel(nt::GetDefaultInstance(), path) {}
+    : NTCommandSchedulerModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTCommandSchedulerModel::NTCommandSchedulerModel(NT_Inst instance,
+NTCommandSchedulerModel::NTCommandSchedulerModel(nt::NetworkTableInstance inst,
                                                  std::string_view path)
-    : m_nt(instance),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_commands(m_nt.GetEntry(fmt::format("{}/Names", path))),
-      m_ids(m_nt.GetEntry(fmt::format("{}/Ids", path))),
-      m_cancel(m_nt.GetEntry(fmt::format("{}/Cancel", path))),
-      m_nameValue(wpi::rsplit(path, '/').second) {
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_commands);
-  m_nt.AddListener(m_ids);
-  m_nt.AddListener(m_cancel);
-}
+    : m_inst{inst},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_commands{inst.GetStringArrayTopic(fmt::format("{}/Names", path))
+                     .Subscribe({})},
+      m_ids{
+          inst.GetIntegerArrayTopic(fmt::format("{}/Ids", path)).Subscribe({})},
+      m_cancel{
+          inst.GetIntegerArrayTopic(fmt::format("{}/Cancel", path)).Publish()},
+      m_nameValue{wpi::rsplit(path, '/').second} {}
 
 void NTCommandSchedulerModel::CancelCommand(size_t index) {
   if (index < m_idsValue.size()) {
-    nt::SetEntryValue(
-        m_cancel, nt::NetworkTableValue::MakeDoubleArray({m_idsValue[index]}));
+    m_cancel.Set({{m_idsValue[index]}});
   }
 }
 
 void NTCommandSchedulerModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    } else if (event.entry == m_commands) {
-      if (event.value && event.value->IsStringArray()) {
-        auto arr = event.value->GetStringArray();
-        m_commandsValue.assign(arr.begin(), arr.end());
-      }
-    } else if (event.entry == m_ids) {
-      if (event.value && event.value->IsDoubleArray()) {
-        auto arr = event.value->GetDoubleArray();
-        m_idsValue.assign(arr.begin(), arr.end());
-      }
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_commands.ReadQueue()) {
+    m_commandsValue = std::move(v.value);
+  }
+  for (auto&& v : m_ids.ReadQueue()) {
+    m_idsValue = std::move(v.value);
   }
 }
 
 bool NTCommandSchedulerModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_commands) != NT_UNASSIGNED;
+  return m_commands.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTCommandSelector.cpp b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
index efcbac2..64c616e 100644
--- a/glass/src/libnt/native/cpp/NTCommandSelector.cpp
+++ b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
@@ -10,38 +10,32 @@
 using namespace glass;
 
 NTCommandSelectorModel::NTCommandSelectorModel(std::string_view path)
-    : NTCommandSelectorModel(nt::GetDefaultInstance(), path) {}
+    : NTCommandSelectorModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTCommandSelectorModel::NTCommandSelectorModel(NT_Inst instance,
+NTCommandSelectorModel::NTCommandSelectorModel(nt::NetworkTableInstance inst,
                                                std::string_view path)
-    : m_nt(instance),
-      m_running(m_nt.GetEntry(fmt::format("{}/running", path))),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_runningData(fmt::format("NTCmd:{}", path)),
-      m_nameValue(wpi::rsplit(path, '/').second) {
+    : m_inst{inst},
+      m_running{inst.GetBooleanTopic(fmt::format("{}/running", path))
+                    .GetEntry(false)},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_runningData{fmt::format("NTCmd:{}", path)},
+      m_nameValue{wpi::rsplit(path, '/').second} {
   m_runningData.SetDigital(true);
-  m_nt.AddListener(m_running);
-  m_nt.AddListener(m_name);
 }
 
 void NTCommandSelectorModel::SetRunning(bool run) {
-  nt::SetEntryValue(m_running, nt::NetworkTableValue::MakeBoolean(run));
+  m_running.Set(run);
 }
 
 void NTCommandSelectorModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_running) {
-      if (event.value && event.value->IsBoolean()) {
-        m_runningData.SetValue(event.value->GetBoolean());
-      }
-    } else if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    }
+  for (auto&& v : m_running.ReadQueue()) {
+    m_runningData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
   }
 }
 
 bool NTCommandSelectorModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_running) != NT_UNASSIGNED;
+  return m_running.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
index b44ea07..57d1fa8 100644
--- a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
+++ b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
@@ -12,46 +12,40 @@
 using namespace glass;
 
 NTDifferentialDriveModel::NTDifferentialDriveModel(std::string_view path)
-    : NTDifferentialDriveModel(nt::GetDefaultInstance(), path) {}
+    : NTDifferentialDriveModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTDifferentialDriveModel::NTDifferentialDriveModel(NT_Inst instance,
-                                                   std::string_view path)
-    : m_nt(instance),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
-      m_lPercent(m_nt.GetEntry(fmt::format("{}/Left Motor Speed", path))),
-      m_rPercent(m_nt.GetEntry(fmt::format("{}/Right Motor Speed", path))),
-      m_nameValue(wpi::rsplit(path, '/').second),
-      m_lPercentData(fmt::format("NTDiffDriveL:{}", path)),
-      m_rPercentData(fmt::format("NTDiffDriveR:{}", path)) {
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_controllable);
-  m_nt.AddListener(m_lPercent);
-  m_nt.AddListener(m_rPercent);
+NTDifferentialDriveModel::NTDifferentialDriveModel(
+    nt::NetworkTableInstance inst, std::string_view path)
+    : m_inst{inst},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path))
+                         .Subscribe(false)},
+      m_lPercent{inst.GetDoubleTopic(fmt::format("{}/Left Motor Speed", path))
+                     .GetEntry(0)},
+      m_rPercent{inst.GetDoubleTopic(fmt::format("{}/Right Motor Speed", path))
+                     .GetEntry(0)},
+      m_nameValue{wpi::rsplit(path, '/').second},
+      m_lPercentData{fmt::format("NTDiffDriveL:{}", path)},
+      m_rPercentData{fmt::format("NTDiffDriveR:{}", path)} {
+  m_wheels.emplace_back("L % Output", &m_lPercentData,
+                        [this](auto value) { m_lPercent.Set(value); });
 
-  m_wheels.emplace_back("L % Output", &m_lPercentData, [this](auto value) {
-    nt::SetEntryValue(m_lPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
-
-  m_wheels.emplace_back("R % Output", &m_rPercentData, [this](auto value) {
-    nt::SetEntryValue(m_rPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
+  m_wheels.emplace_back("R % Output", &m_rPercentData,
+                        [this](auto value) { m_rPercent.Set(value); });
 }
 
 void NTDifferentialDriveModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_name && event.value && event.value->IsString()) {
-      m_nameValue = event.value->GetString();
-    } else if (event.entry == m_lPercent && event.value &&
-               event.value->IsDouble()) {
-      m_lPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_rPercent && event.value &&
-               event.value->IsDouble()) {
-      m_rPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_controllable && event.value &&
-               event.value->IsBoolean()) {
-      m_controllableValue = event.value->GetBoolean();
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_lPercent.ReadQueue()) {
+    m_lPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_rPercent.ReadQueue()) {
+    m_rPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_controllable.ReadQueue()) {
+    m_controllableValue = v.value;
   }
 
   double l = m_lPercentData.GetValue();
@@ -62,5 +56,5 @@
 }
 
 bool NTDifferentialDriveModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_lPercent) != NT_UNASSIGNED;
+  return m_lPercent.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTDigitalInput.cpp b/glass/src/libnt/native/cpp/NTDigitalInput.cpp
index 5de6c29..28b916c 100644
--- a/glass/src/libnt/native/cpp/NTDigitalInput.cpp
+++ b/glass/src/libnt/native/cpp/NTDigitalInput.cpp
@@ -10,34 +10,28 @@
 using namespace glass;
 
 NTDigitalInputModel::NTDigitalInputModel(std::string_view path)
-    : NTDigitalInputModel{nt::GetDefaultInstance(), path} {}
+    : NTDigitalInputModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTDigitalInputModel::NTDigitalInputModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
-      m_value{m_nt.GetEntry(fmt::format("{}/Value", path))},
-      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
+NTDigitalInputModel::NTDigitalInputModel(nt::NetworkTableInstance inst,
+                                         std::string_view path)
+    : m_inst{inst},
+      m_value{
+          inst.GetBooleanTopic(fmt::format("{}/Value", path)).Subscribe(false)},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
       m_valueData{fmt::format("NT_DIn:{}", path)},
       m_nameValue{wpi::rsplit(path, '/').second} {
-  m_nt.AddListener(m_value);
-  m_nt.AddListener(m_name);
-
   m_valueData.SetDigital(true);
 }
 
 void NTDigitalInputModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_value) {
-      if (event.value && event.value->IsBoolean()) {
-        m_valueData.SetValue(event.value->GetBoolean());
-      }
-    } else if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    }
+  for (auto&& v : m_value.ReadQueue()) {
+    m_valueData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
   }
 }
 
 bool NTDigitalInputModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+  return m_value.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
index a09d424..aa9200d 100644
--- a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
+++ b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
@@ -9,43 +9,36 @@
 using namespace glass;
 
 NTDigitalOutputModel::NTDigitalOutputModel(std::string_view path)
-    : NTDigitalOutputModel{nt::GetDefaultInstance(), path} {}
+    : NTDigitalOutputModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTDigitalOutputModel::NTDigitalOutputModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
-      m_value{m_nt.GetEntry(fmt::format("{}/Value", path))},
-      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
-      m_controllable{m_nt.GetEntry(fmt::format("{}/.controllable", path))},
+NTDigitalOutputModel::NTDigitalOutputModel(nt::NetworkTableInstance inst,
+                                           std::string_view path)
+    : m_inst{inst},
+      m_value{
+          inst.GetBooleanTopic(fmt::format("{}/Value", path)).GetEntry(false)},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path))
+                         .Subscribe(false)},
       m_valueData{fmt::format("NT_DOut:{}", path)} {
-  m_nt.AddListener(m_value);
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_controllable);
-
   m_valueData.SetDigital(true);
 }
 
 void NTDigitalOutputModel::SetValue(bool val) {
-  nt::SetEntryValue(m_value, nt::Value::MakeBoolean(val));
+  m_value.Set(val);
 }
 
 void NTDigitalOutputModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_value) {
-      if (event.value && event.value->IsBoolean()) {
-        m_valueData.SetValue(event.value->GetBoolean());
-      }
-    } else if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    } else if (event.entry == m_controllable) {
-      if (event.value && event.value->IsBoolean()) {
-        m_controllableValue = event.value->GetBoolean();
-      }
-    }
+  for (auto&& v : m_value.ReadQueue()) {
+    m_valueData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_controllable.ReadQueue()) {
+    m_controllableValue = v.value;
   }
 }
 
 bool NTDigitalOutputModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+  return m_value.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTFMS.cpp b/glass/src/libnt/native/cpp/NTFMS.cpp
index 84c1ce7..65ffa6a 100644
--- a/glass/src/libnt/native/cpp/NTFMS.cpp
+++ b/glass/src/libnt/native/cpp/NTFMS.cpp
@@ -13,15 +13,19 @@
 using namespace glass;
 
 NTFMSModel::NTFMSModel(std::string_view path)
-    : NTFMSModel{nt::GetDefaultInstance(), path} {}
+    : NTFMSModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
+NTFMSModel::NTFMSModel(nt::NetworkTableInstance inst, std::string_view path)
+    : m_inst{inst},
       m_gameSpecificMessage{
-          m_nt.GetEntry(fmt::format("{}/GameSpecificMessage", path))},
-      m_alliance{m_nt.GetEntry(fmt::format("{}/IsRedAlliance", path))},
-      m_station{m_nt.GetEntry(fmt::format("{}/StationNumber", path))},
-      m_controlWord{m_nt.GetEntry(fmt::format("{}/FMSControlData", path))},
+          inst.GetStringTopic(fmt::format("{}/GameSpecificMessage", path))
+              .Subscribe("")},
+      m_alliance{inst.GetBooleanTopic(fmt::format("{}/IsRedAlliance", path))
+                     .Subscribe(false)},
+      m_station{inst.GetIntegerTopic(fmt::format("{}/StationNumber", path))
+                    .Subscribe(0)},
+      m_controlWord{inst.GetIntegerTopic(fmt::format("{}/FMSControlData", path))
+                        .Subscribe(0)},
       m_fmsAttached{fmt::format("NT_FMS:FMSAttached:{}", path)},
       m_dsAttached{fmt::format("NT_FMS:DSAttached:{}", path)},
       m_allianceStationId{fmt::format("NT_FMS:AllianceStationID:{}", path)},
@@ -29,10 +33,6 @@
       m_enabled{fmt::format("NT_FMS:RobotEnabled:{}", path)},
       m_test{fmt::format("NT_FMS:TestMode:{}", path)},
       m_autonomous{fmt::format("NT_FMS:AutonomousMode:{}", path)} {
-  m_nt.AddListener(m_alliance);
-  m_nt.AddListener(m_station);
-  m_nt.AddListener(m_controlWord);
-
   m_fmsAttached.SetDigital(true);
   m_dsAttached.SetDigital(true);
   m_estop.SetDigital(true);
@@ -43,49 +43,35 @@
 
 std::string_view NTFMSModel::GetGameSpecificMessage(
     wpi::SmallVectorImpl<char>& buf) {
-  buf.clear();
-  auto value = nt::GetEntryValue(m_gameSpecificMessage);
-  if (value && value->IsString()) {
-    auto str = value->GetString();
-    buf.append(str.begin(), str.end());
-  }
-  return std::string_view{buf.data(), buf.size()};
+  return m_gameSpecificMessage.Get(buf, "");
 }
 
 void NTFMSModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_alliance) {
-      if (event.value && event.value->IsBoolean()) {
-        int allianceStationId = m_allianceStationId.GetValue();
-        allianceStationId %= 3;
-        // true if red
-        allianceStationId += 3 * (event.value->GetBoolean() ? 0 : 1);
-        m_allianceStationId.SetValue(allianceStationId);
-      }
-    } else if (event.entry == m_station) {
-      if (event.value && event.value->IsDouble()) {
-        int allianceStationId = m_allianceStationId.GetValue();
-        bool isRed = (allianceStationId < 3);
-        // the NT value is 1-indexed
-        m_allianceStationId.SetValue(event.value->GetDouble() - 1 +
-                                     3 * (isRed ? 0 : 1));
-      }
-    } else if (event.entry == m_controlWord) {
-      if (event.value && event.value->IsDouble()) {
-        uint32_t controlWord = event.value->GetDouble();
-        // See HAL_ControlWord definition
-        auto time = wpi::Now();
-        m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, time);
-        m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, time);
-        m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, time);
-        m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, time);
-        m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, time);
-        m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, time);
-      }
-    }
+  for (auto&& v : m_alliance.ReadQueue()) {
+    int allianceStationId = m_allianceStationId.GetValue();
+    allianceStationId %= 3;
+    // true if red
+    allianceStationId += 3 * (v.value ? 0 : 1);
+    m_allianceStationId.SetValue(allianceStationId, v.time);
+  }
+  for (auto&& v : m_station.ReadQueue()) {
+    int allianceStationId = m_allianceStationId.GetValue();
+    bool isRed = (allianceStationId < 3);
+    // the NT value is 1-indexed
+    m_allianceStationId.SetValue(v.value - 1 + 3 * (isRed ? 0 : 1), v.time);
+  }
+  for (auto&& v : m_controlWord.ReadQueue()) {
+    uint32_t controlWord = v.value;
+    // See HAL_ControlWord definition
+    m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, v.time);
+    m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, v.time);
+    m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, v.time);
+    m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, v.time);
+    m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, v.time);
+    m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, v.time);
   }
 }
 
 bool NTFMSModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED;
+  return m_controlWord.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTField2D.cpp b/glass/src/libnt/native/cpp/NTField2D.cpp
index 47fa9a7..745e9e2 100644
--- a/glass/src/libnt/native/cpp/NTField2D.cpp
+++ b/glass/src/libnt/native/cpp/NTField2D.cpp
@@ -8,6 +8,8 @@
 #include <vector>
 
 #include <fmt/format.h>
+#include <networktables/DoubleArrayTopic.h>
+#include <networktables/MultiSubscriber.h>
 #include <ntcore_cpp.h>
 #include <wpi/Endian.h>
 #include <wpi/MathExtras.h>
@@ -18,24 +20,20 @@
 
 class NTField2DModel::ObjectModel : public FieldObjectModel {
  public:
-  ObjectModel(std::string_view name, NT_Entry entry)
-      : m_name{name}, m_entry{entry} {}
+  ObjectModel(std::string_view name, nt::DoubleArrayTopic topic)
+      : m_name{name}, m_topic{topic} {}
 
   const char* GetName() const override { return m_name.c_str(); }
-  NT_Entry GetEntry() const { return m_entry; }
+  nt::DoubleArrayTopic GetTopic() const { return m_topic; }
 
   void NTUpdate(const nt::Value& value);
 
-  void Update() override {
-    if (auto value = nt::GetEntryValue(m_entry)) {
-      NTUpdate(*value);
-    }
-  }
-  bool Exists() override { return nt::GetEntryType(m_entry) != NT_UNASSIGNED; }
+  void Update() override {}
+  bool Exists() override { return m_topic.Exists(); }
   bool IsReadOnly() override { return false; }
 
-  wpi::span<const frc::Pose2d> GetPoses() override { return m_poses; }
-  void SetPoses(wpi::span<const frc::Pose2d> poses) override;
+  std::span<const frc::Pose2d> GetPoses() override { return m_poses; }
+  void SetPoses(std::span<const frc::Pose2d> poses) override;
   void SetPose(size_t i, frc::Pose2d pose) override;
   void SetPosition(size_t i, frc::Translation2d pos) override;
   void SetRotation(size_t i, frc::Rotation2d rot) override;
@@ -44,7 +42,8 @@
   void UpdateNT();
 
   std::string m_name;
-  NT_Entry m_entry;
+  nt::DoubleArrayTopic m_topic;
+  nt::DoubleArrayPublisher m_pub;
 
   std::vector<frc::Pose2d> m_poses;
 };
@@ -62,66 +61,24 @@
           units::meter_t{arr[i * 3 + 0]}, units::meter_t{arr[i * 3 + 1]},
           frc::Rotation2d{units::degree_t{arr[i * 3 + 2]}}};
     }
-  } else if (value.IsRaw()) {
-    // treat it simply as an array of doubles
-    std::string_view data = value.GetRaw();
-
-    // must be triples of doubles
-    auto size = data.size();
-    if ((size % (3 * 8)) != 0) {
-      return;
-    }
-    m_poses.resize(size / (3 * 8));
-    const char* p = data.data();
-    for (size_t i = 0; i < size / (3 * 8); ++i) {
-      double x = wpi::BitsToDouble(
-          wpi::support::endian::readNext<uint64_t, wpi::support::big,
-                                         wpi::support::unaligned>(p));
-      double y = wpi::BitsToDouble(
-          wpi::support::endian::readNext<uint64_t, wpi::support::big,
-                                         wpi::support::unaligned>(p));
-      double rot = wpi::BitsToDouble(
-          wpi::support::endian::readNext<uint64_t, wpi::support::big,
-                                         wpi::support::unaligned>(p));
-      m_poses[i] = frc::Pose2d{units::meter_t{x}, units::meter_t{y},
-                               frc::Rotation2d{units::degree_t{rot}}};
-    }
   }
 }
 
 void NTField2DModel::ObjectModel::UpdateNT() {
-  if (m_poses.size() < (255 / 3)) {
-    wpi::SmallVector<double, 9> arr;
-    for (auto&& pose : m_poses) {
-      auto& translation = pose.Translation();
-      arr.push_back(translation.X().value());
-      arr.push_back(translation.Y().value());
-      arr.push_back(pose.Rotation().Degrees().value());
-    }
-    nt::SetEntryTypeValue(m_entry, nt::Value::MakeDoubleArray(arr));
-  } else {
-    // send as raw array of doubles if too big for NT array
-    std::vector<char> arr;
-    arr.resize(m_poses.size() * 3 * 8);
-    char* p = arr.data();
-    for (auto&& pose : m_poses) {
-      auto& translation = pose.Translation();
-      wpi::support::endian::write64be(
-          p, wpi::DoubleToBits(translation.X().value()));
-      p += 8;
-      wpi::support::endian::write64be(
-          p, wpi::DoubleToBits(translation.Y().value()));
-      p += 8;
-      wpi::support::endian::write64be(
-          p, wpi::DoubleToBits(pose.Rotation().Degrees().value()));
-      p += 8;
-    }
-    nt::SetEntryTypeValue(m_entry,
-                          nt::Value::MakeRaw({arr.data(), arr.size()}));
+  wpi::SmallVector<double, 9> arr;
+  for (auto&& pose : m_poses) {
+    auto& translation = pose.Translation();
+    arr.push_back(translation.X().value());
+    arr.push_back(translation.Y().value());
+    arr.push_back(pose.Rotation().Degrees().value());
   }
+  if (!m_pub) {
+    m_pub = m_topic.Publish();
+  }
+  m_pub.Set(arr);
 }
 
-void NTField2DModel::ObjectModel::SetPoses(wpi::span<const frc::Pose2d> poses) {
+void NTField2DModel::ObjectModel::SetPoses(std::span<const frc::Pose2d> poses) {
   m_poses.assign(poses.begin(), poses.end());
   UpdateNT();
 }
@@ -149,69 +106,69 @@
 }
 
 NTField2DModel::NTField2DModel(std::string_view path)
-    : NTField2DModel{nt::GetDefaultInstance(), path} {}
+    : NTField2DModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTField2DModel::NTField2DModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
-      m_path{fmt::format("{}/", path)},
-      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))} {
-  m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
-                               NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE);
+NTField2DModel::NTField2DModel(nt::NetworkTableInstance inst,
+                               std::string_view path)
+    : m_path{fmt::format("{}/", path)},
+      m_inst{inst},
+      m_tableSub{inst, {{m_path}}, {.periodic = 0.05, .sendAll = true}},
+      m_nameTopic{inst.GetTopic(fmt::format("{}/.name", path))},
+      m_poller{inst} {
+  m_poller.AddListener(m_tableSub, nt::EventFlags::kTopic |
+                                       nt::EventFlags::kValueAll |
+                                       nt::EventFlags::kImmediate);
 }
 
 NTField2DModel::~NTField2DModel() = default;
 
 void NTField2DModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    // .name
-    if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-      continue;
-    }
-
-    // common case: update of existing entry; search by entry
-    if (event.flags & NT_NOTIFY_UPDATE) {
-      auto it = std::find_if(
-          m_objects.begin(), m_objects.end(),
-          [&](const auto& e) { return e->GetEntry() == event.entry; });
-      if (it != m_objects.end()) {
-        (*it)->NTUpdate(*event.value);
-        continue;
-      }
-    }
-
-    // handle create/delete
-    std::string_view name = event.name;
-    if (wpi::starts_with(name, m_path)) {
-      name.remove_prefix(m_path.size());
+  for (auto&& event : m_poller.ReadQueue()) {
+    if (auto info = event.GetTopicInfo()) {
+      // handle publish/unpublish
+      auto name = wpi::drop_front(info->name, m_path.size());
       if (name.empty() || name[0] == '.') {
         continue;
       }
-      auto [it, match] = Find(event.name);
-      if (event.flags & NT_NOTIFY_DELETE) {
+      auto [it, match] = Find(info->name);
+      if (event.flags & nt::EventFlags::kUnpublish) {
         if (match) {
           m_objects.erase(it);
         }
         continue;
-      } else if (event.flags & NT_NOTIFY_NEW) {
+      } else if (event.flags & nt::EventFlags::kPublish) {
         if (!match) {
           it = m_objects.emplace(
-              it, std::make_unique<ObjectModel>(event.name, event.entry));
+              it, std::make_unique<ObjectModel>(
+                      info->name, nt::DoubleArrayTopic{info->topic}));
         }
       } else if (!match) {
         continue;
       }
-      if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) {
-        (*it)->NTUpdate(*event.value);
+    } else if (auto valueData = event.GetValueEventData()) {
+      // update values
+      // .name
+      if (valueData->topic == m_nameTopic.GetHandle()) {
+        if (valueData->value && valueData->value.IsString()) {
+          m_nameValue = valueData->value.GetString();
+        }
+        continue;
+      }
+
+      auto it =
+          std::find_if(m_objects.begin(), m_objects.end(), [&](const auto& e) {
+            return e->GetTopic().GetHandle() == valueData->topic;
+          });
+      if (it != m_objects.end()) {
+        (*it)->NTUpdate(valueData->value);
+        continue;
       }
     }
   }
 }
 
 bool NTField2DModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+  return m_nameTopic.Exists();
 }
 
 bool NTField2DModel::IsReadOnly() {
@@ -222,8 +179,9 @@
   auto fullName = fmt::format("{}{}", m_path, name);
   auto [it, match] = Find(fullName);
   if (!match) {
-    it = m_objects.emplace(
-        it, std::make_unique<ObjectModel>(fullName, m_nt.GetEntry(fullName)));
+    it = m_objects.emplace(it,
+                           std::make_unique<ObjectModel>(
+                               fullName, m_inst.GetDoubleArrayTopic(fullName)));
   }
   return it->get();
 }
@@ -231,7 +189,6 @@
 void NTField2DModel::RemoveFieldObject(std::string_view name) {
   auto [it, match] = Find(fmt::format("{}{}", m_path, name));
   if (match) {
-    nt::DeleteEntry((*it)->GetEntry());
     m_objects.erase(it);
   }
 }
diff --git a/glass/src/libnt/native/cpp/NTGyro.cpp b/glass/src/libnt/native/cpp/NTGyro.cpp
index 7651d2c..a036b39 100644
--- a/glass/src/libnt/native/cpp/NTGyro.cpp
+++ b/glass/src/libnt/native/cpp/NTGyro.cpp
@@ -10,32 +10,24 @@
 using namespace glass;
 
 NTGyroModel::NTGyroModel(std::string_view path)
-    : NTGyroModel(nt::GetDefaultInstance(), path) {}
+    : NTGyroModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTGyroModel::NTGyroModel(NT_Inst instance, std::string_view path)
-    : m_nt(instance),
-      m_angle(m_nt.GetEntry(fmt::format("{}/Value", path))),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_angleData(fmt::format("NT_Gyro:{}", path)),
-      m_nameValue(wpi::rsplit(path, '/').second) {
-  m_nt.AddListener(m_angle);
-  m_nt.AddListener(m_name);
-}
+NTGyroModel::NTGyroModel(nt::NetworkTableInstance inst, std::string_view path)
+    : m_inst{inst},
+      m_angle{inst.GetDoubleTopic(fmt::format("{}/Value", path)).Subscribe(0)},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe({})},
+      m_angleData{fmt::format("NT_Gyro:{}", path)},
+      m_nameValue{wpi::rsplit(path, '/').second} {}
 
 void NTGyroModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_angle) {
-      if (event.value && event.value->IsDouble()) {
-        m_angleData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_angle.ReadQueue()) {
+    m_angleData.SetValue(v.value, v.time);
   }
 }
 
 bool NTGyroModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_angle) != NT_UNASSIGNED;
+  return m_angle.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
index 28c0a67..cb564e8 100644
--- a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
+++ b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
@@ -12,69 +12,62 @@
 using namespace glass;
 
 NTMecanumDriveModel::NTMecanumDriveModel(std::string_view path)
-    : NTMecanumDriveModel(nt::GetDefaultInstance(), path) {}
+    : NTMecanumDriveModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTMecanumDriveModel::NTMecanumDriveModel(NT_Inst instance,
+NTMecanumDriveModel::NTMecanumDriveModel(nt::NetworkTableInstance inst,
                                          std::string_view path)
-    : m_nt(instance),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
-      m_flPercent(
-          m_nt.GetEntry(fmt::format("{}/Front Left Motor Speed", path))),
-      m_frPercent(
-          m_nt.GetEntry(fmt::format("{}/Front Right Motor Speed", path))),
-      m_rlPercent(m_nt.GetEntry(fmt::format("{}/Rear Left Motor Speed", path))),
-      m_rrPercent(
-          m_nt.GetEntry(fmt::format("{}/Rear Right Motor Speed", path))),
-      m_nameValue(wpi::rsplit(path, '/').second),
-      m_flPercentData(fmt::format("NTMcnmDriveFL:{}", path)),
-      m_frPercentData(fmt::format("NTMcnmDriveFR:{}", path)),
-      m_rlPercentData(fmt::format("NTMcnmDriveRL:{}", path)),
-      m_rrPercentData(fmt::format("NTMcnmDriveRR:{}", path)) {
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_controllable);
-  m_nt.AddListener(m_flPercent);
-  m_nt.AddListener(m_frPercent);
-  m_nt.AddListener(m_rlPercent);
-  m_nt.AddListener(m_rrPercent);
+    : m_inst{inst},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path))
+                         .Subscribe(0)},
+      m_flPercent{
+          inst.GetDoubleTopic(fmt::format("{}/Front Left Motor Speed", path))
+              .GetEntry(0)},
+      m_frPercent{
+          inst.GetDoubleTopic(fmt::format("{}/Front Right Motor Speed", path))
+              .GetEntry(0)},
+      m_rlPercent{
+          inst.GetDoubleTopic(fmt::format("{}/Rear Left Motor Speed", path))
+              .GetEntry(0)},
+      m_rrPercent{
+          inst.GetDoubleTopic(fmt::format("{}/Rear Right Motor Speed", path))
+              .GetEntry(0)},
+      m_nameValue{wpi::rsplit(path, '/').second},
+      m_flPercentData{fmt::format("NTMcnmDriveFL:{}", path)},
+      m_frPercentData{fmt::format("NTMcnmDriveFR:{}", path)},
+      m_rlPercentData{fmt::format("NTMcnmDriveRL:{}", path)},
+      m_rrPercentData{fmt::format("NTMcnmDriveRR:{}", path)} {
+  m_wheels.emplace_back("FL % Output", &m_flPercentData,
+                        [this](auto value) { m_flPercent.Set(value); });
 
-  m_wheels.emplace_back("FL % Output", &m_flPercentData, [this](auto value) {
-    nt::SetEntryValue(m_flPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
+  m_wheels.emplace_back("FR % Output", &m_frPercentData,
+                        [this](auto value) { m_frPercent.Set(value); });
 
-  m_wheels.emplace_back("FR % Output", &m_frPercentData, [this](auto value) {
-    nt::SetEntryValue(m_frPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
+  m_wheels.emplace_back("RL % Output", &m_rlPercentData,
+                        [this](auto value) { m_rlPercent.Set(value); });
 
-  m_wheels.emplace_back("RL % Output", &m_rlPercentData, [this](auto value) {
-    nt::SetEntryValue(m_rlPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
-
-  m_wheels.emplace_back("RR % Output", &m_rrPercentData, [this](auto value) {
-    nt::SetEntryValue(m_rrPercent, nt::NetworkTableValue::MakeDouble(value));
-  });
+  m_wheels.emplace_back("RR % Output", &m_rrPercentData,
+                        [this](auto value) { m_rrPercent.Set(value); });
 }
 
 void NTMecanumDriveModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_name && event.value && event.value->IsString()) {
-      m_nameValue = event.value->GetString();
-    } else if (event.entry == m_flPercent && event.value &&
-               event.value->IsDouble()) {
-      m_flPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_frPercent && event.value &&
-               event.value->IsDouble()) {
-      m_frPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_rlPercent && event.value &&
-               event.value->IsDouble()) {
-      m_rlPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_rrPercent && event.value &&
-               event.value->IsDouble()) {
-      m_rrPercentData.SetValue(event.value->GetDouble());
-    } else if (event.entry == m_controllable && event.value &&
-               event.value->IsBoolean()) {
-      m_controllableValue = event.value->GetBoolean();
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_flPercent.ReadQueue()) {
+    m_flPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_frPercent.ReadQueue()) {
+    m_frPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_rlPercent.ReadQueue()) {
+    m_rlPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_rrPercent.ReadQueue()) {
+    m_rrPercentData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_controllable.ReadQueue()) {
+    m_controllableValue = v.value;
   }
 
   double fl = m_flPercentData.GetValue();
@@ -88,5 +81,5 @@
 }
 
 bool NTMecanumDriveModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_flPercent) != NT_UNASSIGNED;
+  return m_flPercent.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTMechanism2D.cpp b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
index 9c73af2..32ed276 100644
--- a/glass/src/libnt/native/cpp/NTMechanism2D.cpp
+++ b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
@@ -34,16 +34,17 @@
 
 class NTMechanismGroupImpl final {
  public:
-  NTMechanismGroupImpl(NT_Inst inst, std::string_view path,
+  NTMechanismGroupImpl(nt::NetworkTableInstance inst, std::string_view path,
                        std::string_view name)
       : m_inst{inst}, m_path{path}, m_name{name} {}
 
   const char* GetName() const { return m_name.c_str(); }
   void ForEachObject(wpi::function_ref<void(MechanismObjectModel& model)> func);
-  void NTUpdate(const nt::EntryNotification& event, std::string_view name);
+
+  void NTUpdate(const nt::Event& event, std::string_view name);
 
  protected:
-  NT_Inst m_inst;
+  nt::NetworkTableInstance m_inst;
   std::string m_path;
   std::string m_name;
   std::vector<std::unique_ptr<NTMechanismObjectModel>> m_objects;
@@ -51,14 +52,14 @@
 
 class NTMechanismObjectModel final : public MechanismObjectModel {
  public:
-  NTMechanismObjectModel(NT_Inst inst, std::string_view path,
+  NTMechanismObjectModel(nt::NetworkTableInstance inst, std::string_view path,
                          std::string_view name)
       : m_group{inst, path, name},
-        m_type{nt::GetEntry(inst, fmt::format("{}/.type", path))},
-        m_color{nt::GetEntry(inst, fmt::format("{}/color", path))},
-        m_weight{nt::GetEntry(inst, fmt::format("{}/weight", path))},
-        m_angle{nt::GetEntry(inst, fmt::format("{}/angle", path))},
-        m_length{nt::GetEntry(inst, fmt::format("{}/length", path))} {}
+        m_typeTopic{inst.GetTopic(fmt::format("{}/.type", path))},
+        m_colorTopic{inst.GetTopic(fmt::format("{}/color", path))},
+        m_weightTopic{inst.GetTopic(fmt::format("{}/weight", path))},
+        m_angleTopic{inst.GetTopic(fmt::format("{}/angle", path))},
+        m_lengthTopic{inst.GetTopic(fmt::format("{}/length", path))} {}
 
   const char* GetName() const final { return m_group.GetName(); }
   void ForEachObject(
@@ -72,16 +73,16 @@
   frc::Rotation2d GetAngle() const final { return m_angleValue; }
   units::meter_t GetLength() const final { return m_lengthValue; }
 
-  bool NTUpdate(const nt::EntryNotification& event, std::string_view childName);
+  bool NTUpdate(const nt::Event& event, std::string_view name);
 
  private:
   NTMechanismGroupImpl m_group;
 
-  NT_Entry m_type;
-  NT_Entry m_color;
-  NT_Entry m_weight;
-  NT_Entry m_angle;
-  NT_Entry m_length;
+  nt::Topic m_typeTopic;
+  nt::Topic m_colorTopic;
+  nt::Topic m_weightTopic;
+  nt::Topic m_angleTopic;
+  nt::Topic m_lengthTopic;
 
   std::string m_typeValue;
   ImU32 m_colorValue = IM_COL32_WHITE;
@@ -99,7 +100,7 @@
   }
 }
 
-void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event,
+void NTMechanismGroupImpl::NTUpdate(const nt::Event& event,
                                     std::string_view name) {
   if (name.empty()) {
     return;
@@ -115,58 +116,76 @@
       [](const auto& e, std::string_view name) { return e->GetName() < name; });
   bool match = it != m_objects.end() && (*it)->GetName() == name;
 
-  if (event.flags & NT_NOTIFY_NEW) {
-    if (!match) {
-      it = m_objects.emplace(
-          it, std::make_unique<NTMechanismObjectModel>(
-                  m_inst, fmt::format("{}/{}", m_path, name), name));
-      match = true;
+  if (event.GetTopicInfo()) {
+    if (event.flags & nt::EventFlags::kPublish) {
+      if (!match) {
+        it = m_objects.emplace(
+            it, std::make_unique<NTMechanismObjectModel>(
+                    m_inst, fmt::format("{}/{}", m_path, name), name));
+        match = true;
+      }
     }
-  }
-  if (match) {
-    if ((*it)->NTUpdate(event, childName)) {
-      m_objects.erase(it);
+
+    if (match) {
+      if ((*it)->NTUpdate(event, childName)) {
+        m_objects.erase(it);
+      }
+    }
+  } else if (event.GetValueEventData()) {
+    if (match) {
+      (*it)->NTUpdate(event, childName);
     }
   }
 }
 
-bool NTMechanismObjectModel::NTUpdate(const nt::EntryNotification& event,
+bool NTMechanismObjectModel::NTUpdate(const nt::Event& event,
                                       std::string_view childName) {
-  if (event.entry == m_type) {
-    if ((event.flags & NT_NOTIFY_DELETE) != 0) {
-      return true;
+  if (auto info = event.GetTopicInfo()) {
+    if (info->topic == m_typeTopic.GetHandle()) {
+      if (event.flags & nt::EventFlags::kUnpublish) {
+        return true;
+      }
+    } else if (info->topic != m_colorTopic.GetHandle() &&
+               info->topic != m_weightTopic.GetHandle() &&
+               info->topic != m_angleTopic.GetHandle() &&
+               info->topic != m_lengthTopic.GetHandle()) {
+      m_group.NTUpdate(event, childName);
     }
-    if (event.value && event.value->IsString()) {
-      m_typeValue = event.value->GetString();
+  } else if (auto valueData = event.GetValueEventData()) {
+    if (valueData->topic == m_typeTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsString()) {
+        m_typeValue = valueData->value.GetString();
+      }
+    } else if (valueData->topic == m_colorTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsString()) {
+        ConvertColor(valueData->value.GetString(), &m_colorValue);
+      }
+    } else if (valueData->topic == m_weightTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsDouble()) {
+        m_weightValue = valueData->value.GetDouble();
+      }
+    } else if (valueData->topic == m_angleTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsDouble()) {
+        m_angleValue = units::degree_t{valueData->value.GetDouble()};
+      }
+    } else if (valueData->topic == m_lengthTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsDouble()) {
+        m_lengthValue = units::meter_t{valueData->value.GetDouble()};
+      }
+    } else {
+      m_group.NTUpdate(event, childName);
     }
-  } else if (event.entry == m_color) {
-    if (event.value && event.value->IsString()) {
-      ConvertColor(event.value->GetString(), &m_colorValue);
-    }
-  } else if (event.entry == m_weight) {
-    if (event.value && event.value->IsDouble()) {
-      m_weightValue = event.value->GetDouble();
-    }
-  } else if (event.entry == m_angle) {
-    if (event.value && event.value->IsDouble()) {
-      m_angleValue = units::degree_t{event.value->GetDouble()};
-    }
-  } else if (event.entry == m_length) {
-    if (event.value && event.value->IsDouble()) {
-      m_lengthValue = units::meter_t{event.value->GetDouble()};
-    }
-  } else {
-    m_group.NTUpdate(event, childName);
   }
   return false;
 }
 
 class NTMechanism2DModel::RootModel final : public MechanismRootModel {
  public:
-  RootModel(NT_Inst inst, std::string_view path, std::string_view name)
+  RootModel(nt::NetworkTableInstance inst, std::string_view path,
+            std::string_view name)
       : m_group{inst, path, name},
-        m_x{nt::GetEntry(inst, fmt::format("{}/x", path))},
-        m_y{nt::GetEntry(inst, fmt::format("{}/y", path))} {}
+        m_xTopic{inst.GetTopic(fmt::format("{}/x", path))},
+        m_yTopic{inst.GetTopic(fmt::format("{}/y", path))} {}
 
   const char* GetName() const final { return m_group.GetName(); }
   void ForEachObject(
@@ -174,85 +193,70 @@
     m_group.ForEachObject(func);
   }
 
-  bool NTUpdate(const nt::EntryNotification& event, std::string_view childName);
+  bool NTUpdate(const nt::Event& event, std::string_view childName);
 
   frc::Translation2d GetPosition() const override { return m_pos; };
 
  private:
   NTMechanismGroupImpl m_group;
-  NT_Entry m_x;
-  NT_Entry m_y;
+  nt::Topic m_xTopic;
+  nt::Topic m_yTopic;
   frc::Translation2d m_pos;
 };
 
-bool NTMechanism2DModel::RootModel::NTUpdate(const nt::EntryNotification& event,
+bool NTMechanism2DModel::RootModel::NTUpdate(const nt::Event& event,
                                              std::string_view childName) {
-  if ((event.flags & NT_NOTIFY_DELETE) != 0 &&
-      (event.entry == m_x || event.entry == m_y)) {
-    return true;
-  } else if (event.entry == m_x) {
-    if (event.value && event.value->IsDouble()) {
-      m_pos = frc::Translation2d{units::meter_t{event.value->GetDouble()},
-                                 m_pos.Y()};
+  if (auto info = event.GetTopicInfo()) {
+    if (info->topic == m_xTopic.GetHandle() ||
+        info->topic == m_yTopic.GetHandle()) {
+      if (event.flags & nt::EventFlags::kUnpublish) {
+        return true;
+      }
+    } else {
+      m_group.NTUpdate(event, childName);
     }
-  } else if (event.entry == m_y) {
-    if (event.value && event.value->IsDouble()) {
-      m_pos = frc::Translation2d{m_pos.X(),
-                                 units::meter_t{event.value->GetDouble()}};
+  } else if (auto valueData = event.GetValueEventData()) {
+    if (valueData->topic == m_xTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsDouble()) {
+        m_pos = frc::Translation2d{units::meter_t{valueData->value.GetDouble()},
+                                   m_pos.Y()};
+      }
+    } else if (valueData->topic == m_yTopic.GetHandle()) {
+      if (valueData->value && valueData->value.IsDouble()) {
+        m_pos = frc::Translation2d{
+            m_pos.X(), units::meter_t{valueData->value.GetDouble()}};
+      }
+    } else {
+      m_group.NTUpdate(event, childName);
     }
-  } else {
-    m_group.NTUpdate(event, childName);
   }
   return false;
 }
 
 NTMechanism2DModel::NTMechanism2DModel(std::string_view path)
-    : NTMechanism2DModel{nt::GetDefaultInstance(), path} {}
+    : NTMechanism2DModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTMechanism2DModel::NTMechanism2DModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
+NTMechanism2DModel::NTMechanism2DModel(nt::NetworkTableInstance inst,
+                                       std::string_view path)
+    : m_inst{inst},
       m_path{fmt::format("{}/", path)},
-      m_name{m_nt.GetEntry(fmt::format("{}/.name", path))},
-      m_dimensions{m_nt.GetEntry(fmt::format("{}/dims", path))},
-      m_bgColor{m_nt.GetEntry(fmt::format("{}/backgroundColor", path))},
+      m_tableSub{inst, {{m_path}}, {.periodic = 0.05, .sendAll = true}},
+      m_nameTopic{m_inst.GetTopic(fmt::format("{}/.name", path))},
+      m_dimensionsTopic{m_inst.GetTopic(fmt::format("{}/dims", path))},
+      m_bgColorTopic{m_inst.GetTopic(fmt::format("{}/backgroundColor", path))},
+      m_poller{m_inst},
       m_dimensionsValue{1_m, 1_m} {
-  m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
-                               NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE);
+  m_poller.AddListener(m_tableSub, nt::EventFlags::kTopic |
+                                       nt::EventFlags::kValueAll |
+                                       nt::EventFlags::kImmediate);
 }
 
 NTMechanism2DModel::~NTMechanism2DModel() = default;
 
 void NTMechanism2DModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    // .name
-    if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-      continue;
-    }
-
-    // dims
-    if (event.entry == m_dimensions) {
-      if (event.value && event.value->IsDoubleArray()) {
-        auto arr = event.value->GetDoubleArray();
-        if (arr.size() == 2) {
-          m_dimensionsValue = frc::Translation2d{units::meter_t{arr[0]},
-                                                 units::meter_t{arr[1]}};
-        }
-      }
-    }
-
-    // backgroundColor
-    if (event.entry == m_bgColor) {
-      if (event.value && event.value->IsString()) {
-        ConvertColor(event.value->GetString(), &m_bgColorValue);
-      }
-    }
-
-    std::string_view name = event.name;
-    if (wpi::starts_with(name, m_path)) {
-      name.remove_prefix(m_path.size());
+  for (auto&& event : m_poller.ReadQueue()) {
+    if (auto info = event.GetTopicInfo()) {
+      auto name = wpi::drop_front(info->name, m_path.size());
       if (name.empty() || name[0] == '.') {
         continue;
       }
@@ -268,12 +272,11 @@
                                  });
       bool match = it != m_roots.end() && (*it)->GetName() == name;
 
-      if (event.flags & NT_NOTIFY_NEW) {
+      if (event.flags & nt::EventFlags::kPublish) {
         if (!match) {
           it = m_roots.emplace(
-              it,
-              std::make_unique<RootModel>(
-                  m_nt.GetInstance(), fmt::format("{}{}", m_path, name), name));
+              it, std::make_unique<RootModel>(
+                      m_inst, fmt::format("{}{}", m_path, name), name));
           match = true;
         }
       }
@@ -282,12 +285,54 @@
           m_roots.erase(it);
         }
       }
+    } else if (auto valueData = event.GetValueEventData()) {
+      if (valueData->topic == m_nameTopic.GetHandle()) {
+        // .name
+        if (valueData->value && valueData->value.IsString()) {
+          m_nameValue = valueData->value.GetString();
+        }
+      } else if (valueData->topic == m_dimensionsTopic.GetHandle()) {
+        // dims
+        if (valueData->value && valueData->value.IsDoubleArray()) {
+          auto arr = valueData->value.GetDoubleArray();
+          if (arr.size() == 2) {
+            m_dimensionsValue = frc::Translation2d{units::meter_t{arr[0]},
+                                                   units::meter_t{arr[1]}};
+          }
+        }
+      } else if (valueData->topic == m_bgColorTopic.GetHandle()) {
+        // backgroundColor
+        if (valueData->value && valueData->value.IsString()) {
+          ConvertColor(valueData->value.GetString(), &m_bgColorValue);
+        }
+      } else {
+        auto fullName = nt::Topic{valueData->topic}.GetName();
+        auto name = wpi::drop_front(fullName, m_path.size());
+        if (name.empty() || name[0] == '.') {
+          continue;
+        }
+        std::string_view childName;
+        std::tie(name, childName) = wpi::split(name, '/');
+        if (childName.empty()) {
+          continue;
+        }
+
+        auto it = std::lower_bound(m_roots.begin(), m_roots.end(), name,
+                                   [](const auto& e, std::string_view name) {
+                                     return e->GetName() < name;
+                                   });
+        if (it != m_roots.end() && (*it)->GetName() == name) {
+          if ((*it)->NTUpdate(event, childName)) {
+            m_roots.erase(it);
+          }
+        }
+      }
     }
   }
 }
 
 bool NTMechanism2DModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+  return m_nameTopic.Exists();
 }
 
 bool NTMechanism2DModel::IsReadOnly() {
diff --git a/glass/src/libnt/native/cpp/NTMotorController.cpp b/glass/src/libnt/native/cpp/NTMotorController.cpp
new file mode 100644
index 0000000..1de6714
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTMotorController.cpp
@@ -0,0 +1,43 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "glass/networktables/NTMotorController.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTMotorControllerModel::NTMotorControllerModel(std::string_view path)
+    : NTMotorControllerModel(nt::NetworkTableInstance::GetDefault(), path) {}
+
+NTMotorControllerModel::NTMotorControllerModel(nt::NetworkTableInstance inst,
+                                               std::string_view path)
+    : m_inst{inst},
+      m_value{inst.GetDoubleTopic(fmt::format("{}/Value", path)).GetEntry(0)},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path))
+                         .Subscribe(false)},
+      m_valueData{fmt::format("NT_SpdCtrl:{}", path)},
+      m_nameValue{wpi::rsplit(path, '/').second} {}
+
+void NTMotorControllerModel::SetPercent(double value) {
+  m_value.Set(value);
+}
+
+void NTMotorControllerModel::Update() {
+  for (auto&& v : m_value.ReadQueue()) {
+    m_valueData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_controllable.ReadQueue()) {
+    m_controllableValue = v.value;
+  }
+}
+
+bool NTMotorControllerModel::Exists() {
+  return m_value.Exists();
+}
diff --git a/glass/src/libnt/native/cpp/NTPIDController.cpp b/glass/src/libnt/native/cpp/NTPIDController.cpp
index 7936057..1dde27d 100644
--- a/glass/src/libnt/native/cpp/NTPIDController.cpp
+++ b/glass/src/libnt/native/cpp/NTPIDController.cpp
@@ -10,76 +10,62 @@
 using namespace glass;
 
 NTPIDControllerModel::NTPIDControllerModel(std::string_view path)
-    : NTPIDControllerModel(nt::GetDefaultInstance(), path) {}
+    : NTPIDControllerModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTPIDControllerModel::NTPIDControllerModel(NT_Inst instance,
+NTPIDControllerModel::NTPIDControllerModel(nt::NetworkTableInstance inst,
                                            std::string_view path)
-    : m_nt(instance),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
-      m_p(m_nt.GetEntry(fmt::format("{}/p", path))),
-      m_i(m_nt.GetEntry(fmt::format("{}/i", path))),
-      m_d(m_nt.GetEntry(fmt::format("{}/d", path))),
-      m_setpoint(m_nt.GetEntry(fmt::format("{}/setpoint", path))),
-      m_pData(fmt::format("NTPIDCtrlP:{}", path)),
-      m_iData(fmt::format("NTPIDCtrlI:{}", path)),
-      m_dData(fmt::format("NTPIDCtrlD:{}", path)),
-      m_setpointData(fmt::format("NTPIDCtrlStpt:{}", path)),
-      m_nameValue(wpi::rsplit(path, '/').second) {
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_controllable);
-  m_nt.AddListener(m_p);
-  m_nt.AddListener(m_i);
-  m_nt.AddListener(m_d);
-  m_nt.AddListener(m_setpoint);
-}
+    : m_inst{inst},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path))
+                         .Subscribe(false)},
+      m_p{inst.GetDoubleTopic(fmt::format("{}/p", path)).GetEntry(0)},
+      m_i{inst.GetDoubleTopic(fmt::format("{}/i", path)).GetEntry(0)},
+      m_d{inst.GetDoubleTopic(fmt::format("{}/d", path)).GetEntry(0)},
+      m_setpoint{
+          inst.GetDoubleTopic(fmt::format("{}/setpoint", path)).GetEntry(0)},
+      m_pData{fmt::format("NTPIDCtrlP:{}", path)},
+      m_iData{fmt::format("NTPIDCtrlI:{}", path)},
+      m_dData{fmt::format("NTPIDCtrlD:{}", path)},
+      m_setpointData{fmt::format("NTPIDCtrlStpt:{}", path)},
+      m_nameValue{wpi::rsplit(path, '/').second} {}
 
 void NTPIDControllerModel::SetP(double value) {
-  nt::SetEntryValue(m_p, nt::NetworkTableValue::MakeDouble(value));
+  m_p.Set(value);
 }
 
 void NTPIDControllerModel::SetI(double value) {
-  nt::SetEntryValue(m_i, nt::NetworkTableValue::MakeDouble(value));
+  m_i.Set(value);
 }
 
 void NTPIDControllerModel::SetD(double value) {
-  nt::SetEntryValue(m_d, nt::NetworkTableValue::MakeDouble(value));
+  m_d.Set(value);
 }
 
 void NTPIDControllerModel::SetSetpoint(double value) {
-  nt::SetEntryValue(m_setpoint, nt::NetworkTableValue::MakeDouble(value));
+  m_setpoint.Set(value);
 }
 
 void NTPIDControllerModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    } else if (event.entry == m_p) {
-      if (event.value && event.value->IsDouble()) {
-        m_pData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_i) {
-      if (event.value && event.value->IsDouble()) {
-        m_iData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_d) {
-      if (event.value && event.value->IsDouble()) {
-        m_dData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_setpoint) {
-      if (event.value && event.value->IsDouble()) {
-        m_setpointData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_controllable) {
-      if (event.value && event.value->IsBoolean()) {
-        m_controllableValue = event.value->GetBoolean();
-      }
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_p.ReadQueue()) {
+    m_pData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_i.ReadQueue()) {
+    m_iData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_d.ReadQueue()) {
+    m_dData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_setpoint.ReadQueue()) {
+    m_setpointData.SetValue(v.value, v.time);
+  }
+  for (auto&& v : m_controllable.ReadQueue()) {
+    m_controllableValue = v.value;
   }
 }
 
 bool NTPIDControllerModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_setpoint) != NT_UNASSIGNED;
+  return m_setpoint.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTSpeedController.cpp b/glass/src/libnt/native/cpp/NTSpeedController.cpp
deleted file mode 100644
index 3dc351a..0000000
--- a/glass/src/libnt/native/cpp/NTSpeedController.cpp
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "glass/networktables/NTSpeedController.h"
-
-#include <fmt/format.h>
-#include <wpi/StringExtras.h>
-
-using namespace glass;
-
-NTSpeedControllerModel::NTSpeedControllerModel(std::string_view path)
-    : NTSpeedControllerModel(nt::GetDefaultInstance(), path) {}
-
-NTSpeedControllerModel::NTSpeedControllerModel(NT_Inst instance,
-                                               std::string_view path)
-    : m_nt(instance),
-      m_value(m_nt.GetEntry(fmt::format("{}/Value", path))),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))),
-      m_valueData(fmt::format("NT_SpdCtrl:{}", path)),
-      m_nameValue(wpi::rsplit(path, '/').second) {
-  m_nt.AddListener(m_value);
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_controllable);
-}
-
-void NTSpeedControllerModel::SetPercent(double value) {
-  nt::SetEntryValue(m_value, nt::NetworkTableValue::MakeDouble(value));
-}
-
-void NTSpeedControllerModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_value) {
-      if (event.value && event.value->IsDouble()) {
-        m_valueData.SetValue(event.value->GetDouble());
-      }
-    } else if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    } else if (event.entry == m_controllable) {
-      if (event.value && event.value->IsBoolean()) {
-        m_controllableValue = event.value->GetBoolean();
-      }
-    }
-  }
-}
-
-bool NTSpeedControllerModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
-}
diff --git a/glass/src/libnt/native/cpp/NTStringChooser.cpp b/glass/src/libnt/native/cpp/NTStringChooser.cpp
index e6a97fa..b892a2c 100644
--- a/glass/src/libnt/native/cpp/NTStringChooser.cpp
+++ b/glass/src/libnt/native/cpp/NTStringChooser.cpp
@@ -9,67 +9,56 @@
 using namespace glass;
 
 NTStringChooserModel::NTStringChooserModel(std::string_view path)
-    : NTStringChooserModel{nt::GetDefaultInstance(), path} {}
+    : NTStringChooserModel{nt::NetworkTableInstance::GetDefault(), path} {}
 
-NTStringChooserModel::NTStringChooserModel(NT_Inst inst, std::string_view path)
-    : m_nt{inst},
-      m_default{m_nt.GetEntry(fmt::format("{}/default", path))},
-      m_selected{m_nt.GetEntry(fmt::format("{}/selected", path))},
-      m_active{m_nt.GetEntry(fmt::format("{}/active", path))},
-      m_options{m_nt.GetEntry(fmt::format("{}/options", path))} {
-  m_nt.AddListener(m_default);
-  m_nt.AddListener(m_selected);
-  m_nt.AddListener(m_active);
-  m_nt.AddListener(m_options);
-}
-
-void NTStringChooserModel::SetDefault(std::string_view val) {
-  nt::SetEntryValue(m_default, nt::Value::MakeString(val));
+NTStringChooserModel::NTStringChooserModel(nt::NetworkTableInstance inst,
+                                           std::string_view path)
+    : m_inst{inst},
+      m_default{
+          m_inst.GetStringTopic(fmt::format("{}/default", path)).Subscribe("")},
+      m_selected{
+          m_inst.GetStringTopic(fmt::format("{}/selected", path)).GetEntry("")},
+      m_active{
+          m_inst.GetStringTopic(fmt::format("{}/active", path)).Subscribe("")},
+      m_options{m_inst.GetStringArrayTopic(fmt::format("{}/options", path))
+                    .Subscribe({})} {
+  m_selected.GetTopic().SetRetained(true);
 }
 
 void NTStringChooserModel::SetSelected(std::string_view val) {
-  nt::SetEntryValue(m_selected, nt::Value::MakeString(val));
-}
-
-void NTStringChooserModel::SetActive(std::string_view val) {
-  nt::SetEntryValue(m_active, nt::Value::MakeString(val));
-}
-
-void NTStringChooserModel::SetOptions(wpi::span<const std::string> val) {
-  nt::SetEntryValue(m_options, nt::Value::MakeStringArray(val));
+  m_selected.Set(val);
 }
 
 void NTStringChooserModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_default) {
-      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
-        m_defaultValue.clear();
-      } else if (event.value && event.value->IsString()) {
-        m_defaultValue = event.value->GetString();
-      }
-    } else if (event.entry == m_selected) {
-      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
-        m_selectedValue.clear();
-      } else if (event.value && event.value->IsString()) {
-        m_selectedValue = event.value->GetString();
-      }
-    } else if (event.entry == m_active) {
-      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
-        m_activeValue.clear();
-      } else if (event.value && event.value->IsString()) {
-        m_activeValue = event.value->GetString();
-      }
-    } else if (event.entry == m_options) {
-      if ((event.flags & NT_NOTIFY_DELETE) != 0) {
-        m_optionsValue.clear();
-      } else if (event.value && event.value->IsStringArray()) {
-        auto arr = event.value->GetStringArray();
-        m_optionsValue.assign(arr.begin(), arr.end());
-      }
-    }
+  if (!m_default.Exists()) {
+    m_defaultValue.clear();
+  }
+  for (auto&& v : m_default.ReadQueue()) {
+    m_defaultValue = std::move(v.value);
+  }
+
+  if (!m_selected.Exists()) {
+    m_selectedValue.clear();
+  }
+  for (auto&& v : m_selected.ReadQueue()) {
+    m_selectedValue = std::move(v.value);
+  }
+
+  if (!m_active.Exists()) {
+    m_activeValue.clear();
+  }
+  for (auto&& v : m_active.ReadQueue()) {
+    m_activeValue = std::move(v.value);
+  }
+
+  if (!m_options.Exists()) {
+    m_optionsValue.clear();
+  }
+  for (auto&& v : m_options.ReadQueue()) {
+    m_optionsValue = std::move(v.value);
   }
 }
 
 bool NTStringChooserModel::Exists() {
-  return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED;
+  return m_options.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NTSubsystem.cpp b/glass/src/libnt/native/cpp/NTSubsystem.cpp
index b2bdf8c..3078f87 100644
--- a/glass/src/libnt/native/cpp/NTSubsystem.cpp
+++ b/glass/src/libnt/native/cpp/NTSubsystem.cpp
@@ -9,37 +9,30 @@
 using namespace glass;
 
 NTSubsystemModel::NTSubsystemModel(std::string_view path)
-    : NTSubsystemModel(nt::GetDefaultInstance(), path) {}
+    : NTSubsystemModel(nt::NetworkTableInstance::GetDefault(), path) {}
 
-NTSubsystemModel::NTSubsystemModel(NT_Inst instance, std::string_view path)
-    : m_nt(instance),
-      m_name(m_nt.GetEntry(fmt::format("{}/.name", path))),
-      m_defaultCommand(m_nt.GetEntry(fmt::format("{}/.default", path))),
-      m_currentCommand(m_nt.GetEntry(fmt::format("{}/.command", path))) {
-  m_nt.AddListener(m_name);
-  m_nt.AddListener(m_defaultCommand);
-  m_nt.AddListener(m_currentCommand);
+NTSubsystemModel::NTSubsystemModel(nt::NetworkTableInstance inst,
+                                   std::string_view path)
+    : m_inst{inst},
+      m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")},
+      m_defaultCommand{
+          inst.GetStringTopic(fmt::format("{}/.default", path)).Subscribe("")},
+      m_currentCommand{
+          inst.GetStringTopic(fmt::format("{}/.command", path)).Subscribe("")} {
 }
 
 void NTSubsystemModel::Update() {
-  for (auto&& event : m_nt.PollListener()) {
-    if (event.entry == m_name) {
-      if (event.value && event.value->IsString()) {
-        m_nameValue = event.value->GetString();
-      }
-    } else if (event.entry == m_defaultCommand) {
-      if (event.value && event.value->IsString()) {
-        m_defaultCommandValue = event.value->GetString();
-      }
-    } else if (event.entry == m_currentCommand) {
-      if (event.value && event.value->IsString()) {
-        m_currentCommandValue = event.value->GetString();
-      }
-    }
+  for (auto&& v : m_name.ReadQueue()) {
+    m_nameValue = std::move(v.value);
+  }
+  for (auto&& v : m_defaultCommand.ReadQueue()) {
+    m_defaultCommandValue = std::move(v.value);
+  }
+  for (auto&& v : m_currentCommand.ReadQueue()) {
+    m_currentCommandValue = std::move(v.value);
   }
 }
 
 bool NTSubsystemModel::Exists() {
-  return m_nt.IsConnected() &&
-         nt::GetEntryType(m_defaultCommand) != NT_UNASSIGNED;
+  return m_defaultCommand.Exists();
 }
diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp
index bce2b0a..d368359 100644
--- a/glass/src/libnt/native/cpp/NetworkTables.cpp
+++ b/glass/src/libnt/native/cpp/NetworkTables.cpp
@@ -4,32 +4,61 @@
 
 #include "glass/networktables/NetworkTables.h"
 
-#include <networktables/NetworkTableValue.h>
-
 #include <cinttypes>
 #include <cstdio>
 #include <cstring>
 #include <initializer_list>
 #include <memory>
+#include <span>
 #include <string_view>
 #include <vector>
 
 #include <fmt/format.h>
 #include <imgui.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/NetworkTableValue.h>
+#include <ntcore_c.h>
 #include <ntcore_cpp.h>
+#include <ntcore_cpp_types.h>
+#include <wpi/MessagePack.h>
 #include <wpi/SmallString.h>
 #include <wpi/SpanExtras.h>
 #include <wpi/StringExtras.h>
+#include <wpi/mpack.h>
 #include <wpi/raw_ostream.h>
-#include <wpi/span.h>
 
 #include "glass/Context.h"
 #include "glass/DataSource.h"
 #include "glass/Storage.h"
 
 using namespace glass;
+using namespace mpack;
 
-static std::string BooleanArrayToString(wpi::span<const int> in) {
+namespace {
+enum ShowCategory {
+  ShowPersistent,
+  ShowRetained,
+  ShowTransitory,
+  ShowAll,
+};
+}  // namespace
+
+static bool IsVisible(ShowCategory category, bool persistent, bool retained) {
+  switch (category) {
+    case ShowPersistent:
+      return persistent;
+    case ShowRetained:
+      return retained && !persistent;
+    case ShowTransitory:
+      return !retained && !persistent;
+    case ShowAll:
+      return true;
+    default:
+      return false;
+  }
+}
+
+static std::string BooleanArrayToString(std::span<const int> in) {
   std::string rv;
   wpi::raw_string_ostream os{rv};
   os << '[';
@@ -49,11 +78,17 @@
   return rv;
 }
 
-static std::string DoubleArrayToString(wpi::span<const double> in) {
+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(wpi::span<const std::string> in) {
+static std::string StringArrayToString(std::span<const std::string> in) {
   std::string rv;
   wpi::raw_string_ostream os{rv};
   os << '[';
@@ -72,91 +107,399 @@
 }
 
 NetworkTablesModel::NetworkTablesModel()
-    : NetworkTablesModel{nt::GetDefaultInstance()} {}
+    : NetworkTablesModel{nt::NetworkTableInstance::GetDefault()} {}
 
-NetworkTablesModel::NetworkTablesModel(NT_Inst inst)
-    : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {
-  nt::AddPolledEntryListener(m_poller, "",
-                             NT_NOTIFY_LOCAL | NT_NOTIFY_NEW |
-                                 NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
-                                 NT_NOTIFY_FLAGS | NT_NOTIFY_IMMEDIATE);
+NetworkTablesModel::NetworkTablesModel(nt::NetworkTableInstance inst)
+    : m_inst{inst}, m_poller{inst} {
+  m_poller.AddListener({{"", "$"}}, nt::EventFlags::kTopic |
+                                        nt::EventFlags::kValueAll |
+                                        nt::EventFlags::kImmediate);
 }
 
-NetworkTablesModel::~NetworkTablesModel() {
-  nt::DestroyEntryListenerPoller(m_poller);
+NetworkTablesModel::Entry::~Entry() {
+  if (publisher != 0) {
+    nt::Unpublish(publisher);
+  }
 }
 
-NetworkTablesModel::Entry::Entry(nt::EntryNotification&& event)
-    : entry{event.entry},
-      name{std::move(event.name)},
-      value{std::move(event.value)},
-      flags{nt::GetEntryFlags(event.entry)} {
-  UpdateValue();
+void NetworkTablesModel::Entry::UpdateInfo(nt::TopicInfo&& info_) {
+  info = std::move(info_);
+  properties = info.GetProperties();
+
+  persistent = false;
+  auto it = properties.find("persistent");
+  if (it != properties.end()) {
+    if (auto v = it->get_ptr<const bool*>()) {
+      persistent = *v;
+    }
+  }
+
+  retained = false;
+  it = properties.find("retained");
+  if (it != properties.end()) {
+    if (auto v = it->get_ptr<const bool*>()) {
+      retained = *v;
+    }
+  }
 }
 
-void NetworkTablesModel::Entry::UpdateValue() {
-  switch (value->type()) {
+static void UpdateMsgpackValueSource(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, "");
+      break;
+    case mpack::mpack_type_int:
+      out->UpdateFromValue(
+          nt::Value::MakeInteger(mpack_tag_int_value(&tag), time), name, "");
+      break;
+    case mpack::mpack_type_uint:
+      out->UpdateFromValue(
+          nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time), name, "");
+      break;
+    case mpack::mpack_type_float:
+      out->UpdateFromValue(
+          nt::Value::MakeFloat(mpack_tag_float_value(&tag), time), name, "");
+      break;
+    case mpack::mpack_type_double:
+      out->UpdateFromValue(
+          nt::Value::MakeDouble(mpack_tag_double_value(&tag), time), 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,
+                           "");
+      break;
+    }
+    case mpack::mpack_type_bin:
+      // just skip it
+      mpack_skip_bytes(&r, mpack_tag_bin_length(&tag));
+      mpack_done_bin(&r);
+      break;
+    case mpack::mpack_type_array: {
+      if (out->valueChildrenMap) {
+        out->valueChildren.clear();
+        out->valueChildrenMap = false;
+      }
+      out->valueChildren.resize(mpack_tag_array_count(&tag));
+      unsigned int i = 0;
+      for (auto&& child : out->valueChildren) {
+        if (child.name.empty()) {
+          child.name = fmt::format("[{}]", i);
+          child.path = fmt::format("{}{}", name, child.name);
+        }
+        ++i;
+        UpdateMsgpackValueSource(&child, r, child.path, time);  // recurse
+      }
+      mpack_done_array(&r);
+      break;
+    }
+    case mpack::mpack_type_map: {
+      if (!out->valueChildrenMap) {
+        out->valueChildren.clear();
+        out->valueChildrenMap = true;
+      }
+      wpi::StringMap<size_t> elems;
+      for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) {
+        elems[out->valueChildren[i].name] = i;
+      }
+      bool added = false;
+      uint32_t count = mpack_tag_map_count(&tag);
+      for (uint32_t i = 0; i < count; ++i) {
+        std::string key;
+        if (mpack_expect_str(&r, &key) == mpack_ok) {
+          auto it = elems.find(key);
+          if (it != elems.end()) {
+            auto& child = out->valueChildren[it->second];
+            UpdateMsgpackValueSource(&child, r, child.path, time);
+            elems.erase(it);
+          } else {
+            added = true;
+            out->valueChildren.emplace_back();
+            auto& child = out->valueChildren.back();
+            child.name = std::move(key);
+            child.path = fmt::format("{}/{}", name, child.name);
+            UpdateMsgpackValueSource(&child, r, child.path, time);
+          }
+        }
+      }
+      // erase unmatched keys
+      out->valueChildren.erase(
+          std::remove_if(
+              out->valueChildren.begin(), out->valueChildren.end(),
+              [&](const auto& child) { return elems.count(child.name) > 0; }),
+          out->valueChildren.end());
+      if (added) {
+        // sort by name
+        std::sort(out->valueChildren.begin(), out->valueChildren.end(),
+                  [](const auto& a, const auto& b) { return a.name < b.name; });
+      }
+      mpack_done_map(&r);
+      break;
+    }
+    default:
+      out->value = {};
+      mpack_done_type(&r, mpack_tag_type(&tag));
+      break;
+  }
+}
+
+static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
+                                  const wpi::json& j, std::string_view name,
+                                  int64_t time) {
+  switch (j.type()) {
+    case wpi::json::value_t::object: {
+      if (!out->valueChildrenMap) {
+        out->valueChildren.clear();
+        out->valueChildrenMap = true;
+      }
+      wpi::StringMap<size_t> elems;
+      for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) {
+        elems[out->valueChildren[i].name] = i;
+      }
+      bool added = false;
+      for (auto&& kv : j.items()) {
+        auto it = elems.find(kv.key());
+        if (it != elems.end()) {
+          auto& child = out->valueChildren[it->second];
+          UpdateJsonValueSource(&child, kv.value(), child.path, time);
+          elems.erase(it);
+        } else {
+          added = true;
+          out->valueChildren.emplace_back();
+          auto& child = out->valueChildren.back();
+          child.name = kv.key();
+          child.path = fmt::format("{}/{}", name, child.name);
+          UpdateJsonValueSource(&child, kv.value(), child.path, time);
+        }
+      }
+      // erase unmatched keys
+      out->valueChildren.erase(
+          std::remove_if(
+              out->valueChildren.begin(), out->valueChildren.end(),
+              [&](const auto& child) { return elems.count(child.name) > 0; }),
+          out->valueChildren.end());
+      if (added) {
+        // sort by name
+        std::sort(out->valueChildren.begin(), out->valueChildren.end(),
+                  [](const auto& a, const auto& b) { return a.name < b.name; });
+      }
+      break;
+    }
+    case wpi::json::value_t::array: {
+      if (out->valueChildrenMap) {
+        out->valueChildren.clear();
+        out->valueChildrenMap = false;
+      }
+      out->valueChildren.resize(j.size());
+      unsigned int i = 0;
+      for (auto&& child : out->valueChildren) {
+        if (child.name.empty()) {
+          child.name = fmt::format("[{}]", i);
+          child.path = fmt::format("{}{}", name, child.name);
+        }
+        ++i;
+        UpdateJsonValueSource(&child, j[i], child.path, time);  // recurse
+      }
+      break;
+    }
+    case wpi::json::value_t::string:
+      out->UpdateFromValue(
+          nt::Value::MakeString(j.get_ref<const std::string&>(), time), name,
+          "");
+      break;
+    case wpi::json::value_t::boolean:
+      out->UpdateFromValue(nt::Value::MakeBoolean(j.get<bool>(), time), name,
+                           "");
+      break;
+    case wpi::json::value_t::number_integer:
+      out->UpdateFromValue(nt::Value::MakeInteger(j.get<int64_t>(), time), name,
+                           "");
+      break;
+    case wpi::json::value_t::number_unsigned:
+      out->UpdateFromValue(nt::Value::MakeInteger(j.get<uint64_t>(), time),
+                           name, "");
+      break;
+    case wpi::json::value_t::number_float:
+      out->UpdateFromValue(nt::Value::MakeDouble(j.get<double>(), time), name,
+                           "");
+      break;
+    default:
+      out->value = {};
+      break;
+  }
+}
+
+void NetworkTablesModel::ValueSource::UpdateFromValue(
+    nt::Value&& v, std::string_view name, std::string_view typeStr) {
+  value = v;
+  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);
+      source->SetValue(value.GetBoolean() ? 1 : 0, value.last_change());
       source->SetDigital(true);
       break;
-    case NT_DOUBLE:
+    case NT_INTEGER:
+      valueChildren.clear();
       if (!source) {
         source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
       }
-      source->SetValue(value->GetDouble());
+      source->SetValue(value.GetInteger(), value.last_change());
+      source->SetDigital(false);
+      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);
+      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);
       break;
     case NT_BOOLEAN_ARRAY:
-      valueStr = BooleanArrayToString(value->GetBooleanArray());
+      valueChildren.clear();
+      valueStr = BooleanArrayToString(value.GetBooleanArray());
+      break;
+    case NT_INTEGER_ARRAY:
+      valueChildren.clear();
+      valueStr = IntegerArrayToString(value.GetIntegerArray());
+      break;
+    case NT_FLOAT_ARRAY:
+      valueChildren.clear();
+      valueStr = FloatArrayToString(value.GetFloatArray());
       break;
     case NT_DOUBLE_ARRAY:
-      valueStr = DoubleArrayToString(value->GetDoubleArray());
+      valueChildren.clear();
+      valueStr = FloatArrayToString(value.GetDoubleArray());
       break;
     case NT_STRING_ARRAY:
-      valueStr = StringArrayToString(value->GetStringArray());
+      valueChildren.clear();
+      valueStr = StringArrayToString(value.GetStringArray());
+      break;
+    case NT_STRING:
+      if (typeStr == "json") {
+        try {
+          UpdateJsonValueSource(this, wpi::json::parse(value.GetString()), name,
+                                value.last_change());
+        } catch (wpi::json::exception&) {
+          // ignore
+        }
+      } else {
+        valueChildren.clear();
+      }
+      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());
+
+        mpack_reader_destroy(&r);
+      } else {
+        valueChildren.clear();
+      }
       break;
     default:
+      valueChildren.clear();
       break;
   }
 }
 
 void NetworkTablesModel::Update() {
-  bool timedOut = false;
   bool updateTree = false;
-  for (auto&& event : nt::PollEntryListener(m_poller, 0, &timedOut)) {
-    auto& entry = m_entries[event.entry];
-    if (event.flags & NT_NOTIFY_NEW) {
-      if (!entry) {
-        entry = std::make_unique<Entry>(std::move(event));
-        m_sortedEntries.emplace_back(entry.get());
+  for (auto&& event : m_poller.ReadQueue()) {
+    if (auto info = event.GetTopicInfo()) {
+      auto& entry = m_entries[info->topic];
+      if (event.flags & nt::EventFlags::kPublish) {
+        if (!entry) {
+          entry = std::make_unique<Entry>();
+          m_sortedEntries.emplace_back(entry.get());
+          updateTree = true;
+        }
+      }
+      if (event.flags & nt::EventFlags::kUnpublish) {
+        // meta topic handling
+        if (wpi::starts_with(info->name, '$')) {
+          // meta topic handling
+          if (info->name == "$clients") {
+            m_clients.clear();
+          } else if (info->name == "$serverpub") {
+            m_server.publishers.clear();
+          } else if (info->name == "$serversub") {
+            m_server.subscribers.clear();
+          } else if (wpi::starts_with(info->name, "$clientpub$")) {
+            auto it = m_clients.find(wpi::drop_front(info->name, 11));
+            if (it != m_clients.end()) {
+              it->second.publishers.clear();
+            }
+          } else if (wpi::starts_with(info->name, "$clientsub$")) {
+            auto it = m_clients.find(wpi::drop_front(info->name, 11));
+            if (it != m_clients.end()) {
+              it->second.subscribers.clear();
+            }
+          }
+        }
+        auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(),
+                            entry.get());
+        // will be removed completely below
+        if (it != m_sortedEntries.end()) {
+          *it = nullptr;
+        }
+        m_entries.erase(info->topic);
         updateTree = true;
         continue;
       }
-    }
-    if (!entry) {
-      continue;
-    }
-    if (event.flags & NT_NOTIFY_DELETE) {
-      auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(),
-                          entry.get());
-      // will be removed completely below
-      if (it != m_sortedEntries.end()) {
-        *it = nullptr;
+      if (event.flags & nt::EventFlags::kProperties) {
+        updateTree = true;
       }
-      m_entries.erase(event.entry);
-      updateTree = true;
-      continue;
-    }
-    if (event.flags & NT_NOTIFY_UPDATE) {
-      entry->value = std::move(event.value);
-      entry->UpdateValue();
-    }
-    if (event.flags & NT_NOTIFY_FLAGS) {
-      entry->flags = nt::GetEntryFlags(event.entry);
+      if (entry) {
+        entry->UpdateTopic(std::move(event));
+      }
+    } 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);
+        if (wpi::starts_with(entry->info.name, '$') && entry->value.IsRaw() &&
+            entry->info.type_str == "msgpack") {
+          // meta topic handling
+          if (entry->info.name == "$clients") {
+            // need to remove deleted entries as UpdateClients() uses GetEntry()
+            if (updateTree) {
+              std::erase(m_sortedEntries, nullptr);
+            }
+            UpdateClients(entry->value.GetRaw());
+          } else if (entry->info.name == "$serverpub") {
+            m_server.UpdatePublishers(entry->value.GetRaw());
+          } else if (entry->info.name == "$serversub") {
+            m_server.UpdateSubscribers(entry->value.GetRaw());
+          } else if (wpi::starts_with(entry->info.name, "$clientpub$")) {
+            auto it = m_clients.find(wpi::drop_front(entry->info.name, 11));
+            if (it != m_clients.end()) {
+              it->second.UpdatePublishers(entry->value.GetRaw());
+            }
+          } else if (wpi::starts_with(entry->info.name, "$clientsub$")) {
+            auto it = m_clients.find(wpi::drop_front(entry->info.name, 11));
+            if (it != m_clients.end()) {
+              it->second.UpdateSubscribers(entry->value.GetRaw());
+            }
+          }
+        }
+      }
     }
   }
 
@@ -166,20 +509,34 @@
   }
 
   // remove deleted entries
-  m_sortedEntries.erase(
-      std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr),
-      m_sortedEntries.end());
+  std::erase(m_sortedEntries, nullptr);
 
+  RebuildTree();
+}
+
+void NetworkTablesModel::RebuildTree() {
   // sort by name
-  std::sort(m_sortedEntries.begin(), m_sortedEntries.end(),
-            [](const auto& a, const auto& b) { return a->name < b->name; });
+  std::sort(
+      m_sortedEntries.begin(), m_sortedEntries.end(),
+      [](const auto& a, const auto& b) { return a->info.name < b->info.name; });
 
-  // rebuild tree
-  m_root.clear();
+  RebuildTreeImpl(&m_root, ShowAll);
+  RebuildTreeImpl(&m_persistentRoot, ShowPersistent);
+  RebuildTreeImpl(&m_retainedRoot, ShowRetained);
+  RebuildTreeImpl(&m_transitoryRoot, ShowTransitory);
+}
+
+void NetworkTablesModel::RebuildTreeImpl(std::vector<TreeNode>* tree,
+                                         int category) {
+  tree->clear();
   wpi::SmallVector<std::string_view, 16> parts;
   for (auto& entry : m_sortedEntries) {
+    if (!IsVisible(static_cast<ShowCategory>(category), entry->persistent,
+                   entry->retained)) {
+      continue;
+    }
     parts.clear();
-    wpi::split(entry->name, parts, '/', -1, false);
+    wpi::split(entry->info.name, parts, '/', -1, false);
 
     // ignore a raw "/" key
     if (parts.empty()) {
@@ -187,8 +544,8 @@
     }
 
     // get to leaf
-    auto nodes = &m_root;
-    for (auto part : wpi::drop_back(wpi::span{parts.begin(), parts.end()})) {
+    auto nodes = tree;
+    for (auto part : wpi::drop_back(std::span{parts.begin(), parts.end()})) {
       auto it =
           std::find_if(nodes->begin(), nodes->end(),
                        [&](const auto& node) { return node.name == part; });
@@ -196,9 +553,10 @@
         nodes->emplace_back(part);
         // path is from the beginning of the string to the end of the current
         // part; this works because part is a reference to the internals of
-        // entry->name
+        // entry->info.name
         nodes->back().path.assign(
-            entry->name.data(), part.data() + part.size() - entry->name.data());
+            entry->info.name.data(),
+            part.data() + part.size() - entry->info.name.data());
         it = nodes->end() - 1;
       }
       nodes = &it->children;
@@ -217,14 +575,97 @@
 }
 
 bool NetworkTablesModel::Exists() {
-  return nt::IsConnected(m_inst);
+  return true;
 }
 
-static std::shared_ptr<nt::Value> StringToBooleanArray(std::string_view in) {
+NetworkTablesModel::Entry* NetworkTablesModel::GetEntry(std::string_view name) {
+  auto entryIt = std::lower_bound(
+      m_sortedEntries.begin(), m_sortedEntries.end(), name,
+      [](auto&& entry, auto&& name) { return entry->info.name < name; });
+  if (entryIt == m_sortedEntries.end() || (*entryIt)->info.name != name) {
+    return nullptr;
+  }
+  return *entryIt;
+}
+
+NetworkTablesModel::Entry* NetworkTablesModel::AddEntry(NT_Topic topic) {
+  auto& entry = m_entries[topic];
+  if (!entry) {
+    entry = std::make_unique<Entry>();
+    entry->info = nt::GetTopicInfo(topic);
+    entry->properties = entry->info.GetProperties();
+    m_sortedEntries.emplace_back(entry.get());
+  }
+  RebuildTree();
+  return entry.get();
+}
+
+NetworkTablesModel::Client::Subscriber::Subscriber(
+    nt::meta::ClientSubscriber&& oth)
+    : ClientSubscriber{std::move(oth)},
+      topicsStr{StringArrayToString(topics)} {}
+
+void NetworkTablesModel::Client::UpdatePublishers(
+    std::span<const uint8_t> data) {
+  if (auto pubs = nt::meta::DecodeClientPublishers(data)) {
+    publishers = std::move(*pubs);
+  } else {
+    fmt::print(stderr, "Failed to update publishers\n");
+  }
+}
+
+void NetworkTablesModel::Client::UpdateSubscribers(
+    std::span<const uint8_t> data) {
+  if (auto subs = nt::meta::DecodeClientSubscribers(data)) {
+    subscribers.clear();
+    subscribers.reserve(subs->size());
+    for (auto&& sub : *subs) {
+      subscribers.emplace_back(std::move(sub));
+    }
+  } else {
+    fmt::print(stderr, "Failed to update subscribers\n");
+  }
+}
+
+void NetworkTablesModel::UpdateClients(std::span<const uint8_t> data) {
+  auto clientsArr = nt::meta::DecodeClients(data);
+  if (!clientsArr) {
+    return;
+  }
+
+  // we need to create a new map so deletions are reflected
+  std::map<std::string, Client, std::less<>> newClients;
+  for (auto&& client : *clientsArr) {
+    auto& newClient = newClients[client.id];
+    newClient = std::move(client);
+    auto it = m_clients.find(newClient.id);
+    if (it != m_clients.end()) {
+      // transfer from existing
+      newClient.publishers = std::move(it->second.publishers);
+      newClient.subscribers = std::move(it->second.subscribers);
+    } else {
+      // initially populate
+      if (Entry* entry = GetEntry(fmt::format("$clientpub${}", newClient.id))) {
+        if (entry->value.IsRaw() && entry->info.type_str == "msgpack") {
+          newClient.UpdatePublishers(entry->value.GetRaw());
+        }
+      }
+      if (Entry* entry = GetEntry(fmt::format("$clientsub${}", newClient.id))) {
+        if (entry->value.IsRaw() && entry->info.type_str == "msgpack") {
+          newClient.UpdateSubscribers(entry->value.GetRaw());
+        }
+      }
+    }
+  }
+
+  // replace map
+  m_clients = std::move(newClients);
+}
+
+static bool StringToBooleanArray(std::string_view in, std::vector<int>* out) {
   in = wpi::trim(in);
   if (in.empty()) {
-    return nt::NetworkTableValue::MakeBooleanArray(
-        std::initializer_list<bool>{});
+    return false;
   }
   if (in.front() == '[') {
     in.remove_prefix(1);
@@ -235,30 +676,29 @@
   in = wpi::trim(in);
 
   wpi::SmallVector<std::string_view, 16> inSplit;
-  wpi::SmallVector<int, 16> out;
 
   wpi::split(in, inSplit, ',', -1, false);
   for (auto val : inSplit) {
     val = wpi::trim(val);
     if (wpi::equals_lower(val, "true")) {
-      out.emplace_back(1);
+      out->emplace_back(1);
     } else if (wpi::equals_lower(val, "false")) {
-      out.emplace_back(0);
+      out->emplace_back(0);
     } else {
       fmt::print(stderr,
                  "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return nullptr;
+      return false;
     }
   }
 
-  return nt::NetworkTableValue::MakeBooleanArray(out);
+  return true;
 }
 
-static std::shared_ptr<nt::Value> StringToDoubleArray(std::string_view in) {
+static bool StringToIntegerArray(std::string_view in,
+                                 std::vector<int64_t>* out) {
   in = wpi::trim(in);
   if (in.empty()) {
-    return nt::NetworkTableValue::MakeDoubleArray(
-        std::initializer_list<double>{});
+    return false;
   }
   if (in.front() == '[') {
     in.remove_prefix(1);
@@ -269,75 +709,27 @@
   in = wpi::trim(in);
 
   wpi::SmallVector<std::string_view, 16> inSplit;
-  wpi::SmallVector<double, 16> out;
 
   wpi::split(in, inSplit, ',', -1, false);
   for (auto val : inSplit) {
-    if (auto num = wpi::parse_float<double>(wpi::trim(val))) {
-      out.emplace_back(num.value());
+    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 nullptr;
+      return false;
     }
   }
 
-  return nt::NetworkTableValue::MakeDoubleArray(out);
+  return true;
 }
 
-static int fromxdigit(char ch) {
-  if (ch >= 'a' && ch <= 'f') {
-    return (ch - 'a' + 10);
-  } else if (ch >= 'A' && ch <= 'F') {
-    return (ch - 'A' + 10);
-  } else {
-    return ch - '0';
-  }
-}
-
-static std::string_view UnescapeString(std::string_view source,
-                                       wpi::SmallVectorImpl<char>& buf) {
-  assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
-  buf.clear();
-  buf.reserve(source.size() - 2);
-  for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
-    if (*s != '\\') {
-      buf.push_back(*s);
-      continue;
-    }
-    switch (*++s) {
-      case 't':
-        buf.push_back('\t');
-        break;
-      case 'n':
-        buf.push_back('\n');
-        break;
-      case 'x': {
-        if (!isxdigit(*(s + 1))) {
-          buf.push_back('x');  // treat it like a unknown escape
-          break;
-        }
-        int ch = fromxdigit(*++s);
-        if (std::isxdigit(*(s + 1))) {
-          ch <<= 4;
-          ch |= fromxdigit(*++s);
-        }
-        buf.push_back(static_cast<char>(ch));
-        break;
-      }
-      default:
-        buf.push_back(*s);
-        break;
-    }
-  }
-  return {buf.data(), buf.size()};
-}
-
-static std::shared_ptr<nt::Value> StringToStringArray(std::string_view in) {
+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 nt::NetworkTableValue::MakeStringArray(
-        std::initializer_list<std::string>{});
+    return false;
   }
   if (in.front() == '[') {
     in.remove_prefix(1);
@@ -348,7 +740,36 @@
   in = wpi::trim(in);
 
   wpi::SmallVector<std::string_view, 16> inSplit;
-  std::vector<std::string> out;
+
+  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);
@@ -360,49 +781,81 @@
     if (val.front() != '"' || val.back() != '"') {
       fmt::print(stderr,
                  "GUI: NetworkTables: Could not understand value '{}'\n", val);
-      return nullptr;
+      return false;
     }
-    out.emplace_back(UnescapeString(val, buf));
+    val.remove_prefix(1);
+    val.remove_suffix(1);
+    out->emplace_back(wpi::UnescapeCString(val, buf).first);
   }
 
-  return nt::NetworkTableValue::MakeStringArray(std::move(out));
+  return true;
 }
 
-static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry) {
+static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry,
+                                   const char* typeStr,
+                                   NetworkTablesFlags flags) {
   auto& val = entry.value;
   if (!val) {
     return;
   }
 
-  switch (val->type()) {
+  switch (val.type()) {
     case NT_BOOLEAN:
-      ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false");
+      ImGui::LabelText(typeStr ? typeStr : "boolean", "%s",
+                       val.GetBoolean() ? "true" : "false");
       break;
-    case NT_DOUBLE:
-      ImGui::LabelText("double", "%.6f", val->GetDouble());
+    case NT_INTEGER:
+      ImGui::LabelText(typeStr ? typeStr : "int", "%" PRId64, val.GetInteger());
       break;
+    case NT_FLOAT:
+      ImGui::LabelText(typeStr ? typeStr : "double", "%.6f", val.GetFloat());
+      break;
+    case NT_DOUBLE: {
+      unsigned char precision = (flags & NetworkTablesFlags_Precision) >>
+                                kNetworkTablesFlags_PrecisionBitShift;
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+      ImGui::LabelText(typeStr ? typeStr : "double",
+                       fmt::format("%.{}f", precision).c_str(),
+                       val.GetDouble());
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+      break;
+    }
     case NT_STRING: {
       // GetString() comes from a std::string, so it's null terminated
-      ImGui::LabelText("string", "%s", val->GetString().data());
+      ImGui::LabelText(typeStr ? typeStr : "string", "%s",
+                       val.GetString().data());
       break;
     }
     case NT_BOOLEAN_ARRAY:
-      ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str());
+      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("double[]", "%s", entry.valueStr.c_str());
+      ImGui::LabelText(typeStr ? typeStr : "double[]", "%s",
+                       entry.valueStr.c_str());
       break;
     case NT_STRING_ARRAY:
-      ImGui::LabelText("string[]", "%s", entry.valueStr.c_str());
+      ImGui::LabelText(typeStr ? typeStr : "string[]", "%s",
+                       entry.valueStr.c_str());
       break;
     case NT_RAW:
-      ImGui::LabelText("raw", "[...]");
-      break;
-    case NT_RPC:
-      ImGui::LabelText("rpc", "[...]");
+      ImGui::LabelText(typeStr ? typeStr : "raw", "[...]");
       break;
     default:
-      ImGui::LabelText("other", "?");
+      ImGui::LabelText(typeStr ? typeStr : "other", "?");
       break;
   }
 }
@@ -417,85 +870,188 @@
   return textBuffer;
 }
 
-static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry) {
+static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry,
+                                   NetworkTablesFlags flags) {
   auto& val = entry.value;
   if (!val) {
     return;
   }
 
-  ImGui::PushID(entry.name.c_str());
-  switch (val->type()) {
+  const char* typeStr =
+      entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str();
+  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("boolean", &v, boolOptions, 2)) {
-        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeBoolean(v));
+      int v = val.GetBoolean() ? 1 : 0;
+      if (ImGui::Combo(typeStr ? typeStr : "boolean", &v, boolOptions, 2)) {
+        if (entry.publisher == 0) {
+          entry.publisher =
+              nt::Publish(entry.info.topic, NT_BOOLEAN, "boolean");
+        }
+        nt::SetBoolean(entry.publisher, v);
+      }
+      break;
+    }
+    case NT_INTEGER: {
+      int64_t v = val.GetInteger();
+      if (ImGui::InputScalar(typeStr ? typeStr : "int", ImGuiDataType_S64, &v,
+                             nullptr, nullptr, nullptr,
+                             ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (entry.publisher == 0) {
+          entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int");
+        }
+        nt::SetInteger(entry.publisher, v);
+      }
+      break;
+    }
+    case NT_FLOAT: {
+      float v = val.GetFloat();
+      if (ImGui::InputFloat(typeStr ? typeStr : "float", &v, 0, 0, "%.6f",
+                            ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (entry.publisher == 0) {
+          entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float");
+        }
+        nt::SetFloat(entry.publisher, v);
       }
       break;
     }
     case NT_DOUBLE: {
-      double v = val->GetDouble();
-      if (ImGui::InputDouble("double", &v, 0, 0, "%.6f",
+      double v = val.GetDouble();
+      unsigned char precision = (flags & NetworkTablesFlags_Precision) >>
+                                kNetworkTablesFlags_PrecisionBitShift;
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+      if (ImGui::InputDouble(typeStr ? typeStr : "double", &v, 0, 0,
+                             fmt::format("%.{}f", precision).c_str(),
                              ImGuiInputTextFlags_EnterReturnsTrue)) {
-        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v));
+        if (entry.publisher == 0) {
+          entry.publisher = nt::Publish(entry.info.topic, NT_DOUBLE, "double");
+        }
+        nt::SetDouble(entry.publisher, v);
       }
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
       break;
     }
     case NT_STRING: {
-      char* v = GetTextBuffer(val->GetString());
-      if (ImGui::InputText("string", v, kTextBufferSize,
+      char* v = GetTextBuffer(val.GetString());
+      if (ImGui::InputText(typeStr ? typeStr : "string", v, kTextBufferSize,
                            ImGuiInputTextFlags_EnterReturnsTrue)) {
-        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v));
+        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("boolean[]", v, kTextBufferSize,
+      if (ImGui::InputText(typeStr ? typeStr : "boolean[]", v, kTextBufferSize,
                            ImGuiInputTextFlags_EnterReturnsTrue)) {
-        if (auto outv = StringToBooleanArray(v)) {
-          nt::SetEntryValue(entry.entry, std::move(outv));
+        std::vector<int> outv;
+        if (StringToBooleanArray(v, &outv)) {
+          if (entry.publisher == 0) {
+            entry.publisher =
+                nt::Publish(entry.info.topic, NT_BOOLEAN_ARRAY, "boolean[]");
+          }
+          nt::SetBooleanArray(entry.publisher, outv);
+        }
+      }
+      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);
+        }
+      }
+      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("double[]", v, kTextBufferSize,
+      if (ImGui::InputText(typeStr ? typeStr : "double[]", v, kTextBufferSize,
                            ImGuiInputTextFlags_EnterReturnsTrue)) {
-        if (auto outv = StringToDoubleArray(v)) {
-          nt::SetEntryValue(entry.entry, std::move(outv));
+        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("string[]", v, kTextBufferSize,
+      if (ImGui::InputText(typeStr ? typeStr : "string[]", v, kTextBufferSize,
                            ImGuiInputTextFlags_EnterReturnsTrue)) {
-        if (auto outv = StringToStringArray(v)) {
-          nt::SetEntryValue(entry.entry, std::move(outv));
+        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("raw", "[...]");
+      ImGui::LabelText(typeStr ? typeStr : "raw",
+                       val.GetRaw().empty() ? "[]" : "[...]");
       break;
     case NT_RPC:
-      ImGui::LabelText("rpc", "[...]");
+      ImGui::LabelText(typeStr ? typeStr : "rpc", "[...]");
       break;
     default:
-      ImGui::LabelText("other", "?");
+      ImGui::LabelText(typeStr ? typeStr : "other", "?");
       break;
   }
   ImGui::PopID();
 }
 
-static void EmitParentContextMenu(const std::string& path,
+static void CreateTopicMenuItem(NetworkTablesModel* model,
+                                std::string_view path, NT_Type type,
+                                const char* typeStr, bool enabled) {
+  if (ImGui::MenuItem(typeStr, nullptr, false, enabled)) {
+    auto entry =
+        model->AddEntry(nt::GetTopic(model->GetInstance().GetHandle(), path));
+    if (entry->publisher == 0) {
+      entry->publisher = nt::Publish(entry->info.topic, type, typeStr);
+    }
+  }
+}
+
+static void EmitParentContextMenu(NetworkTablesModel* model,
+                                  const std::string& path,
                                   NetworkTablesFlags flags) {
-  // Workaround https://github.com/ocornut/imgui/issues/331
-  bool openWarningPopup = false;
   static char nameBuffer[kTextBufferSize];
   if (ImGui::BeginPopupContextItem(path.c_str())) {
     ImGui::Text("%s", path.c_str());
@@ -517,216 +1073,367 @@
 
       ImGui::Text("Adding: %s", fullNewPath.c_str());
       ImGui::Separator();
-      auto entry = nt::GetEntry(nt::GetDefaultInstance(), fullNewPath);
+      auto entry = model->GetEntry(fullNewPath);
+      bool exists = entry && entry->info.type != NT_Type::NT_UNASSIGNED;
       bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys ||
                       nameBuffer[0] != '\0') &&
-                     nt::GetEntryType(entry) == NT_Type::NT_UNASSIGNED;
-      if (ImGui::MenuItem("string", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeString(""))) {
-          openWarningPopup = true;
-        }
-      }
-      if (ImGui::MenuItem("double", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeDouble(0.0))) {
-          openWarningPopup = true;
-        }
-      }
-      if (ImGui::MenuItem("boolean", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeBoolean(false))) {
-          openWarningPopup = true;
-        }
-      }
-      if (ImGui::MenuItem("string[]", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeStringArray({""}))) {
-          openWarningPopup = true;
-        }
-      }
-      if (ImGui::MenuItem("double[]", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeDoubleArray({0.0}))) {
-          openWarningPopup = true;
-        }
-      }
-      if (ImGui::MenuItem("boolean[]", nullptr, false, enabled)) {
-        if (!nt::SetEntryValue(entry, nt::Value::MakeBooleanArray({false}))) {
-          openWarningPopup = true;
-        }
-      }
+                     !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();
     }
 
-    ImGui::Separator();
-    if (ImGui::MenuItem("Remove All")) {
-      for (auto&& entry : nt::GetEntries(nt::GetDefaultInstance(), path, 0)) {
-        nt::DeleteEntry(entry);
-      }
-    }
-    ImGui::EndPopup();
-  }
-
-  // Workaround https://github.com/ocornut/imgui/issues/331
-  if (openWarningPopup) {
-    ImGui::OpenPopup("Value exists");
-  }
-  if (ImGui::BeginPopupModal("Value exists", nullptr,
-                             ImGuiWindowFlags_AlwaysAutoResize)) {
-    ImGui::Text("The provided name %s already exists in the tree!", nameBuffer);
-    ImGui::Separator();
-
-    if (ImGui::Button("OK", ImVec2(120, 0))) {
-      ImGui::CloseCurrentPopup();
-    }
-    ImGui::SetItemDefaultFocus();
     ImGui::EndPopup();
   }
 }
 
-static void EmitEntry(NetworkTablesModel::Entry& entry, const char* name,
-                      NetworkTablesFlags flags) {
-  if (entry.source) {
+static void EmitValueName(DataSource* source, const char* name,
+                          const char* path) {
+  if (source) {
     ImGui::Selectable(name);
-    entry.source->EmitDrag();
+    source->EmitDrag();
   } else {
-    ImGui::Text("%s", name);
+    ImGui::TextUnformatted(name);
   }
-  if (ImGui::BeginPopupContextItem(entry.name.c_str())) {
-    ImGui::Text("%s", entry.name.c_str());
-    ImGui::Separator();
-    if (ImGui::MenuItem("Remove")) {
-      nt::DeleteEntry(entry.entry);
-    }
+  if (ImGui::BeginPopupContextItem(path)) {
+    ImGui::TextUnformatted(path);
     ImGui::EndPopup();
   }
-  ImGui::NextColumn();
+}
 
-  if (flags & NetworkTablesFlags_ReadOnly) {
-    EmitEntryValueReadonly(entry);
-  } else {
-    EmitEntryValueEditable(entry);
-  }
-  ImGui::NextColumn();
-
-  if (flags & NetworkTablesFlags_ShowFlags) {
-    if ((entry.flags & NT_PERSISTENT) != 0) {
-      ImGui::Text("Persistent");
-    } else if (entry.flags != 0) {
-      ImGui::Text("%02x", entry.flags);
+static void EmitValueTree(
+    const std::vector<NetworkTablesModel::EntryValueTreeNode>& children,
+    NetworkTablesFlags flags) {
+  for (auto&& child : children) {
+    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)) {
+        EmitValueTree(child.valueChildren, flags);
+        TreePop();
+      }
+    } else {
+      EmitEntryValueReadonly(child, nullptr, flags);
     }
-    ImGui::NextColumn();
+  }
+}
+
+static void EmitEntry(NetworkTablesModel* model,
+                      NetworkTablesModel::Entry& entry, const char* name,
+                      NetworkTablesFlags flags, ShowCategory category) {
+  if (!IsVisible(category, entry.persistent, entry.retained)) {
+    return;
+  }
+
+  bool valueChildrenOpen = false;
+  ImGui::TableNextRow();
+  ImGui::TableNextColumn();
+  EmitValueName(entry.source.get(), name, entry.info.name.c_str());
+
+  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());
+    valueChildrenOpen =
+        TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth |
+                              ImGuiTreeNodeFlags_AllowItemOverlap);
+    // make it look like a normal label w/type
+    ImGui::SetCursorPos(pos);
+    ImGui::LabelText(entry.info.type_str.c_str(), "%s", "");
+  } else if (flags & NetworkTablesFlags_ReadOnly) {
+    EmitEntryValueReadonly(
+        entry,
+        entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(),
+        flags);
+  } else {
+    EmitEntryValueEditable(entry, flags);
+  }
+
+  if (flags & NetworkTablesFlags_ShowProperties) {
+    ImGui::TableNextColumn();
+    ImGui::Text("%s", entry.info.properties.c_str());
+    if (ImGui::BeginPopupContextItem(entry.info.name.c_str())) {
+      if (ImGui::Checkbox("persistent", &entry.persistent)) {
+        nt::SetTopicPersistent(entry.info.topic, entry.persistent);
+      }
+      if (ImGui::Checkbox("retained", &entry.retained)) {
+        if (entry.retained) {
+          nt::SetTopicProperty(entry.info.topic, "retained", true);
+        } else {
+          nt::DeleteTopicProperty(entry.info.topic, "retained");
+        }
+      }
+      ImGui::EndPopup();
+    }
   }
 
   if (flags & NetworkTablesFlags_ShowTimestamp) {
+    ImGui::TableNextColumn();
     if (entry.value) {
-      ImGui::Text("%f", (entry.value->last_change() * 1.0e-6) -
+      ImGui::Text("%f", (entry.value.last_change() * 1.0e-6) -
                             (GetZeroTime() * 1.0e-6));
     } else {
       ImGui::TextUnformatted("");
     }
-    ImGui::NextColumn();
   }
-  ImGui::Separator();
+
+  if (flags & NetworkTablesFlags_ShowServerTimestamp) {
+    ImGui::TableNextColumn();
+    if (entry.value && entry.value.server_time() != 0) {
+      if (entry.value.server_time() == 1) {
+        ImGui::TextUnformatted("---");
+      } else {
+        ImGui::Text("%f", entry.value.server_time() * 1.0e-6);
+      }
+    } else {
+      ImGui::TextUnformatted("");
+    }
+  }
+
+  if (valueChildrenOpen) {
+    EmitValueTree(entry.valueChildren, flags);
+    TreePop();
+  }
 }
 
-static void EmitTree(const std::vector<NetworkTablesModel::TreeNode>& tree,
-                     NetworkTablesFlags flags) {
+static void EmitTree(NetworkTablesModel* model,
+                     const std::vector<NetworkTablesModel::TreeNode>& tree,
+                     NetworkTablesFlags flags, ShowCategory category,
+                     bool root) {
   for (auto&& node : tree) {
+    if (root && (flags & NetworkTablesFlags_ShowSpecial) == 0 &&
+        wpi::starts_with(node.name, '$')) {
+      continue;
+    }
     if (node.entry) {
-      EmitEntry(*node.entry, node.name.c_str(), flags);
+      EmitEntry(model, *node.entry, node.name.c_str(), flags, category);
     }
 
     if (!node.children.empty()) {
+      ImGui::TableNextRow();
+      ImGui::TableNextColumn();
       bool open =
           TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth);
-      EmitParentContextMenu(node.path, flags);
-      ImGui::NextColumn();
-      ImGui::NextColumn();
-      if (flags & NetworkTablesFlags_ShowFlags) {
-        ImGui::NextColumn();
-      }
-      if (flags & NetworkTablesFlags_ShowTimestamp) {
-        ImGui::NextColumn();
-      }
-      ImGui::Separator();
+      EmitParentContextMenu(model, node.path, flags);
       if (open) {
-        EmitTree(node.children, flags);
+        EmitTree(model, node.children, flags, category, false);
         TreePop();
       }
     }
   }
 }
 
-void glass::DisplayNetworkTables(NetworkTablesModel* model,
-                                 NetworkTablesFlags flags) {
-  auto inst = model->GetInstance();
-
-  if (flags & NetworkTablesFlags_ShowConnections) {
-    if (CollapsingHeader("Connections")) {
-      ImGui::Columns(4, "connections");
-      ImGui::Text("Id");
-      ImGui::NextColumn();
-      ImGui::Text("Address");
-      ImGui::NextColumn();
-      ImGui::Text("Updated");
-      ImGui::NextColumn();
-      ImGui::Text("Proto");
-      ImGui::NextColumn();
-      ImGui::Separator();
-      for (auto&& i : nt::GetConnections(inst)) {
-        ImGui::Text("%s", i.remote_id.c_str());
-        ImGui::NextColumn();
-        ImGui::Text("%s", i.remote_ip.c_str());
-        ImGui::NextColumn();
-        ImGui::Text("%llu",
-                    static_cast<unsigned long long>(  // NOLINT(runtime/int)
-                        i.last_update));
-        ImGui::NextColumn();
-        ImGui::Text("%d.%d", i.protocol_version >> 8,
-                    i.protocol_version & 0xff);
-        ImGui::NextColumn();
-      }
-      ImGui::Columns();
-    }
-
-    if (!CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) {
-      return;
-    }
+static void DisplayTable(NetworkTablesModel* model,
+                         const std::vector<NetworkTablesModel::TreeNode>& tree,
+                         NetworkTablesFlags flags, ShowCategory category) {
+  if (tree.empty()) {
+    return;
   }
 
-  const bool showFlags = (flags & NetworkTablesFlags_ShowFlags);
+  const bool showProperties = (flags & NetworkTablesFlags_ShowProperties);
   const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp);
+  const bool showServerTimestamp =
+      (flags & NetworkTablesFlags_ShowServerTimestamp);
 
-  static bool first = true;
-  ImGui::Columns(2 + (showFlags ? 1 : 0) + (showTimestamp ? 1 : 0), "values");
-  if (first) {
-    ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth());
-  }
-  ImGui::Text("Name");
-  EmitParentContextMenu("/", flags);
-  ImGui::NextColumn();
-  ImGui::Text("Value");
-  ImGui::NextColumn();
-  if (showFlags) {
-    if (first) {
-      ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize());
-    }
-    ImGui::Text("Flags");
-    ImGui::NextColumn();
+  ImGui::BeginTable("values",
+                    2 + (showProperties ? 1 : 0) + (showTimestamp ? 1 : 0) +
+                        (showServerTimestamp ? 1 : 0),
+                    ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit |
+                        ImGuiTableFlags_BordersInner);
+  ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed,
+                          0.35f * ImGui::GetWindowWidth());
+  ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed,
+                          12 * ImGui::GetFontSize());
+  if (showProperties) {
+    ImGui::TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthFixed,
+                            12 * ImGui::GetFontSize());
   }
   if (showTimestamp) {
-    ImGui::Text("Changed");
-    ImGui::NextColumn();
+    ImGui::TableSetupColumn("Time");
   }
-  ImGui::Separator();
-  first = false;
+  if (showServerTimestamp) {
+    ImGui::TableSetupColumn("Server Time");
+  }
+  ImGui::TableHeadersRow();
 
+  // EmitParentContextMenu(model, "/", flags);
   if (flags & NetworkTablesFlags_TreeView) {
-    EmitTree(model->GetTreeRoot(), flags);
+    switch (category) {
+      case ShowPersistent:
+        PushID("persistent");
+        break;
+      case ShowRetained:
+        PushID("retained");
+        break;
+      case ShowTransitory:
+        PushID("transitory");
+        break;
+      default:
+        break;
+    }
+    EmitTree(model, tree, flags, category, true);
+    if (category != ShowAll) {
+      PopID();
+    }
   } else {
     for (auto entry : model->GetEntries()) {
-      EmitEntry(*entry, entry->name.c_str(), flags);
+      if ((flags & NetworkTablesFlags_ShowSpecial) != 0 ||
+          !wpi::starts_with(entry->info.name, '$')) {
+        EmitEntry(model, *entry, entry->info.name.c_str(), flags, category);
+      }
     }
   }
-  ImGui::Columns();
+  ImGui::EndTable();
+}
+
+static void DisplayClient(const NetworkTablesModel::Client& client) {
+  if (CollapsingHeader("Publishers")) {
+    ImGui::BeginTable("publishers", 2, ImGuiTableFlags_Resizable);
+    ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed,
+                            10 * ImGui::GetFontSize());
+    ImGui::TableSetupColumn("Topic");
+    ImGui::TableHeadersRow();
+    for (auto&& pub : client.publishers) {
+      ImGui::TableNextRow();
+      ImGui::TableNextColumn();
+      ImGui::Text("%" PRId64, pub.uid);
+      ImGui::TableNextColumn();
+      ImGui::Text("%s", pub.topic.c_str());
+    }
+    ImGui::EndTable();
+  }
+  if (CollapsingHeader("Subscribers")) {
+    ImGui::BeginTable(
+        "subscribers", 6,
+        ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp);
+    ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed,
+                            10 * ImGui::GetFontSize());
+    ImGui::TableSetupColumn("Topics", ImGuiTableColumnFlags_WidthStretch, 6.0f);
+    ImGui::TableSetupColumn("Periodic", ImGuiTableColumnFlags_WidthStretch,
+                            1.0f);
+    ImGui::TableSetupColumn("Topics Only", ImGuiTableColumnFlags_WidthStretch,
+                            1.0f);
+    ImGui::TableSetupColumn("Send All", ImGuiTableColumnFlags_WidthStretch,
+                            1.0f);
+    ImGui::TableSetupColumn("Prefix Match", ImGuiTableColumnFlags_WidthStretch,
+                            1.0f);
+    ImGui::TableHeadersRow();
+    for (auto&& sub : client.subscribers) {
+      ImGui::TableNextRow();
+      ImGui::TableNextColumn();
+      ImGui::Text("%" PRId64, sub.uid);
+      ImGui::TableNextColumn();
+      ImGui::Text("%s", sub.topicsStr.c_str());
+      ImGui::TableNextColumn();
+      ImGui::Text("%0.3f", sub.options.periodic);
+      ImGui::TableNextColumn();
+      ImGui::Text(sub.options.topicsOnly ? "Yes" : "No");
+      ImGui::TableNextColumn();
+      ImGui::Text(sub.options.sendAll ? "Yes" : "No");
+      ImGui::TableNextColumn();
+      ImGui::Text(sub.options.prefixMatch ? "Yes" : "No");
+    }
+    ImGui::EndTable();
+  }
+}
+
+void glass::DisplayNetworkTablesInfo(NetworkTablesModel* model) {
+  auto inst = model->GetInstance();
+
+  if (CollapsingHeader("Connections")) {
+    ImGui::BeginTable("connections", 4, ImGuiTableFlags_Resizable);
+    ImGui::TableSetupColumn("Id");
+    ImGui::TableSetupColumn("Address");
+    ImGui::TableSetupColumn("Updated");
+    ImGui::TableSetupColumn("Proto");
+    ImGui::TableSetupScrollFreeze(1, 0);
+    ImGui::TableHeadersRow();
+    for (auto&& i : inst.GetConnections()) {
+      ImGui::TableNextRow();
+      ImGui::TableNextColumn();
+      ImGui::Text("%s", i.remote_id.c_str());
+      ImGui::TableNextColumn();
+      ImGui::Text("%s", i.remote_ip.c_str());
+      ImGui::TableNextColumn();
+      ImGui::Text("%llu",
+                  static_cast<unsigned long long>(  // NOLINT(runtime/int)
+                      i.last_update));
+      ImGui::TableNextColumn();
+      ImGui::Text("%d.%d", i.protocol_version >> 8, i.protocol_version & 0xff);
+    }
+    ImGui::EndTable();
+  }
+
+  auto netMode = inst.GetNetworkMode();
+  if (netMode == NT_NET_MODE_SERVER || netMode == NT_NET_MODE_CLIENT4) {
+    if (CollapsingHeader("Server")) {
+      PushID("Server");
+      ImGui::Indent();
+      DisplayClient(model->GetServer());
+      ImGui::Unindent();
+      PopID();
+    }
+    if (CollapsingHeader("Clients")) {
+      ImGui::Indent();
+      for (auto&& client : model->GetClients()) {
+        if (CollapsingHeader(client.second.id.c_str())) {
+          PushID(client.second.id.c_str());
+          ImGui::Indent();
+          ImGui::Text("%s (version %u.%u)", client.second.conn.c_str(),
+                      client.second.version >> 8, client.second.version & 0xff);
+          DisplayClient(client.second);
+          ImGui::Unindent();
+          PopID();
+        }
+      }
+      ImGui::Unindent();
+    }
+  }
+}
+
+void glass::DisplayNetworkTables(NetworkTablesModel* model,
+                                 NetworkTablesFlags flags) {
+  if (flags & NetworkTablesFlags_CombinedView) {
+    DisplayTable(model, model->GetTreeRoot(), flags, ShowAll);
+  } else {
+    if (CollapsingHeader("Persistent Values", ImGuiTreeNodeFlags_DefaultOpen)) {
+      DisplayTable(model, model->GetPersistentTreeRoot(), flags,
+                   ShowPersistent);
+    }
+
+    if (CollapsingHeader("Retained Values", ImGuiTreeNodeFlags_DefaultOpen)) {
+      DisplayTable(model, model->GetRetainedTreeRoot(), flags, ShowRetained);
+    }
+
+    if (CollapsingHeader("Transitory Values", ImGuiTreeNodeFlags_DefaultOpen)) {
+      DisplayTable(model, model->GetTransitoryTreeRoot(), flags,
+                   ShowTransitory);
+    }
+  }
 }
 
 void NetworkTablesFlagsSettings::Update() {
@@ -734,28 +1441,41 @@
     auto& storage = GetStorage();
     m_pTreeView =
         &storage.GetBool("tree", m_defaultFlags & NetworkTablesFlags_TreeView);
-    m_pShowConnections = &storage.GetBool(
-        "connections", m_defaultFlags & NetworkTablesFlags_ShowConnections);
-    m_pShowFlags = &storage.GetBool(
-        "flags", m_defaultFlags & NetworkTablesFlags_ShowFlags);
+    m_pCombinedView = &storage.GetBool(
+        "combined", m_defaultFlags & NetworkTablesFlags_CombinedView);
+    m_pShowSpecial = &storage.GetBool(
+        "special", m_defaultFlags & NetworkTablesFlags_ShowSpecial);
+    m_pShowProperties = &storage.GetBool(
+        "properties", m_defaultFlags & NetworkTablesFlags_ShowProperties);
     m_pShowTimestamp = &storage.GetBool(
         "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp);
+    m_pShowServerTimestamp = &storage.GetBool(
+        "serverTimestamp",
+        m_defaultFlags & NetworkTablesFlags_ShowServerTimestamp);
     m_pCreateNoncanonicalKeys = &storage.GetBool(
         "createNonCanonical",
         m_defaultFlags & NetworkTablesFlags_CreateNoncanonicalKeys);
+    m_pPrecision = &storage.GetInt(
+        "precision", (m_defaultFlags & NetworkTablesFlags_Precision) >>
+                         kNetworkTablesFlags_PrecisionBitShift);
   }
 
-  m_flags &=
-      ~(NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections |
-        NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp |
-        NetworkTablesFlags_CreateNoncanonicalKeys);
+  m_flags &= ~(
+      NetworkTablesFlags_TreeView | NetworkTablesFlags_CombinedView |
+      NetworkTablesFlags_ShowSpecial | NetworkTablesFlags_ShowProperties |
+      NetworkTablesFlags_ShowTimestamp |
+      NetworkTablesFlags_ShowServerTimestamp |
+      NetworkTablesFlags_CreateNoncanonicalKeys | NetworkTablesFlags_Precision);
   m_flags |=
       (*m_pTreeView ? NetworkTablesFlags_TreeView : 0) |
-      (*m_pShowConnections ? NetworkTablesFlags_ShowConnections : 0) |
-      (*m_pShowFlags ? NetworkTablesFlags_ShowFlags : 0) |
+      (*m_pCombinedView ? NetworkTablesFlags_CombinedView : 0) |
+      (*m_pShowSpecial ? NetworkTablesFlags_ShowSpecial : 0) |
+      (*m_pShowProperties ? NetworkTablesFlags_ShowProperties : 0) |
       (*m_pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0) |
+      (*m_pShowServerTimestamp ? NetworkTablesFlags_ShowServerTimestamp : 0) |
       (*m_pCreateNoncanonicalKeys ? NetworkTablesFlags_CreateNoncanonicalKeys
-                                  : 0);
+                                  : 0) |
+      (*m_pPrecision << kNetworkTablesFlags_PrecisionBitShift);
 }
 
 void NetworkTablesFlagsSettings::DisplayMenu() {
@@ -763,9 +1483,22 @@
     return;
   }
   ImGui::MenuItem("Tree View", "", m_pTreeView);
-  ImGui::MenuItem("Show Connections", "", m_pShowConnections);
-  ImGui::MenuItem("Show Flags", "", m_pShowFlags);
+  ImGui::MenuItem("Combined View", "", m_pCombinedView);
+  ImGui::MenuItem("Show Special", "", m_pShowSpecial);
+  ImGui::MenuItem("Show Properties", "", m_pShowProperties);
   ImGui::MenuItem("Show Timestamp", "", m_pShowTimestamp);
+  ImGui::MenuItem("Show Server Timestamp", "", m_pShowServerTimestamp);
+  if (ImGui::BeginMenu("Decimal Precision")) {
+    static const char* precisionOptions[] = {"1", "2", "3", "4", "5",
+                                             "6", "7", "8", "9", "10"};
+    for (int i = 1; i <= 10; i++) {
+      if (ImGui::MenuItem(precisionOptions[i - 1], nullptr,
+                          i == *m_pPrecision)) {
+        *m_pPrecision = i;
+      }
+    }
+    ImGui::EndMenu();
+  }
   ImGui::Separator();
   ImGui::MenuItem("Allow creation of non-canonical keys", "",
                   m_pCreateNoncanonicalKeys);
@@ -773,9 +1506,13 @@
 
 void NetworkTablesView::Display() {
   m_flags.Update();
-  if (ImGui::BeginPopupContextItem()) {
-    m_flags.DisplayMenu();
-    ImGui::EndPopup();
-  }
   DisplayNetworkTables(m_model, m_flags.GetFlags());
 }
+
+void NetworkTablesView::Settings() {
+  m_flags.DisplayMenu();
+}
+
+bool NetworkTablesView::HasSettings() {
+  return true;
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
deleted file mode 100644
index 5cb5bbc..0000000
--- a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "glass/networktables/NetworkTablesHelper.h"
-
-using namespace glass;
-
-NetworkTablesHelper::NetworkTablesHelper(NT_Inst inst)
-    : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {}
-
-NetworkTablesHelper::~NetworkTablesHelper() {
-  nt::DestroyEntryListenerPoller(m_poller);
-}
-
-bool NetworkTablesHelper::IsConnected() const {
-  return nt::GetNetworkMode(m_inst) == NT_NET_MODE_SERVER ||
-         nt::IsConnected(m_inst);
-}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
index 9ccbb2e..413899e 100644
--- a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
+++ b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
@@ -17,16 +17,16 @@
 using namespace glass;
 
 NetworkTablesProvider::NetworkTablesProvider(Storage& storage)
-    : NetworkTablesProvider{storage, nt::GetDefaultInstance()} {}
+    : NetworkTablesProvider{storage, nt::NetworkTableInstance::GetDefault()} {}
 
-NetworkTablesProvider::NetworkTablesProvider(Storage& storage, NT_Inst inst)
+NetworkTablesProvider::NetworkTablesProvider(Storage& storage,
+                                             nt::NetworkTableInstance inst)
     : Provider{storage.GetChild("windows")},
-      m_nt{inst},
+      m_inst{inst},
+      m_poller{inst},
       m_typeCache{storage.GetChild("types")} {
   storage.SetCustomApply([this] {
-    m_listener =
-        m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW |
-                                 NT_NOTIFY_DELETE | NT_NOTIFY_IMMEDIATE);
+    m_listener = m_poller.AddListener({{""}}, nt::EventFlags::kTopic);
     for (auto&& childIt : m_storage.GetChildren()) {
       auto id = childIt.key();
       auto typePtr = m_typeCache.FindValue(id);
@@ -41,15 +41,14 @@
       }
 
       auto entry = GetOrCreateView(
-          builderIt->second,
-          nt::GetEntry(m_nt.GetInstance(), fmt::format("{}/.type", id)), id);
+          builderIt->second, m_inst.GetTopic(fmt::format("{}/.type", id)), id);
       if (entry) {
         Show(entry, nullptr);
       }
     }
   });
   storage.SetCustomClear([this, &storage] {
-    nt::RemoveEntryListener(m_listener);
+    m_poller.RemoveListener(m_listener);
     m_listener = 0;
     for (auto&& modelEntry : m_modelEntries) {
       modelEntry->model.reset();
@@ -100,35 +99,58 @@
 void NetworkTablesProvider::Update() {
   Provider::Update();
 
-  // add/remove entries from NT changes
-  for (auto&& event : m_nt.PollListener()) {
-    // look for .type fields
-    std::string_view eventName{event.name};
-    if (!wpi::ends_with(eventName, "/.type") || !event.value ||
-        !event.value->IsString()) {
-      continue;
-    }
-    auto tableName = wpi::drop_back(eventName, 6);
-
-    // only handle ones where we have a builder
-    auto builderIt = m_typeMap.find(event.value->GetString());
-    if (builderIt == m_typeMap.end()) {
-      continue;
-    }
-
-    if (event.flags & NT_NOTIFY_DELETE) {
-      auto it = std::find_if(
-          m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) {
-            return static_cast<Entry*>(elem->modelEntry)->typeEntry ==
-                   event.entry;
-          });
-      if (it != m_viewEntries.end()) {
-        m_viewEntries.erase(it);
+  for (auto&& event : m_poller.ReadQueue()) {
+    if (auto info = event.GetTopicInfo()) {
+      // add/remove entries from NT changes
+      // look for .type fields
+      if (!wpi::ends_with(info->name, "/.type") || info->type != NT_STRING ||
+          info->type_str != "string") {
+        continue;
       }
-    } else if (event.flags & NT_NOTIFY_NEW) {
-      GetOrCreateView(builderIt->second, event.entry, tableName);
+
+      if (event.flags & nt::EventFlags::kUnpublish) {
+        auto it = m_topicMap.find(info->topic);
+        if (it != m_topicMap.end()) {
+          m_poller.RemoveListener(it->second.listener);
+          m_topicMap.erase(it);
+        }
+
+        auto it2 = std::find_if(
+            m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) {
+              return static_cast<Entry*>(elem->modelEntry)
+                         ->typeTopic.GetHandle() == info->topic;
+            });
+        if (it2 != m_viewEntries.end()) {
+          m_viewEntries.erase(it2);
+        }
+      } else if (event.flags & nt::EventFlags::kPublish) {
+        // subscribe to it; use a subscriber so we only get string values
+        SubListener sublistener;
+        sublistener.subscriber = nt::StringTopic{info->topic}.Subscribe("");
+        sublistener.listener = m_poller.AddListener(
+            sublistener.subscriber,
+            nt::EventFlags::kValueAll | nt::EventFlags::kImmediate);
+        m_topicMap.try_emplace(info->topic, std::move(sublistener));
+      }
+    } else if (auto valueData = event.GetValueEventData()) {
+      // handle actual .type strings
+      if (!valueData->value.IsString()) {
+        continue;
+      }
+
+      // only handle ones where we have a builder
+      auto builderIt = m_typeMap.find(valueData->value.GetString());
+      if (builderIt == m_typeMap.end()) {
+        continue;
+      }
+
+      auto topicName = nt::GetTopicName(valueData->topic);
+      auto tableName = wpi::drop_back(topicName, 6);
+
+      GetOrCreateView(builderIt->second, nt::Topic{valueData->topic},
+                      tableName);
       // cache the type
-      m_typeCache.SetString(tableName, event.value->GetString());
+      m_typeCache.SetString(tableName, valueData->value.GetString());
     }
   }
 }
@@ -149,7 +171,7 @@
   // get or create model
   if (!entry->modelEntry->model) {
     entry->modelEntry->model =
-        entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str());
+        entry->modelEntry->createModel(m_inst, entry->name.c_str());
   }
   if (!entry->modelEntry->model) {
     return;
@@ -180,22 +202,22 @@
 }
 
 NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView(
-    const Builder& builder, NT_Entry typeEntry, std::string_view name) {
+    const Builder& builder, nt::Topic typeTopic, std::string_view name) {
   // get view entry if it already exists
   auto viewIt = FindViewEntry(name);
   if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) {
     // make sure typeEntry is set in model
-    static_cast<Entry*>((*viewIt)->modelEntry)->typeEntry = typeEntry;
+    static_cast<Entry*>((*viewIt)->modelEntry)->typeTopic = typeTopic;
     return viewIt->get();
   }
 
   // get or create model entry
   auto modelIt = FindModelEntry(name);
   if (modelIt != m_modelEntries.end() && (*modelIt)->name == name) {
-    static_cast<Entry*>(modelIt->get())->typeEntry = typeEntry;
+    static_cast<Entry*>(modelIt->get())->typeTopic = typeTopic;
   } else {
     modelIt = m_modelEntries.emplace(
-        modelIt, std::make_unique<Entry>(typeEntry, name, builder));
+        modelIt, std::make_unique<Entry>(typeTopic, name, builder));
   }
 
   // create new view entry
diff --git a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
index d1fb341..fd6bd52 100644
--- a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
+++ b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
@@ -43,50 +43,66 @@
 
       // if just changing servers in client mode, no need to stop and restart
       unsigned int curMode = nt::GetNetworkMode(m_inst);
-      if (mode != 1 || (curMode & NT_NET_MODE_SERVER) != 0) {
+      if ((mode == 0 || mode == 3) ||
+          (mode == 1 && (curMode & NT_NET_MODE_CLIENT4) == 0) ||
+          (mode == 2 && (curMode & NT_NET_MODE_CLIENT3) == 0)) {
         nt::StopClient(m_inst);
         nt::StopServer(m_inst);
         nt::StopLocal(m_inst);
       }
 
-      if (m_mode != 1 || !dsClient) {
+      if ((m_mode == 0 || m_mode == 3) || !dsClient) {
         nt::StopDSClient(m_inst);
       }
 
       lock.lock();
     } while (mode != m_mode || dsClient != m_dsClient);
 
-    if (m_mode == 1) {
+    if (m_mode == 1 || m_mode == 2) {
       std::string_view serverTeam{m_serverTeam};
       std::optional<unsigned int> team;
+      if (m_mode == 1) {
+        nt::StartClient4(m_inst, m_clientName);
+      } else if (m_mode == 2) {
+        nt::StartClient3(m_inst, m_clientName);
+      }
+
+      unsigned int port = m_mode == 1 ? m_port4 : m_port3;
       if (!wpi::contains(serverTeam, '.') &&
           (team = wpi::parse_integer<unsigned int>(serverTeam, 10))) {
-        nt::StartClientTeam(m_inst, team.value(), NT_DEFAULT_PORT);
+        nt::SetServerTeam(m_inst, team.value(), port);
       } else {
         wpi::SmallVector<std::string_view, 4> serverNames;
-        wpi::SmallVector<std::pair<std::string_view, unsigned int>, 4> servers;
+        std::vector<std::pair<std::string_view, unsigned int>> servers;
         wpi::split(serverTeam, serverNames, ',', -1, false);
         for (auto&& serverName : serverNames) {
-          servers.emplace_back(serverName, NT_DEFAULT_PORT);
+          servers.emplace_back(serverName, port);
         }
-        nt::StartClient(m_inst, servers);
+        nt::SetServer(m_inst, servers);
       }
 
       if (m_dsClient) {
-        nt::StartDSClient(m_inst, NT_DEFAULT_PORT);
+        nt::StartDSClient(m_inst, port);
       }
-    } else if (m_mode == 2) {
+    } else if (m_mode == 3) {
       nt::StartServer(m_inst, m_iniName.c_str(), m_listenAddress.c_str(),
-                      NT_DEFAULT_PORT);
+                      m_port3, m_port4);
     }
   }
 }
 
-NetworkTablesSettings::NetworkTablesSettings(Storage& storage, NT_Inst inst)
-    : m_mode{storage.GetString("mode"), 0, {"Disabled", "Client", "Server"}},
-      m_iniName{storage.GetString("iniName", "networktables.ini")},
+NetworkTablesSettings::NetworkTablesSettings(std::string_view clientName,
+                                             Storage& storage, NT_Inst inst)
+    : m_mode{storage.GetString("mode"),
+             0,
+             {"Disabled", "Client (NT4)", "Client (NT3)", "Server"}},
+      m_persistentFilename{
+          storage.GetString("persistentFilename", "networktables.json")},
       m_serverTeam{storage.GetString("serverTeam")},
       m_listenAddress{storage.GetString("listenAddress")},
+      m_clientName{storage.GetString("clientName", clientName)},
+      m_port3{storage.GetInt("port3", NT_DEFAULT_PORT3)},
+      m_port4{storage.GetInt("port4", NT_DEFAULT_PORT4)},
       m_dsClient{storage.GetBool("dsClient", true)} {
   m_thread.Start(inst);
 }
@@ -101,23 +117,59 @@
   auto thr = m_thread.GetThread();
   thr->m_restart = true;
   thr->m_mode = m_mode.GetValue();
-  thr->m_iniName = m_iniName;
+  thr->m_iniName = m_persistentFilename;
   thr->m_serverTeam = m_serverTeam;
   thr->m_listenAddress = m_listenAddress;
+  thr->m_clientName = m_clientName;
+  thr->m_port3 = m_port3;
+  thr->m_port4 = m_port4;
   thr->m_dsClient = m_dsClient;
   thr->m_cond.notify_one();
 }
 
+static void LimitPortRange(int* port) {
+  if (*port < 0) {
+    *port = 0;
+  } else if (*port > 65535) {
+    *port = 65535;
+  }
+}
+
 bool NetworkTablesSettings::Display() {
-  m_mode.Combo("Mode", m_serverOption ? 3 : 2);
+  m_mode.Combo("Mode", m_serverOption ? 4 : 3);
   switch (m_mode.GetValue()) {
     case 1:
+    case 2: {
       ImGui::InputText("Team/IP", &m_serverTeam);
+      int* port = m_mode.GetValue() == 1 ? &m_port4 : &m_port3;
+      if (ImGui::InputInt("Port", port)) {
+        LimitPortRange(port);
+      }
+      ImGui::SameLine();
+      if (ImGui::SmallButton("Default")) {
+        *port = m_mode.GetValue() == 1 ? NT_DEFAULT_PORT4 : NT_DEFAULT_PORT3;
+      }
+      ImGui::InputText("Network Identity", &m_clientName);
       ImGui::Checkbox("Get Address from DS", &m_dsClient);
       break;
-    case 2:
+    }
+    case 3:
       ImGui::InputText("Listen Address", &m_listenAddress);
-      ImGui::InputText("ini Filename", &m_iniName);
+      if (ImGui::InputInt("NT3 port", &m_port3)) {
+        LimitPortRange(&m_port3);
+      }
+      ImGui::SameLine();
+      if (ImGui::SmallButton("Default##default3")) {
+        m_port3 = NT_DEFAULT_PORT3;
+      }
+      if (ImGui::InputInt("NT4 port", &m_port4)) {
+        LimitPortRange(&m_port4);
+      }
+      ImGui::SameLine();
+      if (ImGui::SmallButton("Default##default4")) {
+        m_port4 = NT_DEFAULT_PORT4;
+      }
+      ImGui::InputText("Persistent Filename", &m_persistentFilename);
       break;
     default:
       break;
diff --git a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
index 0a5f234..ef61025 100644
--- a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
+++ b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
@@ -12,8 +12,8 @@
 #include "glass/networktables/NTGyro.h"
 #include "glass/networktables/NTMecanumDrive.h"
 #include "glass/networktables/NTMechanism2D.h"
+#include "glass/networktables/NTMotorController.h"
 #include "glass/networktables/NTPIDController.h"
-#include "glass/networktables/NTSpeedController.h"
 #include "glass/networktables/NTStringChooser.h"
 #include "glass/networktables/NTSubsystem.h"
 #include "glass/networktables/NetworkTablesProvider.h"
@@ -23,7 +23,7 @@
 void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) {
   provider.Register(
       NTCommandSchedulerModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTCommandSchedulerModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -34,7 +34,7 @@
       });
   provider.Register(
       NTCommandSelectorModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTCommandSelectorModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -45,7 +45,7 @@
       });
   provider.Register(
       NTDifferentialDriveModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTDifferentialDriveModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -56,7 +56,7 @@
       });
   provider.Register(
       NTFMSModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTFMSModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -66,7 +66,7 @@
       });
   provider.Register(
       NTDigitalInputModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTDigitalInputModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -77,7 +77,7 @@
       });
   provider.Register(
       NTDigitalOutputModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTDigitalOutputModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -88,7 +88,7 @@
       });
   provider.Register(
       NTField2DModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTField2DModel>(inst, path);
       },
       [=](Window* win, Model* model, const char* path) {
@@ -100,7 +100,7 @@
       });
   provider.Register(
       NTGyroModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTGyroModel>(inst, path);
       },
       [](Window* win, Model* model, const char* path) {
@@ -110,7 +110,7 @@
       });
   provider.Register(
       NTMecanumDriveModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTMecanumDriveModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -120,7 +120,7 @@
       });
   provider.Register(
       NTMechanism2DModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTMechanism2DModel>(inst, path);
       },
       [=](Window* win, Model* model, const char* path) {
@@ -132,7 +132,7 @@
       });
   provider.Register(
       NTPIDControllerModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTPIDControllerModel>(inst, path);
       },
       [](Window* win, Model* model, const char* path) {
@@ -142,19 +142,19 @@
         });
       });
   provider.Register(
-      NTSpeedControllerModel::kType,
-      [](NT_Inst inst, const char* path) {
-        return std::make_unique<NTSpeedControllerModel>(inst, path);
+      NTMotorControllerModel::kType,
+      [](nt::NetworkTableInstance inst, const char* path) {
+        return std::make_unique<NTMotorControllerModel>(inst, path);
       },
       [](Window* win, Model* model, const char* path) {
         win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
         return MakeFunctionView([=] {
-          DisplaySpeedController(static_cast<NTSpeedControllerModel*>(model));
+          DisplayMotorController(static_cast<NTMotorControllerModel*>(model));
         });
       });
   provider.Register(
       NTStringChooserModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTStringChooserModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
@@ -165,7 +165,7 @@
       });
   provider.Register(
       NTSubsystemModel::kType,
-      [](NT_Inst inst, const char* path) {
+      [](nt::NetworkTableInstance inst, const char* path) {
         return std::make_unique<NTSubsystemModel>(inst, path);
       },
       [](Window* win, Model* model, const char*) {
diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
index 54dc778..980e1f5 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
@@ -4,14 +4,18 @@
 
 #pragma once
 
+#include <stdint.h>
+
 #include <string>
 #include <string_view>
 #include <vector>
 
-#include <ntcore_cpp.h>
+#include <networktables/IntegerArrayTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringArrayTopic.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/CommandScheduler.h"
 
 namespace glass {
@@ -20,7 +24,7 @@
   static constexpr const char* kType = "Scheduler";
 
   explicit NTCommandSchedulerModel(std::string_view path);
-  NTCommandSchedulerModel(NT_Inst instance, std::string_view path);
+  NTCommandSchedulerModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const std::vector<std::string>& GetCurrentCommands() override {
@@ -34,14 +38,14 @@
   bool IsReadOnly() override { return false; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_name;
-  NT_Entry m_commands;
-  NT_Entry m_ids;
-  NT_Entry m_cancel;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_name;
+  nt::StringArraySubscriber m_commands;
+  nt::IntegerArraySubscriber m_ids;
+  nt::IntegerArrayPublisher m_cancel;
 
   std::string m_nameValue;
   std::vector<std::string> m_commandsValue;
-  std::vector<double> m_idsValue;
+  std::vector<int64_t> m_idsValue;
 };
 }  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
index c936665..ab35484 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
@@ -7,10 +7,11 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/CommandSelector.h"
 
 namespace glass {
@@ -19,7 +20,7 @@
   static constexpr const char* kType = "Command";
 
   explicit NTCommandSelectorModel(std::string_view path);
-  NTCommandSelectorModel(NT_Inst instance, std::string_view path);
+  NTCommandSelectorModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   DataSource* GetRunningData() override { return &m_runningData; }
@@ -30,9 +31,9 @@
   bool IsReadOnly() override { return false; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_running;
-  NT_Entry m_name;
+  nt::NetworkTableInstance m_inst;
+  nt::BooleanEntry m_running;
+  nt::StringSubscriber m_name;
 
   DataSource m_runningData;
   std::string m_nameValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
index 49b3eb0..5e34669 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
@@ -8,10 +8,12 @@
 #include <string_view>
 #include <vector>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/DoubleTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/Drive.h"
 
 namespace glass {
@@ -20,7 +22,8 @@
   static constexpr const char* kType = "DifferentialDrive";
 
   explicit NTDifferentialDriveModel(std::string_view path);
-  NTDifferentialDriveModel(NT_Inst instance, std::string_view path);
+  NTDifferentialDriveModel(nt::NetworkTableInstance instance,
+                           std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
@@ -35,11 +38,11 @@
   bool IsReadOnly() override { return !m_controllableValue; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_name;
-  NT_Entry m_controllable;
-  NT_Entry m_lPercent;
-  NT_Entry m_rPercent;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_name;
+  nt::BooleanSubscriber m_controllable;
+  nt::DoubleEntry m_lPercent;
+  nt::DoubleEntry m_rPercent;
 
   std::string m_nameValue;
   bool m_controllableValue = false;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
index cd3dfeb..7aa9234 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
@@ -7,11 +7,12 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
 #include "glass/hardware/DIO.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 
 namespace glass {
 
@@ -21,7 +22,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTDigitalInputModel(std::string_view path);
-  NTDigitalInputModel(NT_Inst inst, std::string_view path);
+  NTDigitalInputModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
 
@@ -42,9 +43,9 @@
   bool IsReadOnly() override { return true; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_value;
-  NT_Entry m_name;
+  nt::NetworkTableInstance m_inst;
+  nt::BooleanSubscriber m_value;
+  nt::StringSubscriber m_name;
 
   DataSource m_valueData;
   std::string m_nameValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
index 8ed1ee7..fb6a151 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
@@ -7,11 +7,12 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
 #include "glass/hardware/DIO.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 
 namespace glass {
 
@@ -21,7 +22,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTDigitalOutputModel(std::string_view path);
-  NTDigitalOutputModel(NT_Inst inst, std::string_view path);
+  NTDigitalOutputModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
 
@@ -42,10 +43,10 @@
   bool IsReadOnly() override { return !m_controllableValue; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_value;
-  NT_Entry m_name;
-  NT_Entry m_controllable;
+  nt::NetworkTableInstance m_inst;
+  nt::BooleanEntry m_value;
+  nt::StringSubscriber m_name;
+  nt::BooleanSubscriber m_controllable;
 
   DataSource m_valueData;
   std::string m_nameValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTFMS.h b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
index b19a9f0..5898080 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTFMS.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
@@ -6,10 +6,12 @@
 
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/IntegerTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/FMS.h"
 
 namespace glass {
@@ -20,7 +22,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTFMSModel(std::string_view path);
-  NTFMSModel(NT_Inst inst, std::string_view path);
+  NTFMSModel(nt::NetworkTableInstance inst, std::string_view path);
 
   DataSource* GetFmsAttachedData() override { return &m_fmsAttached; }
   DataSource* GetDsAttachedData() override { return &m_dsAttached; }
@@ -45,18 +47,18 @@
   void SetEnabled(bool val) override {}
   void SetTest(bool val) override {}
   void SetAutonomous(bool val) override {}
-  void SetGameSpecificMessage(const char* val) override {}
+  void SetGameSpecificMessage(std::string_view val) override {}
 
   void Update() override;
   bool Exists() override;
   bool IsReadOnly() override { return true; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_gameSpecificMessage;
-  NT_Entry m_alliance;
-  NT_Entry m_station;
-  NT_Entry m_controlWord;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_gameSpecificMessage;
+  nt::BooleanSubscriber m_alliance;
+  nt::IntegerSubscriber m_station;
+  nt::IntegerSubscriber m_controlWord;
 
   DataSource m_fmsAttached;
   DataSource m_dsAttached;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTField2D.h b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
index f966e0f..40265ed 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTField2D.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
@@ -10,9 +10,12 @@
 #include <utility>
 #include <vector>
 
+#include <networktables/MultiSubscriber.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/NetworkTableListener.h>
+#include <networktables/StringTopic.h>
 #include <ntcore_cpp.h>
 
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/Field2D.h"
 
 namespace glass {
@@ -23,7 +26,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTField2DModel(std::string_view path);
-  NTField2DModel(NT_Inst inst, std::string_view path);
+  NTField2DModel(nt::NetworkTableInstance inst, std::string_view path);
   ~NTField2DModel() override;
 
   const char* GetPath() const { return m_path.c_str(); }
@@ -40,9 +43,11 @@
           func) override;
 
  private:
-  NetworkTablesHelper m_nt;
   std::string m_path;
-  NT_Entry m_name;
+  nt::NetworkTableInstance m_inst;
+  nt::MultiSubscriber m_tableSub;
+  nt::StringTopic m_nameTopic;
+  nt::NetworkTableListenerPoller m_poller;
   std::string m_nameValue;
 
   class ObjectModel;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTGyro.h b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
index db303a5..dc91acc 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTGyro.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
@@ -7,11 +7,12 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/DoubleTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
 #include "glass/hardware/Gyro.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 
 namespace glass {
 class NTGyroModel : public GyroModel {
@@ -19,7 +20,7 @@
   static constexpr const char* kType = "Gyro";
 
   explicit NTGyroModel(std::string_view path);
-  NTGyroModel(NT_Inst instance, std::string_view path);
+  NTGyroModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const char* GetSimDevice() const override { return nullptr; }
@@ -32,9 +33,9 @@
   bool IsReadOnly() override { return true; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_angle;
-  NT_Entry m_name;
+  nt::NetworkTableInstance m_inst;
+  nt::DoubleSubscriber m_angle;
+  nt::StringSubscriber m_name;
 
   DataSource m_angleData;
   std::string m_nameValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
index ce7d234..38895bc 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
@@ -8,10 +8,12 @@
 #include <string_view>
 #include <vector>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/DoubleTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/Drive.h"
 
 namespace glass {
@@ -20,7 +22,7 @@
   static constexpr const char* kType = "MecanumDrive";
 
   explicit NTMecanumDriveModel(std::string_view path);
-  NTMecanumDriveModel(NT_Inst instance, std::string_view path);
+  NTMecanumDriveModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
@@ -35,13 +37,13 @@
   bool IsReadOnly() override { return !m_controllableValue; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_name;
-  NT_Entry m_controllable;
-  NT_Entry m_flPercent;
-  NT_Entry m_frPercent;
-  NT_Entry m_rlPercent;
-  NT_Entry m_rrPercent;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_name;
+  nt::BooleanSubscriber m_controllable;
+  nt::DoubleEntry m_flPercent;
+  nt::DoubleEntry m_frPercent;
+  nt::DoubleEntry m_rlPercent;
+  nt::DoubleEntry m_rrPercent;
 
   std::string m_nameValue;
   bool m_controllableValue = false;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
index 81f7df1..e8ffa9d 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
@@ -11,9 +11,10 @@
 #include <vector>
 
 #include <frc/geometry/Translation2d.h>
-#include <ntcore_cpp.h>
+#include <networktables/MultiSubscriber.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/NetworkTableListener.h>
 
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/Mechanism2D.h"
 
 namespace glass {
@@ -24,7 +25,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTMechanism2DModel(std::string_view path);
-  NTMechanism2DModel(NT_Inst inst, std::string_view path);
+  NTMechanism2DModel(nt::NetworkTableInstance inst, std::string_view path);
   ~NTMechanism2DModel() override;
 
   const char* GetPath() const { return m_path.c_str(); }
@@ -42,12 +43,13 @@
       wpi::function_ref<void(MechanismRootModel& model)> func) override;
 
  private:
-  NetworkTablesHelper m_nt;
+  nt::NetworkTableInstance m_inst;
   std::string m_path;
-
-  NT_Entry m_name;
-  NT_Entry m_dimensions;
-  NT_Entry m_bgColor;
+  nt::MultiSubscriber m_tableSub;
+  nt::Topic m_nameTopic;
+  nt::Topic m_dimensionsTopic;
+  nt::Topic m_bgColorTopic;
+  nt::NetworkTableListenerPoller m_poller;
 
   std::string m_nameValue;
   frc::Translation2d m_dimensionsValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h b/glass/src/libnt/native/include/glass/networktables/NTMotorController.h
similarity index 61%
rename from glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
rename to glass/src/libnt/native/include/glass/networktables/NTMotorController.h
index a79c266..574a55b 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTMotorController.h
@@ -7,19 +7,21 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/DoubleTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/hardware/SpeedController.h"
-#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/hardware/MotorController.h"
 
 namespace glass {
-class NTSpeedControllerModel : public SpeedControllerModel {
+class NTMotorControllerModel : public MotorControllerModel {
  public:
   static constexpr const char* kType = "Motor Controller";
 
-  explicit NTSpeedControllerModel(std::string_view path);
-  NTSpeedControllerModel(NT_Inst instance, std::string_view path);
+  explicit NTMotorControllerModel(std::string_view path);
+  NTMotorControllerModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const char* GetSimDevice() const override { return nullptr; }
@@ -32,10 +34,10 @@
   bool IsReadOnly() override { return !m_controllableValue; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_value;
-  NT_Entry m_name;
-  NT_Entry m_controllable;
+  nt::NetworkTableInstance m_inst;
+  nt::DoubleEntry m_value;
+  nt::StringSubscriber m_name;
+  nt::BooleanSubscriber m_controllable;
 
   DataSource m_valueData;
   std::string m_nameValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
index b975641..f901f72 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
@@ -7,10 +7,12 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/BooleanTopic.h>
+#include <networktables/DoubleTopic.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/PIDController.h"
 
 namespace glass {
@@ -19,7 +21,7 @@
   static constexpr const char* kType = "PIDController";
 
   explicit NTPIDControllerModel(std::string_view path);
-  NTPIDControllerModel(NT_Inst instance, std::string_view path);
+  NTPIDControllerModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
 
@@ -38,13 +40,13 @@
   bool IsReadOnly() override { return !m_controllableValue; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_name;
-  NT_Entry m_controllable;
-  NT_Entry m_p;
-  NT_Entry m_i;
-  NT_Entry m_d;
-  NT_Entry m_setpoint;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_name;
+  nt::BooleanSubscriber m_controllable;
+  nt::DoubleEntry m_p;
+  nt::DoubleEntry m_i;
+  nt::DoubleEntry m_d;
+  nt::DoubleEntry m_setpoint;
 
   DataSource m_pData;
   DataSource m_iData;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
index 2d806c9..d770a74 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
@@ -7,9 +7,10 @@
 #include <string>
 #include <vector>
 
-#include <ntcore_cpp.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringArrayTopic.h>
+#include <networktables/StringTopic.h>
 
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/StringChooser.h"
 
 namespace glass {
@@ -20,7 +21,7 @@
 
   // path is to the table containing ".type", excluding the trailing /
   explicit NTStringChooserModel(std::string_view path);
-  NTStringChooserModel(NT_Inst inst, std::string_view path);
+  NTStringChooserModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const std::string& GetDefault() override { return m_defaultValue; }
   const std::string& GetSelected() override { return m_selectedValue; }
@@ -29,21 +30,18 @@
     return m_optionsValue;
   }
 
-  void SetDefault(std::string_view val) override;
   void SetSelected(std::string_view val) override;
-  void SetActive(std::string_view val) override;
-  void SetOptions(wpi::span<const std::string> val) override;
 
   void Update() override;
   bool Exists() override;
   bool IsReadOnly() override { return false; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_default;
-  NT_Entry m_selected;
-  NT_Entry m_active;
-  NT_Entry m_options;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_default;
+  nt::StringEntry m_selected;
+  nt::StringSubscriber m_active;
+  nt::StringArraySubscriber m_options;
 
   std::string m_defaultValue;
   std::string m_selectedValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
index c5862cf..6c1ee3a 100644
--- a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
+++ b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
@@ -7,10 +7,10 @@
 #include <string>
 #include <string_view>
 
-#include <ntcore_cpp.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/StringTopic.h>
 
 #include "glass/DataSource.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 #include "glass/other/Subsystem.h"
 
 namespace glass {
@@ -19,7 +19,7 @@
   static constexpr const char* kType = "Subsystem";
 
   explicit NTSubsystemModel(std::string_view path);
-  NTSubsystemModel(NT_Inst instance, std::string_view path);
+  NTSubsystemModel(nt::NetworkTableInstance inst, std::string_view path);
 
   const char* GetName() const override { return m_nameValue.c_str(); }
   const char* GetDefaultCommand() const override {
@@ -34,10 +34,10 @@
   bool IsReadOnly() override { return true; }
 
  private:
-  NetworkTablesHelper m_nt;
-  NT_Entry m_name;
-  NT_Entry m_defaultCommand;
-  NT_Entry m_currentCommand;
+  nt::NetworkTableInstance m_inst;
+  nt::StringSubscriber m_name;
+  nt::StringSubscriber m_defaultCommand;
+  nt::StringSubscriber m_currentCommand;
 
   std::string m_nameValue;
   std::string m_defaultCommandValue;
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
index 8538eaa..a7aa514 100644
--- a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
@@ -4,13 +4,20 @@
 
 #pragma once
 
+#include <functional>
+#include <map>
 #include <memory>
+#include <span>
 #include <string>
 #include <string_view>
+#include <utility>
 #include <vector>
 
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/NetworkTableListener.h>
 #include <ntcore_cpp.h>
 #include <wpi/DenseMap.h>
+#include <wpi/json.h>
 
 #include "glass/Model.h"
 #include "glass/View.h"
@@ -21,28 +28,64 @@
 
 class NetworkTablesModel : public Model {
  public:
-  struct Entry {
-    explicit Entry(nt::EntryNotification&& event);
+  struct EntryValueTreeNode;
 
-    void UpdateValue();
+  struct ValueSource {
+    void UpdateFromValue(nt::Value&& v, std::string_view name,
+                         std::string_view typeStr);
 
-    /** Entry handle. */
-    NT_Entry entry;
-
-    /** Entry name. */
-    std::string name;
-
-    /** The value. */
-    std::shared_ptr<nt::Value> value;
-
-    /** Flags. */
-    unsigned int flags = 0;
+    /** The latest value. */
+    nt::Value value;
 
     /** String representation of the value (for arrays / complex values). */
     std::string valueStr;
 
     /** Data source (for numeric values). */
     std::unique_ptr<DataSource> source;
+
+    /** Children of this node, sorted by name/index */
+    std::vector<EntryValueTreeNode> valueChildren;
+
+    /** Whether or not the children represent a map */
+    bool valueChildrenMap = false;
+  };
+
+  struct EntryValueTreeNode : public ValueSource {
+    /** Short name (e.g. of just this node) */
+    std::string name;
+
+    /** Full path */
+    std::string path;
+  };
+
+  struct Entry : public ValueSource {
+    Entry() = default;
+    Entry(const Entry&) = delete;
+    Entry& operator=(const Entry&) = delete;
+    ~Entry();
+
+    void UpdateTopic(nt::Event&& event) {
+      if (std::holds_alternative<nt::TopicInfo>(event.data)) {
+        UpdateInfo(std::get<nt::TopicInfo>(std::move(event.data)));
+      }
+    }
+    void UpdateInfo(nt::TopicInfo&& info_);
+
+    /** Topic information. */
+    nt::TopicInfo info;
+
+    /** JSON representation of the topic properties. */
+    wpi::json properties;
+
+    /** Specific common property flags. */
+    bool persistent{false};
+    bool retained{false};
+
+    /** Publisher (created when the value changes). */
+    NT_Publisher publisher{0};
+
+    std::vector<nt::meta::TopicPublisher> publishers;
+    std::vector<nt::meta::TopicSubscriber> subscribers;
   };
 
   struct TreeNode {
@@ -64,41 +107,89 @@
     std::vector<TreeNode> children;
   };
 
+  struct Client : public nt::meta::Client {
+    Client() = default;
+    /*implicit*/ Client(nt::meta::Client&& oth)  // NOLINT
+        : nt::meta::Client{std::move(oth)} {}
+
+    struct Subscriber : public nt::meta::ClientSubscriber {
+      /*implicit*/ Subscriber(nt::meta::ClientSubscriber&& oth);  // NOLINT
+      std::string topicsStr;
+    };
+
+    std::vector<nt::meta::ClientPublisher> publishers;
+    std::vector<Subscriber> subscribers;
+
+    void UpdatePublishers(std::span<const uint8_t> data);
+    void UpdateSubscribers(std::span<const uint8_t> data);
+  };
+
   NetworkTablesModel();
-  explicit NetworkTablesModel(NT_Inst inst);
-  ~NetworkTablesModel() override;
+  explicit NetworkTablesModel(nt::NetworkTableInstance inst);
 
   void Update() override;
   bool Exists() override;
 
-  NT_Inst GetInstance() { return m_inst; }
-  const std::vector<Entry*>& GetEntries() { return m_sortedEntries; }
-  const std::vector<TreeNode>& GetTreeRoot() { return m_root; }
+  nt::NetworkTableInstance GetInstance() { return m_inst; }
+  const std::vector<Entry*>& GetEntries() const { return m_sortedEntries; }
+  const std::vector<TreeNode>& GetTreeRoot() const { return m_root; }
+  const std::vector<TreeNode>& GetPersistentTreeRoot() const {
+    return m_persistentRoot;
+  }
+  const std::vector<TreeNode>& GetRetainedTreeRoot() const {
+    return m_retainedRoot;
+  }
+  const std::vector<TreeNode>& GetTransitoryTreeRoot() const {
+    return m_transitoryRoot;
+  }
+  const std::map<std::string, Client, std::less<>>& GetClients() const {
+    return m_clients;
+  }
+  const Client& GetServer() const { return m_server; }
+  Entry* GetEntry(std::string_view name);
+  Entry* AddEntry(NT_Topic topic);
 
  private:
-  NT_Inst m_inst;
-  NT_EntryListenerPoller m_poller;
-  wpi::DenseMap<NT_Entry, std::unique_ptr<Entry>> m_entries;
+  void RebuildTree();
+  void RebuildTreeImpl(std::vector<TreeNode>* tree, int category);
+  void UpdateClients(std::span<const uint8_t> data);
+
+  nt::NetworkTableInstance m_inst;
+  nt::NetworkTableListenerPoller m_poller;
+  wpi::DenseMap<NT_Topic, std::unique_ptr<Entry>> m_entries;
 
   // sorted by name
   std::vector<Entry*> m_sortedEntries;
 
   std::vector<TreeNode> m_root;
+  std::vector<TreeNode> m_persistentRoot;
+  std::vector<TreeNode> m_retainedRoot;
+  std::vector<TreeNode> m_transitoryRoot;
+
+  std::map<std::string, Client, std::less<>> m_clients;
+  Client m_server;
 };
 
 using NetworkTablesFlags = int;
 
+static constexpr const int kNetworkTablesFlags_PrecisionBitShift = 9;
+
 enum NetworkTablesFlags_ {
   NetworkTablesFlags_TreeView = 1 << 0,
-  NetworkTablesFlags_ReadOnly = 1 << 1,
-  NetworkTablesFlags_ShowConnections = 1 << 2,
-  NetworkTablesFlags_ShowFlags = 1 << 3,
-  NetworkTablesFlags_ShowTimestamp = 1 << 4,
-  NetworkTablesFlags_CreateNoncanonicalKeys = 1 << 5,
-  NetworkTablesFlags_Default = 1 & ~NetworkTablesFlags_ReadOnly &
-                               ~NetworkTablesFlags_CreateNoncanonicalKeys
+  NetworkTablesFlags_CombinedView = 1 << 1,
+  NetworkTablesFlags_ReadOnly = 1 << 2,
+  NetworkTablesFlags_ShowSpecial = 1 << 3,
+  NetworkTablesFlags_ShowProperties = 1 << 4,
+  NetworkTablesFlags_ShowTimestamp = 1 << 5,
+  NetworkTablesFlags_ShowServerTimestamp = 1 << 6,
+  NetworkTablesFlags_CreateNoncanonicalKeys = 1 << 7,
+  NetworkTablesFlags_Precision = 0xff << kNetworkTablesFlags_PrecisionBitShift,
+  NetworkTablesFlags_Default = NetworkTablesFlags_TreeView |
+                               (6 << kNetworkTablesFlags_PrecisionBitShift),
 };
 
+void DisplayNetworkTablesInfo(NetworkTablesModel* model);
+
 void DisplayNetworkTables(
     NetworkTablesModel* model,
     NetworkTablesFlags flags = NetworkTablesFlags_Default);
@@ -116,10 +207,13 @@
 
  private:
   bool* m_pTreeView = nullptr;
-  bool* m_pShowConnections = nullptr;
-  bool* m_pShowFlags = nullptr;
+  bool* m_pCombinedView = nullptr;
+  bool* m_pShowSpecial = nullptr;
+  bool* m_pShowProperties = nullptr;
   bool* m_pShowTimestamp = nullptr;
+  bool* m_pShowServerTimestamp = nullptr;
   bool* m_pCreateNoncanonicalKeys = nullptr;
+  int* m_pPrecision = nullptr;
   NetworkTablesFlags m_defaultFlags;  // NOLINT
   NetworkTablesFlags m_flags;         // NOLINT
 };
@@ -132,6 +226,8 @@
       : m_model{model}, m_flags{defaultFlags} {}
 
   void Display() override;
+  void Settings() override;
+  bool HasSettings() override;
 
  private:
   NetworkTablesModel* m_model;
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
deleted file mode 100644
index aba3252..0000000
--- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#pragma once
-
-#include <string_view>
-#include <vector>
-
-#include <ntcore_cpp.h>
-
-namespace glass {
-
-class NetworkTablesHelper {
- public:
-  explicit NetworkTablesHelper(NT_Inst inst);
-  ~NetworkTablesHelper();
-
-  NetworkTablesHelper(const NetworkTablesHelper&) = delete;
-  NetworkTablesHelper& operator=(const NetworkTablesHelper&) = delete;
-
-  NT_Inst GetInstance() const { return m_inst; }
-  NT_EntryListenerPoller GetPoller() const { return m_poller; }
-
-  NT_Entry GetEntry(std::string_view name) const {
-    return nt::GetEntry(m_inst, name);
-  }
-
-  static constexpr int kDefaultListenerFlags =
-      NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
-      NT_NOTIFY_IMMEDIATE;
-
-  NT_EntryListener AddListener(NT_Entry entry,
-                               unsigned int flags = kDefaultListenerFlags) {
-    return nt::AddPolledEntryListener(m_poller, entry, flags);
-  }
-
-  NT_EntryListener AddListener(std::string_view prefix,
-                               unsigned int flags = kDefaultListenerFlags) {
-    return nt::AddPolledEntryListener(m_poller, prefix, flags);
-  }
-
-  std::vector<nt::EntryNotification> PollListener() {
-    bool timedOut = false;
-    return nt::PollEntryListener(m_poller, 0, &timedOut);
-  }
-
-  bool IsConnected() const;
-
- private:
-  NT_Inst m_inst;
-  NT_EntryListenerPoller m_poller;
-};
-
-}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
index a8f0f9b..cba34cc 100644
--- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
@@ -9,13 +9,15 @@
 #include <string_view>
 #include <vector>
 
-#include <ntcore_c.h>
-#include <ntcore_cpp.h>
+#include <networktables/NetworkTableInstance.h>
+#include <networktables/NetworkTableListener.h>
+#include <networktables/StringTopic.h>
+#include <networktables/Topic.h>
+#include <wpi/DenseMap.h>
 #include <wpi/StringMap.h>
 
 #include "glass/Model.h"
 #include "glass/Provider.h"
-#include "glass/networktables/NetworkTablesHelper.h"
 
 namespace glass {
 
@@ -23,9 +25,10 @@
 
 namespace detail {
 struct NTProviderFunctions {
-  using Exists = std::function<bool(NT_Inst inst, const char* path)>;
-  using CreateModel =
-      std::function<std::unique_ptr<Model>(NT_Inst inst, const char* path)>;
+  using Exists =
+      std::function<bool(nt::NetworkTableInstance inst, const char* path)>;
+  using CreateModel = std::function<std::unique_ptr<Model>(
+      nt::NetworkTableInstance inst, const char* path)>;
   using ViewExists = std::function<bool(Model*, const char* path)>;
   using CreateView =
       std::function<std::unique_ptr<View>(Window*, Model*, const char* path)>;
@@ -41,14 +44,14 @@
   using Provider::CreateViewFunc;
 
   explicit NetworkTablesProvider(Storage& storage);
-  NetworkTablesProvider(Storage& storage, NT_Inst inst);
+  NetworkTablesProvider(Storage& storage, nt::NetworkTableInstance inst);
 
   /**
    * Get the NetworkTables instance being used for this provider.
    *
    * @return NetworkTables instance
    */
-  NT_Inst GetInstance() const { return m_nt.GetInstance(); }
+  nt::NetworkTableInstance GetInstance() const { return m_inst; }
 
   /**
    * Perform global initialization.  This should be called prior to
@@ -74,8 +77,9 @@
  private:
   void Update() override;
 
-  NetworkTablesHelper m_nt;
-  NT_EntryListener m_listener{0};
+  nt::NetworkTableInstance m_inst;
+  nt::NetworkTableListenerPoller m_poller;
+  NT_Listener m_listener{0};
 
   // cached mapping from table name to type string
   Storage& m_typeCache;
@@ -88,17 +92,25 @@
   // mapping from .type string to model/view creators
   wpi::StringMap<Builder> m_typeMap;
 
+  struct SubListener {
+    nt::StringSubscriber subscriber;
+    NT_Listener listener;
+  };
+
+  // mapping from .type topic to subscriber/listener
+  wpi::DenseMap<NT_Topic, SubListener> m_topicMap;
+
   struct Entry : public ModelEntry {
-    Entry(NT_Entry typeEntry, std::string_view name, const Builder& builder)
-        : ModelEntry{name, [](NT_Inst, const char*) { return true; },
+    Entry(nt::Topic typeTopic, std::string_view name, const Builder& builder)
+        : ModelEntry{name, [](auto, const char*) { return true; },
                      builder.createModel},
-          typeEntry{typeEntry} {}
-    NT_Entry typeEntry;
+          typeTopic{typeTopic} {}
+    nt::Topic typeTopic;
   };
 
   void Show(ViewEntry* entry, Window* window) override;
 
-  ViewEntry* GetOrCreateView(const Builder& builder, NT_Entry typeEntry,
+  ViewEntry* GetOrCreateView(const Builder& builder, nt::Topic typeTopic,
                              std::string_view name);
 };
 
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
index a1a1c93..5f2ea19 100644
--- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
@@ -22,7 +22,7 @@
 
 class NetworkTablesSettings {
  public:
-  explicit NetworkTablesSettings(Storage& storage,
+  explicit NetworkTablesSettings(std::string_view clientName, Storage& storage,
                                  NT_Inst inst = nt::GetDefaultInstance());
 
   /**
@@ -37,9 +37,12 @@
   bool m_restart = true;
   bool m_serverOption = true;
   EnumSetting m_mode;
-  std::string& m_iniName;
+  std::string& m_persistentFilename;
   std::string& m_serverTeam;
   std::string& m_listenAddress;
+  std::string& m_clientName;
+  int& m_port3;
+  int& m_port4;
   bool& m_dsClient;
 
   class Thread : public wpi::SafeThread {
@@ -54,6 +57,9 @@
     std::string m_iniName;
     std::string m_serverTeam;
     std::string m_listenAddress;
+    std::string m_clientName;
+    int m_port3;
+    int m_port4;
     bool m_dsClient;
   };
   wpi::SafeThreadOwner<Thread> m_thread;