Squashed 'third_party/allwpilib/' changes from 66b57f032..e473a00f9

e473a00f9 [wpiutil] Base64: Add unsigned span/vector variants (#3702)
52f2d580e [wpiutil] raw_uv_ostream: Add reset() (#3701)
d7b1e3576 [wpiutil] WebSocket: move std::function (#3700)
93799fbe9 [examples] Fix description of TrapezoidProfileSubsystem (#3699)
b84644740 [wpimath] Document pose estimator states, inputs, and outputs (#3698)
2dc35c139 [wpimath] Fix classpaths for JNI class loads (#3697)
2cb171f6f [docs] Set Doxygen extract_all to true and fix Doxygen failures (#3695)
a939cd9c8 [wpimath] Print uncontrollable/unobservable models in LQR and KF (#3694)
d5270d113 [wpimath] Clean up C++ StateSpaceUtil tests (#3692)
b20903960 [wpimath] Remove redundant discretization tests from StateSpaceUtilTest (#3689)
c0cb545b4 [wpilibc] Add deprecated Doxygen attribute to SpeedController (#3691)
35c9f66a7 [wpilib] Rename PneumaticsHub to PneumaticHub (#3686)
796d03d10 [wpiutil] Remove unused LLVM header (#3688)
8723caf78 [wpilibj] Make Java TrapezoidProfile.Constraints an immutable class (#3687)
187f50a34 [wpimath] Catch incorrect parameters to state-space models earlier (#3680)
8d04606c4 Replace instances of frc-characterization with SysId (NFC) (#3681)
b82d4f6e5 [hal, cscore, ntcore] Use WPI common handle type base
87e34967e [wpiutil] Add synchronization primitives
e32499c54 [wpiutil] Add ParallelTcpConnector (#3655)
aa0b49228 [wpilib] Remove redundant "quick turn" docs for curvature drive (NFC) (#3674)
57301a7f9 [hal] REVPH: Start closed-loop compressor control on init (#3673)
d1842ea8f [wpilib] Improve interrupt docs (NFC) (#3679)
558151061 [wpiutil] Add DsClient (#3654)
181723e57 Replace `.to<double>()` and `.template to<double>()` with `.value()` (#3667)
6bc1db44b [commands] Add pointer overload of AddRequirements (artf6003) (#3669)
737b57ed5 [wpimath] Update to drake v0.35.0 (#3665)
4d287d1ae [build] Upgrade WPIJREArtifact to JRE 2022-11.0.12u5 (#3666)
f26eb5ada [hal] Fix another typo (get -> gets) (NFC) (#3663)
94ed275ba [hal] Fix misspelling (numer -> number) (NFC) (#3662)
ac2f44da3 [wpiutil] uv: use move for std::function (#3653)
75fa1fbfb [wpiutil] json::serializer: Optimize construction (#3647)
5e689faea [wpiutil] Import MessagePack implementation (mpack) (#3650)
649a50b40 [wpiutil] Add LEB128 byte-by-byte reader (#3652)
e94397a97 [wpiutil] Move json_serializer.h to public headers (#3646)
4ec58724d [wpiutil] uv::Tcp: Clarify SetNoDelay documentation (#3649)
8cb294aa4 [wpiutil] WebSocket: Make Shutdown() public (#3651)
2b3a9a52b [wpiutil] json: Fix map iterator key() for std::string_view (#3645)
138cbb94b [wpiutil] uv::Async: Add direct call for no-parameter specialization (#3648)
e56d6dea8 [ci] Update testbench pool image to ubuntu-latest (#3643)
43f30e44e [build] Enable comments in doxygen source files (#3644)
9e6db17ef [build] Enable doxygen preprocessor expansion of WPI_DEPRECATED (#3642)
0e631ad2f Add WPILib version to issue template (#3641)
6229d8d2f [build] Docs: set case_sense_names to false (#3392)
4647d09b5 [docs] Fix Doxygen warnings, add CI docs lint job (#3639)
4ad3a5402 [hal] Fix PWM allocation channel (#3637)
05e5feac4 [docs] Fix brownout docs (NFC) (#3638)
67df469c5 [examples] Remove old command-based templates and examples (#3263)
689e9ccfb [hal, wpilib] Add brownout voltage configuration (#3632)
9cd4bc407 [docs] Add useLocal to avoid using installer artifacts (#3634)
61996c2bb [cscore] Fix Java direct callback notifications (#3631)
6d3dd99eb [build] Update to newest native-utils (#3633)
f0b484892 [wpiutil] Fix StringMap iterator equality check (#3629)
8352cbb7a Update development build instructions for 2022 (#3616)
6da08b71d [examples] Fix Intermediate Vision Java Example description (#3628)
5d99059bf [wpiutil] Remove optional.h (#3627)
fa41b106a [glass, wpiutil] Add missing format args (#3626)
4e3fd7d42 [build] Enable Zc:__cplusplus for Windows (#3625)
791d8354d [build] Suppress deprecation/removal warnings for old commands (#3618)
10f19e6fc [hal, wpilib] Add REV PneumaticsHub (#3600)
4c61a1305 [ntcore] Revert to per-element copy for toNative() (#3621)
7b3f62244 [wpiutil] SendableRegistry: Print exception stacktrace (#3620)
d347928e4 [hal] Use better error for when console out is enabled while attempting to use onboard serial port (#3622)
cc31079a1 [hal] Use setcap instead of setuid for setting thread priorities (#3613)
4676648b7 [wpimath] Upgrade to Drake v0.34.0 (#3607)
c7594c911 [build] Allow building wpilibc in cmake without cscore and opencv (#3605)
173cb7359 [wpilib] Add TimesliceRobot (#3502)
af295879f [hal] Set error status for I2C port out of range (#3603)
95dd20a15 [build] Enable spotbugs (#3601)
b65fce86b [wpilib] Remove Timer lock in wpilibj and update docs (#3602)
3b8d3bbcb Remove unused and add missing deprecated.h includes (#3599)
f9e976467 [examples] Rename DriveTrain classes to Drivetrain (#3594)
118a27be2 [wpilib] Add Timer tests (#3595)
59c89428e [wpilib] Deprecate Timer::HasPeriodPassed() (#3598)
202ca5e78 Force C++17 in .clang-format (#3597)
d6f185d8e Rename tests for consistency (#3592)
54ca474db [ci] Enable asan and tsan in CI for tests that pass (#3591)
1ca383b23 Add Debouncer (#3590)
179fde3a7 [build] Update to 2022 native utils and gradle 7 (#3588)
50198ffcf [examples] Add Mechanism2d visualization to Elevator Sim (#3587)
a446c2559 [examples] Synchronize C++ and Java Mechanism2d examples (#3589)
a7fb83103 [ci] clang-tidy: Generate compilation commands DB with Gradle (#3585)
4f5e0c9f8 [examples] Update ArmSimulation example to use Mechanism2d (#3572)
8164b91dc [CI] Print CMake test output on failure (#3583)
4d5fca27e [wpilib] Impove Mechanism2D documentation (NFC) (#3584)
fe59e4b9f Make C++ test names more consistent (#3586)
5c8868549 [wpilibc] Fix C++ MechanisimRoot2D to use same NT entries as Java/Glass (#3582)
9359431ba [wpimath] Clean up Eigen usage
72716f51c [wpimath] Upgrade to Eigen 3.4
382deef75 [wpimath] Explicitly export wpimath symbols
161e21173 [ntcore] Match standard handle layout, only allow 16 instances (#3577)
263a24811 [wpimath] Use jinja for codegen (#3574)
725251d29 [wpilib] Increase tolerances of DifferentialDriveSimTest (#3581)
4dff87301 [wpimath] Make LinearFilter::Factorial() constexpr (#3579)
60ede67ab [hal, wpilib] Switch PCM to be a single object that is allowed to be duplicated (#3475)
906bfc846 [build] Add CMake build support for sanitizers (#3576)
0d4f08ad9 [hal] Simplify string copy of joystick name (#3575)
a52bf87b7 [wpiutil] Add Java function package (#3570)
40c7645d6 [wpiutil] UidVector: Return old object from erase() (#3571)
5b886a23f [wpiutil] jni_util: Add size, operator[] to JArrayRef (#3569)
65797caa7 [sim] Fix halsim_ds_socket stringop overflow warning from GCC 10 (#3568)
66abb3988 [hal] Update runtime enum to allow selecting roborio 2 (#3565)
95a12e0ee [hal] UidSetter: Don't revert euid if its already current value (#3566)
27951442b [wpimath] Use external Eigen headers only (#3564)
c42e053ae [docs] Update to doxygen 1.9.2 (#3562)
e7048c8c8 [docs] Disable doxygen linking for common words that are also classes (#3563)
d8e0b6c97 [wpilibj] Fix java async interrupts (#3559)
5e6c34c61 Update to 2022 roborio image (#3537)
828f073eb [wpiutil] Fix uv::Buffer memory leaks caught by asan (#3555)
2dd5701ac [cscore] Fix mutex use-after-free in cscore test (#3557)
531439198 [ntcore] Fix NetworkTables memory leaks caught by asan (#3556)
3d9a4d585 [wpilibc] Fix AnalogTriggerOutput memory leak reported by asan (#3554)
54eda5928 [wpiutil] Ignore ubsan vptr upcast warning in SendableHelper moves (#3553)
5a4f75c9f [wpilib] Replace Speed controller comments with motor controller (NFC) (#3551)
7810f665f [wpiutil] Fix bug in uleb128 (#3540)
697e2dd33 [wpilib] Fix errant jaguar reference in comments (NFC) (#3550)
936c64ff5 [docs] Enable -linksource for javadocs (#3549)
1ea654954 [build] Upgrade CMake build to googletest 1.11.0 (#3548)
32d9949e4 [wpimath] Move controller tests to wpimath (#3541)
01ba56a8a [hal] Replace strncpy with memcpy (#3539)
e109c4251 [build] Rename makeSim flag to forceGazebo to better describe what it does (#3535)
e4c709164 [docs] Use a doxygen theme and add logo (#3533)
960b6e589 [wpimath] Fix Javadoc warning (#3532)
82eef8d5e [hal] Remove over current fault HAL functions from REV PDH (#3526)
aa3848b2c [wpimath] Move RobotDriveBase::ApplyDeadband() to MathUtil (#3529)
3b5d0d141 [wpimath] Add LinearFilter::BackwardFiniteDifference() (#3528)
c8fc715fe [wpimath] Upgrade drake files to v0.33.0 (#3531)
e5fe3a8e1 [build] Treat javadoc warnings as errors in CI and fix warnings (#3530)
e0c6cd3dc [wpimath] Add an operator for composing two Transform2ds (#3527)
2edd510ab [sim] Add sim wrappers for sensors that use SimDevice (#3517)
2b3e2ebc1 [hal] Fix HAL Notifier thread priority setting (#3522)
ab4cb5932 [gitignore] Update gitignore to ignore bazel / clion files (#3524)
57c8615af [build] Generate spotless patch on failure (#3523)
b90317321 Replace std::cout and std::cerr with fmt::print() (#3519)
10cc8b89c [hal] [wpilib] Add initial support for the REV PDH (#3503)
5d9ae3cdb [hal] Set HAL Notifier thread as RT by default (#3482)
192d251ee [wpilibcIntegrationTests] Properly disable DMA integration tests (#3514)
031962608 [wpilib] Add PS4Controller, remove Hand from GenericHID/XboxController (#3345)
25f6f478a [wpilib] Rename DriverStation::IsOperatorControl() to IsTeleop() (#3505)
e80f09f84 [wpilibj] Add unit tests (#3501)
c159f91f0 [wpilib] Only read DS control word once in IterativeRobotBase (#3504)
eb790a74d Add rio development docs documenting myRobot deploy tasks (#3508)
e47451f5a [wpimath] Replace auto with Eigen types (#3511)
252b8c83b Remove Java formatting from build task in CI (#3507)
09666ff29 Shorten Gazebo CI build (#3506)
baf2e501d Update myRobot to use 2021 java (#3509)
5ac60f0a2 [wpilib] Remove IterativeRobotBase mode init prints (#3500)
fb2ee8ec3 [wpilib] Add TimedRobot functions for running code on mode exit (#3499)
94e0db796 [wpilibc] Add more unit tests (#3494)
b25324695 [wpilibj] Add units to parameter names (NFC) (#3497)
1ac73a247 [hal] Rename PowerDistributionPanel to PowerDistribution (#3466)
2014115bc [examples] frisbeebot: Fix typo and reflow comments (NFC) (#3498)
4a944dc39 [examples] Consistently use 0 for controller port (#3496)
3838cc4ec Use unicode characters in docs equations (#3487)
85748f2e6 [examples] Add C++ TankDrive example (#3493)
d7b8aa56d [wpilibj] Rename DriverStation In[Mode] functions to follow style guide (#3488)
16e096cf8 [build] Fix CMake Windows CI (#3490)
50af74c38 [wpimath] Clean up NumericalIntegration and add Discretization tests (#3489)
bfc209b12 Automate fmt update (#3486)
e7f9331e4 [build] Update to Doxygen 1.9.1 (#3008)
ab8e8aa2a [wpimath] Update drake with upstream (#3484)
1ef826d1d [wpimath] Fix IOException path in WPIMath JNI (#3485)
52bddaa97 [wpimath] Disable iostream support for units and enable fmtlib (#3481)
e4dc3908b [wpiutil] Upgrade to fmtlib 8.0.1 (#3483)
1daadb812 [wpimath] Implement Dormand-Prince integration method (#3476)
9c2723391 [cscore] Add [[nodiscard]] to GrabFrame functions (#3479)
7a8796414 [wpilib] Add Notifier integration tests (#3480)
f8f13c536 [wpilibcExamples] Prefix decimal numbers with 0 (#3478)
1adb69c0f [ntcore] Use "NetworkTables" instead of "Network Tables" in NT specs (#3477)
5f5830b96 Upload wpiformat diff if one exists (#3474)
9fb4f35bb [wpimath] Add tests for DARE overload with Q, R, and N matrices (#3472)
c002e6f92 Run wpiformat (#3473)
c154e5262 [wpilib] Make solenoids exclusive use, PCM act like old sendable compressor (#3464)
6ddef1cca [hal] JNI setDIO: use a boolean and not a short (#3469)
9d68d9582 Remove extra newlines after open curly braces (NFC) (#3471)
a4233e1a1 [wpimath] Add script for updating Drake (#3470)
39373c6d2 Update README.md for new GCC version requirement (#3467)
d29acc90a [wpigui] Add option to reset UI on exit (#3463)
a371235b0 [ntcore] Fix dangling pointer in logger (#3465)
53b4891a5 [wpilibcintegrationtests] Fix deprecated Preferences usage (#3461)
646ded912 [wpimath] Remove incorrect discretization in pose estimators (#3460)
ea0b8f48e Fix some deprecation warnings due to fmtlib upgrade (#3459)
2067d7e30 [wpilibjexamples] Add wpimathjni, wpiutiljni to library path (#3455)
866571ab4 [wpiutil] Upgrade to fmtlib 8.0.0 (#3457)
4e1fa0308 [build] Skip PDB copy on windows build servers (#3458)
b45572167 [build] Change CI back to 18.04 docker images (#3456)
57a160f1b [wpilibc] Fix LiveWindow deprecation warning in RobotBase skeleton template (#3454)
29ae8640d [HLT] Implement duty cycle cross connect tests (#3453)
ee6377e54 [HLT] Add relay and analog cross connects (#3452)
b0f1ae7ea [build] CMake: Build the HAL even if WITH_CSCORE=OFF (#3449)
7aae2b72d Replace std::to_string() with fmt::format() (#3451)
73fcbbd74 [HLT] Add relay digital cross connect tests (#3450)
e7bedde83 [HLT] Add PWM tests that use DMA as the back end (#3447)
7253edb1e [wpilibc] Timer: Fix deprecated warning (#3446)
efa28125c [wpilibc] Add message to RobotBase on how to read stacktrace (#3444)
9832fcfe1 [hal] Fix DIO direction getter (#3445)
49c71f9f2 [wpilibj] Clarify robot quit message (#3364)
791770cf6 [wpimath] Move controller from wpilibj to wpimath (#3439)
9ce9188ff [wpimath] Add ReportWarning to MathShared (#3441)
362066a9b [wpilib] Deprecate getInstance() in favor of static functions (#3440)
26ff9371d Initial commit of cross connect integration test project (#3434)
4a36f86c8 [hal] Add support for DMA to Java (#3158)
85144e47f [commands] Unbreak build (#3438)
b417d961e Split Sendable into NT and non-NT portions (#3432)
ef4ea84cb [commands] Change grouping decorator impl to flatten nested group structures (#3335)
b422665a3 [examples] Invert right side of drive subsystems (#3437)
186dadf14 [hal] Error if attempting to set DIO output on an input port (#3436)
04e64db94 Remove redundant C++ lambda parentheses (NFC) (#3433)
f60994ad2 [wpiutil] Rename Java package to edu.wpi.first.util (#3431)
cfa1ca96f [wpilibc] Make ShuffleboardValue non-copyable (#3430)
4d9ff7643 Fix documentation warnings generated by JavaDoc (NFC) (#3428)
9e1b7e046 [build] Fix clang-tidy and clang-format (#3429)
a77c6ff3a [build] Upgrade clang-format and clang-tidy (NFC) (#3422)
099fde97d [wpilib] Improve PDP comments (NFC) (#3427)
f8fc2463e [wpilibc, wpiutil] Clean up includes (NFC) (#3426)
e246b7884 [wpimath] Clean up member initialization in feedforward classes (#3425)
c1e128bd5 Disable frivolous PMD warnings and enable PMD in ntcore (#3419)
8284075ee Run "Lint and Format" CI job on push as well as pull request (#3412)
f7db09a12 [wpimath] Move C++ filters into filter folder to match Java (#3417)
f9c3d54bd [wpimath] Reset error covariance in pose estimator ResetPosition() (#3418)
0773f4033 [hal] Ensure HAL status variables are initialized to zero (#3421)
d068fb321 [build] Upgrade CI to use 20.04 docker images (#3420)
8d054c940 [wpiutil] Remove STLExtras.h
80f1d7921 [wpiutil] Split function_ref to a separate header
64f541325 Use wpi::span instead of wpi::ArrayRef across all libraries (#3414)
2abbbd9e7 [build] clang-tidy: Remove bugprone-exception-escape (#3415)
a5c471af7 [wpimath] Add LQR template specialization for 2x2 system
edd2f0232 [wpimath] Add DARE solver for Q, R, and N with LQR ctor overloads
b2c3b2dd8 Use std::string_view and fmtlib across all libraries (#3402)
4f1cecb8e [wpiutil] Remove Path.h (#3413)
b336eac34 [build] Publish halsim_ws_core to Maven
2a09f6fa4 [build] Also build sim modules as static libraries
0e702eb79 [hal] Add a unified PCM object (#3331)
dea841103 [wpimath] Add fmtlib formatter overloads for Eigen::Matrix and units (#3409)
82856cf81 [wpiutil] Improve wpi::circular_buffer iterators (#3410)
8aecda03e [wpilib] Fix a documentation typo (#3408)
5c817082a [wpilib] Remove InterruptableSensorBase and replace with interrupt classes (#2410)
15c521a7f [wpimath] Fix drivetrain system identification (#3406)
989de4a1b [build] Force all linker warnings to be fatal for rio builds (#3407)
d9eeb45b0 [wpilibc] Add units to Ultrasonic class API (#3403)
fe570e000 [wpiutil] Replace llvm filesystem with C++17 filesystem (#3401)
01dc0249d [wpimath] Move SlewRateLimiter from wpilib to wpimath (#3399)
93523d572 [wpilibc] Clean up integration tests (#3400)
4f7a4464d [wpiutil] Rewrite StringExtras for std::string_view (#3394)
e09293a15 [wpilibc] Transition C++ classes to units::second_t (#3396)
827b17a52 [build] Create run tasks for Glass and OutlineViewer (#3397)
a61037996 [wpiutil] Avoid MSVC warning on span include (#3393)
4e2c3051b [wpilibc] Use std::string_view instead of Twine (#3380)
50915cb7e [wpilibc] MotorSafety::GetDescription(): Return std::string (#3390)
f4e2d26d5 [wpilibc] Move NullDeleter from frc/Base.h to wpi/NullDeleter.h (#3387)
cb0051ae6 [wpilibc] SimDeviceSim: use fmtlib (#3389)
a238cec12 [wpiutil] Deprecate wpi::math constants in favor of wpi::numbers (#3383)
393bf23c0 [ntcore, cscore, wpiutil] Standardize template impl files on .inc extension (NFC) (#3124)
e7d9ba135 [sim] Disable flaky web server integration tests (#3388)
0a0003c11 [wpilibjExamples] Fix name of Java swerve drive pose estimator example (#3382)
7e1b27554 [wpilibc] Use default copies and moves when possible (#3381)
fb2a56e2d [wpilibc] Remove START_ROBOT_CLASS macro (#3384)
84218bfb4 [wpilibc] Remove frc namespace shim (#3385)
dd7824340 [wpilibc] Remove C++ compiler version static asserts (#3386)
484cf9c0e [wpimath] Suppress the -Wmaybe-uninitialized warning in Eigen (#3378)
a04d1b4f9 [wpilibc] DriverStation: Remove ReportError and ReportWarning
831c10bdf [wpilibc] Errors: Use fmtlib
87603e400 [wpiutil] Import fmtlib (#3375)
442621672 [wpiutil] Add ArrayRef/std::span/wpi::span implicit conversions
bc15b953b [wpiutil] Add std::span implementation
6d20b1204 [wpiutil] StringRef, Twine, raw_ostream: Add std::string_view support (#3373)
2385c2a43 [wpilibc] Remove Utility.h (#3376)
87384ea68 [wpilib] Fix PIDController continuous range error calculations (#3170)
04dae799a [wpimath] Add SimpleMotorFeedforward::Calculate(velocity, nextVelocity) overload (#3183)
0768c3903 [wpilib] DifferentialDrive: Remove right side inversion (#3340)
8dd8d4d2d [wpimath] Fix redundant nested math package introduced by #3316 (#3368)
49b06beed [examples] Add Field2d to RamseteController example (#3371)
4c562a445 [wpimath] Fix typo in comment of update_eigen.py (#3369)
fdbbf1188 [wpimath] Add script for updating Eigen
f1e64b349 [wpimath] Move Eigen unsupported folder into eigeninclude
224f3a05c [sim] Fix build error when building with GCC 11.1 (#3361)
ff56d6861 [wpilibj] Fix SpeedController deprecated warnings (#3360)
1873fbefb [examples] Fix Swerve and Mecanum examples (#3359)
80b479e50 [examples] Fix SwerveBot example to use unique encoder ports (#3358)
1f7c9adee [wpilibjExamples] Fix pose estimator examples (#3356)
9ebc3b058 [outlineviewer] Change default size to 600x400 (#3353)
e21b443a4 [build] Gradle: Make C++ examples runnable (#3348)
da590120c [wpilibj] Add MotorController.setVoltage default (#3347)
561d53885 [build] Update opencv to 4.5.2, imgui/implot to latest (#3344)
44ad67ca8 [wpilibj] Preferences: Add missing Deprecated annotation (#3343)
3fe8fc75a [wpilibc] Revert "Return reference from GetInstance" (#3342)
3cc2da332 Merge branch '2022'
a3cd90dd7 [wpimath] Fix classpath used by generate_numbers.py (#3339)
d6cfdd3ba [wpilib] Preferences: Deprecate Put* in favor of Set* (#3337)
ba08baabb [wpimath] Update Drake DARE solver to v0.29.0 (#3336)
497b712f6 [wpilib] Make IterativeRobotBase::m_period private with getter
f00dfed7a [wpilib] Remove IterativeRobot base class
3c0846168 [hal] Use last error reporting instead of PARAMETER_OUT_OF_RANGE (#3328)
5ef2b4fdc [wpilibj] Fix @deprecated warning for SerialPort constructor (#3329)
23d2326d1 [hal] Report previous allocation location for indexed resource duplicates (#3322)
e338f9f19 [build] Fix wpilibc runCpp task (#3327)
c8ff626fe [wpimath] Move Java classes to edu.wpi.first.math (#3316)
4e424d51f [wpilibj] DifferentialDrivetrainSim: Rename constants to match the style guide (#3312)
6b50323b0 [cscore] Use Lock2DSize if possible for Windows USB cameras (#3326)
65c148536 [wpilibc] Fix "control reaches end of non-void function" warning (#3324)
f99f62bee [wpiutil] uv Handle: Use malloc/free instead of new/delete (#3325)
365f5449c [wpimath] Fix MecanumDriveKinematics (#3266)
ff52f207c [glass, wpilib] Rewrite Mechanism2d (#3281)
ee0eed143 [wpimath] Add DCMotor factory function for Romi motors (#3319)
512738072 [hal] Add HAL_GetLastError to enable better error messages from HAL calls (#3320)
ced654880 [glass, outlineviewer] Update Mac icons to macOS 11 style (#3313)
936d3b9f8 [templates] Add Java template for educational robot (#3309)
6e31230ad [examples] Fix odometry update in SwerveControllerCommand example (#3310)
05ebe9318 Merge branch 'main' into 2022
aaf24e255 [wpilib] Fix initial heading behavior in HolonomicDriveController (#3290)
8d961dfd2 [wpilibc] Remove ErrorBase (#3306)
659b37ef9 [wpiutil] StackTrace: Include offset on Linux (#3305)
0abf6c904 [wpilib] Move motor controllers to motorcontrol package (#3302)
4630191fa [wpiutil] circular_buffer: Use value initialization instead of passing zero (#3303)
b7b178f49 [wpilib] Remove Potentiometer interface
687066af3 [wpilib] Remove GyroBase
6b168ab0c [wpilib] Remove PIDController, PIDOutput, PIDSource
948625de9 [wpimath] Document conversion from filter cutoff frequency to time constant (#3299)
3848eb8b1 [wpilibc] Fix flywhel -> flywheel typo in FlywheelSim (#3298)
3abe0b9d4 [cscore] Move java package to edu.wpi.first.cscore (#3294)
d7fabe81f [wpilib] Remove RobotDrive (#3295)
1dc81669c [wpilib] Remove GearTooth (#3293)
01d0e1260 [wpilib] Revert move of RomiGyro into main wpilibc/j (#3296)
397e569aa [ntcore] Remove "using wpi" from nt namespace
79267f9e6 [ntcore] Remove NetworkTable -> nt::NetworkTable shim
48ebe5736 [ntcore] Remove deprecated Java interfaces and classes
c2064c78b [ntcore] Remove deprecated ITable interfaces
36608a283 [ntcore] Remove deprecated C++ APIs
a1c87e1e1 [glass] LogView: Add "copy to clipboard" button (#3274)
fa7240a50 [wpimath] Fix typo in quintic spline basis matrix
ffb4d38e2 [wpimath] Add derivation for spline basis matrices
f57c188f2 [wpilib] Add AnalogEncoder(int) ctor (#3273)
8471c4fb2 [wpilib] FieldObject2d: Add setTrajectory() method (#3277)
c97acd18e [glass] Field2d enhancements (#3234)
ffb590bfc [wpilib] Fix Compressor sendable properties (#3269)
6137f98eb [hal] Rename SimValueCallback2 to SimValueCallback (#3212)
a6f653969 [hal] Move registerSimPeriodic functions to HAL package (#3211)
10c038d9b [glass] Plot: Fix window creation after removal (#3264)
2d2eaa3ef [wpigui] Ensure window will be initially visible (#3256)
4d28b1f0c [wpimath] Use JNI for trajectory serialization (#3257)
3de800a60 [wpimath] TrajectoryUtil.h: Comment formatting (NFC) (#3262)
eff592377 [glass] Plot: Don't overwrite series ID (#3260)
a79faace1 [wpilibc] Return reference from GetInstance (#3247)
9550777b9 [wpilib] PWMSpeedController: Use PWM by composition (#3248)
c8521a3c3 [glass] Plot: Set reasonable default window size (#3261)
d71eb2cf3 [glass] Plot: Show full source name as tooltip and in popup (#3255)
160fb740f [hal] Use std::lround() instead of adding 0.5 and truncating (#3012)
48e9f3951 [wpilibj] Remove wpilibj package CameraServer (#3213)
8afa596fd [wpilib] Remove deprecated Sendable functions and SendableBase (#3210)
d3e45c297 [wpimath] Make C++ geometry classes immutable (#3249)
2c98939c1 [glass] StringChooser: Don't call SameLine() at end
a18a7409f [glass] NTStringChooser: Clear value of deleted entries
2f19cf452 [glass] NetworkTablesHelper: listen to delete events
da96707dc Merge branch 'main' into 2022
c3a8bdc24 [build] Fix clang-tidy action (#3246)
21624ef27 Add ImGui OutlineViewer (#3220)
1032c9b91 [wpiutil] Unbreak wpi::Format on Windows (#3242)
2e07902d7 [glass] NTField2D: Fix name lookup (#3233)
6e23e1840 [wpilibc] Remove WPILib.h (#3235)
3e22e4506 [wpilib] Make KoP drivetrain simulation weight 60 lbs (#3228)
79d1bd6c8 [glass] NetworkTablesSetting: Allow disable of server option (#3227)
fe341a16f [examples] Use more logical elevator setpoints in GearsBot (#3198)
62abf46b3 [glass] NetworkTablesSettings: Don't block GUI (#3226)
a95a5e0d9 [glass] Move NetworkTablesSettings to libglassnt (#3224)
d6f6ceaba [build] Run Spotless formatter (NFC) (#3221)
0922f8af5 [commands] CommandScheduler.requiring(): Note return can be null (NFC) (#2934)
6812302ff [examples] Make DriveDistanceOffboard example work in sim (#3199)
f3f86b8e7 [wpimath] Add pose estimator overload for vision + std dev measurement (#3200)
1a2680b9e [wpilibj] Change CommandBase.withName() to return CommandBase (#3209)
435bbb6a8 [command] RamseteCommand: Output 0 if interrupted (#3216)
3cf44e0a5 [hal] Add function for changing HAL Notifier thread priority (#3218)
40b367513 [wpimath] Units.java: Add kg-lb conversions (#3203)
9f563d584 [glass] NT: Fix return value in StringToDoubleArray (#3208)
af4adf537 [glass] Auto-size plots to fit window (#3193)
2560146da [sim] GUI: Add option to show prefix in Other Devices (#3186)
eae3a6397 gitignore: Ignore .cache directory (#3196)
959611420 [wpilib] Require non-zero positive value for PIDController.period (#3175)
9522f2e8c [wpimath] Add methods to concatenate trajectories (#3139)
e42a0b6cf [wpimath] Rotation2d comment formatting (NFC) (#3162)
d1c7032de [wpimath] Fix order of setting gyro offset in pose estimators (#3176)
d241bc81a [sim] Add DoubleSolenoidSim and SolenoidSim classes (#3177)
cb7f39afa [wpilibc] Add RobotController::GetBatteryVoltage() to C++ (#3179)
99b5ad9eb [wpilibj] Fix warnings that are not unused variables or deprecation (#3161)
c14b23775 [build] Fixup doxygen generated include dirs to match what users would need (#3154)
d447c7dc3 [sim] Add SimDeviceSim ctor overloads (#3134)
247420c9c [build] Remove jcenter repo (#3157)
04b112e00 [build] Include debug info in plugin published artifacts (#3149)
be0ce9900 [examples] Use PWMSparkMax instead of PWMVictorSPX (#3156)
69e8d0b65 [wpilib] Move RomiGyro into main wpilibc/j (#3143)
94e685e1b [wpimath] Add custom residual support to EKF (#3148)
5899f3dd2 [sim] GUI: Make keyboard settings loading more robust (#3167)
f82aa1d56 [wpilib] Fix HolonomicDriveController atReference() behavior (#3163)
fe5c2cf4b [wpimath] Remove ControllerUtil.java (#3169)
43d40c6e9 [wpiutil] Suppress unchecked cast in CombinedRuntimeLoader (#3155)
3d44d8f79 [wpimath] Fix argument order in UKF docs (NFC) (#3147)
ba6fe8ff2 [cscore] Add USB camera change event (#3123)
533725888 [build] Tweak OpenCV cmake search paths to work better on Linux (#3144)
29bf9d6ef [cscore] Add polled support to listener
483beb636 [ntcore] Move CallbackManager to wpiutil
fdaec7759 [examples] Instantiate m_ramseteController in example (#3142)
8494a5761 Rename default branch to main (#3140)
45590eea2 [wpigui] Hardcode window scale to 1 on macOS (#3135)
834a64920 [build] Publish libglass and libglassnt to Maven (#3127)
2c2ccb361 [wpimath] Fix Rotation2d equality operator (#3128)
fb5c8c39a [wpigui] clang-tidy: readability-braces-around-statements
f7d39193a [wpigui] Fix copyright in pfd and wpigui_metal.mm
aec796b21 [ntcore] Fix conditional jump on uninitialized value (#3125)
fb13bb239 [sim] GUI: Add right click popup for keyboard joystick settings (#3119)
c517ec677 [build] Update thirdparty-imgui to 1.79-2 (#3118)
e8cbf2a71 [wpimath] Fix typo in SwerveDrivePoseEstimator doc (NFC) (#3112)
e9c86df46 [wpimath] Add tests for swerve module optimization (#3100)
6ba8c289c [examples] Remove negative of ArcadeDrive(fwd, ..) in the C++ Getting Started Example (#3102)
3f1672e89 [hal] Add SimDevice createInt() and createLong() (#3110)
15be5cbf1 [examples] Fix segfault in GearsBot C++ example (#3111)
4cf0e5e6d Add quick links to API documentation in README (#3082)
6b1898f12 Fix RT priority docs (NFC) (#3098)
b3426e9c0 [wpimath] Fix missing whitespace in pose estimator doc (#3097)
38c1a1f3e [examples] Fix feildRelative -> fieldRelative typo in XControllerCommand examples (#3104)
4488e25f1 [glass] Shorten SmartDashboard window names (#3096)
cfdb3058e [wpilibj] Update SimDeviceSimTest (#3095)
64adff5fe [examples] Fix typo in ArcadeDrive constructor parameter name (#3092)
6efc58e3d [build] Fix issues with build on windows, deprecations, and native utils (#3090)
f393989a5 [wpimath, wpiutil] Add wpi::array for compile time size checking (#3087)
d6ed20c1e [build] Set macOS deployment target to 10.14 (#3088)
7c524014c [hal] Add [[nodiscard]] to HAL_WaitForNotifierAlarm() (#3085)
406d055f0 [wpilib] Fixup wouldHitLowerLimit in elevator and arm simulation classes. (#3076)
04a90b5dd [examples] Don't continually set setpoint in PotentiometerPID Examples (#3084)
8c5bfa013 [sim] GUI: Add max value setting for keyboard joysticks (#3083)
bc80c5535 [hal] Add SimValue reset() function (#3064)
9c3b51ca0 [wpilib] Document simulation APIs (#3079)
26584ff14 [wpimath] Add model description to LinearSystemId Javadocs (#3080)
42c3d5286 [examples] Sync Java and C++ trajectories in sim example (#3081)
64e72f710 [wpilibc] Add missing function RoboRioSim::ResetData (#3073)
e95503798 [wpimath] Add optimize() to SwerveModuleState (#3065)
fb99910c2 [hal] Add SimInt and SimLong wrappers for int/long SimValue (#3066)
e620bd4d3 [doc] Add machine-readable websocket specification (#3059)
a44e761d9 [glass] Add support for plot Y axis labels
ea1974d57 [wpigui] Update imgui and implot to latest
85a0bd43c [wpimath] Add RKF45 integration (#3047)
278e0f126 [glass] Use .controllable to set widgets' read-only state (#3035)
d8652cfd4 [wpimath] Make Java DCMotor API consistent with C++ and fix motor calcs (#3046)
377b7065a [build] Add toggleOffOn to Java spotless (#3053)
1e9c79c58 [sim] Use plant output to retrieve simulated position (#3043)
78147aa34 [sim] GUI: Fix Keyboard Joystick (#3052)
cd4a2265b [ntcore] Fix NetworkTableEntry::GetRaw() (#3051)
767ac1de1 [build] Use deploy key for doc publish (#3048)
d762215d1 [build] Add publish documentation script (#3040)
1fd09593c [examples] Add missing TestInit method to GettingStarted Example (#3039)
e45a0f6ce [examples] Add RomiGyro to the Romi Reference example (#3037)
94f852572 Update imaging link and fix typo (#3038)
d73cf64e5 [examples] Update RomiReference to match motor directions (#3036)
f945462ba Bump copyright year to 2021 (#3033)
b05946175 [wpimath] Catch Drake JNI exceptions and rethrow them (#3032)
62f0f8190 [wpimath] Deduplicate angle modulus functions (#2998)
bf8c0da4b [glass] Add "About" popup with version number (#3031)
dfdd6b389 [build] Increase Gradle heap size in Gazebo build (#3028)
f5e0fc3e9 Finish clang-tidy cleanups (#3003)
d741101fe [sim] Revert accidental commit of WSProvider_PDP.h (#3027)
e1620799c [examples] Add C++ RomiReference example (#2969)
749c7adb1 [command] Fix use-after-free in CommandScheduler (#3024)
921a73391 [sim] Add WS providers for AddressableLED, PCM, and Solenoid (#3026)
26d0004fe [build] Split Actions into different yml files (#3025)
948af6d5b [wpilib] PWMSpeedController.get(): Apply Inversion (#3016)
670a187a3 [wpilibc] SuppliedValueWidget.h: Forward declare ShuffleboardContainer (#3021)
be9f72502 [ntcore] NetworkTableValue: Use std::forward instead of std::move (#3022)
daf3f4cb1 [cscore] cscore_raw_cv.h: Fix error in PutFrame() (#3019)
5acda4cc7 [wpimath] ElevatorFeedforward.h: Add time.h include
8452af606 [wpimath] units/radiation.h: Add mass.h include
630d44952 [hal] ErrorsInternal.h: Add stdint.h include
7372cf7d9 [cscore] Windows NetworkUtil.cpp: Add missing include
b7e46c558 Include .h from .inc/.inl files (NFC) (#3017)
bf8f8710e [examples] Update Romi template and example (#2996)
6ffe5b775 [glass] Ensure NetworkTableTree parent context menu has an id (#3015)
be0805b85 [build] Update to WPILibVersioningPlugin 4.1.0 (#3014)
65b2359b2 [build] Add spotless for other files (#3007)
8651aa73e [examples] Enable NT Flush in Field2d examples (#3013)
78b542737 [build] Add Gazebo build to Actions CI (#3004)
fccf86532 [sim] DriverStationGui: Fix two bugs (#3010)
185741760 [sim] WSProvider_Joystick: Fix off-by-1 in incoming buttons (#3011)
ee7114a58 [glass] Add drive class widgets (#2975)
00fa91d0d [glass] Use ImGui style for gyro widget colors (#3009)
b7a25bfc3 ThirdPartyNotices: Add portable file dialogs license (#3005)
a2e46b9a1 [glass] modernize-use-nullptr (NFC) (#3006)
a751fa22d [build] Apply spotless for java formatting (#1768)
e563a0b7d [wpimath] Make LinearSystemLoop move-constructible and move-assignable (#2967)
49085ca94 [glass] Add context menus to remove and add NetworkTables values (#2979)
560a850a2 [glass] Add NetworkTables Log window (#2997)
66782e231 [sim] Create Left/Right drivetrain current accessors (#3001)
b60eb1544 clang-tidy: bugprone-virtual-near-miss
cbe59fa3b clang-tidy: google-explicit-constructor
c97c6dc06 clang-tidy: google-readability-casting (NFC)
32fa97d68 clang-tidy: modernize-use-nullptr (NFC)
aee460326 clang-tidy: modernize-pass-by-value
29c7da5f1 clang-tidy: modernize-make-unique
6131f4e32 clang-tidy: modernize-concat-nested-namespaces (NFC)
67e03e625 clang-tidy: modernize-use-equals-default
b124f9101 clang-tidy: modernize-use-default-member-init
d11a3a638 clang-tidy: modernize-use-override (NFC)
4cc0706b0 clang-tidy: modernize-use-using (NFC)
885f5a978 [wpilibc] Speed up ScopedTracerTest (#2999)
60b596457 [wpilibj] Fix typos (NFC) (#3000)
6e1919414 [build] Bring naming checkstyle rules up to date with Google Style guide (#1781)
8c8ec5e63 [wpilibj] Suppress unchecked cast warnings (#2995)
b8413ddd5 [wpiutil] Add noexcept to timestamp static functions (#2994)
5d976b6e1 [glass] Load NetworkTableView settings on first draw (#2993)
2b4317452 Replace NOLINT(runtime/explicit) comments with NOLINT (NFC) (#2992)
1c3011ba4 [glass] Fix handling of "/" NetworkTables key (#2991)
574a42f3b [hal] Fix UnsafeManipulateDIO status check (#2987)
9005cd59e [wpilib] Clamp input voltage in sim classes (#2955)
dd494d4ab [glass] NetworkTablesModel::Update(): Avoid use-after-move (#2988)
7cca469a1 [wpimath] NormalizeAngle: Make inline, remove unnamed namespace (#2986)
2aed432b4 Add braces to C++ single-line loops and conditionals (NFC) (#2973)
0291a3ff5 [wpiutil] StringRef: Add noexcept to several constructors (#2984)
5d7315280 [wpimath] Update UnitsTest.cpp copyright (#2985)
254931b9a [wpimath] Remove LinearSystem from LinearSystemLoop (#2968)
aa89744c9 Update OtherVersions.md to include wpimath info (#2983)
1cda3f5ad [glass] Fix styleguide (#2976)
8f1f64ffb Remove year from file copyright message (NFC) (#2972)
2bc0a7795 [examples] Fix wpiformat warning about utility include (#2971)
4204da6ad [glass] Add application icon
7ac39b10f [wpigui] Add icon support
6b567e006 [wpimath] Add support for varying vision standard deviations in pose estimators (#2956)
df299d6ed [wpimath] Add UnscentedKalmanFilter::Correct() overload (#2966)
4e34f0523 [examples] Use ADXRS450_GyroSim class in simulation example (#2964)
9962f6fd7 [wpilib] Give Field2d a default Sendable name (#2953)
f9d492f4b [sim] GUI: Show "Other Devices" window by default (#2961)
a8bb2ef1c [sim] Fix ADXRS450_GyroSim and DutyCycleEncoderSim (#2963)
240c629cd [sim] Try to guess "Map Gamepad" setting (#2960)
952567dd3 [wpilibc] Add missing move constructors and assignment operators (#2959)
10b396b4c [sim] Various WebSockets fixes and enhancements (#2952)
699bbe21a [examples] Fix comments in Gearsbot to match implementation (NFC) (#2957)
27b67deca [glass] Add more widgets (#2947)
581b7ec55 [wpilib] Add option to flush NetworkTables every iterative loop
acfbb1a44 [ntcore] DispatcherBase::Flush: Use wpi::Now()
d85a6d8fe [ntcore] Reduce limit on flush and update rate to 5 ms
20fbb5c63 [sim] Fix stringop truncation warning from GCC 10 (#2945)
1051a06a7 [glass] Show NT timestamps in seconds (#2944)
98dfc2620 [glass] Fix plots (#2943)
1ba0a2ced [sim] GUI: Add keyboard virtual joystick support (#2940)
4afb13f98 [examples] Replace M_PI with wpi::math::pi (#2938)
b27d33675 [examples] Enhance Romi templates (#2931)
00b9ae77f [sim] Change default WS port number to 3300 (#2932)
65219f309 [examples] Update Field2d position in periodic() (#2928)
f78d1d434 [sim] Process WS Encoder reset internally (#2927)
941edca59 [hal] Add Java SimDeviceDataJNI.getSimDeviceName (#2924)
a699435ed [wpilibj] Fix FlywheelSim argument order in constructor (#2922)
66d641718 [examples] Add tasks to run Java examples (#2920)
558e37c41 [examples] Add simple differential drive simulation example (#2918)
4f40d991e [glass] Switch name of Glass back to glass (#2919)
549af9900 [build] Update native-utils to 2021.0.6 (#2914)
b33693009 [glass] Change basename of glass to Glass (#2915)
c9a0edfb8 [glass] Package macOS application bundle
2c5668af4 [wpigui] Add platform-specific preferences save
751dea32a [wpilibc] Try to work around ABI break introduced in #2901 (#2917)
cd8f4bfb1 [build] Package up msvc runtime into maven artifact (#2913)
a6cfcc686 [wpilibc] Move SendableChooser Doxygen comments to header (NFC) (#2911)
b8c4f603d [wpimath] Upgrade to Eigen 3.3.9 (#2910)
0075e4b39 [wpilibj] Fix NPE in Field2d (#2909)
125af556c [simulation] Fix halsim_gui ntcore and wpiutil deps (#2908)
963ad5c25 [wpilib] Add noise to Differential Drive simulator (#2903)
387f56cb7 [examples] Add Romi reference Java example and templates (#2905)
b3deda38c [examples] Zero motors on disabledInit() in sim physics examples (#2906)
2a5ca7745 [glass] Add glass: an application for display of robot data
727940d84 [wpilib] Move Field2d to SmartDashboard
8cd42478e [wpilib] SendableBuilder: Make GetTable() visible
c11d34b26 [command] Use addCommands in command group templates (#2900)
339d7445b [sim] Add HAL hooks for simulationPeriodic (#2881)
d16f05f2c [wpilib] Fix SmartDashboard update order (#2896)
5427b32a4 [wpiutil] unique_function: Restrict implicit conversion (#2899)
f73701239 [ntcore] Add missing SetDefault initializer_list functions (#2898)
f5a6fc070 [sim] Add initialized flag for all solenoids on a PCM (#2897)
bdf5ba91a [wpilibj] Fix typo in ElevatorSim (#2895)
bc8f33877 [wpilib] Add pose estimators (#2867)
3413bfc06 [wpilib] PIDController: Recompute the error in AtSetpoint() (#2822)
2056f0ce0 [wpilib] Fix bugs in Hatchbot examples (#2893)
5eb8cfd69 [wpilibc] Fix MatchDataSender (#2892)
e6a425448 [build] Delete test folders after tests execute (#2891)
d478ad00d [imgui] Allow usage of imgui_stdlib (#2889)
53eda861d [build] Add unit-testing infrastructure to examples (#2863)
cc1d86ba6 [sim] Add title to simulator GUI window (#2888)
f0528f00e [build] CMake: Use project-specific binary and source dirs (#2886)
5cd2ad124 [wpilibc] Add Color::operator!= (#2887)
6c00e7a90 [build] CI CMake: build with GUI enabled (#2884)
53170bbb5 Update roboRIO toolchain installation instructions (#2883)
467258e05 [sim] GUI: Add option to not zero disconnected joysticks (#2876)
129be23c9 Clarify JDK installation instructions in readme (#2882)
8e9290e86 [build] Add separate CMake setting for wpimath (#2885)
7cf5bebf8 [wpilibj] Cache NT writes from DriverStation (#2780)
f7f9087fb [command] Fix timing issue in RamseteCommand (#2871)
256e7904f [wpilibj] SimDeviceSim: Fix sim value changed callback (#2880)
c8ea1b6c3 [wpilib] Add function to adjust LQR controller gain for pure time delay (#2878)
2816b06c0 [sim] HAL_GetControlWord: Fully zero output (#2873)
4c695ea08 Add toolchain installation instructions to README (#2875)
a14d51806 [wpimath] DCMotor: fix doc typo (NFC) (#2868)
017097791 [build] CMake: build sim extensions as shared libs (#2866)
f61726b5a [build] Fix cmake-config files (#2865)
fc27fdac5 [wpilibc] Cache NT values from driver station (#2768)
47c59859e [sim] Make SimDevice callbacks synchronous (#2861)
6e76ab9c0 [build] Turn on WITH_GUI for Windows cmake CI
5f78b7670 [build] Set GLFW_INSTALL to OFF
5e0808c84 [wpigui] Fix Windows cmake build
508f05a47 [imgui] Fix typo in Windows CMake target sources

Change-Id: I1737b45965f31803a96676bedc7dc40e337aa321
git-subtree-dir: third_party/allwpilib
git-subtree-split: e473a00f9785f9949e5ced30901baeaf426d2fc9
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
new file mode 100644
index 0000000..ccc6412
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp
@@ -0,0 +1,58 @@
+// 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/NTCommandScheduler.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTCommandSchedulerModel::NTCommandSchedulerModel(std::string_view path)
+    : NTCommandSchedulerModel(nt::GetDefaultInstance(), path) {}
+
+NTCommandSchedulerModel::NTCommandSchedulerModel(NT_Inst instance,
+                                                 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);
+}
+
+void NTCommandSchedulerModel::CancelCommand(size_t index) {
+  if (index < m_idsValue.size()) {
+    nt::SetEntryValue(
+        m_cancel, nt::NetworkTableValue::MakeDoubleArray({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());
+      }
+    }
+  }
+}
+
+bool NTCommandSchedulerModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_commands) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTCommandSelector.cpp b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
new file mode 100644
index 0000000..efcbac2
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTCommandSelector.cpp
@@ -0,0 +1,47 @@
+// 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/NTCommandSelector.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTCommandSelectorModel::NTCommandSelectorModel(std::string_view path)
+    : NTCommandSelectorModel(nt::GetDefaultInstance(), path) {}
+
+NTCommandSelectorModel::NTCommandSelectorModel(NT_Inst instance,
+                                               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_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));
+}
+
+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();
+      }
+    }
+  }
+}
+
+bool NTCommandSelectorModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_running) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
new file mode 100644
index 0000000..b44ea07
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp
@@ -0,0 +1,66 @@
+// 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/NTDifferentialDrive.h"
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <wpi/MathExtras.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTDifferentialDriveModel::NTDifferentialDriveModel(std::string_view path)
+    : NTDifferentialDriveModel(nt::GetDefaultInstance(), 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);
+
+  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));
+  });
+}
+
+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();
+    }
+  }
+
+  double l = m_lPercentData.GetValue();
+  double r = m_rPercentData.GetValue();
+
+  m_speedVector = ImVec2(0.0, -(l + r) / 2.0);
+  m_rotation = (l - r) / 2.0;
+}
+
+bool NTDifferentialDriveModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_lPercent) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDigitalInput.cpp b/glass/src/libnt/native/cpp/NTDigitalInput.cpp
new file mode 100644
index 0000000..5de6c29
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDigitalInput.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/NTDigitalInput.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTDigitalInputModel::NTDigitalInputModel(std::string_view path)
+    : NTDigitalInputModel{nt::GetDefaultInstance(), 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))},
+      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();
+      }
+    }
+  }
+}
+
+bool NTDigitalInputModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
new file mode 100644
index 0000000..a09d424
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp
@@ -0,0 +1,51 @@
+// 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/NTDigitalOutput.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTDigitalOutputModel::NTDigitalOutputModel(std::string_view path)
+    : NTDigitalOutputModel{nt::GetDefaultInstance(), 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))},
+      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));
+}
+
+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();
+      }
+    }
+  }
+}
+
+bool NTDigitalOutputModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTFMS.cpp b/glass/src/libnt/native/cpp/NTFMS.cpp
new file mode 100644
index 0000000..84c1ce7
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTFMS.cpp
@@ -0,0 +1,91 @@
+// 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/NTFMS.h"
+
+#include <stdint.h>
+
+#include <fmt/format.h>
+#include <wpi/SmallVector.h>
+#include <wpi/timestamp.h>
+
+using namespace glass;
+
+NTFMSModel::NTFMSModel(std::string_view path)
+    : NTFMSModel{nt::GetDefaultInstance(), path} {}
+
+NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path)
+    : m_nt{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))},
+      m_fmsAttached{fmt::format("NT_FMS:FMSAttached:{}", path)},
+      m_dsAttached{fmt::format("NT_FMS:DSAttached:{}", path)},
+      m_allianceStationId{fmt::format("NT_FMS:AllianceStationID:{}", path)},
+      m_estop{fmt::format("NT_FMS:EStop:{}", path)},
+      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);
+  m_enabled.SetDigital(true);
+  m_test.SetDigital(true);
+  m_autonomous.SetDigital(true);
+}
+
+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()};
+}
+
+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);
+      }
+    }
+  }
+}
+
+bool NTFMSModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTField2D.cpp b/glass/src/libnt/native/cpp/NTField2D.cpp
new file mode 100644
index 0000000..47fa9a7
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTField2D.cpp
@@ -0,0 +1,255 @@
+// 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/NTField2D.h"
+
+#include <algorithm>
+#include <vector>
+
+#include <fmt/format.h>
+#include <ntcore_cpp.h>
+#include <wpi/Endian.h>
+#include <wpi/MathExtras.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+class NTField2DModel::ObjectModel : public FieldObjectModel {
+ public:
+  ObjectModel(std::string_view name, NT_Entry entry)
+      : m_name{name}, m_entry{entry} {}
+
+  const char* GetName() const override { return m_name.c_str(); }
+  NT_Entry GetEntry() const { return m_entry; }
+
+  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; }
+  bool IsReadOnly() override { return false; }
+
+  wpi::span<const frc::Pose2d> GetPoses() override { return m_poses; }
+  void SetPoses(wpi::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;
+
+ private:
+  void UpdateNT();
+
+  std::string m_name;
+  NT_Entry m_entry;
+
+  std::vector<frc::Pose2d> m_poses;
+};
+
+void NTField2DModel::ObjectModel::NTUpdate(const nt::Value& value) {
+  if (value.IsDoubleArray()) {
+    auto arr = value.GetDoubleArray();
+    auto size = arr.size();
+    if ((size % 3) != 0) {
+      return;
+    }
+    m_poses.resize(size / 3);
+    for (size_t i = 0; i < size / 3; ++i) {
+      m_poses[i] = frc::Pose2d{
+          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()}));
+  }
+}
+
+void NTField2DModel::ObjectModel::SetPoses(wpi::span<const frc::Pose2d> poses) {
+  m_poses.assign(poses.begin(), poses.end());
+  UpdateNT();
+}
+
+void NTField2DModel::ObjectModel::SetPose(size_t i, frc::Pose2d pose) {
+  if (i < m_poses.size()) {
+    m_poses[i] = pose;
+    UpdateNT();
+  }
+}
+
+void NTField2DModel::ObjectModel::SetPosition(size_t i,
+                                              frc::Translation2d pos) {
+  if (i < m_poses.size()) {
+    m_poses[i] = frc::Pose2d{pos, m_poses[i].Rotation()};
+    UpdateNT();
+  }
+}
+
+void NTField2DModel::ObjectModel::SetRotation(size_t i, frc::Rotation2d rot) {
+  if (i < m_poses.size()) {
+    m_poses[i] = frc::Pose2d{m_poses[i].Translation(), rot};
+    UpdateNT();
+  }
+}
+
+NTField2DModel::NTField2DModel(std::string_view path)
+    : NTField2DModel{nt::GetDefaultInstance(), 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() = 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());
+      if (name.empty() || name[0] == '.') {
+        continue;
+      }
+      auto [it, match] = Find(event.name);
+      if (event.flags & NT_NOTIFY_DELETE) {
+        if (match) {
+          m_objects.erase(it);
+        }
+        continue;
+      } else if (event.flags & NT_NOTIFY_NEW) {
+        if (!match) {
+          it = m_objects.emplace(
+              it, std::make_unique<ObjectModel>(event.name, event.entry));
+        }
+      } else if (!match) {
+        continue;
+      }
+      if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) {
+        (*it)->NTUpdate(*event.value);
+      }
+    }
+  }
+}
+
+bool NTField2DModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+}
+
+bool NTField2DModel::IsReadOnly() {
+  return false;
+}
+
+FieldObjectModel* NTField2DModel::AddFieldObject(std::string_view name) {
+  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)));
+  }
+  return it->get();
+}
+
+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);
+  }
+}
+
+void NTField2DModel::ForEachFieldObject(
+    wpi::function_ref<void(FieldObjectModel& model, std::string_view name)>
+        func) {
+  for (auto&& obj : m_objects) {
+    if (obj->Exists()) {
+      func(*obj, wpi::drop_front(obj->GetName(), m_path.size()));
+    }
+  }
+}
+
+std::pair<NTField2DModel::Objects::iterator, bool> NTField2DModel::Find(
+    std::string_view fullName) {
+  auto it = std::lower_bound(
+      m_objects.begin(), m_objects.end(), fullName,
+      [](const auto& e, std::string_view name) { return e->GetName() < name; });
+  return {it, it != m_objects.end() && (*it)->GetName() == fullName};
+}
diff --git a/glass/src/libnt/native/cpp/NTGyro.cpp b/glass/src/libnt/native/cpp/NTGyro.cpp
new file mode 100644
index 0000000..7651d2c
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTGyro.cpp
@@ -0,0 +1,41 @@
+// 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/NTGyro.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTGyroModel::NTGyroModel(std::string_view path)
+    : NTGyroModel(nt::GetDefaultInstance(), 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);
+}
+
+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();
+      }
+    }
+  }
+}
+
+bool NTGyroModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_angle) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
new file mode 100644
index 0000000..28c0a67
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp
@@ -0,0 +1,92 @@
+// 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/NTMecanumDrive.h"
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <wpi/MathExtras.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTMecanumDriveModel::NTMecanumDriveModel(std::string_view path)
+    : NTMecanumDriveModel(nt::GetDefaultInstance(), path) {}
+
+NTMecanumDriveModel::NTMecanumDriveModel(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_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_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) {
+    nt::SetEntryValue(m_frPercent, nt::NetworkTableValue::MakeDouble(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));
+  });
+}
+
+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();
+    }
+  }
+
+  double fl = m_flPercentData.GetValue();
+  double fr = m_frPercentData.GetValue();
+  double rl = m_rlPercentData.GetValue();
+  double rr = m_rrPercentData.GetValue();
+
+  m_speedVector =
+      ImVec2((fl - fr - rl + rr) / 4.0f, -(fl + fr + rl + rr) / 4.0f);
+  m_rotation = -(-fl + fr - rl + rr) / 4;
+}
+
+bool NTMecanumDriveModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_flPercent) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTMechanism2D.cpp b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
new file mode 100644
index 0000000..9c73af2
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTMechanism2D.cpp
@@ -0,0 +1,302 @@
+// 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/NTMechanism2D.h"
+
+#include <algorithm>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <ntcore_cpp.h>
+#include <wpi/StringExtras.h>
+
+#include "glass/other/Mechanism2D.h"
+
+using namespace glass;
+
+// Convert "#RRGGBB" hex color to ImU32 color
+static void ConvertColor(std::string_view in, ImU32* out) {
+  if (in.size() != 7 || in[0] != '#') {
+    return;
+  }
+  if (auto v = wpi::parse_integer<ImU32>(wpi::drop_front(in), 16)) {
+    ImU32 val = v.value();
+    *out = IM_COL32((val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff, 255);
+  }
+}
+
+namespace {
+
+class NTMechanismObjectModel;
+
+class NTMechanismGroupImpl final {
+ public:
+  NTMechanismGroupImpl(NT_Inst 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);
+
+ protected:
+  NT_Inst m_inst;
+  std::string m_path;
+  std::string m_name;
+  std::vector<std::unique_ptr<NTMechanismObjectModel>> m_objects;
+};
+
+class NTMechanismObjectModel final : public MechanismObjectModel {
+ public:
+  NTMechanismObjectModel(NT_Inst 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))} {}
+
+  const char* GetName() const final { return m_group.GetName(); }
+  void ForEachObject(
+      wpi::function_ref<void(MechanismObjectModel& model)> func) final {
+    m_group.ForEachObject(func);
+  }
+
+  const char* GetType() const final { return m_typeValue.c_str(); }
+  ImU32 GetColor() const final { return m_colorValue; }
+  double GetWeight() const final { return m_weightValue; }
+  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);
+
+ private:
+  NTMechanismGroupImpl m_group;
+
+  NT_Entry m_type;
+  NT_Entry m_color;
+  NT_Entry m_weight;
+  NT_Entry m_angle;
+  NT_Entry m_length;
+
+  std::string m_typeValue;
+  ImU32 m_colorValue = IM_COL32_WHITE;
+  double m_weightValue = 1.0;
+  frc::Rotation2d m_angleValue;
+  units::meter_t m_lengthValue = 0.0_m;
+};
+
+}  // namespace
+
+void NTMechanismGroupImpl::ForEachObject(
+    wpi::function_ref<void(MechanismObjectModel& model)> func) {
+  for (auto&& obj : m_objects) {
+    func(*obj);
+  }
+}
+
+void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event,
+                                    std::string_view name) {
+  if (name.empty()) {
+    return;
+  }
+  std::string_view childName;
+  std::tie(name, childName) = wpi::split(name, '/');
+  if (childName.empty()) {
+    return;
+  }
+
+  auto it = std::lower_bound(
+      m_objects.begin(), m_objects.end(), name,
+      [](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 (match) {
+    if ((*it)->NTUpdate(event, childName)) {
+      m_objects.erase(it);
+    }
+  }
+}
+
+bool NTMechanismObjectModel::NTUpdate(const nt::EntryNotification& event,
+                                      std::string_view childName) {
+  if (event.entry == m_type) {
+    if ((event.flags & NT_NOTIFY_DELETE) != 0) {
+      return true;
+    }
+    if (event.value && event.value->IsString()) {
+      m_typeValue = event.value->GetString();
+    }
+  } 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)
+      : m_group{inst, path, name},
+        m_x{nt::GetEntry(inst, fmt::format("{}/x", path))},
+        m_y{nt::GetEntry(inst, fmt::format("{}/y", path))} {}
+
+  const char* GetName() const final { return m_group.GetName(); }
+  void ForEachObject(
+      wpi::function_ref<void(MechanismObjectModel& model)> func) final {
+    m_group.ForEachObject(func);
+  }
+
+  bool NTUpdate(const nt::EntryNotification& 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;
+  frc::Translation2d m_pos;
+};
+
+bool NTMechanism2DModel::RootModel::NTUpdate(const nt::EntryNotification& 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()};
+    }
+  } 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 {
+    m_group.NTUpdate(event, childName);
+  }
+  return false;
+}
+
+NTMechanism2DModel::NTMechanism2DModel(std::string_view path)
+    : NTMechanism2DModel{nt::GetDefaultInstance(), path} {}
+
+NTMechanism2DModel::NTMechanism2DModel(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_dimensions{m_nt.GetEntry(fmt::format("{}/dims", path))},
+      m_bgColor{m_nt.GetEntry(fmt::format("{}/backgroundColor", path))},
+      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);
+}
+
+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());
+      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;
+                                 });
+      bool match = it != m_roots.end() && (*it)->GetName() == name;
+
+      if (event.flags & NT_NOTIFY_NEW) {
+        if (!match) {
+          it = m_roots.emplace(
+              it,
+              std::make_unique<RootModel>(
+                  m_nt.GetInstance(), fmt::format("{}{}", m_path, name), name));
+          match = true;
+        }
+      }
+      if (match) {
+        if ((*it)->NTUpdate(event, childName)) {
+          m_roots.erase(it);
+        }
+      }
+    }
+  }
+}
+
+bool NTMechanism2DModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
+}
+
+bool NTMechanism2DModel::IsReadOnly() {
+  return false;
+}
+
+void NTMechanism2DModel::ForEachRoot(
+    wpi::function_ref<void(MechanismRootModel& model)> func) {
+  for (auto&& obj : m_roots) {
+    func(*obj);
+  }
+}
diff --git a/glass/src/libnt/native/cpp/NTPIDController.cpp b/glass/src/libnt/native/cpp/NTPIDController.cpp
new file mode 100644
index 0000000..7936057
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTPIDController.cpp
@@ -0,0 +1,85 @@
+// 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/NTPIDController.h"
+
+#include <fmt/format.h>
+#include <wpi/StringExtras.h>
+
+using namespace glass;
+
+NTPIDControllerModel::NTPIDControllerModel(std::string_view path)
+    : NTPIDControllerModel(nt::GetDefaultInstance(), path) {}
+
+NTPIDControllerModel::NTPIDControllerModel(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_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);
+}
+
+void NTPIDControllerModel::SetP(double value) {
+  nt::SetEntryValue(m_p, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetI(double value) {
+  nt::SetEntryValue(m_i, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetD(double value) {
+  nt::SetEntryValue(m_d, nt::NetworkTableValue::MakeDouble(value));
+}
+
+void NTPIDControllerModel::SetSetpoint(double value) {
+  nt::SetEntryValue(m_setpoint, nt::NetworkTableValue::MakeDouble(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();
+      }
+    }
+  }
+}
+
+bool NTPIDControllerModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_setpoint) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTSpeedController.cpp b/glass/src/libnt/native/cpp/NTSpeedController.cpp
new file mode 100644
index 0000000..3dc351a
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTSpeedController.cpp
@@ -0,0 +1,52 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#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
new file mode 100644
index 0000000..e6a97fa
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTStringChooser.cpp
@@ -0,0 +1,75 @@
+// 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/NTStringChooser.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTStringChooserModel::NTStringChooserModel(std::string_view path)
+    : NTStringChooserModel{nt::GetDefaultInstance(), 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));
+}
+
+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));
+}
+
+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());
+      }
+    }
+  }
+}
+
+bool NTStringChooserModel::Exists() {
+  return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NTSubsystem.cpp b/glass/src/libnt/native/cpp/NTSubsystem.cpp
new file mode 100644
index 0000000..b2bdf8c
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NTSubsystem.cpp
@@ -0,0 +1,45 @@
+// 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/NTSubsystem.h"
+
+#include <fmt/format.h>
+
+using namespace glass;
+
+NTSubsystemModel::NTSubsystemModel(std::string_view path)
+    : NTSubsystemModel(nt::GetDefaultInstance(), 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);
+}
+
+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();
+      }
+    }
+  }
+}
+
+bool NTSubsystemModel::Exists() {
+  return m_nt.IsConnected() &&
+         nt::GetEntryType(m_defaultCommand) != NT_UNASSIGNED;
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp
new file mode 100644
index 0000000..596ef0a
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTables.cpp
@@ -0,0 +1,780 @@
+// 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/NetworkTables.h"
+
+#include <networktables/NetworkTableValue.h>
+
+#include <cinttypes>
+#include <cstdio>
+#include <cstring>
+#include <initializer_list>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <imgui.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallString.h>
+#include <wpi/SpanExtras.h>
+#include <wpi/StringExtras.h>
+#include <wpi/raw_ostream.h>
+#include <wpi/span.h>
+
+#include "glass/Context.h"
+#include "glass/DataSource.h"
+
+using namespace glass;
+
+static std::string BooleanArrayToString(wpi::span<const int> in) {
+  std::string rv;
+  wpi::raw_string_ostream os{rv};
+  os << '[';
+  bool first = true;
+  for (auto v : in) {
+    if (!first) {
+      os << ',';
+    }
+    first = false;
+    if (v) {
+      os << "true";
+    } else {
+      os << "false";
+    }
+  }
+  os << ']';
+  return rv;
+}
+
+static std::string DoubleArrayToString(wpi::span<const double> in) {
+  return fmt::format("[{:.6f}]", fmt::join(in, ","));
+}
+
+static std::string StringArrayToString(wpi::span<const std::string> in) {
+  std::string rv;
+  wpi::raw_string_ostream os{rv};
+  os << '[';
+  bool first = true;
+  for (auto&& v : in) {
+    if (!first) {
+      os << ',';
+    }
+    first = false;
+    os << '"';
+    os.write_escaped(v);
+    os << '"';
+  }
+  os << ']';
+  return rv;
+}
+
+NetworkTablesModel::NetworkTablesModel()
+    : NetworkTablesModel{nt::GetDefaultInstance()} {}
+
+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::DestroyEntryListenerPoller(m_poller);
+}
+
+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::UpdateValue() {
+  switch (value->type()) {
+    case NT_BOOLEAN:
+      if (!source) {
+        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
+      }
+      source->SetValue(value->GetBoolean() ? 1 : 0);
+      source->SetDigital(true);
+      break;
+    case NT_DOUBLE:
+      if (!source) {
+        source = std::make_unique<DataSource>(fmt::format("NT:{}", name));
+      }
+      source->SetValue(value->GetDouble());
+      source->SetDigital(false);
+      break;
+    case NT_BOOLEAN_ARRAY:
+      valueStr = BooleanArrayToString(value->GetBooleanArray());
+      break;
+    case NT_DOUBLE_ARRAY:
+      valueStr = DoubleArrayToString(value->GetDoubleArray());
+      break;
+    case NT_STRING_ARRAY:
+      valueStr = StringArrayToString(value->GetStringArray());
+      break;
+    default:
+      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());
+        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;
+      }
+      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);
+    }
+  }
+
+  // shortcut common case (updates)
+  if (!updateTree) {
+    return;
+  }
+
+  // remove deleted entries
+  m_sortedEntries.erase(
+      std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr),
+      m_sortedEntries.end());
+
+  // sort by name
+  std::sort(m_sortedEntries.begin(), m_sortedEntries.end(),
+            [](const auto& a, const auto& b) { return a->name < b->name; });
+
+  // rebuild tree
+  m_root.clear();
+  wpi::SmallVector<std::string_view, 16> parts;
+  for (auto& entry : m_sortedEntries) {
+    parts.clear();
+    wpi::split(entry->name, parts, '/', -1, false);
+
+    // ignore a raw "/" key
+    if (parts.empty()) {
+      continue;
+    }
+
+    // get to leaf
+    auto nodes = &m_root;
+    for (auto part : wpi::drop_back(wpi::span{parts.begin(), parts.end()})) {
+      auto it =
+          std::find_if(nodes->begin(), nodes->end(),
+                       [&](const auto& node) { return node.name == part; });
+      if (it == nodes->end()) {
+        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
+        nodes->back().path.assign(
+            entry->name.data(), part.data() + part.size() - entry->name.data());
+        it = nodes->end() - 1;
+      }
+      nodes = &it->children;
+    }
+
+    auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) {
+      return node.name == parts.back();
+    });
+    if (it == nodes->end()) {
+      nodes->emplace_back(parts.back());
+      // no need to set path, as it's identical to entry->name
+      it = nodes->end() - 1;
+    }
+    it->entry = entry;
+  }
+}
+
+bool NetworkTablesModel::Exists() {
+  return nt::IsConnected(m_inst);
+}
+
+static std::shared_ptr<nt::Value> StringToBooleanArray(std::string_view in) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeBooleanArray(
+        std::initializer_list<bool>{});
+  }
+  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::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);
+    } else if (wpi::equals_lower(val, "false")) {
+      out.emplace_back(0);
+    } else {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+  }
+
+  return nt::NetworkTableValue::MakeBooleanArray(out);
+}
+
+static std::shared_ptr<nt::Value> StringToDoubleArray(std::string_view in) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeDoubleArray(
+        std::initializer_list<double>{});
+  }
+  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::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());
+    } else {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+  }
+
+  return nt::NetworkTableValue::MakeDoubleArray(out);
+}
+
+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) {
+  in = wpi::trim(in);
+  if (in.empty()) {
+    return nt::NetworkTableValue::MakeStringArray(
+        std::initializer_list<std::string>{});
+  }
+  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;
+  std::vector<std::string> out;
+  wpi::SmallString<32> buf;
+
+  wpi::split(in, inSplit, ',', -1, false);
+  for (auto val : inSplit) {
+    val = wpi::trim(val);
+    if (val.empty()) {
+      continue;
+    }
+    if (val.front() != '"' || val.back() != '"') {
+      fmt::print(stderr,
+                 "GUI: NetworkTables: Could not understand value '{}'\n", val);
+      return nullptr;
+    }
+    out.emplace_back(UnescapeString(val, buf));
+  }
+
+  return nt::NetworkTableValue::MakeStringArray(std::move(out));
+}
+
+static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry) {
+  auto& val = entry.value;
+  if (!val) {
+    return;
+  }
+
+  switch (val->type()) {
+    case NT_BOOLEAN:
+      ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false");
+      break;
+    case NT_DOUBLE:
+      ImGui::LabelText("double", "%.6f", val->GetDouble());
+      break;
+    case NT_STRING: {
+      // GetString() comes from a std::string, so it's null terminated
+      ImGui::LabelText("string", "%s", val->GetString().data());
+      break;
+    }
+    case NT_BOOLEAN_ARRAY:
+      ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_DOUBLE_ARRAY:
+      ImGui::LabelText("double[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_STRING_ARRAY:
+      ImGui::LabelText("string[]", "%s", entry.valueStr.c_str());
+      break;
+    case NT_RAW:
+      ImGui::LabelText("raw", "[...]");
+      break;
+    case NT_RPC:
+      ImGui::LabelText("rpc", "[...]");
+      break;
+    default:
+      ImGui::LabelText("other", "?");
+      break;
+  }
+}
+
+static constexpr size_t kTextBufferSize = 4096;
+
+static char* GetTextBuffer(std::string_view in) {
+  static char textBuffer[kTextBufferSize];
+  size_t len = (std::min)(in.size(), kTextBufferSize - 1);
+  std::memcpy(textBuffer, in.data(), len);
+  textBuffer[len] = '\0';
+  return textBuffer;
+}
+
+static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry) {
+  auto& val = entry.value;
+  if (!val) {
+    return;
+  }
+
+  ImGui::PushID(entry.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));
+      }
+      break;
+    }
+    case NT_DOUBLE: {
+      double v = val->GetDouble();
+      if (ImGui::InputDouble("double", &v, 0, 0, "%.6f",
+                             ImGuiInputTextFlags_EnterReturnsTrue)) {
+        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v));
+      }
+      break;
+    }
+    case NT_STRING: {
+      char* v = GetTextBuffer(val->GetString());
+      if (ImGui::InputText("string", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v));
+      }
+      break;
+    }
+    case NT_BOOLEAN_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("boolean[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToBooleanArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_DOUBLE_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("double[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToDoubleArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_STRING_ARRAY: {
+      char* v = GetTextBuffer(entry.valueStr);
+      if (ImGui::InputText("string[]", v, kTextBufferSize,
+                           ImGuiInputTextFlags_EnterReturnsTrue)) {
+        if (auto outv = StringToStringArray(v)) {
+          nt::SetEntryValue(entry.entry, std::move(outv));
+        }
+      }
+      break;
+    }
+    case NT_RAW:
+      ImGui::LabelText("raw", "[...]");
+      break;
+    case NT_RPC:
+      ImGui::LabelText("rpc", "[...]");
+      break;
+    default:
+      ImGui::LabelText("other", "?");
+      break;
+  }
+  ImGui::PopID();
+}
+
+static void EmitParentContextMenu(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());
+    ImGui::Separator();
+
+    if (ImGui::BeginMenu("Add new...")) {
+      if (ImGui::IsWindowAppearing()) {
+        nameBuffer[0] = '\0';
+      }
+
+      ImGui::InputTextWithHint("New item name", "example", nameBuffer,
+                               kTextBufferSize);
+      std::string fullNewPath;
+      if (path == "/") {
+        fullNewPath = path + nameBuffer;
+      } else {
+        fullNewPath = fmt::format("{}/{}", path, nameBuffer);
+      }
+
+      ImGui::Text("Adding: %s", fullNewPath.c_str());
+      ImGui::Separator();
+      auto entry = nt::GetEntry(nt::GetDefaultInstance(), fullNewPath);
+      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;
+        }
+      }
+
+      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) {
+    ImGui::Selectable(name);
+    entry.source->EmitDrag();
+  } else {
+    ImGui::Text("%s", 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);
+    }
+    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);
+    }
+    ImGui::NextColumn();
+  }
+
+  if (flags & NetworkTablesFlags_ShowTimestamp) {
+    if (entry.value) {
+      ImGui::Text("%f", (entry.value->last_change() * 1.0e-6) -
+                            (GetZeroTime() * 1.0e-6));
+    } else {
+      ImGui::TextUnformatted("");
+    }
+    ImGui::NextColumn();
+  }
+  ImGui::Separator();
+}
+
+static void EmitTree(const std::vector<NetworkTablesModel::TreeNode>& tree,
+                     NetworkTablesFlags flags) {
+  for (auto&& node : tree) {
+    if (node.entry) {
+      EmitEntry(*node.entry, node.name.c_str(), flags);
+    }
+
+    if (!node.children.empty()) {
+      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();
+      if (open) {
+        EmitTree(node.children, flags);
+        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;
+    }
+  }
+
+  const bool showFlags = (flags & NetworkTablesFlags_ShowFlags);
+  const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp);
+
+  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();
+  }
+  if (showTimestamp) {
+    ImGui::Text("Changed");
+    ImGui::NextColumn();
+  }
+  ImGui::Separator();
+  first = false;
+
+  if (flags & NetworkTablesFlags_TreeView) {
+    EmitTree(model->GetTreeRoot(), flags);
+  } else {
+    for (auto entry : model->GetEntries()) {
+      EmitEntry(*entry, entry->name.c_str(), flags);
+    }
+  }
+  ImGui::Columns();
+}
+
+void NetworkTablesFlagsSettings::Update() {
+  if (!m_pTreeView) {
+    auto& storage = GetStorage();
+    m_pTreeView = storage.GetBoolRef(
+        "tree", m_defaultFlags & NetworkTablesFlags_TreeView);
+    m_pShowConnections = storage.GetBoolRef(
+        "connections", m_defaultFlags & NetworkTablesFlags_ShowConnections);
+    m_pShowFlags = storage.GetBoolRef(
+        "flags", m_defaultFlags & NetworkTablesFlags_ShowFlags);
+    m_pShowTimestamp = storage.GetBoolRef(
+        "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp);
+    m_pCreateNoncanonicalKeys = storage.GetBoolRef(
+        "createNonCanonical",
+        m_defaultFlags & NetworkTablesFlags_CreateNoncanonicalKeys);
+  }
+
+  m_flags &=
+      ~(NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections |
+        NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp |
+        NetworkTablesFlags_CreateNoncanonicalKeys);
+  m_flags |=
+      (*m_pTreeView ? NetworkTablesFlags_TreeView : 0) |
+      (*m_pShowConnections ? NetworkTablesFlags_ShowConnections : 0) |
+      (*m_pShowFlags ? NetworkTablesFlags_ShowFlags : 0) |
+      (*m_pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0) |
+      (*m_pCreateNoncanonicalKeys ? NetworkTablesFlags_CreateNoncanonicalKeys
+                                  : 0);
+}
+
+void NetworkTablesFlagsSettings::DisplayMenu() {
+  if (!m_pTreeView) {
+    return;
+  }
+  ImGui::MenuItem("Tree View", "", m_pTreeView);
+  ImGui::MenuItem("Show Connections", "", m_pShowConnections);
+  ImGui::MenuItem("Show Flags", "", m_pShowFlags);
+  ImGui::MenuItem("Show Timestamp", "", m_pShowTimestamp);
+  ImGui::Separator();
+  ImGui::MenuItem("Allow creation of non-canonical keys", "",
+                  m_pCreateNoncanonicalKeys);
+}
+
+void NetworkTablesView::Display() {
+  m_flags.Update();
+  if (ImGui::BeginPopupContextItem()) {
+    m_flags.DisplayMenu();
+    ImGui::EndPopup();
+  }
+  DisplayNetworkTables(m_model, m_flags.GetFlags());
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
new file mode 100644
index 0000000..5cb5bbc
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
@@ -0,0 +1,19 @@
+// 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
new file mode 100644
index 0000000..8d991cb
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
@@ -0,0 +1,205 @@
+// 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/NetworkTablesProvider.h"
+
+#include <algorithm>
+
+#include <fmt/format.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpigui.h>
+
+using namespace glass;
+
+NetworkTablesProvider::NetworkTablesProvider(std::string_view iniName)
+    : NetworkTablesProvider{iniName, nt::GetDefaultInstance()} {}
+
+NetworkTablesProvider::NetworkTablesProvider(std::string_view iniName,
+                                             NT_Inst inst)
+    : Provider{fmt::format("{}Window", iniName)},
+      m_nt{inst},
+      m_typeCache{iniName} {
+  m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
+                           NT_NOTIFY_IMMEDIATE);
+}
+
+void NetworkTablesProvider::GlobalInit() {
+  Provider::GlobalInit();
+  wpi::gui::AddInit([this] { m_typeCache.Initialize(); });
+}
+
+void NetworkTablesProvider::DisplayMenu() {
+  wpi::SmallVector<std::string_view, 6> path;
+  wpi::SmallString<64> name;
+  for (auto&& entry : m_viewEntries) {
+    path.clear();
+    wpi::split(entry->name, path, '/', -1, false);
+
+    bool fullDepth = true;
+    int depth = 0;
+    for (; depth < (static_cast<int>(path.size()) - 1); ++depth) {
+      name = path[depth];
+      if (!ImGui::BeginMenu(name.c_str())) {
+        fullDepth = false;
+        break;
+      }
+    }
+
+    if (fullDepth) {
+      bool visible = entry->window && entry->window->IsVisible();
+      bool wasVisible = visible;
+      // FIXME: enabled?
+      // data is the last item, so is guaranteed to be null-terminated
+      ImGui::MenuItem(path.back().data(), nullptr, &visible, true);
+      if (!wasVisible && visible) {
+        Show(entry.get(), entry->window);
+      } else if (wasVisible && !visible && entry->window) {
+        entry->window->SetVisible(false);
+      }
+    }
+
+    for (; depth > 0; --depth) {
+      ImGui::EndMenu();
+    }
+  }
+}
+
+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);
+      }
+    } else if (event.flags & NT_NOTIFY_NEW) {
+      GetOrCreateView(builderIt->second, event.entry, tableName);
+      // cache the type
+      m_typeCache[tableName].SetName(event.value->GetString());
+    }
+  }
+
+  // check for visible windows that need displays (typically this is due to
+  // file loading)
+  for (auto&& window : m_windows) {
+    if (!window->IsVisible() || window->HasView()) {
+      continue;
+    }
+    auto id = window->GetId();
+    auto typeIt = m_typeCache.find(id);
+    if (typeIt == m_typeCache.end()) {
+      continue;
+    }
+
+    // only handle ones where we have a builder
+    auto builderIt = m_typeMap.find(typeIt->second.GetName());
+    if (builderIt == m_typeMap.end()) {
+      continue;
+    }
+
+    auto entry = GetOrCreateView(
+        builderIt->second,
+        nt::GetEntry(m_nt.GetInstance(), fmt::format("{}/.type", id)), id);
+    if (entry) {
+      Show(entry, window.get());
+    }
+  }
+}
+
+void NetworkTablesProvider::Register(std::string_view typeName,
+                                     CreateModelFunc createModel,
+                                     CreateViewFunc createView) {
+  m_typeMap[typeName] = Builder{std::move(createModel), std::move(createView)};
+}
+
+void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) {
+  // if there's already a window, just show it
+  if (entry->window) {
+    entry->window->SetVisible(true);
+    return;
+  }
+
+  // get or create model
+  if (!entry->modelEntry->model) {
+    entry->modelEntry->model =
+        entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str());
+  }
+  if (!entry->modelEntry->model) {
+    return;
+  }
+
+  // the window might exist and we're just not associated to it yet
+  if (!window) {
+    window = GetOrAddWindow(entry->name, true);
+  }
+  if (!window) {
+    return;
+  }
+  if (wpi::starts_with(entry->name, "/SmartDashboard/")) {
+    window->SetDefaultName(
+        fmt::format("{} (SmartDashboard)", wpi::drop_front(entry->name, 16)));
+  }
+  entry->window = window;
+
+  // create view
+  auto view = entry->createView(window, entry->modelEntry->model.get(),
+                                entry->name.c_str());
+  if (!view) {
+    return;
+  }
+  window->SetView(std::move(view));
+
+  entry->window->SetVisible(true);
+}
+
+NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView(
+    const Builder& builder, NT_Entry typeEntry, 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;
+    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;
+  } else {
+    modelIt = m_modelEntries.emplace(
+        modelIt, std::make_unique<Entry>(typeEntry, name, builder));
+  }
+
+  // create new view entry
+  viewIt = m_viewEntries.emplace(
+      viewIt,
+      std::make_unique<ViewEntry>(
+          name, modelIt->get(), [](Model*, const char*) { return true; },
+          builder.createView));
+
+  return viewIt->get();
+}
diff --git a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
new file mode 100644
index 0000000..28f4de4
--- /dev/null
+++ b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp
@@ -0,0 +1,133 @@
+// 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/NetworkTablesSettings.h"
+
+#include <optional>
+#include <string_view>
+#include <utility>
+
+#include <imgui.h>
+#include <imgui_stdlib.h>
+#include <ntcore_cpp.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+
+#include "glass/Context.h"
+
+using namespace glass;
+
+void NetworkTablesSettings::Thread::Main() {
+  while (m_active) {
+    // wait to be woken up
+    std::unique_lock lock(m_mutex);
+    m_cond.wait(lock, [&] { return !m_active || m_restart; });
+    if (!m_active) {
+      break;
+    }
+
+    // clear restart flag
+    m_restart = false;
+
+    int mode;
+    bool dsClient;
+
+    do {
+      mode = m_mode;
+      dsClient = m_dsClient;
+
+      // release lock while stopping to avoid blocking GUI
+      lock.unlock();
+
+      // 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) {
+        nt::StopClient(m_inst);
+        nt::StopServer(m_inst);
+        nt::StopLocal(m_inst);
+      }
+
+      if (m_mode != 1 || !dsClient) {
+        nt::StopDSClient(m_inst);
+      }
+
+      lock.lock();
+    } while (mode != m_mode || dsClient != m_dsClient);
+
+    if (m_mode == 1) {
+      std::string_view serverTeam{m_serverTeam};
+      std::optional<unsigned int> team;
+      if (!wpi::contains(serverTeam, '.') &&
+          (team = wpi::parse_integer<unsigned int>(serverTeam, 10))) {
+        nt::StartClientTeam(m_inst, team.value(), NT_DEFAULT_PORT);
+      } else {
+        wpi::SmallVector<std::string_view, 4> serverNames;
+        wpi::SmallVector<std::pair<std::string_view, unsigned int>, 4> servers;
+        wpi::split(serverTeam, serverNames, ',', -1, false);
+        for (auto&& serverName : serverNames) {
+          servers.emplace_back(serverName, NT_DEFAULT_PORT);
+        }
+        nt::StartClient(m_inst, servers);
+      }
+
+      if (m_dsClient) {
+        nt::StartDSClient(m_inst, NT_DEFAULT_PORT);
+      }
+    } else if (m_mode == 2) {
+      nt::StartServer(m_inst, m_iniName.c_str(), m_listenAddress.c_str(),
+                      NT_DEFAULT_PORT);
+    }
+  }
+}
+
+NetworkTablesSettings::NetworkTablesSettings(NT_Inst inst,
+                                             const char* storageName) {
+  auto& storage = glass::GetStorage(storageName);
+  m_pMode = storage.GetIntRef("mode");
+  m_pIniName = storage.GetStringRef("iniName", "networktables.ini");
+  m_pServerTeam = storage.GetStringRef("serverTeam");
+  m_pListenAddress = storage.GetStringRef("listenAddress");
+  m_pDsClient = storage.GetBoolRef("dsClient", true);
+
+  m_thread.Start(inst);
+}
+
+void NetworkTablesSettings::Update() {
+  if (!m_restart) {
+    return;
+  }
+  m_restart = false;
+
+  // do actual operation on thread
+  auto thr = m_thread.GetThread();
+  thr->m_restart = true;
+  thr->m_mode = *m_pMode;
+  thr->m_iniName = *m_pIniName;
+  thr->m_serverTeam = *m_pServerTeam;
+  thr->m_listenAddress = *m_pListenAddress;
+  thr->m_dsClient = *m_pDsClient;
+  thr->m_cond.notify_one();
+}
+
+bool NetworkTablesSettings::Display() {
+  static const char* modeOptions[] = {"Disabled", "Client", "Server"};
+  ImGui::Combo("Mode", m_pMode, modeOptions, m_serverOption ? 3 : 2);
+  switch (*m_pMode) {
+    case 1:
+      ImGui::InputText("Team/IP", m_pServerTeam);
+      ImGui::Checkbox("Get Address from DS", m_pDsClient);
+      break;
+    case 2:
+      ImGui::InputText("Listen Address", m_pListenAddress);
+      ImGui::InputText("ini Filename", m_pIniName);
+      break;
+    default:
+      break;
+  }
+  if (ImGui::Button("Apply")) {
+    m_restart = true;
+    return true;
+  }
+  return false;
+}
diff --git a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
new file mode 100644
index 0000000..0a5f234
--- /dev/null
+++ b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp
@@ -0,0 +1,176 @@
+// 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/NTCommandScheduler.h"
+#include "glass/networktables/NTCommandSelector.h"
+#include "glass/networktables/NTDifferentialDrive.h"
+#include "glass/networktables/NTDigitalInput.h"
+#include "glass/networktables/NTDigitalOutput.h"
+#include "glass/networktables/NTFMS.h"
+#include "glass/networktables/NTField2D.h"
+#include "glass/networktables/NTGyro.h"
+#include "glass/networktables/NTMecanumDrive.h"
+#include "glass/networktables/NTMechanism2D.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"
+
+using namespace glass;
+
+void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) {
+  provider.Register(
+      NTCommandSchedulerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTCommandSchedulerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(400, 200);
+        return MakeFunctionView([=] {
+          DisplayCommandScheduler(static_cast<NTCommandSchedulerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTCommandSelectorModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTCommandSelectorModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayCommandSelector(static_cast<NTCommandSelectorModel*>(model));
+        });
+      });
+  provider.Register(
+      NTDifferentialDriveModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDifferentialDriveModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(300, 350);
+        return MakeFunctionView([=] {
+          DisplayDrive(static_cast<NTDifferentialDriveModel*>(model));
+        });
+      });
+  provider.Register(
+      NTFMSModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTFMSModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView(
+            [=] { DisplayFMS(static_cast<FMSModel*>(model)); });
+      });
+  provider.Register(
+      NTDigitalInputModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDigitalInputModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayDIO(static_cast<NTDigitalInputModel*>(model), 0, true);
+        });
+      });
+  provider.Register(
+      NTDigitalOutputModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTDigitalOutputModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayDIO(static_cast<NTDigitalOutputModel*>(model), 0, true);
+        });
+      });
+  provider.Register(
+      NTField2DModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTField2DModel>(inst, path);
+      },
+      [=](Window* win, Model* model, const char* path) {
+        win->SetDefaultPos(200, 200);
+        win->SetDefaultSize(400, 200);
+        win->SetPadding(0, 0);
+        return std::make_unique<Field2DView>(
+            static_cast<NTField2DModel*>(model));
+      });
+  provider.Register(
+      NTGyroModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTGyroModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetDefaultSize(320, 380);
+        return MakeFunctionView(
+            [=] { DisplayGyro(static_cast<NTGyroModel*>(model)); });
+      });
+  provider.Register(
+      NTMecanumDriveModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTMecanumDriveModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetDefaultSize(300, 350);
+        return MakeFunctionView(
+            [=] { DisplayDrive(static_cast<NTMecanumDriveModel*>(model)); });
+      });
+  provider.Register(
+      NTMechanism2DModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTMechanism2DModel>(inst, path);
+      },
+      [=](Window* win, Model* model, const char* path) {
+        win->SetDefaultPos(400, 400);
+        win->SetDefaultSize(200, 200);
+        win->SetPadding(0, 0);
+        return std::make_unique<Mechanism2DView>(
+            static_cast<NTMechanism2DModel*>(model));
+      });
+  provider.Register(
+      NTPIDControllerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTPIDControllerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayPIDController(static_cast<NTPIDControllerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTSpeedControllerModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTSpeedControllerModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char* path) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplaySpeedController(static_cast<NTSpeedControllerModel*>(model));
+        });
+      });
+  provider.Register(
+      NTStringChooserModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTStringChooserModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView([=] {
+          DisplayStringChooser(static_cast<NTStringChooserModel*>(model));
+        });
+      });
+  provider.Register(
+      NTSubsystemModel::kType,
+      [](NT_Inst inst, const char* path) {
+        return std::make_unique<NTSubsystemModel>(inst, path);
+      },
+      [](Window* win, Model* model, const char*) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        return MakeFunctionView(
+            [=] { DisplaySubsystem(static_cast<NTSubsystemModel*>(model)); });
+      });
+}
diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
new file mode 100644
index 0000000..54dc778
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h
@@ -0,0 +1,47 @@
+// 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>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/CommandScheduler.h"
+
+namespace glass {
+class NTCommandSchedulerModel : public CommandSchedulerModel {
+ public:
+  static constexpr const char* kType = "Scheduler";
+
+  explicit NTCommandSchedulerModel(std::string_view path);
+  NTCommandSchedulerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<std::string>& GetCurrentCommands() override {
+    return m_commandsValue;
+  }
+
+  void CancelCommand(size_t index) override;
+
+  void Update() override;
+  bool Exists() override;
+  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;
+
+  std::string m_nameValue;
+  std::vector<std::string> m_commandsValue;
+  std::vector<double> 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
new file mode 100644
index 0000000..c936665
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h
@@ -0,0 +1,40 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/CommandSelector.h"
+
+namespace glass {
+class NTCommandSelectorModel : public CommandSelectorModel {
+ public:
+  static constexpr const char* kType = "Command";
+
+  explicit NTCommandSelectorModel(std::string_view path);
+  NTCommandSelectorModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  DataSource* GetRunningData() override { return &m_runningData; }
+  void SetRunning(bool run) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return false; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_running;
+  NT_Entry m_name;
+
+  DataSource m_runningData;
+  std::string m_nameValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
new file mode 100644
index 0000000..49b3eb0
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h
@@ -0,0 +1,53 @@
+// 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>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Drive.h"
+
+namespace glass {
+class NTDifferentialDriveModel : public DriveModel {
+ public:
+  static constexpr const char* kType = "DifferentialDrive";
+
+  explicit NTDifferentialDriveModel(std::string_view path);
+  NTDifferentialDriveModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
+    return m_wheels;
+  }
+
+  ImVec2 GetSpeedVector() const override { return m_speedVector; }
+  double GetRotation() const override { return m_rotation; }
+
+  void Update() override;
+  bool Exists() override;
+  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;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+  DataSource m_lPercentData;
+  DataSource m_rPercentData;
+
+  std::vector<DriveModel::WheelInfo> m_wheels;
+  ImVec2 m_speedVector;
+  double m_rotation;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
new file mode 100644
index 0000000..cd3dfeb
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h
@@ -0,0 +1,53 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/DIO.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+
+class NTDigitalInputModel : public DIOModel {
+ public:
+  static constexpr const char* kType = "Digital Input";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTDigitalInputModel(std::string_view path);
+  NTDigitalInputModel(NT_Inst inst, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DPWMModel* GetDPWM() override { return nullptr; }
+  DutyCycleModel* GetDutyCycle() override { return nullptr; }
+  EncoderModel* GetEncoder() override { return nullptr; }
+
+  bool IsInput() const override { return true; }
+
+  DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(bool val) override {}
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
new file mode 100644
index 0000000..8ed1ee7
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h
@@ -0,0 +1,55 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/DIO.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+
+class NTDigitalOutputModel : public DIOModel {
+ public:
+  static constexpr const char* kType = "Digital Output";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTDigitalOutputModel(std::string_view path);
+  NTDigitalOutputModel(NT_Inst inst, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DPWMModel* GetDPWM() override { return nullptr; }
+  DutyCycleModel* GetDutyCycle() override { return nullptr; }
+  EncoderModel* GetEncoder() override { return nullptr; }
+
+  bool IsInput() const override { return true; }
+
+  DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(bool val) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTFMS.h b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
new file mode 100644
index 0000000..b19a9f0
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTFMS.h
@@ -0,0 +1,70 @@
+// 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 <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/FMS.h"
+
+namespace glass {
+
+class NTFMSModel : public FMSModel {
+ public:
+  static constexpr const char* kType = "FMSInfo";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTFMSModel(std::string_view path);
+  NTFMSModel(NT_Inst inst, std::string_view path);
+
+  DataSource* GetFmsAttachedData() override { return &m_fmsAttached; }
+  DataSource* GetDsAttachedData() override { return &m_dsAttached; }
+  DataSource* GetAllianceStationIdData() override {
+    return &m_allianceStationId;
+  }
+  // NT does not provide match time
+  DataSource* GetMatchTimeData() override { return nullptr; }
+  DataSource* GetEStopData() override { return &m_estop; }
+  DataSource* GetEnabledData() override { return &m_enabled; }
+  DataSource* GetTestData() override { return &m_test; }
+  DataSource* GetAutonomousData() override { return &m_autonomous; }
+  std::string_view GetGameSpecificMessage(
+      wpi::SmallVectorImpl<char>& buf) override;
+
+  // NT is read-only (it's continually set by robot code)
+  void SetFmsAttached(bool val) override {}
+  void SetDsAttached(bool val) override {}
+  void SetAllianceStationId(int val) override {}
+  void SetMatchTime(double val) override {}
+  void SetEStop(bool val) override {}
+  void SetEnabled(bool val) override {}
+  void SetTest(bool val) override {}
+  void SetAutonomous(bool val) override {}
+  void SetGameSpecificMessage(const char* 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;
+
+  DataSource m_fmsAttached;
+  DataSource m_dsAttached;
+  DataSource m_allianceStationId;
+  DataSource m_estop;
+  DataSource m_enabled;
+  DataSource m_test;
+  DataSource m_autonomous;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTField2D.h b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
new file mode 100644
index 0000000..f966e0f
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTField2D.h
@@ -0,0 +1,55 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Field2D.h"
+
+namespace glass {
+
+class NTField2DModel : public Field2DModel {
+ public:
+  static constexpr const char* kType = "Field2d";
+
+  // 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() override;
+
+  const char* GetPath() const { return m_path.c_str(); }
+  const char* GetName() const { return m_nameValue.c_str(); }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override;
+
+  FieldObjectModel* AddFieldObject(std::string_view name) override;
+  void RemoveFieldObject(std::string_view name) override;
+  void ForEachFieldObject(
+      wpi::function_ref<void(FieldObjectModel& model, std::string_view name)>
+          func) override;
+
+ private:
+  NetworkTablesHelper m_nt;
+  std::string m_path;
+  NT_Entry m_name;
+  std::string m_nameValue;
+
+  class ObjectModel;
+  using Objects = std::vector<std::unique_ptr<ObjectModel>>;
+  Objects m_objects;
+
+  std::pair<Objects::iterator, bool> Find(std::string_view fullName);
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTGyro.h b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
new file mode 100644
index 0000000..db303a5
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTGyro.h
@@ -0,0 +1,42 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/Gyro.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+class NTGyroModel : public GyroModel {
+ public:
+  static constexpr const char* kType = "Gyro";
+
+  explicit NTGyroModel(std::string_view path);
+  NTGyroModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DataSource* GetAngleData() override { return &m_angleData; }
+  void SetAngle(double value) override {}
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_angle;
+  NT_Entry m_name;
+
+  DataSource m_angleData;
+  std::string m_nameValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
new file mode 100644
index 0000000..ce7d234
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h
@@ -0,0 +1,57 @@
+// 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>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Drive.h"
+
+namespace glass {
+class NTMecanumDriveModel : public DriveModel {
+ public:
+  static constexpr const char* kType = "MecanumDrive";
+
+  explicit NTMecanumDriveModel(std::string_view path);
+  NTMecanumDriveModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const std::vector<DriveModel::WheelInfo>& GetWheels() const override {
+    return m_wheels;
+  }
+
+  ImVec2 GetSpeedVector() const override { return m_speedVector; }
+  double GetRotation() const override { return m_rotation; }
+
+  void Update() override;
+  bool Exists() override;
+  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;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+  DataSource m_flPercentData;
+  DataSource m_frPercentData;
+  DataSource m_rlPercentData;
+  DataSource m_rrPercentData;
+
+  std::vector<DriveModel::WheelInfo> m_wheels;
+  ImVec2 m_speedVector;
+  double m_rotation;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
new file mode 100644
index 0000000..81f7df1
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h
@@ -0,0 +1,60 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <frc/geometry/Translation2d.h>
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Mechanism2D.h"
+
+namespace glass {
+
+class NTMechanism2DModel : public Mechanism2DModel {
+ public:
+  static constexpr const char* kType = "Mechanism2d";
+
+  // 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() override;
+
+  const char* GetPath() const { return m_path.c_str(); }
+  const char* GetName() const { return m_nameValue.c_str(); }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override;
+
+  frc::Translation2d GetDimensions() const override {
+    return m_dimensionsValue;
+  }
+  ImU32 GetBackgroundColor() const override { return m_bgColorValue; }
+  void ForEachRoot(
+      wpi::function_ref<void(MechanismRootModel& model)> func) override;
+
+ private:
+  NetworkTablesHelper m_nt;
+  std::string m_path;
+
+  NT_Entry m_name;
+  NT_Entry m_dimensions;
+  NT_Entry m_bgColor;
+
+  std::string m_nameValue;
+  frc::Translation2d m_dimensionsValue;
+  ImU32 m_bgColorValue = 0;
+
+  class RootModel;
+  std::vector<std::unique_ptr<RootModel>> m_roots;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
new file mode 100644
index 0000000..b975641
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h
@@ -0,0 +1,57 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/PIDController.h"
+
+namespace glass {
+class NTPIDControllerModel : public PIDControllerModel {
+ public:
+  static constexpr const char* kType = "PIDController";
+
+  explicit NTPIDControllerModel(std::string_view path);
+  NTPIDControllerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+
+  DataSource* GetPData() override { return &m_pData; }
+  DataSource* GetIData() override { return &m_iData; }
+  DataSource* GetDData() override { return &m_dData; }
+  DataSource* GetSetpointData() override { return &m_setpointData; }
+
+  void SetP(double value) override;
+  void SetI(double value) override;
+  void SetD(double value) override;
+  void SetSetpoint(double value) override;
+
+  void Update() override;
+  bool Exists() override;
+  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;
+
+  DataSource m_pData;
+  DataSource m_iData;
+  DataSource m_dData;
+  DataSource m_setpointData;
+
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
new file mode 100644
index 0000000..756d947
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h
@@ -0,0 +1,44 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/hardware/SpeedController.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+
+namespace glass {
+class NTSpeedControllerModel : public SpeedControllerModel {
+ public:
+  static constexpr const char* kType = "Speed Controller";
+
+  explicit NTSpeedControllerModel(std::string_view path);
+  NTSpeedControllerModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetSimDevice() const override { return nullptr; }
+
+  DataSource* GetPercentData() override { return &m_valueData; }
+  void SetPercent(double value) override;
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return !m_controllableValue; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_value;
+  NT_Entry m_name;
+  NT_Entry m_controllable;
+
+  DataSource m_valueData;
+  std::string m_nameValue;
+  bool m_controllableValue = false;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
new file mode 100644
index 0000000..2d806c9
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h
@@ -0,0 +1,54 @@
+// 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>
+#include <vector>
+
+#include <ntcore_cpp.h>
+
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/StringChooser.h"
+
+namespace glass {
+
+class NTStringChooserModel : public StringChooserModel {
+ public:
+  static constexpr const char* kType = "String Chooser";
+
+  // path is to the table containing ".type", excluding the trailing /
+  explicit NTStringChooserModel(std::string_view path);
+  NTStringChooserModel(NT_Inst inst, std::string_view path);
+
+  const std::string& GetDefault() override { return m_defaultValue; }
+  const std::string& GetSelected() override { return m_selectedValue; }
+  const std::string& GetActive() override { return m_activeValue; }
+  const std::vector<std::string>& GetOptions() override {
+    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;
+
+  std::string m_defaultValue;
+  std::string m_selectedValue;
+  std::string m_activeValue;
+  std::vector<std::string> m_optionsValue;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
new file mode 100644
index 0000000..c5862cf
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h
@@ -0,0 +1,46 @@
+// 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>
+#include <string_view>
+
+#include <ntcore_cpp.h>
+
+#include "glass/DataSource.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/other/Subsystem.h"
+
+namespace glass {
+class NTSubsystemModel : public SubsystemModel {
+ public:
+  static constexpr const char* kType = "Subsystem";
+
+  explicit NTSubsystemModel(std::string_view path);
+  NTSubsystemModel(NT_Inst instance, std::string_view path);
+
+  const char* GetName() const override { return m_nameValue.c_str(); }
+  const char* GetDefaultCommand() const override {
+    return m_defaultCommandValue.c_str();
+  }
+  const char* GetCurrentCommand() const override {
+    return m_currentCommandValue.c_str();
+  }
+
+  void Update() override;
+  bool Exists() override;
+  bool IsReadOnly() override { return true; }
+
+ private:
+  NetworkTablesHelper m_nt;
+  NT_Entry m_name;
+  NT_Entry m_defaultCommand;
+  NT_Entry m_currentCommand;
+
+  std::string m_nameValue;
+  std::string m_defaultCommandValue;
+  std::string m_currentCommandValue;
+};
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
new file mode 100644
index 0000000..8538eaa
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h
@@ -0,0 +1,141 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+#include <wpi/DenseMap.h>
+
+#include "glass/Model.h"
+#include "glass/View.h"
+
+namespace glass {
+
+class DataSource;
+
+class NetworkTablesModel : public Model {
+ public:
+  struct Entry {
+    explicit Entry(nt::EntryNotification&& event);
+
+    void UpdateValue();
+
+    /** Entry handle. */
+    NT_Entry entry;
+
+    /** Entry name. */
+    std::string name;
+
+    /** The value. */
+    std::shared_ptr<nt::Value> value;
+
+    /** Flags. */
+    unsigned int flags = 0;
+
+    /** String representation of the value (for arrays / complex values). */
+    std::string valueStr;
+
+    /** Data source (for numeric values). */
+    std::unique_ptr<DataSource> source;
+  };
+
+  struct TreeNode {
+    explicit TreeNode(std::string_view name) : name{name} {}
+
+    /** Short name (e.g. of just this node) */
+    std::string name;
+
+    /**
+     * Full path if entry is null (otherwise use entry->name),
+     * excluding trailing /
+     */
+    std::string path;
+
+    /** Null if no value at this node */
+    Entry* entry = nullptr;
+
+    /** Children of node, sorted by name */
+    std::vector<TreeNode> children;
+  };
+
+  NetworkTablesModel();
+  explicit NetworkTablesModel(NT_Inst inst);
+  ~NetworkTablesModel() override;
+
+  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; }
+
+ private:
+  NT_Inst m_inst;
+  NT_EntryListenerPoller m_poller;
+  wpi::DenseMap<NT_Entry, std::unique_ptr<Entry>> m_entries;
+
+  // sorted by name
+  std::vector<Entry*> m_sortedEntries;
+
+  std::vector<TreeNode> m_root;
+};
+
+using NetworkTablesFlags = int;
+
+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
+};
+
+void DisplayNetworkTables(
+    NetworkTablesModel* model,
+    NetworkTablesFlags flags = NetworkTablesFlags_Default);
+
+class NetworkTablesFlagsSettings {
+ public:
+  explicit NetworkTablesFlagsSettings(
+      NetworkTablesFlags defaultFlags = NetworkTablesFlags_Default)
+      : m_defaultFlags{defaultFlags}, m_flags{defaultFlags} {}
+
+  void Update();
+  void DisplayMenu();
+
+  NetworkTablesFlags GetFlags() const { return m_flags; }
+
+ private:
+  bool* m_pTreeView = nullptr;
+  bool* m_pShowConnections = nullptr;
+  bool* m_pShowFlags = nullptr;
+  bool* m_pShowTimestamp = nullptr;
+  bool* m_pCreateNoncanonicalKeys = nullptr;
+  NetworkTablesFlags m_defaultFlags;  // NOLINT
+  NetworkTablesFlags m_flags;         // NOLINT
+};
+
+class NetworkTablesView : public View {
+ public:
+  explicit NetworkTablesView(
+      NetworkTablesModel* model,
+      NetworkTablesFlags defaultFlags = NetworkTablesFlags_Default)
+      : m_model{model}, m_flags{defaultFlags} {}
+
+  void Display() override;
+
+ private:
+  NetworkTablesModel* m_model;
+  NetworkTablesFlagsSettings m_flags;
+};
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
new file mode 100644
index 0000000..aba3252
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h
@@ -0,0 +1,55 @@
+// 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
new file mode 100644
index 0000000..17374ca
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h
@@ -0,0 +1,114 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <ntcore_cpp.h>
+#include <wpi/StringMap.h>
+
+#include "glass/Model.h"
+#include "glass/Provider.h"
+#include "glass/networktables/NetworkTablesHelper.h"
+#include "glass/support/IniSaverInfo.h"
+#include "glass/support/IniSaverString.h"
+
+namespace glass {
+
+class Window;
+
+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 ViewExists = std::function<bool(Model*, const char* path)>;
+  using CreateView =
+      std::function<std::unique_ptr<View>(Window*, Model*, const char* path)>;
+};
+}  // namespace detail
+
+/**
+ * A provider for NetworkTables (SmartDashboard style) models and views.
+ */
+class NetworkTablesProvider : private Provider<detail::NTProviderFunctions> {
+ public:
+  using Provider::CreateModelFunc;
+  using Provider::CreateViewFunc;
+
+  explicit NetworkTablesProvider(std::string_view iniName);
+  NetworkTablesProvider(std::string_view iniName, NT_Inst inst);
+
+  /**
+   * Get the NetworkTables instance being used for this provider.
+   *
+   * @return NetworkTables instance
+   */
+  NT_Inst GetInstance() const { return m_nt.GetInstance(); }
+
+  /**
+   * Perform global initialization.  This should be called prior to
+   * wpi::gui::Initialize().
+   */
+  void GlobalInit() override;
+
+  /**
+   * Displays menu contents as a tree of available NetworkTables views.
+   */
+  void DisplayMenu() override;
+
+  /**
+   * Registers a NetworkTables model and view.
+   *
+   * @param typeName SmartDashboard .type value to match
+   * @param createModel functor to create model
+   * @param createView functor to create view
+   */
+  void Register(std::string_view typeName, CreateModelFunc createModel,
+                CreateViewFunc createView);
+
+  using WindowManager::AddWindow;
+
+ private:
+  void Update() override;
+
+  NetworkTablesHelper m_nt;
+
+  // cached mapping from table name to type string
+  IniSaverString<NameInfo> m_typeCache;
+
+  struct Builder {
+    CreateModelFunc createModel;
+    CreateViewFunc createView;
+  };
+
+  // mapping from .type string to model/view creators
+  wpi::StringMap<Builder> m_typeMap;
+
+  struct Entry : public ModelEntry {
+    Entry(NT_Entry typeEntry, std::string_view name, const Builder& builder)
+        : ModelEntry{name, [](NT_Inst, const char*) { return true; },
+                     builder.createModel},
+          typeEntry{typeEntry} {}
+    NT_Entry typeEntry;
+  };
+
+  void Show(ViewEntry* entry, Window* window) override;
+
+  ViewEntry* GetOrCreateView(const Builder& builder, NT_Entry typeEntry,
+                             std::string_view name);
+};
+
+/**
+ * Add "standard" set of NetworkTables models/views.
+ *
+ * @param provider NetworkTables provider
+ */
+void AddStandardNetworkTablesViews(NetworkTablesProvider& provider);
+
+}  // namespace glass
diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
new file mode 100644
index 0000000..7738541
--- /dev/null
+++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h
@@ -0,0 +1,59 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <string>
+
+#include <ntcore_cpp.h>
+#include <wpi/SafeThread.h>
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+}  // namespace wpi
+
+namespace glass {
+
+class NetworkTablesSettings {
+ public:
+  explicit NetworkTablesSettings(
+      NT_Inst inst = nt::GetDefaultInstance(),
+      const char* storageName = "NetworkTables Settings");
+
+  /**
+   * Enables or disables the server option.  Default is enabled.
+   */
+  void EnableServerOption(bool enable) { m_serverOption = enable; }
+
+  void Update();
+  bool Display();
+
+ private:
+  bool m_restart = true;
+  bool m_serverOption = true;
+  int* m_pMode;
+  std::string* m_pIniName;
+  std::string* m_pServerTeam;
+  std::string* m_pListenAddress;
+  bool* m_pDsClient;
+
+  class Thread : public wpi::SafeThread {
+   public:
+    explicit Thread(NT_Inst inst) : m_inst{inst} {}
+
+    void Main() override;
+
+    NT_Inst m_inst;
+    bool m_restart = false;
+    int m_mode;
+    std::string m_iniName;
+    std::string m_serverTeam;
+    std::string m_listenAddress;
+    bool m_dsClient;
+  };
+  wpi::SafeThreadOwner<Thread> m_thread;
+};
+
+}  // namespace glass