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/simulation/frc_gazebo_plugins/README.md b/simulation/frc_gazebo_plugins/README.md
index 4a5a59a..b89bbc5 100644
--- a/simulation/frc_gazebo_plugins/README.md
+++ b/simulation/frc_gazebo_plugins/README.md
@@ -13,7 +13,7 @@
 
 This command:
 
-    `./gradlew build -PmakeSim`
+    `./gradlew build -PforceGazebo`
 
 will force it to attempt to build.
 
diff --git a/simulation/frc_gazebo_plugins/build.gradle b/simulation/frc_gazebo_plugins/build.gradle
index d9db2b9..f42b70b 100644
--- a/simulation/frc_gazebo_plugins/build.gradle
+++ b/simulation/frc_gazebo_plugins/build.gradle
@@ -21,10 +21,10 @@
     gazebo_linker_args = "pkg-config --libs gazebo protobuf".execute().text.split()
 } catch(Exception ex) { }
 
-if (project.hasProperty("makeSim")) {
+if (project.hasProperty("forceGazebo")) {
     if (!gazebo_version?.trim()) {
         println "Gazebo development files are not available. (pkg-config --modversion gazebo failed)"
-        println "makeSim set. Forcing build - failure likely."
+        println "forceGazebo set. Forcing build - failure likely."
     }
 } else {
     ext.skip_frc_plugins = true
@@ -56,7 +56,6 @@
                 cpp.lib library:  "${component.name}", linkage: "static"
             }
         }
-
     }
 
     /* TODO:  Finish writing the test case */
@@ -97,7 +96,7 @@
             //    project(':gmock').addGmockToLinker(it)
             //}
             //else {
-                buildable = false
+            buildable = false
             //}
         }
     }
diff --git a/simulation/frc_gazebo_plugins/src/clock/cpp/clock.cpp b/simulation/frc_gazebo_plugins/src/clock/cpp/clock.cpp
index c7440eb..d588708 100644
--- a/simulation/frc_gazebo_plugins/src/clock/cpp/clock.cpp
+++ b/simulation/frc_gazebo_plugins/src/clock/cpp/clock.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "clock.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/clock/headers/clock.h b/simulation/frc_gazebo_plugins/src/clock/headers/clock.h
index fcdc2bd..3ff7d12 100644
--- a/simulation/frc_gazebo_plugins/src/clock/headers/clock.h
+++ b/simulation/frc_gazebo_plugins/src/clock/headers/clock.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/clockTest/cpp/clockTest.cpp b/simulation/frc_gazebo_plugins/src/clockTest/cpp/ClockTest.cpp
similarity index 67%
rename from simulation/frc_gazebo_plugins/src/clockTest/cpp/clockTest.cpp
rename to simulation/frc_gazebo_plugins/src/clockTest/cpp/ClockTest.cpp
index 275425c..84bde78 100644
--- a/simulation/frc_gazebo_plugins/src/clockTest/cpp/clockTest.cpp
+++ b/simulation/frc_gazebo_plugins/src/clockTest/cpp/ClockTest.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <simulation/gz_msgs/msgs.h>
 
@@ -21,9 +18,11 @@
 static char* world_sdf;
 static double latest_time;
 
-void cb(gazebo::msgs::ConstFloat64Ptr& msg) { latest_time = msg->data(); }
+void cb(gazebo::msgs::ConstFloat64Ptr& msg) {
+  latest_time = msg->data();
+}
 
-TEST(ClockTests, test_clock) {
+TEST(ClockTest, Clock) {
   gazebo::physics::WorldPtr world;
 
   ASSERT_TRUE(library);
@@ -67,9 +66,13 @@
 int main(int argc, char** argv) {
   testing::InitGoogleTest(&argc, argv);
 
-  if (argc >= 1) library = argv[1];
+  if (argc >= 1) {
+    library = argv[1];
+  }
 
-  if (argc >= 2) world_sdf = argv[2];
+  if (argc >= 2) {
+    world_sdf = argv[2];
+  }
 
   return RUN_ALL_TESTS();
 }
diff --git a/simulation/frc_gazebo_plugins/src/dc_motor/cpp/dc_motor.cpp b/simulation/frc_gazebo_plugins/src/dc_motor/cpp/dc_motor.cpp
index 8cc3039..7951dfc 100644
--- a/simulation/frc_gazebo_plugins/src/dc_motor/cpp/dc_motor.cpp
+++ b/simulation/frc_gazebo_plugins/src/dc_motor/cpp/dc_motor.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "dc_motor.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/dc_motor/headers/dc_motor.h b/simulation/frc_gazebo_plugins/src/dc_motor/headers/dc_motor.h
index f9c2523..44440ac 100644
--- a/simulation/frc_gazebo_plugins/src/dc_motor/headers/dc_motor.h
+++ b/simulation/frc_gazebo_plugins/src/dc_motor/headers/dc_motor.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/drive_motor/cpp/drive_motor.cpp b/simulation/frc_gazebo_plugins/src/drive_motor/cpp/drive_motor.cpp
index e6511bb..01679e9 100644
--- a/simulation/frc_gazebo_plugins/src/drive_motor/cpp/drive_motor.cpp
+++ b/simulation/frc_gazebo_plugins/src/drive_motor/cpp/drive_motor.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "drive_motor.h"
 
@@ -90,7 +87,9 @@
 
 static double computeForce(double input, double velocity, double max) {
   double output = input;
-  if (max == 0.0) return output;
+  if (max == 0.0) {
+    return output;
+  }
   if (std::fabs(velocity) >= max) {
     output = 0;
   } else {
@@ -107,7 +106,8 @@
   ignition::math::Vector3d velocity = parent->GetRelativeLinearVel().Ign();
 #endif
 
-  if (signal == 0) return;
+  if (signal == 0)
+    return;
 
   double x = computeForce(signal * dx * multiplier, velocity.X(),
                           std::fabs(maxSpeed * dx));
diff --git a/simulation/frc_gazebo_plugins/src/drive_motor/headers/drive_motor.h b/simulation/frc_gazebo_plugins/src/drive_motor/headers/drive_motor.h
index 2033223..8516961 100644
--- a/simulation/frc_gazebo_plugins/src/drive_motor/headers/drive_motor.h
+++ b/simulation/frc_gazebo_plugins/src/drive_motor/headers/drive_motor.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/encoder/cpp/encoder.cpp b/simulation/frc_gazebo_plugins/src/encoder/cpp/encoder.cpp
index 37551f2..c204701 100644
--- a/simulation/frc_gazebo_plugins/src/encoder/cpp/encoder.cpp
+++ b/simulation/frc_gazebo_plugins/src/encoder/cpp/encoder.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "encoder.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/encoder/headers/encoder.h b/simulation/frc_gazebo_plugins/src/encoder/headers/encoder.h
index 4ecc17a..72f3542 100644
--- a/simulation/frc_gazebo_plugins/src/encoder/headers/encoder.h
+++ b/simulation/frc_gazebo_plugins/src/encoder/headers/encoder.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/gyro/cpp/gyro.cpp b/simulation/frc_gazebo_plugins/src/gyro/cpp/gyro.cpp
index 65d42b7..b30d9b7 100644
--- a/simulation/frc_gazebo_plugins/src/gyro/cpp/gyro.cpp
+++ b/simulation/frc_gazebo_plugins/src/gyro/cpp/gyro.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "gyro.h"
 
@@ -23,9 +20,12 @@
   }
 
   std::string axisString = sdf->Get<std::string>("axis");
-  if (axisString == "roll") axis = Roll;
-  if (axisString == "pitch") axis = Pitch;
-  if (axisString == "yaw") axis = Yaw;
+  if (axisString == "roll")
+    axis = Roll;
+  if (axisString == "pitch")
+    axis = Pitch;
+  if (axisString == "yaw")
+    axis = Yaw;
 
   if (sdf->HasElement("units")) {
     radians = sdf->Get<std::string>("units") != "degrees";
diff --git a/simulation/frc_gazebo_plugins/src/gyro/headers/gyro.h b/simulation/frc_gazebo_plugins/src/gyro/headers/gyro.h
index 3faa144..adc332b 100644
--- a/simulation/frc_gazebo_plugins/src/gyro/headers/gyro.h
+++ b/simulation/frc_gazebo_plugins/src/gyro/headers/gyro.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -16,7 +13,7 @@
 #include "simulation/gz_msgs/msgs.h"
 
 /// \brief The axis about which to measure rotation.
-typedef enum { Roll /*X*/, Pitch /*Y*/, Yaw /*Z*/ } ROTATION;
+enum ROTATION { Roll /*X*/, Pitch /*Y*/, Yaw /*Z*/ };
 
 /**
  * \brief Plugin for reading the speed and relative angle of a link.
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/external_limit_switch.cpp b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/external_limit_switch.cpp
index c5780ae..b15ab71 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/external_limit_switch.cpp
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/external_limit_switch.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "external_limit_switch.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/internal_limit_switch.cpp b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/internal_limit_switch.cpp
index adfd860..f01ceb6 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/internal_limit_switch.cpp
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/internal_limit_switch.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "internal_limit_switch.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/limit_switch.cpp b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/limit_switch.cpp
index 34b7ae3..c380bae 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/cpp/limit_switch.cpp
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/cpp/limit_switch.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "limit_switch.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/headers/external_limit_switch.h b/simulation/frc_gazebo_plugins/src/limit_switch/headers/external_limit_switch.h
index 2114b71..8a39f61 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/headers/external_limit_switch.h
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/headers/external_limit_switch.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -22,7 +19,7 @@
   explicit ExternalLimitSwitch(sdf::ElementPtr sdf);
 
   /// \brief Returns true when the switch is triggered.
-  virtual bool Get();
+  bool Get() override;
 
  private:
   gazebo::sensors::ContactSensorPtr sensor;
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/headers/internal_limit_switch.h b/simulation/frc_gazebo_plugins/src/limit_switch/headers/internal_limit_switch.h
index 870e8e3..11159c7 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/headers/internal_limit_switch.h
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/headers/internal_limit_switch.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -21,7 +18,7 @@
   InternalLimitSwitch(gazebo::physics::ModelPtr model, sdf::ElementPtr sdf);
 
   /// \brief Returns true when the switch is triggered.
-  virtual bool Get();
+  bool Get() override;
 
  private:
   gazebo::physics::JointPtr joint;
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/headers/limit_switch.h b/simulation/frc_gazebo_plugins/src/limit_switch/headers/limit_switch.h
index 23ae2cc..089cfa1 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/headers/limit_switch.h
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/headers/limit_switch.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/limit_switch/headers/switch.h b/simulation/frc_gazebo_plugins/src/limit_switch/headers/switch.h
index 0387647..cc7018e 100644
--- a/simulation/frc_gazebo_plugins/src/limit_switch/headers/switch.h
+++ b/simulation/frc_gazebo_plugins/src/limit_switch/headers/switch.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/pneumatic_piston/cpp/pneumatic_piston.cpp b/simulation/frc_gazebo_plugins/src/pneumatic_piston/cpp/pneumatic_piston.cpp
index f0e4c7e..a1f8d8d 100644
--- a/simulation/frc_gazebo_plugins/src/pneumatic_piston/cpp/pneumatic_piston.cpp
+++ b/simulation/frc_gazebo_plugins/src/pneumatic_piston/cpp/pneumatic_piston.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "pneumatic_piston.h"
 
@@ -81,7 +78,8 @@
        the lack of the forward signal suffices.
        Note that a true simulation would not allow a SingleSolenoid to
        have reverse force, but we put that in the hands of the model builder.*/
-    if (reverse_topic.empty() || reverse_signal) force = reverse_force;
+    if (reverse_topic.empty() || reverse_signal)
+      force = reverse_force;
   }
   joint->SetForce(0, force);
 }
diff --git a/simulation/frc_gazebo_plugins/src/pneumatic_piston/headers/pneumatic_piston.h b/simulation/frc_gazebo_plugins/src/pneumatic_piston/headers/pneumatic_piston.h
index ba8becf..bcc02b4 100644
--- a/simulation/frc_gazebo_plugins/src/pneumatic_piston/headers/pneumatic_piston.h
+++ b/simulation/frc_gazebo_plugins/src/pneumatic_piston/headers/pneumatic_piston.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/potentiometer/cpp/potentiometer.cpp b/simulation/frc_gazebo_plugins/src/potentiometer/cpp/potentiometer.cpp
index 140afd1..9691a4d 100644
--- a/simulation/frc_gazebo_plugins/src/potentiometer/cpp/potentiometer.cpp
+++ b/simulation/frc_gazebo_plugins/src/potentiometer/cpp/potentiometer.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "potentiometer.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/potentiometer/headers/potentiometer.h b/simulation/frc_gazebo_plugins/src/potentiometer/headers/potentiometer.h
index f9111cb..5fe6d66 100644
--- a/simulation/frc_gazebo_plugins/src/potentiometer/headers/potentiometer.h
+++ b/simulation/frc_gazebo_plugins/src/potentiometer/headers/potentiometer.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/rangefinder/cpp/rangefinder.cpp b/simulation/frc_gazebo_plugins/src/rangefinder/cpp/rangefinder.cpp
index a01f10f..be8bbe6 100644
--- a/simulation/frc_gazebo_plugins/src/rangefinder/cpp/rangefinder.cpp
+++ b/simulation/frc_gazebo_plugins/src/rangefinder/cpp/rangefinder.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "rangefinder.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/rangefinder/headers/rangefinder.h b/simulation/frc_gazebo_plugins/src/rangefinder/headers/rangefinder.h
index 6a395b6..6fee59b 100644
--- a/simulation/frc_gazebo_plugins/src/rangefinder/headers/rangefinder.h
+++ b/simulation/frc_gazebo_plugins/src/rangefinder/headers/rangefinder.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/frc_gazebo_plugins/src/servo/cpp/servo.cpp b/simulation/frc_gazebo_plugins/src/servo/cpp/servo.cpp
index ca9d361..e090d0c 100644
--- a/simulation/frc_gazebo_plugins/src/servo/cpp/servo.cpp
+++ b/simulation/frc_gazebo_plugins/src/servo/cpp/servo.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "servo.h"
 
diff --git a/simulation/frc_gazebo_plugins/src/servo/headers/servo.h b/simulation/frc_gazebo_plugins/src/servo/headers/servo.h
index 6d8ab08..d934385 100644
--- a/simulation/frc_gazebo_plugins/src/servo/headers/servo.h
+++ b/simulation/frc_gazebo_plugins/src/servo/headers/servo.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/gz_msgs/README.md b/simulation/gz_msgs/README.md
index 7df7a79..6850be7 100644
--- a/simulation/gz_msgs/README.md
+++ b/simulation/gz_msgs/README.md
@@ -7,6 +7,6 @@
 
 If it's not found via pkg-config, then it's build is diabled.
 
-You can force it by specifying -PmakeSim on the gradle command line.
+You can force it by specifying -PforceGazebo on the gradle command line.
 
 If you are installing FRCSim with the script, then this *should* have be done for you.
diff --git a/simulation/gz_msgs/build.gradle b/simulation/gz_msgs/build.gradle
index d07dc09..ccc933e 100644
--- a/simulation/gz_msgs/build.gradle
+++ b/simulation/gz_msgs/build.gradle
@@ -1,7 +1,7 @@
 plugins {
     id 'cpp'
     id 'java'
-    id 'com.google.protobuf' version '0.8.8'
+    id 'com.google.protobuf' version '0.8.17'
     id 'edu.wpi.first.NativeUtils'
 }
 
@@ -13,11 +13,9 @@
 apply from: "${rootDir}/shared/config.gradle"
 
 /* Use a sort of poor man's autoconf to find the protobuf development
-   files; on Debian, those are supplied by libprotobuf-dev.
-
-   This should get skipped on Windows.
-
-   TODO:  Add Windows support for the simulation code */
+ files; on Debian, those are supplied by libprotobuf-dev.
+ This should get skipped on Windows.
+ TODO:  Add Windows support for the simulation code */
 
 def protobuf_version = ""
 try {
@@ -26,10 +24,10 @@
 } catch(Exception ex) {
 }
 
-if (project.hasProperty("makeSim")) {
+if (project.hasProperty("forceGazebo")) {
     if (!protobuf_version?.trim()) {
         println "Protobuf is not available. (pkg-config --modversion protobuf failed)"
-        println "makeSim set. Forcing build - failure likely."
+        println "forceGazebo set. Forcing build - failure likely."
     }
 } else {
     ext.skip_gz_msgs = true
@@ -41,14 +39,14 @@
 }
 
 dependencies {
-      implementation "com.google.protobuf:protobuf-java:${protobuf_version}"
-      implementation "com.google.protobuf:protoc:${protobuf_version}"
+    implementation "com.google.protobuf:protobuf-java:${protobuf_version}"
+    implementation "com.google.protobuf:protoc:${protobuf_version}"
 }
 
 /* There is a nice gradle plugin for protobuf, and the protoc tool
-   is included; using it simplifies our build process.
-   The trick is that we have to use the same version as the system
-   copy of libprotobuf-dev */
+ is included; using it simplifies our build process.
+ The trick is that we have to use the same version as the system
+ copy of libprotobuf-dev */
 protobuf {
     protoc {
         artifact = "com.google.protobuf:protoc:${protobuf_version}"
diff --git a/simulation/halsim_ds_socket/CMakeLists.txt b/simulation/halsim_ds_socket/CMakeLists.txt
index 3e2518c..bc9adb1 100644
--- a/simulation/halsim_ds_socket/CMakeLists.txt
+++ b/simulation/halsim_ds_socket/CMakeLists.txt
@@ -4,7 +4,7 @@
 
 file(GLOB halsim_ds_socket_src src/main/native/cpp/*.cpp)
 
-add_library(halsim_ds_socket MODULE ${halsim_ds_socket_src})
+add_library(halsim_ds_socket SHARED ${halsim_ds_socket_src})
 wpilib_target_warnings(halsim_ds_socket)
 set_target_properties(halsim_ds_socket PROPERTIES DEBUG_POSTFIX "d")
 target_link_libraries(halsim_ds_socket PUBLIC hal)
diff --git a/simulation/halsim_ds_socket/build.gradle b/simulation/halsim_ds_socket/build.gradle
index 440e5d9..63c5576 100644
--- a/simulation/halsim_ds_socket/build.gradle
+++ b/simulation/halsim_ds_socket/build.gradle
@@ -49,7 +49,7 @@
             lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
             lib library: pluginName, linkage: 'shared'
             if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                nativeUtils.useRequiredLibrary(it, 'netcomm_shared', 'chipobject_shared', 'visa_shared', 'ni_runtime_shared')
+                nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
             }
         }
     }
diff --git a/simulation/halsim_ds_socket/src/dev/native/cpp/main.cpp b/simulation/halsim_ds_socket/src/dev/native/cpp/main.cpp
index 2c1e83a..cf3adc6 100644
--- a/simulation/halsim_ds_socket/src/dev/native/cpp/main.cpp
+++ b/simulation/halsim_ds_socket/src/dev/native/cpp/main.cpp
@@ -1,16 +1,11 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <thread>
 
 #include <hal/DriverStation.h>
 #include <hal/HALBase.h>
-#include <wpi/Format.h>
-#include <wpi/raw_ostream.h>
 
 extern "C" int HALSIM_InitExtension(void);
 
diff --git a/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp b/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
index 4a87f13..1aff988 100644
--- a/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
+++ b/simulation/halsim_ds_socket/src/main/native/cpp/DSCommPacket.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "DSCommPacket.h"
 
@@ -15,8 +12,7 @@
 
 #include <hal/simulation/DriverStationData.h>
 #include <hal/simulation/MockHooks.h>
-#include <wpi/ArrayRef.h>
-#include <wpi/Format.h>
+#include <wpi/span.h>
 
 using namespace halsim;
 
@@ -49,8 +45,10 @@
   m_alliance_station = static_cast<HAL_AllianceStationID>(station_code);
 }
 
-void DSCommPacket::ReadMatchtimeTag(wpi::ArrayRef<uint8_t> tagData) {
-  if (tagData.size() < 6) return;
+void DSCommPacket::ReadMatchtimeTag(wpi::span<const uint8_t> tagData) {
+  if (tagData.size() < 6) {
+    return;
+  }
 
   uint32_t store = tagData[2] << 24;
   store |= tagData[3] << 16;
@@ -65,7 +63,7 @@
   m_match_time = matchTime;
 }
 
-void DSCommPacket::ReadJoystickTag(wpi::ArrayRef<uint8_t> dataInput,
+void DSCommPacket::ReadJoystickTag(wpi::span<const uint8_t> dataInput,
                                    int index) {
   DSCommJoystickPacket& stick = m_joystick_packets[index];
   stick.ResetUdp();
@@ -74,7 +72,7 @@
     return;
   }
 
-  dataInput = dataInput.slice(2);
+  dataInput = dataInput.subspan(2);
 
   // Read axes
   int axesLength = dataInput[0];
@@ -88,7 +86,7 @@
   }
   stick.axes.count = axesLength;
 
-  dataInput = dataInput.slice(1 + axesLength);
+  dataInput = dataInput.subspan(1 + axesLength);
 
   // Read Buttons
   int buttonCount = dataInput[0];
@@ -99,7 +97,7 @@
   }
   stick.buttons.count = buttonCount;
 
-  dataInput = dataInput.slice(1 + numBytes);
+  dataInput = dataInput.subspan(1 + numBytes);
 
   int povsLength = dataInput[0];
   for (int i = 0; i < povsLength * 2; i += 2) {
@@ -114,11 +112,11 @@
 /*----------------------------------------------------------------------------
 **  Communication methods
 **--------------------------------------------------------------------------*/
-void DSCommPacket::DecodeTCP(wpi::ArrayRef<uint8_t> packet) {
+void DSCommPacket::DecodeTCP(wpi::span<const uint8_t> packet) {
   // No header
   while (!packet.empty()) {
     int tagLength = packet[0] << 8 | packet[1];
-    auto tagPacket = packet.slice(0, tagLength + 2);
+    auto tagPacket = packet.subspan(0, tagLength + 2);
 
     if (tagLength == 0) {
       return;
@@ -135,12 +133,14 @@
         ReadNewMatchInfoTag(tagPacket);
         break;
     }
-    packet = packet.slice(tagLength + 2);
+    packet = packet.subspan(tagLength + 2);
   }
 }
 
-void DSCommPacket::DecodeUDP(wpi::ArrayRef<uint8_t> packet) {
-  if (packet.size() < 6) return;
+void DSCommPacket::DecodeUDP(wpi::span<const uint8_t> packet) {
+  if (packet.size() < 6) {
+    return;
+  }
   // Decode fixed header
   m_hi = packet[0];
   m_lo = packet[1];
@@ -149,17 +149,19 @@
   SetAlliance(packet[5]);
 
   // Return if packet finished
-  if (packet.size() == 6) return;
+  if (packet.size() == 6) {
+    return;
+  }
 
   // Else, handle tagged data
-  packet = packet.slice(6);
+  packet = packet.subspan(6);
 
   int joystickNum = 0;
 
   // Loop to handle multiple tags
   while (!packet.empty()) {
     auto tagLength = packet[0];
-    auto tagPacket = packet.slice(0, tagLength + 1);
+    auto tagPacket = packet.subspan(0, tagLength + 1);
 
     switch (packet[1]) {
       case kJoystickDataTag:
@@ -170,13 +172,15 @@
         ReadMatchtimeTag(tagPacket);
         break;
     }
-    packet = packet.slice(tagLength + 1);
+    packet = packet.subspan(tagLength + 1);
   }
 }
 
-void DSCommPacket::ReadNewMatchInfoTag(wpi::ArrayRef<uint8_t> data) {
+void DSCommPacket::ReadNewMatchInfoTag(wpi::span<const uint8_t> data) {
   // Size 2 bytes, tag 1 byte
-  if (data.size() <= 3) return;
+  if (data.size() <= 3) {
+    return;
+  }
 
   int nameLength = std::min<size_t>(data[3], sizeof(matchInfo.eventName) - 1);
 
@@ -186,9 +190,11 @@
 
   matchInfo.eventName[nameLength] = '\0';
 
-  data = data.slice(4 + nameLength);
+  data = data.subspan(4 + nameLength);
 
-  if (data.size() < 4) return;
+  if (data.size() < 4) {
+    return;
+  }
 
   matchInfo.matchType = static_cast<HAL_MatchType>(
       data[0]);  // None, Practice, Qualification, Elimination, Test
@@ -198,9 +204,11 @@
   HALSIM_SetMatchInfo(&matchInfo);
 }
 
-void DSCommPacket::ReadGameSpecificMessageTag(wpi::ArrayRef<uint8_t> data) {
+void DSCommPacket::ReadGameSpecificMessageTag(wpi::span<const uint8_t> data) {
   // Size 2 bytes, tag 1 byte
-  if (data.size() <= 3) return;
+  if (data.size() <= 3) {
+    return;
+  }
 
   int length = std::min<size_t>(((data[0] << 8) | data[1]) - 1,
                                 sizeof(matchInfo.gameSpecificMessage));
@@ -212,9 +220,11 @@
 
   HALSIM_SetMatchInfo(&matchInfo);
 }
-void DSCommPacket::ReadJoystickDescriptionTag(wpi::ArrayRef<uint8_t> data) {
-  if (data.size() < 3) return;
-  data = data.slice(3);
+void DSCommPacket::ReadJoystickDescriptionTag(wpi::span<const uint8_t> data) {
+  if (data.size() < 3) {
+    return;
+  }
+  data = data.subspan(3);
   int joystickNum = data[0];
   DSCommJoystickPacket& packet = m_joystick_packets[joystickNum];
   packet.ResetTcp();
@@ -225,14 +235,16 @@
   for (int i = 0; i < nameLength; i++) {
     packet.descriptor.name[i] = data[4 + i];
   }
-  data = data.slice(4 + nameLength);
+  data = data.subspan(4 + nameLength);
   packet.descriptor.name[nameLength] = '\0';
   int axesCount = data[0];
   packet.descriptor.axisCount = axesCount;
-  for (int i = 0; i < axesCount; i++) {
+  for (int i = 0,
+           len = std::min<int>(axesCount, sizeof(packet.descriptor.axisTypes));
+       i < len; i++) {
     packet.descriptor.axisTypes[i] = data[1 + i];
   }
-  data = data.slice(1 + axesCount);
+  data = data.subspan(1 + axesCount);
 
   packet.descriptor.buttonCount = data[0];
   packet.descriptor.povCount = data[1];
diff --git a/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp b/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
index bafe8b3..799cdd9 100644
--- a/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_ds_socket/src/main/native/cpp/main.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.
 
 /*----------------------------------------------------------------------------
 **  This extension reimplements enough of the FRC_Network layer to enable the
@@ -17,13 +14,14 @@
 #include <sys/types.h>
 
 #include <atomic>
+#include <cstdio>
 #include <cstring>
+#include <string_view>
 
 #include <DSCommPacket.h>
+#include <fmt/format.h>
 #include <hal/Extensions.h>
 #include <wpi/EventLoopRunner.h>
-#include <wpi/StringRef.h>
-#include <wpi/raw_ostream.h>
 #include <wpi/raw_uv_ostream.h>
 #include <wpi/uv/Tcp.h>
 #include <wpi/uv/Timer.h>
@@ -53,14 +51,16 @@
 }
 
 static void HandleTcpDataStream(Buffer& buf, size_t size, DataStore& store) {
-  wpi::StringRef data{buf.base, size};
+  std::string_view data{buf.base, size};
   while (!data.empty()) {
     if (store.m_frameSize == (std::numeric_limits<size_t>::max)()) {
       if (store.m_frame.size() < 2u) {
         size_t toCopy = (std::min)(2u - store.m_frame.size(), data.size());
-        store.m_frame.append(data.bytes_begin(), data.bytes_begin() + toCopy);
-        data = data.drop_front(toCopy);
-        if (store.m_frame.size() < 2u) return;  // need more data
+        store.m_frame.append(data.data(), data.data() + toCopy);
+        data.remove_prefix(toCopy);
+        if (store.m_frame.size() < 2u) {
+          return;  // need more data
+        }
       }
       store.m_frameSize = (static_cast<uint16_t>(store.m_frame[0]) << 8) |
                           static_cast<uint16_t>(store.m_frame[1]);
@@ -68,8 +68,8 @@
     if (store.m_frameSize != (std::numeric_limits<size_t>::max)()) {
       size_t need = store.m_frameSize - (store.m_frame.size() - 2);
       size_t toCopy = (std::min)(need, data.size());
-      store.m_frame.append(data.bytes_begin(), data.bytes_begin() + toCopy);
-      data = data.drop_front(toCopy);
+      store.m_frame.append(data.data(), data.data() + toCopy);
+      data.remove_prefix(toCopy);
       need -= toCopy;
       if (need == 0) {
         auto ds = store.dsPacket;
@@ -116,13 +116,12 @@
   struct sockaddr_in simAddr;
   NameToAddr("127.0.0.1", 1135, &simAddr);
   simLoopTimer->timeout.connect([udpLocal = udp.get(), simAddr] {
-    udpLocal->Send(simAddr, wpi::ArrayRef<Buffer>{singleByte.get(), 1},
-                   [](auto buf, Error err) {
-                     if (err) {
-                       wpi::errs() << err.str() << "\n";
-                       wpi::errs().flush();
-                     }
-                   });
+    udpLocal->Send(simAddr, {singleByte.get(), 1}, [](auto buf, Error err) {
+      if (err) {
+        fmt::print(stderr, "{}\n", err.str());
+        std::fflush(stderr);
+      }
+    });
   });
   simLoopTimer->Start(Timer::Time{100}, Timer::Time{100});
 
@@ -131,8 +130,7 @@
                                                const sockaddr& recSock,
                                                unsigned int port) {
     auto ds = udpLocal->GetLoop()->GetData<halsim::DSCommPacket>();
-    ds->DecodeUDP(
-        wpi::ArrayRef<uint8_t>{reinterpret_cast<uint8_t*>(buf.base), len});
+    ds->DecodeUDP({reinterpret_cast<uint8_t*>(buf.base), len});
 
     struct sockaddr_in outAddr;
     std::memcpy(&outAddr, &recSock, sizeof(sockaddr_in));
@@ -147,8 +145,8 @@
     udpLocal->Send(outAddr, sendBufs, [](auto bufs, Error err) {
       GetBufferPool().Release(bufs);
       if (err) {
-        wpi::errs() << err.str() << "\n";
-        wpi::errs().flush();
+        fmt::print(stderr, "{}\n", err.str());
+        std::fflush(stderr);
       }
     });
     ds->SendUDPToHALSim();
@@ -178,12 +176,12 @@
   static bool once = false;
 
   if (once) {
-    wpi::errs() << "Error: cannot invoke HALSIM_InitExtension twice.\n";
+    std::fputs("Error: cannot invoke HALSIM_InitExtension twice.\n", stderr);
     return -1;
   }
   once = true;
 
-  wpi::outs() << "DriverStationSocket Initializing.\n";
+  std::puts("DriverStationSocket Initializing.");
 
   HAL_RegisterExtension("ds_socket", &gDSConnected);
 
@@ -193,7 +191,7 @@
 
   eventLoopRunner->ExecAsync(SetupEventLoop);
 
-  wpi::outs() << "DriverStationSocket Initialized!\n";
+  std::puts("DriverStationSocket Initialized!");
   return 0;
 }
 }  // extern "C"
diff --git a/simulation/halsim_ds_socket/src/main/native/include/DSCommJoystickPacket.h b/simulation/halsim_ds_socket/src/main/native/include/DSCommJoystickPacket.h
index 99851f2..db430a1 100644
--- a/simulation/halsim_ds_socket/src/main/native/include/DSCommJoystickPacket.h
+++ b/simulation/halsim_ds_socket/src/main/native/include/DSCommJoystickPacket.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_ds_socket/src/main/native/include/DSCommPacket.h b/simulation/halsim_ds_socket/src/main/native/include/DSCommPacket.h
index f189d6a..1285711 100644
--- a/simulation/halsim_ds_socket/src/main/native/include/DSCommPacket.h
+++ b/simulation/halsim_ds_socket/src/main/native/include/DSCommPacket.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -11,8 +8,8 @@
 
 #include <DSCommJoystickPacket.h>
 #include <hal/simulation/DriverStationData.h>
-#include <wpi/ArrayRef.h>
 #include <wpi/raw_uv_ostream.h>
+#include <wpi/span.h>
 
 class DSCommPacketTest;
 
@@ -23,8 +20,8 @@
 
  public:
   DSCommPacket(void);
-  void DecodeTCP(wpi::ArrayRef<uint8_t> packet);
-  void DecodeUDP(wpi::ArrayRef<uint8_t> packet);
+  void DecodeTCP(wpi::span<const uint8_t> packet);
+  void DecodeUDP(wpi::span<const uint8_t> packet);
   void SendUDPToHALSim(void);
   void SetupSendBuffer(wpi::raw_uv_ostream& buf);
 
@@ -56,11 +53,11 @@
   void SetAlliance(uint8_t station_code);
   void SetupSendHeader(wpi::raw_uv_ostream& buf);
   void SetupJoystickTag(wpi::raw_uv_ostream& buf);
-  void ReadMatchtimeTag(wpi::ArrayRef<uint8_t> tagData);
-  void ReadJoystickTag(wpi::ArrayRef<uint8_t> data, int index);
-  void ReadNewMatchInfoTag(wpi::ArrayRef<uint8_t> data);
-  void ReadGameSpecificMessageTag(wpi::ArrayRef<uint8_t> data);
-  void ReadJoystickDescriptionTag(wpi::ArrayRef<uint8_t> data);
+  void ReadMatchtimeTag(wpi::span<const uint8_t> tagData);
+  void ReadJoystickTag(wpi::span<const uint8_t> data, int index);
+  void ReadNewMatchInfoTag(wpi::span<const uint8_t> data);
+  void ReadGameSpecificMessageTag(wpi::span<const uint8_t> data);
+  void ReadJoystickDescriptionTag(wpi::span<const uint8_t> data);
 
   uint8_t m_hi;
   uint8_t m_lo;
diff --git a/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp b/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
index 08caa2f..84df27c 100644
--- a/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
+++ b/simulation/halsim_ds_socket/src/test/native/cpp/DSCommPacketTest.cpp
@@ -1,36 +1,34 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "DSCommPacket.h"
 #include "gtest/gtest.h"
 
 class DSCommPacketTest : public ::testing::Test {
  public:
-  DSCommPacketTest() {}
+  DSCommPacketTest() = default;
 
   void SendJoysticks() { commPacket.SendJoysticks(); }
 
-  halsim::DSCommJoystickPacket& ReadJoystickTag(wpi::ArrayRef<uint8_t> data,
+  halsim::DSCommJoystickPacket& ReadJoystickTag(wpi::span<const uint8_t> data,
                                                 int index) {
     commPacket.ReadJoystickTag(data, index);
     return commPacket.m_joystick_packets[index];
   }
 
-  halsim::DSCommJoystickPacket& ReadDescriptorTag(wpi::ArrayRef<uint8_t> data) {
+  halsim::DSCommJoystickPacket& ReadDescriptorTag(
+      wpi::span<const uint8_t> data) {
     commPacket.ReadJoystickDescriptionTag(data);
     return commPacket.m_joystick_packets[data[3]];
   }
 
-  HAL_MatchInfo& ReadNewMatchInfoTag(wpi::ArrayRef<uint8_t> data) {
+  HAL_MatchInfo& ReadNewMatchInfoTag(wpi::span<const uint8_t> data) {
     commPacket.ReadNewMatchInfoTag(data);
     return commPacket.matchInfo;
   }
 
-  HAL_MatchInfo& ReadGameSpecificTag(wpi::ArrayRef<uint8_t> data) {
+  HAL_MatchInfo& ReadGameSpecificTag(wpi::span<const uint8_t> data) {
     commPacket.ReadGameSpecificMessageTag(data);
     return commPacket.matchInfo;
   }
@@ -70,7 +68,8 @@
     std::array<uint8_t, 12> _buttons{{0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1}};
 
     std::array<uint8_t, 2> _button_bytes{{0, 0}};
-    for (int btn = 0; btn < 8; btn++) _button_bytes[1] |= _buttons[btn] << btn;
+    for (int btn = 0; btn < 8; btn++)
+      _button_bytes[1] |= _buttons[btn] << btn;
     for (int btn = 8; btn < 12; btn++)
       _button_bytes[0] |= _buttons[btn] << (btn - 8);
 
diff --git a/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp b/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
index c6b6c58..6aea19a 100644
--- a/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_ds_socket/src/test/native/cpp/main.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <hal/HALBase.h>
 
diff --git a/simulation/halsim_gazebo/build.gradle b/simulation/halsim_gazebo/build.gradle
index d476ef0..8d340d3 100644
--- a/simulation/halsim_gazebo/build.gradle
+++ b/simulation/halsim_gazebo/build.gradle
@@ -19,10 +19,10 @@
     gazebo_linker_args = "pkg-config --libs gazebo protobuf".execute().text.split()
 } catch(Exception ex) { }
 
-if (project.hasProperty("makeSim")) {
+if (project.hasProperty("forceGazebo")) {
     if (!gazebo_version?.trim()) {
         println "Gazebo development files are not available. (pkg-config --modversion gazebo failed)"
-        println "makeSim set. Forcing build - failure likely."
+        println "forceGazebo set. Forcing build - failure likely."
     }
 } else {
     ext.skip_frc_plugins = true
@@ -35,7 +35,6 @@
 if (!gz_msgs_project.hasProperty('skip_gz_msgs') && !project.hasProperty('skip_frc_plugins')) {
 
     apply from: "${rootDir}/shared/plugins/setupBuild.gradle"
-
 }
 
 model {
@@ -48,6 +47,7 @@
             linker.args gazebo_linker_args
             cppCompiler.args gazebo_cppflags
             lib project: ":simulation:gz_msgs", library: "gz_msgs", linkage: "static"
+            lib project: ":wpiutil", library: "wpiutil", linkage: "static"
         }
     }
 }
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboAnalogIn.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboAnalogIn.cpp
index ce20221..d9ce9a9 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboAnalogIn.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboAnalogIn.cpp
@@ -1,14 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboAnalogIn.h"
 
-#include <string>
-
+#include <fmt/format.h>
 #include <hal/Power.h>
 #include <hal/Value.h>
 #include <hal/simulation/AnalogInData.h>
@@ -33,14 +29,14 @@
 void GazeboAnalogIn::Listen() {
   if (!m_sub)
     m_sub = m_halsim->node.Subscribe<gazebo::msgs::Float64>(
-        "~/simulator/analog/" + std::to_string(m_index),
+        fmt::format("~/simulator/analog/{}", m_index),
         &GazeboAnalogIn::Callback, this);
 }
 
 void GazeboAnalogIn::Callback(const gazebo::msgs::ConstFloat64Ptr& msg) {
   /* This value is going to be divided by the 5V rail in the HAL, so
      we multiply by that value to make the change neutral */
-  int32_t status;
+  int32_t status = 0;
   HALSIM_SetAnalogInVoltage(m_index,
                             msg->data() * HAL_GetUserVoltage5V(&status));
 }
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboDIO.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboDIO.cpp
index f95b4f5..85d4ea6 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboDIO.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboDIO.cpp
@@ -1,14 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboDIO.h"
 
-#include <string>
-
+#include <fmt/format.h>
 #include <hal/Value.h>
 #include <hal/simulation/DIOData.h>
 #include <hal/simulation/NotifyListener.h>
@@ -32,8 +28,7 @@
 void GazeboDIO::Listen() {
   if (!m_sub)
     m_sub = m_halsim->node.Subscribe<gazebo::msgs::Bool>(
-        "~/simulator/dio/" + std::to_string(m_index), &GazeboDIO::Callback,
-        this);
+        fmt::format("~/simulator/dio/{}", m_index), &GazeboDIO::Callback, this);
 }
 
 void GazeboDIO::Callback(const gazebo::msgs::ConstBoolPtr& msg) {
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboEncoder.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboEncoder.cpp
index 03c2f79..519bf2b 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboEncoder.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboEncoder.cpp
@@ -1,14 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboEncoder.h"
 
-#include <string>
-
+#include <fmt/format.h>
 #include <hal/Value.h>
 #include <hal/simulation/EncoderData.h>
 #include <hal/simulation/NotifyListener.h>
@@ -33,7 +29,8 @@
 static void encoder_reverse_callback(const char* name, void* param,
                                      const struct HAL_Value* value) {
   GazeboEncoder* encoder = static_cast<GazeboEncoder*>(param);
-  if (encoder->IsInitialized()) encoder->SetReverse(value->data.v_boolean);
+  if (encoder->IsInitialized())
+    encoder->SetReverse(value->data.v_boolean);
 }
 
 GazeboEncoder::GazeboEncoder(int index, HALSimGazebo* halsim) {
@@ -53,21 +50,21 @@
 void GazeboEncoder::Control(const char* command) {
   if (!m_pub) {
     m_pub = m_halsim->node.Advertise<gazebo::msgs::String>(
-        "~/simulator/encoder/dio/" +
-        std::to_string(HALSIM_GetEncoderDigitalChannelA(m_index)) + "/control");
+        fmt::format("~/simulator/encoder/dio/{}/control",
+                    HALSIM_GetEncoderDigitalChannelA(m_index)));
     m_pub->WaitForConnection(gazebo::common::Time(1, 0));
   }
   gazebo::msgs::String msg;
   msg.set_data(command);
-  if (m_pub) m_pub->Publish(msg);
+  if (m_pub)
+    m_pub->Publish(msg);
 }
 
 void GazeboEncoder::Listen() {
   if (!m_sub)
     m_sub = m_halsim->node.Subscribe<gazebo::msgs::Float64>(
-        "~/simulator/encoder/dio/" +
-            std::to_string(HALSIM_GetEncoderDigitalChannelA(m_index)) +
-            "/position",
+        fmt::format("~/simulator/encoder/dio/{}/position",
+                    HALSIM_GetEncoderDigitalChannelA(m_index)),
         &GazeboEncoder::Callback, this);
 }
 
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboNode.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboNode.cpp
index 13103b7..1c847ca 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboNode.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboNode.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboNode.h"
 
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboPCM.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboPCM.cpp
index 1802f2e..2549061 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboPCM.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboPCM.cpp
@@ -1,17 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboPCM.h"
 
-#include <string>
-
+#include <fmt/format.h>
 #include <hal/Value.h>
+#include <hal/simulation/CTREPCMData.h>
 #include <hal/simulation/NotifyListener.h>
-#include <hal/simulation/PCMData.h>
 
 #include "simulation/gz_msgs/msgs.h"
 
@@ -24,7 +20,8 @@
 static void output_callback(const char* name, void* param,
                             const struct HAL_Value* value) {
   GazeboPCM* pcm = static_cast<GazeboPCM*>(param);
-  if (pcm->IsInitialized()) pcm->Publish(value->data.v_boolean);
+  if (pcm->IsInitialized())
+    pcm->Publish(value->data.v_boolean);
 }
 
 GazeboPCM::GazeboPCM(int index, int channel, HALSimGazebo* halsim) {
@@ -32,24 +29,23 @@
   m_channel = channel;
   m_halsim = halsim;
   m_pub = NULL;
-  HALSIM_RegisterPCMSolenoidInitializedCallback(index, channel, init_callback,
-                                                this, true);
-  HALSIM_RegisterPCMSolenoidOutputCallback(index, channel, output_callback,
-                                           this, true);
+  HALSIM_RegisterCTREPCMInitializedCallback(index, init_callback, this, true);
+  HALSIM_RegisterCTREPCMSolenoidOutputCallback(index, channel, output_callback,
+                                               this, true);
 }
 
 void GazeboPCM::Publish(bool value) {
   if (!m_pub) {
     m_pub = m_halsim->node.Advertise<gazebo::msgs::Bool>(
-        "~/simulator/pneumatic/" + std::to_string(m_index + 1) + "/" +
-        std::to_string(m_channel));
+        fmt::format("~/simulator/pneumatic/{}/{}", m_index + 1, m_channel));
     m_pub->WaitForConnection(gazebo::common::Time(1, 0));
   }
   gazebo::msgs::Bool msg;
   msg.set_data(value);
-  if (m_pub) m_pub->Publish(msg);
+  if (m_pub)
+    m_pub->Publish(msg);
 }
 
 void GazeboPCM_SetPressureSwitch(int index, bool value) {
-  HALSIM_SetPCMPressureSwitch(index, value);
+  HALSIM_SetCTREPCMPressureSwitch(index, value);
 }
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/GazeboPWM.cpp b/simulation/halsim_gazebo/src/main/native/cpp/GazeboPWM.cpp
index a297255..0780f07 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/GazeboPWM.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/GazeboPWM.cpp
@@ -1,14 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "GazeboPWM.h"
 
-#include <string>
-
+#include <fmt/format.h>
 #include <hal/Value.h>
 #include <hal/simulation/NotifyListener.h>
 #include <hal/simulation/PWMData.h>
@@ -24,7 +20,8 @@
 static void speed_callback(const char* name, void* param,
                            const struct HAL_Value* value) {
   GazeboPWM* pwm = static_cast<GazeboPWM*>(param);
-  if (pwm->IsInitialized()) pwm->Publish(value->data.v_double);
+  if (pwm->IsInitialized())
+    pwm->Publish(value->data.v_double);
 }
 
 GazeboPWM::GazeboPWM(int port, HALSimGazebo* halsim) {
@@ -37,10 +34,11 @@
 void GazeboPWM::Publish(double value) {
   if (!m_pub) {
     m_pub = m_halsim->node.Advertise<gazebo::msgs::Float64>(
-        "~/simulator/pwm/" + std::to_string(m_port));
+        fmt::format("~/simulator/pwm/{}", m_port));
     m_pub->WaitForConnection(gazebo::common::Time(1, 0));
   }
   gazebo::msgs::Float64 msg;
   msg.set_data(value);
-  if (m_pub) m_pub->Publish(msg);
+  if (m_pub)
+    m_pub->Publish(msg);
 }
diff --git a/simulation/halsim_gazebo/src/main/native/cpp/main.cpp b/simulation/halsim_gazebo/src/main/native/cpp/main.cpp
index fa1c85a..a3d0576 100644
--- a/simulation/halsim_gazebo/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_gazebo/src/main/native/cpp/main.cpp
@@ -1,12 +1,8 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <iostream>
-
+#include <fmt/core.h>
 #include <hal/Ports.h>
 
 #include "GazeboAnalogIn.h"
@@ -22,14 +18,14 @@
 
 extern "C" {
 int HALSIM_InitExtension(void) {
-  std::cout << "Gazebo Simulator Initializing." << std::endl;
+  fmt::print("Gazebo Simulator Initializing.\n");
 
   if (!halsim.node.Connect()) {
-    std::cerr << "Error: unable to connect to Gazebo.  Is it running?."
-              << std::endl;
+    fmt::print(stderr,
+               "Error: unable to connect to Gazebo.  Is it running?.\n");
     return -1;
   }
-  std::cout << "Gazebo Simulator Connected." << std::endl;
+  fmt::print("Gazebo Simulator Connected.\n");
 
   for (int i = 0; i < HALSimGazebo::kPWMCount; i++)
     halsim.pwms[i] = new GazeboPWM(i, &halsim);
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboAnalogIn.h b/simulation/halsim_gazebo/src/main/native/include/GazeboAnalogIn.h
index 9b1bbde..c54db0d 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboAnalogIn.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboAnalogIn.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboDIO.h b/simulation/halsim_gazebo/src/main/native/include/GazeboDIO.h
index e96650e..25ef0ad 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboDIO.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboDIO.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboEncoder.h b/simulation/halsim_gazebo/src/main/native/include/GazeboEncoder.h
index fafeb6a..b25a688 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboEncoder.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboEncoder.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboNode.h b/simulation/halsim_gazebo/src/main/native/include/GazeboNode.h
index cc68599..75acc55 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboNode.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboNode.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboPCM.h b/simulation/halsim_gazebo/src/main/native/include/GazeboPCM.h
index d74b20f..f964f3f 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboPCM.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboPCM.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/GazeboPWM.h b/simulation/halsim_gazebo/src/main/native/include/GazeboPWM.h
index 1321e8a..f4b131f 100644
--- a/simulation/halsim_gazebo/src/main/native/include/GazeboPWM.h
+++ b/simulation/halsim_gazebo/src/main/native/include/GazeboPWM.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gazebo/src/main/native/include/HALSimGazebo.h b/simulation/halsim_gazebo/src/main/native/include/HALSimGazebo.h
index f7d8193..c1e454a 100644
--- a/simulation/halsim_gazebo/src/main/native/include/HALSimGazebo.h
+++ b/simulation/halsim_gazebo/src/main/native/include/HALSimGazebo.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2014-2018 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gui/CMakeLists.txt b/simulation/halsim_gui/CMakeLists.txt
index 424ff5b..949f9f1 100644
--- a/simulation/halsim_gui/CMakeLists.txt
+++ b/simulation/halsim_gui/CMakeLists.txt
@@ -5,12 +5,12 @@
 
 file(GLOB halsim_gui_src src/main/native/cpp/*.cpp)
 
-add_library(halsim_gui MODULE ${halsim_gui_src})
+add_library(halsim_gui SHARED ${halsim_gui_src})
 wpilib_target_warnings(halsim_gui)
 set_target_properties(halsim_gui PROPERTIES DEBUG_POSTFIX "d")
 
 wpilib_link_macos_gui(halsim_gui)
-target_link_libraries(halsim_gui PUBLIC hal ntcore wpimath PRIVATE wpigui)
+target_link_libraries(halsim_gui PUBLIC hal wpimath PRIVATE libglassnt libglass)
 
 target_include_directories(halsim_gui PRIVATE src/main/native/include)
 
diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle
index 0fd5c2c..7c26ac8 100644
--- a/simulation/halsim_gui/build.gradle
+++ b/simulation/halsim_gui/build.gradle
@@ -3,8 +3,6 @@
     description = "A plugin that creates a simulation gui"
 
     ext {
-        includeWpiutil = true
-        includeNtCore = true
         pluginName = 'halsim_gui'
     }
 
@@ -23,8 +21,12 @@
     model {
         binaries {
             all {
-                lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
+                lib project: ':glass', library: 'glassnt', linkage: 'static'
+                lib project: ':glass', library: 'glass', linkage: 'static'
                 lib project: ':wpigui', library: 'wpigui', linkage: 'static'
+                lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
+                lib project: ':ntcore', library: 'ntcore', linkage: 'shared'
+                lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
                 nativeUtils.useRequiredLibrary(it, 'imgui_static')
                 if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) {
                     it.buildable = false
diff --git a/simulation/halsim_gui/src/dev/native/cpp/main.cpp b/simulation/halsim_gui/src/dev/native/cpp/main.cpp
index d23379d..a3e363e 100644
--- a/simulation/halsim_gui/src/dev/native/cpp/main.cpp
+++ b/simulation/halsim_gui/src/dev/native/cpp/main.cpp
@@ -1,8 +1,5 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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.
 
 int main() {}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp
deleted file mode 100644
index fa772ad..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp
+++ /dev/null
@@ -1,75 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "AccelerometerGui.h"
-
-#include <cstdio>
-#include <memory>
-
-#include <hal/Value.h>
-#include <hal/simulation/AccelerometerData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "SimDeviceGui.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerX, "X Accel");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerY, "Y Accel");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerZ, "Z Accel");
-}  // namespace
-
-static std::unique_ptr<AccelerometerXSource> gAccelXSource;
-static std::unique_ptr<AccelerometerYSource> gAccelYSource;
-static std::unique_ptr<AccelerometerZSource> gAccelZSource;
-
-static void UpdateAccelSources() {
-  if (!HALSIM_GetAccelerometerActive(0)) return;
-  if (!gAccelXSource) gAccelXSource = std::make_unique<AccelerometerXSource>(0);
-  if (!gAccelYSource) gAccelYSource = std::make_unique<AccelerometerYSource>(0);
-  if (!gAccelZSource) gAccelZSource = std::make_unique<AccelerometerZSource>(0);
-}
-
-static void DisplayAccelerometers() {
-  if (!HALSIM_GetAccelerometerActive(0)) return;
-  if (SimDeviceGui::StartDevice("BuiltInAccel")) {
-    HAL_Value value;
-
-    // Range
-    value = HAL_MakeEnum(HALSIM_GetAccelerometerRange(0));
-    static const char* rangeOptions[] = {"2G", "4G", "8G"};
-    SimDeviceGui::DisplayValue("Range", true, &value, rangeOptions, 3);
-
-    // X Accel
-    value = HAL_MakeDouble(gAccelXSource->GetValue());
-    if (SimDeviceGui::DisplayValueSource("X Accel", false, &value,
-                                         gAccelXSource.get()))
-      HALSIM_SetAccelerometerX(0, value.data.v_double);
-
-    // Y Accel
-    value = HAL_MakeDouble(gAccelYSource->GetValue());
-    if (SimDeviceGui::DisplayValueSource("Y Accel", false, &value,
-                                         gAccelYSource.get()))
-      HALSIM_SetAccelerometerY(0, value.data.v_double);
-
-    // Z Accel
-    value = HAL_MakeDouble(gAccelZSource->GetValue());
-    if (SimDeviceGui::DisplayValueSource("Z Accel", false, &value,
-                                         gAccelZSource.get()))
-      HALSIM_SetAccelerometerZ(0, value.data.v_double);
-
-    SimDeviceGui::FinishDevice();
-  }
-}
-
-void AccelerometerGui::Initialize() {
-  HALSimGui::AddExecute(UpdateAccelSources);
-  SimDeviceGui::Add(DisplayAccelerometers);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h b/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h
deleted file mode 100644
index e1fd81b..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class AccelerometerGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp
new file mode 100644
index 0000000..2267656
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp
@@ -0,0 +1,64 @@
+// 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 "AccelerometerSimGui.h"
+
+#include <glass/hardware/Accelerometer.h>
+#include <glass/other/DeviceTree.h>
+
+#include <memory>
+
+#include <hal/Value.h>
+#include <hal/simulation/AccelerometerData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+#include "SimDeviceGui.h"
+
+using namespace glass;
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerX, "X Accel");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerY, "Y Accel");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerZ, "Z Accel");
+
+class AccelerometerSimModel : public glass::AccelerometerModel {
+ public:
+  explicit AccelerometerSimModel(int32_t index)
+      : m_index{index}, m_xData{m_index}, m_yData{m_index}, m_zData{m_index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetAccelerometerActive(m_index); }
+
+  glass::DataSource* GetXData() override { return &m_xData; }
+  glass::DataSource* GetYData() override { return &m_yData; }
+  glass::DataSource* GetZData() override { return &m_zData; }
+
+  int GetRange() override { return HALSIM_GetAccelerometerRange(m_index); }
+
+  void SetX(double val) override { HALSIM_SetAccelerometerX(m_index, val); }
+  void SetY(double val) override { HALSIM_SetAccelerometerY(m_index, val); }
+  void SetZ(double val) override { HALSIM_SetAccelerometerZ(m_index, val); }
+  void SetRange(int val) override {
+    HALSIM_SetAccelerometerRange(m_index,
+                                 static_cast<HAL_AccelerometerRange>(val));
+  }
+
+ private:
+  int32_t m_index;
+  AccelerometerXSource m_xData;
+  AccelerometerYSource m_yData;
+  AccelerometerZSource m_zData;
+};
+}  // namespace
+
+void AccelerometerSimGui::Initialize() {
+  SimDeviceGui::GetDeviceTree().Add(
+      std::make_unique<AccelerometerSimModel>(0), [](glass::Model* model) {
+        glass::DisplayAccelerometerDevice(
+            static_cast<AccelerometerSimModel*>(model));
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h
new file mode 100644
index 0000000..9028324
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class AccelerometerSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp
index 36b085c..23a5238 100644
--- a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp
@@ -1,126 +1,113 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "AddressableLEDGui.h"
 
+#include <glass/hardware/LEDDisplay.h>
+
 #include <hal/Ports.h>
 #include <hal/simulation/AddressableLEDData.h>
 #include <imgui.h>
-#include <imgui_internal.h>
-#include <wpi/SmallVector.h>
-#include <wpi/StringRef.h>
 
-#include "ExtraGuiWidgets.h"
 #include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
 
 using namespace halsimgui;
 
 namespace {
-struct LEDDisplayInfo {
-  int numColumns = 10;
-  LEDConfig config;
+class AddressableLEDModel : public glass::LEDDisplayModel {
+ public:
+  explicit AddressableLEDModel(int32_t index) : m_index{index} {}
 
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out);
+  void Update() override {}
+  bool Exists() override {
+    return HALSIM_GetAddressableLEDInitialized(m_index);
+  }
+
+  bool IsRunning() override { return HALSIM_GetAddressableLEDRunning(m_index); }
+
+  wpi::span<const Data> GetData(wpi::SmallVectorImpl<Data>&) override {
+    size_t length = HALSIM_GetAddressableLEDData(m_index, m_data);
+    return {reinterpret_cast<Data*>(m_data), length};
+  }
+
+ private:
+  int32_t m_index;
+
+  HAL_AddressableLEDData m_data[HAL_kAddressableLEDMaxLength];
+};
+
+class AddressableLEDsModel : public glass::LEDDisplaysModel {
+ public:
+  AddressableLEDsModel() : m_models(HAL_GetNumAddressableLEDs()) {}
+
+  void Update() override;
+  bool Exists() override;
+
+  size_t GetNumLEDDisplays() override { return m_models.size(); }
+
+  void ForEachLEDDisplay(
+      wpi::function_ref<void(glass::LEDDisplayModel& model, int index)> func)
+      override;
+
+ private:
+  std::vector<std::unique_ptr<AddressableLEDModel>> m_models;
 };
 }  // namespace
 
-static IniSaver<LEDDisplayInfo> gDisplaySettings{"AddressableLED"};
-
-bool LEDDisplayInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name == "columns") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    numColumns = num;
-  } else if (name == "serpentine") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    config.serpentine = num != 0;
-  } else if (name == "order") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    config.order = static_cast<LEDConfig::Order>(num);
-  } else if (name == "start") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    config.start = static_cast<LEDConfig::Start>(num);
-  } else {
-    return false;
-  }
-  return true;
-}
-
-void LEDDisplayInfo::WriteIni(ImGuiTextBuffer* out) {
-  out->appendf("columns=%d\nserpentine=%d\norder=%d\nstart=%d\n", numColumns,
-               config.serpentine ? 1 : 0, static_cast<int>(config.order),
-               static_cast<int>(config.start));
-}
-
-static void DisplayAddressableLEDs() {
-  bool hasAny = false;
-  static const int numLED = HAL_GetNumAddressableLEDs();
-
-  for (int i = 0; i < numLED; ++i) {
-    if (!HALSIM_GetAddressableLEDInitialized(i)) continue;
-    hasAny = true;
-
-    if (numLED > 1) ImGui::Text("LEDs[%d]", i);
-
-    static HAL_AddressableLEDData data[HAL_kAddressableLEDMaxLength];
-    int length = HALSIM_GetAddressableLEDData(i, data);
-    bool running = HALSIM_GetAddressableLEDRunning(i);
-    auto& info = gDisplaySettings[i];
-
-    ImGui::PushItemWidth(ImGui::GetFontSize() * 6);
-    ImGui::LabelText("Length", "%d", length);
-    ImGui::LabelText("Running", "%s", running ? "Yes" : "No");
-    ImGui::InputInt("Columns", &info.numColumns);
-    {
-      static const char* options[] = {"Row Major", "Column Major"};
-      int val = info.config.order;
-      if (ImGui::Combo("Order", &val, options, 2))
-        info.config.order = static_cast<LEDConfig::Order>(val);
-    }
-    {
-      static const char* options[] = {"Upper Left", "Lower Left", "Upper Right",
-                                      "Lower Right"};
-      int val = info.config.start;
-      if (ImGui::Combo("Start", &val, options, 4))
-        info.config.start = static_cast<LEDConfig::Start>(val);
-    }
-    ImGui::Checkbox("Serpentine", &info.config.serpentine);
-    if (info.numColumns < 1) info.numColumns = 1;
-    ImGui::PopItemWidth();
-
-    // show as LED indicators
-    static int values[HAL_kAddressableLEDMaxLength];
-    static ImU32 colors[HAL_kAddressableLEDMaxLength];
-
-    if (!running) {
-      colors[0] = IM_COL32(128, 128, 128, 255);
-      for (int j = 0; j < length; ++j) values[j] = -1;
-    } else {
-      for (int j = 0; j < length; ++j) {
-        values[j] = j + 1;
-        colors[j] = IM_COL32(data[j].r, data[j].g, data[j].b, 255);
+void AddressableLEDsModel::Update() {
+  for (int i = 0; i < static_cast<int>(m_models.size()); ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetAddressableLEDInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<AddressableLEDModel>(i);
       }
+      if (model) {
+        model->Update();
+      }
+    } else {
+      model.reset();
     }
-
-    DrawLEDs(values, length, info.numColumns, colors, 0, 0, info.config);
   }
-  if (!hasAny) ImGui::Text("No addressable LEDs");
+}
+
+bool AddressableLEDsModel::Exists() {
+  for (auto&& model : m_models) {
+    if (model && model->Exists()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void AddressableLEDsModel::ForEachLEDDisplay(
+    wpi::function_ref<void(glass::LEDDisplayModel& model, int index)> func) {
+  for (int i = 0; i < static_cast<int>(m_models.size()); ++i) {
+    if (m_models[i]) {
+      func(*m_models[i], i);
+    }
+  }
+}
+
+static bool AddressableLEDsExists() {
+  static const int numLED = HAL_GetNumAddressableLEDs();
+  for (int i = 0; i < numLED; ++i) {
+    if (HALSIM_GetAddressableLEDInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
 }
 
 void AddressableLEDGui::Initialize() {
-  gDisplaySettings.Initialize();
-  HALSimGui::AddWindow("Addressable LEDs", DisplayAddressableLEDs,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetWindowVisibility("Addressable LEDs", HALSimGui::kHide);
-  HALSimGui::SetDefaultWindowPos("Addressable LEDs", 290, 100);
+  HALSimGui::halProvider.Register(
+      "Addressable LEDs", [] { return AddressableLEDsExists(); },
+      [] { return std::make_unique<AddressableLEDsModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(290, 100);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayLEDDisplays(static_cast<AddressableLEDsModel*>(model));
+        });
+      });
 }
diff --git a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.h b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.h
index e376b9b..920b965 100644
--- a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.h
+++ b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp
deleted file mode 100644
index 5df30e6..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp
+++ /dev/null
@@ -1,80 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "AnalogGyroGui.h"
-
-#include <cstdio>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/Value.h>
-#include <hal/simulation/AnalogGyroData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "SimDeviceGui.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroAngle, "AGyro Angle");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroRate, "AGyro Rate");
-struct AnalogGyroSource {
-  explicit AnalogGyroSource(int32_t index) : angle{index}, rate{index} {}
-  AnalogGyroAngleSource angle;
-  AnalogGyroRateSource rate;
-};
-}  // namespace
-
-static std::vector<std::unique_ptr<AnalogGyroSource>> gAnalogGyroSources;
-
-static void UpdateAnalogGyroSources() {
-  for (int i = 0, iend = gAnalogGyroSources.size(); i < iend; ++i) {
-    auto& source = gAnalogGyroSources[i];
-    if (HALSIM_GetAnalogGyroInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<AnalogGyroSource>(i);
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayAnalogGyros() {
-  for (int i = 0, iend = gAnalogGyroSources.size(); i < iend; ++i) {
-    if (auto source = gAnalogGyroSources[i].get()) {
-      char name[32];
-      std::snprintf(name, sizeof(name), "AnalogGyro[%d]", i);
-      if (SimDeviceGui::StartDevice(name)) {
-        HAL_Value value;
-
-        // angle
-        value = HAL_MakeDouble(source->angle.GetValue());
-        if (SimDeviceGui::DisplayValueSource("Angle", false, &value,
-                                             &source->angle))
-          HALSIM_SetAnalogGyroAngle(i, value.data.v_double);
-
-        // rate
-        value = HAL_MakeDouble(source->rate.GetValue());
-        if (SimDeviceGui::DisplayValueSource("Rate", false, &value,
-                                             &source->rate))
-          HALSIM_SetAnalogGyroRate(i, value.data.v_double);
-
-        SimDeviceGui::FinishDevice();
-      }
-    }
-  }
-}
-
-void AnalogGyroGui::Initialize() {
-  gAnalogGyroSources.resize(HAL_GetNumAccumulators());
-  HALSimGui::AddExecute(UpdateAnalogGyroSources);
-  SimDeviceGui::Add(DisplayAnalogGyros);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h
deleted file mode 100644
index e528279..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class AnalogGyroGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp
new file mode 100644
index 0000000..12149ec
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp
@@ -0,0 +1,98 @@
+// 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 "AnalogGyroSimGui.h"
+
+#include <glass/hardware/AnalogGyro.h>
+#include <glass/other/DeviceTree.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/Value.h>
+#include <hal/simulation/AnalogGyroData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+#include "SimDeviceGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroAngle, "AGyro Angle");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroRate, "AGyro Rate");
+
+class AnalogGyroSimModel : public glass::AnalogGyroModel {
+ public:
+  explicit AnalogGyroSimModel(int32_t index)
+      : m_index{index}, m_angle{index}, m_rate{index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetAnalogGyroInitialized(m_index); }
+
+  glass::DataSource* GetAngleData() override { return &m_angle; }
+  glass::DataSource* GetRateData() override { return &m_rate; }
+
+  void SetAngle(double val) override {
+    HALSIM_SetAnalogGyroAngle(m_index, val);
+  }
+  void SetRate(double val) override { HALSIM_SetAnalogGyroRate(m_index, val); }
+
+ private:
+  int32_t m_index;
+  AnalogGyroAngleSource m_angle;
+  AnalogGyroRateSource m_rate;
+};
+
+class AnalogGyrosSimModel : public glass::AnalogGyrosModel {
+ public:
+  AnalogGyrosSimModel() : m_models(HAL_GetNumAccumulators()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachAnalogGyro(
+      wpi::function_ref<void(glass::AnalogGyroModel& model, int index)> func)
+      override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<AnalogGyroSimModel>> m_models;
+};
+}  // namespace
+
+void AnalogGyrosSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetAnalogGyroInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<AnalogGyroSimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void AnalogGyrosSimModel::ForEachAnalogGyro(
+    wpi::function_ref<void(glass::AnalogGyroModel& model, int index)> func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+void AnalogGyroSimGui::Initialize() {
+  SimDeviceGui::GetDeviceTree().Add(
+      std::make_unique<AnalogGyrosSimModel>(), [](glass::Model* model) {
+        glass::DisplayAnalogGyrosDevice(
+            static_cast<AnalogGyrosSimModel*>(model));
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h
new file mode 100644
index 0000000..89f91ca
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class AnalogGyroSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp
deleted file mode 100644
index 8ba1459..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp
+++ /dev/null
@@ -1,102 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "AnalogInputGui.h"
-
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/AnalogGyroData.h>
-#include <hal/simulation/AnalogInData.h>
-#include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogInVoltage, "AIn");
-}  // namespace
-
-// indexed by channel
-static IniSaver<NameInfo> gAnalogInputs{"AnalogInput"};
-static std::vector<std::unique_ptr<AnalogInVoltageSource>> gAnalogInputSources;
-
-static void UpdateAnalogInputSources() {
-  for (int i = 0, iend = gAnalogInputSources.size(); i < iend; ++i) {
-    auto& source = gAnalogInputSources[i];
-    if (HALSIM_GetAnalogInInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<AnalogInVoltageSource>(i);
-        source->SetName(gAnalogInputs[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayAnalogInputs() {
-  ImGui::Text("(Use Ctrl+Click to edit value)");
-  bool hasInputs = false;
-  static const int numAccum = HAL_GetNumAccumulators();
-  bool first = true;
-  for (int i = 0, iend = gAnalogInputSources.size(); i < iend; ++i) {
-    if (auto source = gAnalogInputSources[i].get()) {
-      ImGui::PushID(i);
-      hasInputs = true;
-
-      if (!first) {
-        ImGui::Spacing();
-        ImGui::Spacing();
-      } else {
-        first = false;
-      }
-
-      auto& info = gAnalogInputs[i];
-      // build label
-      char label[128];
-      info.GetLabel(label, sizeof(label), "In", i);
-
-      if (i < numAccum && HALSIM_GetAnalogGyroInitialized(i)) {
-        ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-        ImGui::LabelText(label, "AnalogGyro[%d]", i);
-        ImGui::PopStyleColor();
-      } else if (auto simDevice = HALSIM_GetAnalogInSimDevice(i)) {
-        ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-        ImGui::LabelText(label, "%s", HALSIM_GetSimDeviceName(simDevice));
-        ImGui::PopStyleColor();
-      } else {
-        float val = source->GetValue();
-        if (source->SliderFloat(label, &val, 0.0, 5.0))
-          HALSIM_SetAnalogInVoltage(i, val);
-      }
-
-      // context menu to change name
-      if (info.PopupEditName(i)) {
-        source->SetName(info.GetName());
-      }
-      ImGui::PopID();
-    }
-  }
-  if (!hasInputs) ImGui::Text("No analog inputs");
-}
-
-void AnalogInputGui::Initialize() {
-  gAnalogInputs.Initialize();
-  gAnalogInputSources.resize(HAL_GetNumAnalogInputs());
-
-  HALSimGui::AddExecute(UpdateAnalogInputSources);
-  HALSimGui::AddWindow("Analog Inputs", DisplayAnalogInputs,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("Analog Inputs", 640, 20);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h
deleted file mode 100644
index 74c3e63..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class AnalogInputGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp
new file mode 100644
index 0000000..5a3b2d8
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp
@@ -0,0 +1,122 @@
+// 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 "AnalogInputSimGui.h"
+
+#include <glass/View.h>
+#include <glass/hardware/AnalogInput.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/AnalogGyroData.h>
+#include <hal/simulation/AnalogInData.h>
+#include <hal/simulation/SimDeviceData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogInVoltage, "AIn");
+
+class AnalogInputSimModel : public glass::AnalogInputModel {
+ public:
+  explicit AnalogInputSimModel(int32_t index)
+      : m_index{index}, m_voltageData{m_index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetAnalogInInitialized(m_index); }
+
+  bool IsGyro() const override {
+    return m_index < HAL_GetNumAccumulators() &&
+           HALSIM_GetAnalogGyroInitialized(m_index);
+  }
+
+  const char* GetSimDevice() const override {
+    if (auto simDevice = HALSIM_GetAnalogInSimDevice(m_index)) {
+      return HALSIM_GetSimDeviceName(simDevice);
+    } else {
+      return nullptr;
+    }
+  }
+
+  glass::DataSource* GetVoltageData() override { return &m_voltageData; }
+
+  void SetVoltage(double val) override {
+    HALSIM_SetAnalogInVoltage(m_index, val);
+  }
+
+ private:
+  int32_t m_index;
+  AnalogInVoltageSource m_voltageData;
+};
+
+class AnalogInputsSimModel : public glass::AnalogInputsModel {
+ public:
+  AnalogInputsSimModel() : m_models(HAL_GetNumAnalogInputs()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachAnalogInput(
+      wpi::function_ref<void(glass::AnalogInputModel& model, int index)> func)
+      override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<AnalogInputSimModel>> m_models;
+};
+}  // namespace
+
+void AnalogInputsSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetAnalogInInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<AnalogInputSimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void AnalogInputsSimModel::ForEachAnalogInput(
+    wpi::function_ref<void(glass::AnalogInputModel& model, int index)> func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool AnalogInputsAnyInitialized() {
+  static const int32_t num = HAL_GetNumAnalogInputs();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetAnalogInInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void AnalogInputSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "Analog Inputs", AnalogInputsAnyInitialized,
+      [] { return std::make_unique<AnalogInputsSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(640, 20);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayAnalogInputs(static_cast<AnalogInputsSimModel*>(model));
+        });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h
new file mode 100644
index 0000000..ca358f4
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class AnalogInputSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp
deleted file mode 100644
index 3e7f4ea..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp
+++ /dev/null
@@ -1,81 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "AnalogOutGui.h"
-
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/AnalogOutData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-#include "SimDeviceGui.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogOutVoltage, "AOut");
-}  // namespace
-
-static IniSaver<NameInfo> gAnalogOuts{"AnalogOut"};  // indexed by channel
-static std::vector<std::unique_ptr<AnalogOutVoltageSource>> gAnalogOutSources;
-
-static void UpdateAnalogOutSources() {
-  for (int i = 0, iend = gAnalogOutSources.size(); i < iend; ++i) {
-    auto& source = gAnalogOutSources[i];
-    if (HALSIM_GetAnalogOutInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<AnalogOutVoltageSource>(i);
-        source->SetName(gAnalogOuts[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayAnalogOutputs() {
-  int count = 0;
-  for (auto&& source : gAnalogOutSources) {
-    if (source) ++count;
-  }
-
-  if (count == 0) return;
-
-  if (SimDeviceGui::StartDevice("Analog Outputs")) {
-    for (int i = 0, iend = gAnalogOutSources.size(); i < iend; ++i) {
-      if (auto source = gAnalogOutSources[i].get()) {
-        ImGui::PushID(i);
-
-        auto& info = gAnalogOuts[i];
-        char label[128];
-        info.GetLabel(label, sizeof(label), "Out", i);
-        HAL_Value value = HAL_MakeDouble(source->GetValue());
-        SimDeviceGui::DisplayValueSource(label, true, &value, source);
-
-        if (info.PopupEditName(i)) {
-          if (source) source->SetName(info.GetName());
-        }
-        ImGui::PopID();
-      }
-    }
-
-    SimDeviceGui::FinishDevice();
-  }
-}
-
-void AnalogOutGui::Initialize() {
-  gAnalogOuts.Initialize();
-  gAnalogOutSources.resize(HAL_GetNumAnalogOutputs());
-  HALSimGui::AddExecute(UpdateAnalogOutSources);
-  SimDeviceGui::Add(DisplayAnalogOutputs);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h
deleted file mode 100644
index dd01699..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class AnalogOutGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp
new file mode 100644
index 0000000..8e02fb6
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp
@@ -0,0 +1,93 @@
+// 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 "AnalogOutputSimGui.h"
+
+#include <glass/hardware/AnalogOutput.h>
+#include <glass/other/DeviceTree.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/AnalogOutData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+#include "SimDeviceGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogOutVoltage, "AOut");
+
+class AnalogOutputSimModel : public glass::AnalogOutputModel {
+ public:
+  explicit AnalogOutputSimModel(int32_t index)
+      : m_index{index}, m_voltageData{m_index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetAnalogOutInitialized(m_index); }
+
+  glass::DataSource* GetVoltageData() override { return &m_voltageData; }
+
+  void SetVoltage(double val) override {
+    HALSIM_SetAnalogOutVoltage(m_index, val);
+  }
+
+ private:
+  int32_t m_index;
+  AnalogOutVoltageSource m_voltageData;
+};
+
+class AnalogOutputsSimModel : public glass::AnalogOutputsModel {
+ public:
+  AnalogOutputsSimModel() : m_models(HAL_GetNumAnalogOutputs()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachAnalogOutput(
+      wpi::function_ref<void(glass::AnalogOutputModel& model, int index)> func)
+      override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<AnalogOutputSimModel>> m_models;
+};
+}  // namespace
+
+void AnalogOutputsSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetAnalogOutInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<AnalogOutputSimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void AnalogOutputsSimModel::ForEachAnalogOutput(
+    wpi::function_ref<void(glass::AnalogOutputModel& model, int index)> func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+void AnalogOutputSimGui::Initialize() {
+  SimDeviceGui::GetDeviceTree().Add(
+      std::make_unique<AnalogOutputsSimModel>(), [](glass::Model* model) {
+        glass::DisplayAnalogOutputsDevice(
+            static_cast<AnalogOutputsSimModel*>(model));
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h
new file mode 100644
index 0000000..f7a816b
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class AnalogOutputSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp b/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp
deleted file mode 100644
index 43215fa..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "CompressorGui.h"
-
-#include <cstdio>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/Value.h>
-#include <hal/simulation/PCMData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "SimDeviceGui.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMCompressorOn, "Compressor On");
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMClosedLoopEnabled, "Closed Loop");
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMPressureSwitch, "Pressure Switch");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PCMCompressorCurrent, "Comp Current");
-struct CompressorSource {
-  explicit CompressorSource(int32_t index)
-      : running{index}, enabled{index}, pressureSwitch{index}, current{index} {}
-  PCMCompressorOnSource running;
-  PCMClosedLoopEnabledSource enabled;
-  PCMPressureSwitchSource pressureSwitch;
-  PCMCompressorCurrentSource current;
-};
-}  // namespace
-
-static std::vector<std::unique_ptr<CompressorSource>> gCompressorSources;
-
-static void UpdateCompressorSources() {
-  for (int i = 0, iend = gCompressorSources.size(); i < iend; ++i) {
-    auto& source = gCompressorSources[i];
-    if (HALSIM_GetPCMCompressorInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<CompressorSource>(i);
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayCompressors() {
-  for (int i = 0, iend = gCompressorSources.size(); i < iend; ++i) {
-    if (auto source = gCompressorSources[i].get()) {
-      char name[32];
-      std::snprintf(name, sizeof(name), "Compressor[%d]", i);
-      if (SimDeviceGui::StartDevice(name)) {
-        HAL_Value value;
-
-        // enabled
-        if (HALSimGui::AreOutputsDisabled())
-          value = HAL_MakeBoolean(false);
-        else
-          value = HAL_MakeBoolean(source->running.GetValue());
-        if (SimDeviceGui::DisplayValueSource("Running", false, &value,
-                                             &source->running))
-          HALSIM_SetPCMCompressorOn(i, value.data.v_boolean);
-
-        // closed loop
-        value = HAL_MakeEnum(source->enabled.GetValue() ? 1 : 0);
-        static const char* enabledOptions[] = {"disabled", "enabled"};
-        if (SimDeviceGui::DisplayValueSource("Closed Loop", true, &value,
-                                             &source->enabled, enabledOptions,
-                                             2))
-          HALSIM_SetPCMClosedLoopEnabled(i, value.data.v_enum);
-
-        // pressure switch
-        value = HAL_MakeEnum(source->pressureSwitch.GetValue() ? 1 : 0);
-        static const char* switchOptions[] = {"full", "low"};
-        if (SimDeviceGui::DisplayValueSource("Pressure", false, &value,
-                                             &source->pressureSwitch,
-                                             switchOptions, 2))
-          HALSIM_SetPCMPressureSwitch(i, value.data.v_enum);
-
-        // compressor current
-        value = HAL_MakeDouble(source->current.GetValue());
-        if (SimDeviceGui::DisplayValueSource("Current (A)", false, &value,
-                                             &source->current))
-          HALSIM_SetPCMCompressorCurrent(i, value.data.v_double);
-
-        SimDeviceGui::FinishDevice();
-      }
-    }
-  }
-}
-
-void CompressorGui::Initialize() {
-  gCompressorSources.resize(HAL_GetNumPCMModules());
-  HALSimGui::AddExecute(UpdateCompressorSources);
-  SimDeviceGui::Add(DisplayCompressors);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.h b/simulation/halsim_gui/src/main/native/cpp/CompressorGui.h
deleted file mode 100644
index f403ece..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class CompressorGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp
deleted file mode 100644
index 2f82ff2..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp
+++ /dev/null
@@ -1,214 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "DIOGui.h"
-
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/DIOData.h>
-#include <hal/simulation/DigitalPWMData.h>
-#include <hal/simulation/DutyCycleData.h>
-#include <hal/simulation/EncoderData.h>
-#include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(DIOValue, "DIO");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DigitalPWMDutyCycle, "DPWM");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DutyCycleOutput, "DutyCycle");
-}  // namespace
-
-static IniSaver<NameInfo> gDIO{"DIO"};
-static std::vector<std::unique_ptr<DIOValueSource>> gDIOSources;
-static std::vector<std::unique_ptr<DigitalPWMDutyCycleSource>> gDPWMSources;
-static std::vector<std::unique_ptr<DutyCycleOutputSource>> gDutyCycleSources;
-
-static void LabelSimDevice(const char* name, HAL_SimDeviceHandle simDevice) {
-  ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-  ImGui::LabelText(name, "%s", HALSIM_GetSimDeviceName(simDevice));
-  ImGui::PopStyleColor();
-}
-
-static void UpdateDIOSources() {
-  for (int i = 0, iend = gDIOSources.size(); i < iend; ++i) {
-    auto& source = gDIOSources[i];
-    if (HALSIM_GetDIOInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<DIOValueSource>(i);
-        source->SetName(gDIO[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void UpdateDPWMSources() {
-  const int numDIO = gDIOSources.size();
-  for (int i = 0, iend = gDPWMSources.size(); i < iend; ++i) {
-    auto& source = gDPWMSources[i];
-    if (HALSIM_GetDigitalPWMInitialized(i)) {
-      if (!source) {
-        int channel = HALSIM_GetDigitalPWMPin(i);
-        if (channel >= 0 && channel < numDIO) {
-          source = std::make_unique<DigitalPWMDutyCycleSource>(i, channel);
-          source->SetName(gDIO[channel].GetName());
-        }
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void UpdateDutyCycleSources() {
-  const int numDIO = gDIOSources.size();
-  for (int i = 0, iend = gDutyCycleSources.size(); i < iend; ++i) {
-    auto& source = gDutyCycleSources[i];
-    if (HALSIM_GetDutyCycleInitialized(i)) {
-      if (!source) {
-        int channel = HALSIM_GetDutyCycleDigitalChannel(i);
-        if (channel >= 0 && channel < numDIO) {
-          source = std::make_unique<DutyCycleOutputSource>(i, channel);
-          source->SetName(gDIO[channel].GetName());
-        }
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayDIO() {
-  bool hasAny = false;
-  const int numDIO = gDIOSources.size();
-  const int numPWM = gDPWMSources.size();
-  static const int numEncoder = HAL_GetNumEncoders();
-  const int numDutyCycle = gDutyCycleSources.size();
-  static auto pwmMap = std::make_unique<int[]>(numDIO);
-  static auto encoderMap = std::make_unique<int[]>(numDIO);
-  static auto dutyCycleMap = std::make_unique<int[]>(numDIO);
-
-  std::memset(pwmMap.get(), 0, numDIO * sizeof(pwmMap[0]));
-  std::memset(encoderMap.get(), 0, numDIO * sizeof(encoderMap[0]));
-  std::memset(dutyCycleMap.get(), 0, numDIO * sizeof(dutyCycleMap[0]));
-
-  for (int i = 0; i < numPWM; ++i) {
-    if (auto source = gDPWMSources[i].get()) {
-      int channel = source->GetChannel();
-      if (channel >= 0 && channel < numDIO) pwmMap[channel] = i + 1;
-    }
-  }
-
-  for (int i = 0; i < numEncoder; ++i) {
-    if (HALSIM_GetEncoderInitialized(i)) {
-      int channel;
-      channel = HALSIM_GetEncoderDigitalChannelA(i);
-      if (channel >= 0 && channel < numDIO) encoderMap[channel] = i + 1;
-      channel = HALSIM_GetEncoderDigitalChannelB(i);
-      if (channel >= 0 && channel < numDIO) encoderMap[channel] = i + 1;
-    }
-  }
-
-  for (int i = 0; i < numDutyCycle; ++i) {
-    if (auto source = gDutyCycleSources[i].get()) {
-      int channel = source->GetChannel();
-      if (channel >= 0 && channel < numDIO) dutyCycleMap[channel] = i + 1;
-    }
-  }
-
-  ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
-  for (int i = 0; i < numDIO; ++i) {
-    if (auto dioSource = gDIOSources[i].get()) {
-      ImGui::PushID(i);
-      hasAny = true;
-      DigitalPWMDutyCycleSource* dpwmSource = nullptr;
-      DutyCycleOutputSource* dutyCycleSource = nullptr;
-      auto& info = gDIO[i];
-      char label[128];
-      if (pwmMap[i] > 0) {
-        dpwmSource = gDPWMSources[pwmMap[i] - 1].get();
-        info.GetLabel(label, sizeof(label), "PWM", i);
-        if (auto simDevice = HALSIM_GetDIOSimDevice(i)) {
-          LabelSimDevice(label, simDevice);
-        } else {
-          dpwmSource->LabelText(label, "%0.3f", dpwmSource->GetValue());
-        }
-      } else if (encoderMap[i] > 0) {
-        info.GetLabel(label, sizeof(label), " In", i);
-        if (auto simDevice = HALSIM_GetEncoderSimDevice(encoderMap[i] - 1)) {
-          LabelSimDevice(label, simDevice);
-        } else {
-          ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-          ImGui::LabelText(label, "Encoder[%d,%d]",
-                           HALSIM_GetEncoderDigitalChannelA(encoderMap[i] - 1),
-                           HALSIM_GetEncoderDigitalChannelB(encoderMap[i] - 1));
-          ImGui::PopStyleColor();
-        }
-      } else if (dutyCycleMap[i] > 0) {
-        dutyCycleSource = gDutyCycleSources[dutyCycleMap[i] - 1].get();
-        info.GetLabel(label, sizeof(label), "Dty", i);
-        if (auto simDevice =
-                HALSIM_GetDutyCycleSimDevice(dutyCycleMap[i] - 1)) {
-          LabelSimDevice(label, simDevice);
-        } else {
-          double val = dutyCycleSource->GetValue();
-          if (dutyCycleSource->InputDouble(label, &val))
-            HALSIM_SetDutyCycleOutput(dutyCycleMap[i] - 1, val);
-        }
-      } else if (!HALSIM_GetDIOIsInput(i)) {
-        info.GetLabel(label, sizeof(label), "Out", i);
-        if (auto simDevice = HALSIM_GetDIOSimDevice(i)) {
-          LabelSimDevice(label, simDevice);
-        } else {
-          dioSource->LabelText(
-              label, "%s", dioSource->GetValue() != 0 ? "1 (high)" : "0 (low)");
-        }
-      } else {
-        info.GetLabel(label, sizeof(label), " In", i);
-        if (auto simDevice = HALSIM_GetDIOSimDevice(i)) {
-          LabelSimDevice(label, simDevice);
-        } else {
-          static const char* options[] = {"0 (low)", "1 (high)"};
-          int val = dioSource->GetValue() != 0 ? 1 : 0;
-          if (dioSource->Combo(label, &val, options, 2))
-            HALSIM_SetDIOValue(i, val);
-        }
-      }
-      if (info.PopupEditName(i)) {
-        dioSource->SetName(info.GetName());
-        if (dpwmSource) dpwmSource->SetName(info.GetName());
-        if (dutyCycleSource) dutyCycleSource->SetName(info.GetName());
-      }
-      ImGui::PopID();
-    }
-  }
-  ImGui::PopItemWidth();
-  if (!hasAny) ImGui::Text("No Digital I/O");
-}
-
-void DIOGui::Initialize() {
-  gDIO.Initialize();
-  gDIOSources.resize(HAL_GetNumDigitalChannels());
-  gDPWMSources.resize(HAL_GetNumDigitalPWMOutputs());
-  gDutyCycleSources.resize(HAL_GetNumDutyCycles());
-
-  HALSimGui::AddExecute(UpdateDIOSources);
-  HALSimGui::AddExecute(UpdateDPWMSources);
-  HALSimGui::AddExecute(UpdateDutyCycleSources);
-  HALSimGui::AddWindow("DIO", DisplayDIO, ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("DIO", 470, 20);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOGui.h b/simulation/halsim_gui/src/main/native/cpp/DIOGui.h
deleted file mode 100644
index 70181e3..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class DIOGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp
new file mode 100644
index 0000000..22c06dd
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp
@@ -0,0 +1,245 @@
+// 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 "DIOSimGui.h"
+
+#include <glass/hardware/DIO.h>
+#include <glass/hardware/Encoder.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/DIOData.h>
+#include <hal/simulation/DigitalPWMData.h>
+#include <hal/simulation/DutyCycleData.h>
+#include <hal/simulation/EncoderData.h>
+#include <hal/simulation/SimDeviceData.h>
+
+#include "EncoderSimGui.h"
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(DIOValue, "DIO");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DigitalPWMDutyCycle, "DPWM");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DutyCycleOutput, "DutyCycle");
+
+class DPWMSimModel : public glass::DPWMModel {
+ public:
+  DPWMSimModel(int32_t index, int32_t dioChannel)
+      : m_dioChannel{dioChannel}, m_index{index}, m_valueData{index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetDigitalPWMInitialized(m_index); }
+
+  const char* GetSimDevice() const override {
+    if (auto simDevice = HALSIM_GetDIOSimDevice(m_dioChannel)) {
+      return HALSIM_GetSimDeviceName(simDevice);
+    } else {
+      return nullptr;
+    }
+  }
+
+  glass::DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(double val) override {
+    HALSIM_SetDigitalPWMDutyCycle(m_index, val);
+  }
+
+ private:
+  int32_t m_dioChannel;
+  int32_t m_index;
+  DigitalPWMDutyCycleSource m_valueData;
+};
+
+class DutyCycleSimModel : public glass::DutyCycleModel {
+ public:
+  explicit DutyCycleSimModel(int32_t index)
+      : m_index{index}, m_valueData{index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetDutyCycleInitialized(m_index); }
+
+  const char* GetSimDevice() const override {
+    if (auto simDevice = HALSIM_GetDutyCycleSimDevice(m_index)) {
+      return HALSIM_GetSimDeviceName(simDevice);
+    } else {
+      return nullptr;
+    }
+  }
+
+  glass::DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(double val) override {
+    HALSIM_SetDutyCycleOutput(m_index, val);
+  }
+
+ private:
+  int32_t m_index;
+  DutyCycleOutputSource m_valueData;
+};
+
+class DIOSimModel : public glass::DIOModel {
+ public:
+  explicit DIOSimModel(int32_t channel)
+      : m_channel{channel}, m_valueData{channel} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetDIOInitialized(m_channel); }
+
+  bool IsReadOnly() override { return !IsInput(); }
+
+  const char* GetName() const override { return ""; }
+
+  const char* GetSimDevice() const override {
+    if (auto simDevice = HALSIM_GetDIOSimDevice(m_channel)) {
+      return HALSIM_GetSimDeviceName(simDevice);
+    } else {
+      return nullptr;
+    }
+  }
+
+  DPWMSimModel* GetDPWM() override { return m_dpwmSource; }
+  DutyCycleSimModel* GetDutyCycle() override { return m_dutyCycleSource; }
+  glass::EncoderModel* GetEncoder() override { return m_encoderSource; }
+
+  void SetDPWM(DPWMSimModel* model) { m_dpwmSource = model; }
+  void SetDutyCycle(DutyCycleSimModel* model) { m_dutyCycleSource = model; }
+  void SetEncoder(glass::EncoderModel* model) { m_encoderSource = model; }
+
+  bool IsInput() const override { return HALSIM_GetDIOIsInput(m_channel); }
+
+  glass::DataSource* GetValueData() override { return &m_valueData; }
+
+  void SetValue(bool val) override { HALSIM_SetDIOValue(m_channel, val); }
+
+ private:
+  int32_t m_channel;
+  DIOValueSource m_valueData;
+  DPWMSimModel* m_dpwmSource = nullptr;
+  DutyCycleSimModel* m_dutyCycleSource = nullptr;
+  glass::EncoderModel* m_encoderSource = nullptr;
+};
+
+class DIOsSimModel : public glass::DIOsModel {
+ public:
+  DIOsSimModel()
+      : m_dioModels(HAL_GetNumDigitalChannels()),
+        m_dpwmModels(HAL_GetNumDigitalPWMOutputs()),
+        m_dutyCycleModels(HAL_GetNumDutyCycles()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachDIO(
+      wpi::function_ref<void(glass::DIOModel& model, int index)> func) override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<DIOSimModel>> m_dioModels;
+  // indexed by index
+  std::vector<std::unique_ptr<DPWMSimModel>> m_dpwmModels;
+  std::vector<std::unique_ptr<DutyCycleSimModel>> m_dutyCycleModels;
+};
+}  // namespace
+
+void DIOsSimModel::Update() {
+  const int32_t numDIO = m_dioModels.size();
+  for (int i = 0; i < numDIO; ++i) {
+    auto& model = m_dioModels[i];
+    if (HALSIM_GetDIOInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<DIOSimModel>(i);
+      }
+      model->SetDPWM(nullptr);
+      model->SetDutyCycle(nullptr);
+      model->SetEncoder(nullptr);
+    } else {
+      model.reset();
+    }
+  }
+
+  const int32_t numPWM = m_dpwmModels.size();
+  for (int32_t i = 0; i < numPWM; ++i) {
+    auto& model = m_dpwmModels[i];
+    if (HALSIM_GetDigitalPWMInitialized(i)) {
+      if (!model) {
+        int channel = HALSIM_GetDigitalPWMPin(i);
+        if (channel >= 0 && channel < numDIO && m_dioModels[channel]) {
+          model = std::make_unique<DPWMSimModel>(i, channel);
+          m_dioModels[channel]->SetDPWM(model.get());
+        }
+      }
+    } else {
+      model.reset();
+    }
+  }
+
+  const int32_t numDutyCycle = m_dutyCycleModels.size();
+  for (int32_t i = 0; i < numDutyCycle; ++i) {
+    auto& model = m_dutyCycleModels[i];
+    if (HALSIM_GetDutyCycleInitialized(i)) {
+      if (!model) {
+        int channel = HALSIM_GetDutyCycleDigitalChannel(i);
+        if (channel >= 0 && channel < numDIO && m_dioModels[channel]) {
+          model = std::make_unique<DutyCycleSimModel>(i);
+          m_dioModels[channel]->SetDutyCycle(model.get());
+        }
+      }
+    } else {
+      model.reset();
+    }
+  }
+
+  EncoderSimGui::GetEncodersModel().ForEachEncoder([&](auto& encoder, int i) {
+    int channel = encoder.GetChannelA();
+    if (channel >= 0 && channel < numDIO && m_dioModels[channel]) {
+      m_dioModels[channel]->SetEncoder(&encoder);
+    }
+    channel = encoder.GetChannelB();
+    if (channel >= 0 && channel < numDIO && m_dioModels[channel]) {
+      m_dioModels[channel]->SetEncoder(&encoder);
+    }
+  });
+}
+
+void DIOsSimModel::ForEachDIO(
+    wpi::function_ref<void(glass::DIOModel& model, int index)> func) {
+  const int32_t numDIO = m_dioModels.size();
+  for (int32_t i = 0; i < numDIO; ++i) {
+    if (auto model = m_dioModels[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool DIOAnyInitialized() {
+  static const int32_t num = HAL_GetNumDigitalChannels();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetDIOInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void DIOSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "DIO", DIOAnyInitialized, [] { return std::make_unique<DIOsSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(470, 20);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayDIOs(static_cast<DIOsSimModel*>(model),
+                             HALSimGui::halProvider.AreOutputsEnabled());
+        });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h
new file mode 100644
index 0000000..9e9e52b
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class DIOSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
index 96b53cb..42ec8a3 100644
--- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp
@@ -1,90 +1,196 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "DriverStationGui.h"
 
+#include <glass/other/FMS.h>
+#include <glass/support/ExtraGuiWidgets.h>
+#include <glass/support/IniSaverInfo.h>
+
+#include <algorithm>
 #include <atomic>
 #include <cstring>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <vector>
 
 #include <GLFW/glfw3.h>
+#include <fmt/format.h>
+#include <hal/DriverStationTypes.h>
 #include <hal/simulation/DriverStationData.h>
 #include <hal/simulation/MockHooks.h>
 #include <imgui.h>
 #include <imgui_internal.h>
-#include <wpi/Format.h>
-#include <wpi/SmallString.h>
-#include <wpi/StringRef.h>
-#include <wpi/raw_ostream.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+#include <wpigui.h>
 
-#include "ExtraGuiWidgets.h"
-#include "GuiDataSource.h"
+#include "HALDataSource.h"
 #include "HALSimGui.h"
-#include "IniSaverInfo.h"
 
 using namespace halsimgui;
 
 namespace {
 
-struct SystemJoystick {
-  bool present = false;
-  int axisCount = 0;
-  const float* axes = nullptr;
-  int buttonCount = 0;
-  const unsigned char* buttons = nullptr;
-  int hatCount = 0;
-  const unsigned char* hats = nullptr;
-  const char* name = nullptr;
-  bool isGamepad = false;
-  GLFWgamepadstate gamepadState;
-
-  bool anyButtonPressed = false;
-
-  void Update(int i);
-};
-
-struct RobotJoystick {
-  NameInfo name;
-  std::string guid;
-  const SystemJoystick* sys = nullptr;
-  bool useGamepad = false;
-
+struct HALJoystickData {
+  HALJoystickData() {
+    std::memset(&desc, 0, sizeof(desc));
+    desc.type = -1;
+    std::memset(&axes, 0, sizeof(axes));
+    std::memset(&buttons, 0, sizeof(buttons));
+    std::memset(&povs, 0, sizeof(povs));
+  }
   HAL_JoystickDescriptor desc;
   HAL_JoystickAxes axes;
   HAL_JoystickButtons buttons;
   HAL_JoystickPOVs povs;
+};
 
-  void Clear();
+class SystemJoystick {
+ public:
+  virtual ~SystemJoystick() = default;
+
+  bool IsPresent() const { return m_present; }
+  bool IsAnyButtonPressed() const { return m_anyButtonPressed; }
+  bool IsGamepad() const { return m_isGamepad; }
+
+  virtual void SettingsDisplay() {}
+  virtual void Update() = 0;
+  virtual const char* GetName() const = 0;
+  virtual void GetData(HALJoystickData* data, bool mapGamepad) const = 0;
+  virtual const char* GetGUID() const = 0;
+  virtual int GetIndex() const = 0;
+
+ protected:
+  bool m_present = false;
+  bool m_anyButtonPressed = false;
+  bool m_isGamepad = false;
+};
+
+class GlfwSystemJoystick : public SystemJoystick {
+ public:
+  explicit GlfwSystemJoystick(int i) : m_index{i} {}
+
+  void Update() override;
+  const char* GetName() const override { return m_name ? m_name : "(null)"; }
+  void GetData(HALJoystickData* data, bool mapGamepad) const override;
+  const char* GetGUID() const override { return glfwGetJoystickGUID(m_index); }
+  int GetIndex() const override { return m_index; }
+
+ private:
+  int m_index;
+  int m_axisCount = 0;
+  const float* m_axes = nullptr;
+  int m_buttonCount = 0;
+  const unsigned char* m_buttons = nullptr;
+  int m_hatCount = 0;
+  const unsigned char* m_hats = nullptr;
+  const char* m_name = nullptr;
+  GLFWgamepadstate m_gamepadState;
+};
+
+class KeyboardJoystick : public SystemJoystick {
+ public:
+  explicit KeyboardJoystick(int index);
+
+  void SettingsDisplay() override;
+  void Update() override;
+  const char* GetName() const override { return m_name; }
+  void GetData(HALJoystickData* data, bool mapGamepad) const override {
+    *data = m_data;
+  }
+  const char* GetGUID() const override { return m_guid; }
+  int GetIndex() const override { return m_index + GLFW_JOYSTICK_LAST + 1; }
+
+  void ClearKey(int key);
+  virtual const char* GetKeyName(int key) const = 0;
+
+  void ReadIni(std::string_view name, std::string_view value);
+  void WriteIni(ImGuiTextBuffer* out_buf) const;
+
+ protected:
+  void EditKey(const char* label, int* key);
+
+  int m_index;
+  char m_name[20];
+  char m_guid[20];
+
+  static int* s_keyEdit;
+
+  HALJoystickData m_data;
+
+  struct AxisConfig {
+    int incKey = -1;
+    int decKey = -1;
+    float keyRate = 0.05f;
+    float decayRate = 0.05f;
+    float maxAbsValue = 1.0f;
+  };
+  AxisConfig m_axisConfig[HAL_kMaxJoystickAxes];
+
+  static constexpr int kMaxButtonCount = 32;
+  int m_buttonKey[kMaxButtonCount];
+
+  struct PovConfig {
+    int key0 = -1;
+    int key45 = -1;
+    int key90 = -1;
+    int key135 = -1;
+    int key180 = -1;
+    int key225 = -1;
+    int key270 = -1;
+    int key315 = -1;
+  };
+
+  PovConfig m_povConfig[HAL_kMaxJoystickPOVs];
+};
+
+class GlfwKeyboardJoystick : public KeyboardJoystick {
+ public:
+  explicit GlfwKeyboardJoystick(int index, bool noDefaults = false);
+
+  const char* GetKeyName(int key) const override;
+};
+
+struct RobotJoystick {
+  glass::NameInfo name;
+  std::string guid;
+  const SystemJoystick* sys = nullptr;
+  bool useGamepad = false;
+
+  HALJoystickData data;
+
+  void Clear() { data = HALJoystickData{}; }
   void Update();
   void SetHAL(int i);
   void GetHAL(int i);
-  bool IsButtonPressed(int i) { return (buttons.buttons & (1u << i)) != 0; }
+  bool IsButtonPressed(int i) {
+    return (data.buttons.buttons & (1u << i)) != 0;
+  }
 };
 
-class JoystickSource {
+class JoystickModel {
  public:
-  explicit JoystickSource(int index);
-  ~JoystickSource() {
+  explicit JoystickModel(int index);
+  ~JoystickModel() {
     HALSIM_CancelDriverStationNewDataCallback(m_callback);
-    for (int i = 0; i < buttonCount; ++i) delete buttons[i];
+    for (int i = 0; i < buttonCount; ++i) {
+      delete buttons[i];
+    }
   }
-  JoystickSource(const JoystickSource&) = delete;
-  JoystickSource& operator=(const JoystickSource&) = delete;
+  JoystickModel(const JoystickModel&) = delete;
+  JoystickModel& operator=(const JoystickModel&) = delete;
 
   int axisCount;
   int buttonCount;
   int povCount;
-  std::unique_ptr<GuiDataSource> axes[HAL_kMaxJoystickAxes];
+  std::unique_ptr<glass::DataSource> axes[HAL_kMaxJoystickAxes];
   // use pointer instead of unique_ptr to allow it to be passed directly
   // to DrawLEDSources()
-  GuiDataSource* buttons[32];
-  std::unique_ptr<GuiDataSource> povs[HAL_kMaxJoystickPOVs];
+  glass::DataSource* buttons[32];
+  std::unique_ptr<glass::DataSource> povs[HAL_kMaxJoystickPOVs];
 
  private:
   static void CallbackFunc(const char*, void* param, const HAL_Value*);
@@ -92,103 +198,188 @@
   int m_index;
   int32_t m_callback;
 };
+
+class FMSSimModel : public glass::FMSModel {
+ public:
+  FMSSimModel();
+
+  glass::DataSource* GetFmsAttachedData() override { return &m_fmsAttached; }
+  glass::DataSource* GetDsAttachedData() override { return &m_dsAttached; }
+  glass::DataSource* GetAllianceStationIdData() override {
+    return &m_allianceStationId;
+  }
+  glass::DataSource* GetMatchTimeData() override { return &m_matchTime; }
+  glass::DataSource* GetEStopData() override { return &m_estop; }
+  glass::DataSource* GetEnabledData() override { return &m_enabled; }
+  glass::DataSource* GetTestData() override { return &m_test; }
+  glass::DataSource* GetAutonomousData() override { return &m_autonomous; }
+  std::string_view GetGameSpecificMessage(
+      wpi::SmallVectorImpl<char>& buf) override {
+    HAL_MatchInfo info;
+    HALSIM_GetMatchInfo(&info);
+    buf.clear();
+    buf.append(info.gameSpecificMessage,
+               info.gameSpecificMessage + info.gameSpecificMessageSize);
+    return std::string_view(buf.begin(), buf.size());
+  }
+
+  void SetFmsAttached(bool val) override {
+    HALSIM_SetDriverStationFmsAttached(val);
+  }
+  void SetDsAttached(bool val) override {
+    HALSIM_SetDriverStationDsAttached(val);
+  }
+  void SetAllianceStationId(int val) override {
+    HALSIM_SetDriverStationAllianceStationId(
+        static_cast<HAL_AllianceStationID>(val));
+  }
+  void SetMatchTime(double val) override {
+    HALSIM_SetDriverStationMatchTime(val);
+    int32_t status = 0;
+    m_startMatchTime = HAL_GetFPGATime(&status) * 1.0e-6 - val;
+  }
+  void SetEStop(bool val) override { HALSIM_SetDriverStationEStop(val); }
+  void SetEnabled(bool val) override { HALSIM_SetDriverStationEnabled(val); }
+  void SetTest(bool val) override { HALSIM_SetDriverStationTest(val); }
+  void SetAutonomous(bool val) override {
+    HALSIM_SetDriverStationAutonomous(val);
+  }
+  void SetGameSpecificMessage(const char* val) override {
+    HALSIM_SetGameSpecificMessage(val);
+  }
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  bool IsReadOnly() override;
+
+  bool m_matchTimeEnabled = true;
+
+ private:
+  glass::DataSource m_fmsAttached{"FMS:FMSAttached"};
+  glass::DataSource m_dsAttached{"FMS:DSAttached"};
+  glass::DataSource m_allianceStationId{"FMS:AllianceStationID"};
+  glass::DataSource m_matchTime{"FMS:MatchTime"};
+  glass::DataSource m_estop{"FMS:EStop"};
+  glass::DataSource m_enabled{"FMS:RobotEnabled"};
+  glass::DataSource m_test{"FMS:TestMode"};
+  glass::DataSource m_autonomous{"FMS:AutonomousMode"};
+  double m_startMatchTime = 0.0;
+  double m_prevTime = 0.0;
+};
+
 }  // namespace
 
 // system joysticks
-static SystemJoystick gSystemJoysticks[GLFW_JOYSTICK_LAST + 1];
-static int gNumSystemJoysticks = 0;
+static std::vector<std::unique_ptr<SystemJoystick>> gGlfwJoysticks;
+static int gNumGlfwJoysticks = 0;
+static std::vector<std::unique_ptr<GlfwKeyboardJoystick>> gKeyboardJoysticks;
 
 // robot joysticks
 static RobotJoystick gRobotJoysticks[HAL_kMaxJoysticks];
-static std::unique_ptr<JoystickSource> gJoystickSources[HAL_kMaxJoysticks];
+static std::unique_ptr<JoystickModel> gJoystickSources[HAL_kMaxJoysticks];
+
+// FMS
+static std::unique_ptr<FMSSimModel> gFMSModel;
+
+// Window management
+DSManager DriverStationGui::dsManager{"DSManager"};
 
 static bool gDisableDS = false;
+static bool gZeroDisconnectedJoysticks = true;
+static bool gUseEnableDisableHotkeys = false;
+static bool gUseEstopHotkey = false;
 static std::atomic<bool>* gDSSocketConnected = nullptr;
 
 static inline bool IsDSDisabled() {
   return gDisableDS || (gDSSocketConnected && *gDSSocketConnected);
 }
 
-JoystickSource::JoystickSource(int index) : m_index{index} {
+JoystickModel::JoystickModel(int index) : m_index{index} {
   HAL_JoystickAxes halAxes;
   HALSIM_GetJoystickAxes(index, &halAxes);
   axisCount = halAxes.count;
   for (int i = 0; i < axisCount; ++i) {
-    axes[i] = std::make_unique<GuiDataSource>("Joystick[" + wpi::Twine{index} +
-                                              "] Axis[" + wpi::Twine{i} +
-                                              wpi::Twine{']'});
+    axes[i] = std::make_unique<glass::DataSource>(
+        fmt::format("Joystick[{}] Axis[{}]", index, i));
   }
 
   HAL_JoystickButtons halButtons;
   HALSIM_GetJoystickButtons(index, &halButtons);
   buttonCount = halButtons.count;
   for (int i = 0; i < buttonCount; ++i) {
-    buttons[i] =
-        new GuiDataSource("Joystick[" + wpi::Twine{index} + "] Button[" +
-                          wpi::Twine{i + 1} + wpi::Twine{']'});
+    buttons[i] = new glass::DataSource(
+        fmt::format("Joystick[{}] Button[{}]", index, i + 1));
     buttons[i]->SetDigital(true);
   }
-  for (int i = buttonCount; i < 32; ++i) buttons[i] = nullptr;
+  for (int i = buttonCount; i < 32; ++i) {
+    buttons[i] = nullptr;
+  }
 
   HAL_JoystickPOVs halPOVs;
   HALSIM_GetJoystickPOVs(index, &halPOVs);
   povCount = halPOVs.count;
   for (int i = 0; i < povCount; ++i) {
-    povs[i] = std::make_unique<GuiDataSource>("Joystick[" + wpi::Twine{index} +
-                                              "] POV[" + wpi::Twine{i} +
-                                              wpi::Twine{']'});
+    povs[i] = std::make_unique<glass::DataSource>(
+        fmt::format("Joystick[{}] POV [{}]", index, i));
   }
 
   m_callback =
       HALSIM_RegisterDriverStationNewDataCallback(CallbackFunc, this, true);
 }
 
-void JoystickSource::CallbackFunc(const char*, void* param, const HAL_Value*) {
-  auto self = static_cast<JoystickSource*>(param);
+void JoystickModel::CallbackFunc(const char*, void* param, const HAL_Value*) {
+  auto self = static_cast<JoystickModel*>(param);
 
   HAL_JoystickAxes halAxes;
   HALSIM_GetJoystickAxes(self->m_index, &halAxes);
   for (int i = 0; i < halAxes.count; ++i) {
-    if (auto axis = self->axes[i].get()) axis->SetValue(halAxes.axes[i]);
+    if (auto axis = self->axes[i].get()) {
+      axis->SetValue(halAxes.axes[i]);
+    }
   }
 
   HAL_JoystickButtons halButtons;
   HALSIM_GetJoystickButtons(self->m_index, &halButtons);
   for (int i = 0; i < halButtons.count; ++i) {
-    if (auto button = self->buttons[i])
+    if (auto button = self->buttons[i]) {
       button->SetValue((halButtons.buttons & (1u << i)) != 0 ? 1 : 0);
+    }
   }
 
   HAL_JoystickPOVs halPOVs;
   HALSIM_GetJoystickPOVs(self->m_index, &halPOVs);
   for (int i = 0; i < halPOVs.count; ++i) {
-    if (auto pov = self->povs[i].get()) pov->SetValue(halPOVs.povs[i]);
+    if (auto pov = self->povs[i].get()) {
+      pov->SetValue(halPOVs.povs[i]);
+    }
   }
 }
 
 // read/write joystick mapping to ini file
 static void* JoystickReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
                               const char* name) {
-  int num;
-  if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr;
-  if (num < 0 || num >= HAL_kMaxJoysticks) return nullptr;
+  int num = wpi::parse_integer<int>(name, 10).value_or(-1);
+  if (num < 0 || num >= HAL_kMaxJoysticks) {
+    return nullptr;
+  }
   return &gRobotJoysticks[num];
 }
 
 static void JoystickReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                             void* entry, const char* lineStr) {
+                             void* entry, const char* line) {
   RobotJoystick* joy = static_cast<RobotJoystick*>(entry);
   // format: guid=guid or useGamepad=0/1
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
+  auto [name, value] = wpi::split(line, '=');
+  name = wpi::trim(name);
+  value = wpi::trim(value);
   if (name == "guid") {
     joy->guid = value;
   } else if (name == "useGamepad") {
-    int num;
-    if (value.getAsInteger(10, num)) return;
-    joy->useGamepad = num;
+    if (auto num = wpi::parse_integer<int>(value, 10)) {
+      joy->useGamepad = num.value();
+    }
   } else {
     joy->name.ReadIni(name, value);
   }
@@ -198,14 +389,52 @@
                              ImGuiTextBuffer* out_buf) {
   for (int i = 0; i < HAL_kMaxJoysticks; ++i) {
     auto& joy = gRobotJoysticks[i];
-    if (!joy.name.HasName() && !joy.sys) continue;
+    if (!joy.name.HasName() && !joy.sys) {
+      continue;
+    }
     out_buf->appendf("[Joystick][%d]\nuseGamepad=%d\n", i,
                      joy.useGamepad ? 1 : 0);
-    if (joy.name.HasName()) joy.name.WriteIni(out_buf);
-    if (joy.sys) {
-      const char* guid = glfwGetJoystickGUID(joy.sys - gSystemJoysticks);
-      if (guid) out_buf->appendf("guid=%s\n", guid);
+    if (joy.name.HasName()) {
+      joy.name.WriteIni(out_buf);
     }
+    if (joy.sys) {
+      const char* guid = joy.sys->GetGUID();
+      if (guid) {
+        out_buf->appendf("guid=%s\n", guid);
+      }
+    }
+    out_buf->append("\n");
+  }
+}
+
+// read/write keyboard joystick mapping to ini file
+static void* KeyboardJoystickReadOpen(ImGuiContext* ctx,
+                                      ImGuiSettingsHandler* handler,
+                                      const char* name) {
+  int num = wpi::parse_integer<int>(name, 10).value_or(-1);
+  if (num < 0 || num >= static_cast<int>(gKeyboardJoysticks.size())) {
+    return nullptr;
+  }
+  auto joy = gKeyboardJoysticks[num].get();
+  *joy = GlfwKeyboardJoystick(num, true);
+  return joy;
+}
+
+static void KeyboardJoystickReadLine(ImGuiContext* ctx,
+                                     ImGuiSettingsHandler* handler, void* entry,
+                                     const char* line) {
+  auto joy = static_cast<KeyboardJoystick*>(entry);
+  // format: guid=guid or useGamepad=0/1
+  auto [name, value] = wpi::split(line, '=');
+  joy->ReadIni(wpi::trim(name), wpi::trim(value));
+}
+
+static void KeyboardJoystickWriteAll(ImGuiContext* ctx,
+                                     ImGuiSettingsHandler* handler,
+                                     ImGuiTextBuffer* out_buf) {
+  for (unsigned int i = 0; i < gKeyboardJoysticks.size(); ++i) {
+    out_buf->appendf("[KeyboardJoystick][%u]\n", i);
+    gKeyboardJoysticks[i]->WriteIni(out_buf);
     out_buf->append("\n");
   }
 }
@@ -214,62 +443,83 @@
 static void* DriverStationReadOpen(ImGuiContext* ctx,
                                    ImGuiSettingsHandler* handler,
                                    const char* name) {
-  if (name == wpi::StringRef{"Main"}) return &gDisableDS;
+  if (name == std::string_view{"Main"}) {
+    return &gDisableDS;
+  }
   return nullptr;
 }
 
 static void DriverStationReadLine(ImGuiContext* ctx,
                                   ImGuiSettingsHandler* handler, void* entry,
-                                  const char* lineStr) {
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
+                                  const char* line) {
+  auto [name, value] = wpi::split(line, '=');
+  name = wpi::trim(name);
+  value = wpi::trim(value);
   if (name == "disable") {
-    int num;
-    if (value.getAsInteger(10, num)) return;
-    gDisableDS = num;
+    if (auto num = wpi::parse_integer<int>(value, 10)) {
+      gDisableDS = num.value();
+    }
+  } else if (name == "zeroDisconnectedJoysticks") {
+    if (auto num = wpi::parse_integer<int>(value, 10)) {
+      gZeroDisconnectedJoysticks = num.value();
+    }
+  } else if (name == "enableDisableKeys") {
+    if (auto num = wpi::parse_integer<int>(value, 10)) {
+      gUseEnableDisableHotkeys = num.value();
+    }
+  } else if (name == "estopKey") {
+    if (auto num = wpi::parse_integer<int>(value, 10)) {
+      gUseEstopHotkey = num.value();
+    }
   }
 }
 
 static void DriverStationWriteAll(ImGuiContext* ctx,
                                   ImGuiSettingsHandler* handler,
                                   ImGuiTextBuffer* out_buf) {
-  out_buf->appendf("[DriverStation][Main]\ndisable=%d\n\n", gDisableDS ? 1 : 0);
+  out_buf->appendf(
+      "[DriverStation][Main]\ndisable=%d\nzeroDisconnectedJoysticks=%d\n"
+      "enableDisableKeys=%d\nestopKey=%d\n\n",
+      gDisableDS ? 1 : 0, gZeroDisconnectedJoysticks ? 1 : 0,
+      gUseEnableDisableHotkeys ? 1 : 0, gUseEstopHotkey ? 1 : 0);
 }
 
-void SystemJoystick::Update(int i) {
-  bool wasPresent = present;
-  present = glfwJoystickPresent(i);
+void GlfwSystemJoystick::Update() {
+  bool wasPresent = m_present;
+  m_present = glfwJoystickPresent(m_index);
 
-  if (!present) return;
-  axes = glfwGetJoystickAxes(i, &axisCount);
-  buttons = glfwGetJoystickButtons(i, &buttonCount);
-  hats = glfwGetJoystickHats(i, &hatCount);
-  isGamepad = glfwGetGamepadState(i, &gamepadState);
+  if (!m_present) {
+    return;
+  }
+  m_axes = glfwGetJoystickAxes(m_index, &m_axisCount);
+  m_buttons = glfwGetJoystickButtons(m_index, &m_buttonCount);
+  m_hats = glfwGetJoystickHats(m_index, &m_hatCount);
+  m_isGamepad = glfwGetGamepadState(m_index, &m_gamepadState);
 
-  anyButtonPressed = false;
-  for (int j = 0; j < buttonCount; ++j) {
-    if (buttons[j]) {
-      anyButtonPressed = true;
+  m_anyButtonPressed = false;
+  for (int j = 0; j < m_buttonCount; ++j) {
+    if (m_buttons[j]) {
+      m_anyButtonPressed = true;
       break;
     }
   }
-  for (int j = 0; j < hatCount; ++j) {
-    if (hats[j] != GLFW_HAT_CENTERED) {
-      anyButtonPressed = true;
+  for (int j = 0; j < m_hatCount; ++j) {
+    if (m_hats[j] != GLFW_HAT_CENTERED) {
+      m_anyButtonPressed = true;
       break;
     }
   }
 
-  if (!present || wasPresent) return;
-  name = glfwGetJoystickName(i);
+  if (!m_present || wasPresent) {
+    return;
+  }
+  m_name = glfwGetJoystickName(m_index);
 
   // try to find matching GUID
-  if (const char* guid = glfwGetJoystickGUID(i)) {
+  if (const char* guid = glfwGetJoystickGUID(m_index)) {
     for (auto&& joy : gRobotJoysticks) {
       if (guid == joy.guid) {
-        joy.sys = &gSystemJoysticks[i];
+        joy.sys = this;
         joy.guid.clear();
         break;
       }
@@ -300,88 +550,582 @@
   }
 }
 
-void RobotJoystick::Clear() {
-  std::memset(&desc, 0, sizeof(desc));
-  desc.type = -1;
-  std::memset(&axes, 0, sizeof(axes));
-  std::memset(&buttons, 0, sizeof(buttons));
-  std::memset(&povs, 0, sizeof(povs));
-}
-
-void RobotJoystick::Update() {
-  Clear();
-
-  if (!sys || !sys->present) return;
+void GlfwSystemJoystick::GetData(HALJoystickData* data, bool mapGamepad) const {
+  if (!m_present) {
+    return;
+  }
 
   // use gamepad mappings if present and enabled
   const float* sysAxes;
   const unsigned char* sysButtons;
-  if (sys->isGamepad && useGamepad) {
-    sysAxes = sys->gamepadState.axes;
+  if (m_isGamepad && mapGamepad) {
+    sysAxes = m_gamepadState.axes;
     // don't remap on windows
 #ifdef _WIN32
-    sysButtons = sys->buttons;
+    sysButtons = m_buttons;
 #else
-    sysButtons = sys->gamepadState.buttons;
+    sysButtons = m_gamepadState.buttons;
 #endif
   } else {
-    sysAxes = sys->axes;
-    sysButtons = sys->buttons;
+    sysAxes = m_axes;
+    sysButtons = m_buttons;
   }
 
   // copy into HAL structures
-  desc.isXbox = sys->isGamepad ? 1 : 0;
-  desc.type = sys->isGamepad ? 21 : 20;
-  std::strncpy(desc.name, sys->name, 256);
-  desc.axisCount = (std::min)(sys->axisCount, HAL_kMaxJoystickAxes);
+  data->desc.isXbox = m_isGamepad ? 1 : 0;
+  data->desc.type = m_isGamepad ? 21 : 20;
+  std::strncpy(data->desc.name, m_name, sizeof(data->desc.name) - 1);
+  data->desc.name[sizeof(data->desc.name) - 1] = '\0';
+  data->desc.axisCount = (std::min)(m_axisCount, HAL_kMaxJoystickAxes);
   // desc.axisTypes ???
-  desc.buttonCount = (std::min)(sys->buttonCount, 32);
-  desc.povCount = (std::min)(sys->hatCount, HAL_kMaxJoystickPOVs);
+  data->desc.buttonCount = (std::min)(m_buttonCount, 32);
+  data->desc.povCount = (std::min)(m_hatCount, HAL_kMaxJoystickPOVs);
 
-  buttons.count = desc.buttonCount;
-  for (int j = 0; j < buttons.count; ++j)
-    buttons.buttons |= (sysButtons[j] ? 1u : 0u) << j;
+  data->buttons.count = data->desc.buttonCount;
+  for (int j = 0; j < data->buttons.count; ++j) {
+    data->buttons.buttons |= (sysButtons[j] ? 1u : 0u) << j;
+  }
 
-  axes.count = desc.axisCount;
-  if (sys->isGamepad && useGamepad) {
+  data->axes.count = data->desc.axisCount;
+  if (m_isGamepad && mapGamepad) {
     // the FRC DriverStation maps gamepad (XInput) trigger values to 0-1 range
     // on axis 2 and 3.
-    axes.axes[0] = sysAxes[0];
-    axes.axes[1] = sysAxes[1];
-    axes.axes[2] = 0.5 + sysAxes[4] / 2.0;
-    axes.axes[3] = 0.5 + sysAxes[5] / 2.0;
-    axes.axes[4] = sysAxes[2];
-    axes.axes[5] = sysAxes[3];
+    data->axes.axes[0] = sysAxes[0];
+    data->axes.axes[1] = sysAxes[1];
+    data->axes.axes[2] = 0.5 + sysAxes[4] / 2.0;
+    data->axes.axes[3] = 0.5 + sysAxes[5] / 2.0;
+    data->axes.axes[4] = sysAxes[2];
+    data->axes.axes[5] = sysAxes[3];
 
     // the start button for gamepads is not mapped on the FRC DriverStation
     // platforms, so remove it if present
-    if (buttons.count == 11) {
-      --desc.buttonCount;
-      --buttons.count;
-      buttons.buttons =
-          (buttons.buttons & 0xff) | ((buttons.buttons >> 1) & 0x300);
+    if (data->buttons.count == 11) {
+      --data->desc.buttonCount;
+      --data->buttons.count;
+      data->buttons.buttons = (data->buttons.buttons & 0xff) |
+                              ((data->buttons.buttons >> 1) & 0x300);
     }
   } else {
-    std::memcpy(axes.axes, sysAxes, axes.count * sizeof(&axes.axes[0]));
+    std::memcpy(data->axes.axes, sysAxes,
+                data->axes.count * sizeof(&data->axes.axes[0]));
   }
 
-  povs.count = desc.povCount;
-  for (int j = 0; j < povs.count; ++j) povs.povs[j] = HatToAngle(sys->hats[j]);
+  data->povs.count = data->desc.povCount;
+  for (int j = 0; j < data->povs.count; ++j) {
+    data->povs.povs[j] = HatToAngle(m_hats[j]);
+  }
+}
+
+KeyboardJoystick::KeyboardJoystick(int index) : m_index{index} {
+  std::snprintf(m_name, sizeof(m_name), "Keyboard %d", index);
+  std::snprintf(m_guid, sizeof(m_guid), "Keyboard%d", index);
+
+  // init axes
+  m_data.axes.count = 0;
+
+  // init buttons
+  m_data.buttons.count = 0;
+  for (int i = 0; i < kMaxButtonCount; ++i) {
+    m_buttonKey[i] = -1;
+  }
+
+  // init POVs
+  m_data.povs.count = 0;
+
+  // init desc structure
+  m_data.desc.isXbox = 0;
+  m_data.desc.type = 20;
+  std::strncpy(m_data.desc.name, m_name, 256);
+}
+
+int* KeyboardJoystick::s_keyEdit = nullptr;
+
+void KeyboardJoystick::EditKey(const char* label, int* key) {
+  ImGui::PushID(label);
+  ImGui::Text("%s", label);
+  ImGui::SameLine();
+  char editLabel[32];
+  std::snprintf(editLabel, sizeof(editLabel), "%s###edit",
+                s_keyEdit == key ? "(press key)" : GetKeyName(*key));
+  if (ImGui::SmallButton(editLabel)) {
+    s_keyEdit = key;
+  }
+  ImGui::SameLine();
+  if (ImGui::SmallButton("Clear")) {
+    *key = -1;
+  }
+  ImGui::PopID();
+}
+
+void KeyboardJoystick::SettingsDisplay() {
+  if (s_keyEdit) {
+    ImGuiIO& io = ImGui::GetIO();
+    for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); ++i) {
+      if (io.KeysDown[i]) {
+        // remove all other uses
+        for (auto&& joy : gKeyboardJoysticks) {
+          joy->ClearKey(i);
+        }
+        *s_keyEdit = i;
+        s_keyEdit = nullptr;
+        break;
+      }
+    }
+  }
+
+  char label[64];
+  ImGui::PushItemWidth(ImGui::GetFontSize() * 6);
+  // axes
+  if (ImGui::CollapsingHeader("Axes", ImGuiTreeNodeFlags_DefaultOpen)) {
+    ImGui::PushID("Axes");
+    int axisCount = m_data.axes.count;
+    if (ImGui::InputInt("Count", &axisCount)) {
+      if (axisCount < 0) {
+        axisCount = 0;
+      }
+      if (axisCount > HAL_kMaxJoystickAxes) {
+        axisCount = HAL_kMaxJoystickAxes;
+      }
+      m_data.axes.count = axisCount;
+    }
+    for (int i = 0; i < axisCount; ++i) {
+      std::snprintf(label, sizeof(label), "Axis %d", i);
+      if (ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen)) {
+        EditKey("Increase", &m_axisConfig[i].incKey);
+        EditKey("Decrease", &m_axisConfig[i].decKey);
+        ImGui::InputFloat("Key Rate", &m_axisConfig[i].keyRate);
+        ImGui::InputFloat("Decay Rate", &m_axisConfig[i].decayRate);
+
+        float maxAbsValue = m_axisConfig[i].maxAbsValue;
+        if (ImGui::InputFloat("Max Absolute Value", &maxAbsValue)) {
+          m_axisConfig[i].maxAbsValue = std::clamp(maxAbsValue, -1.0f, 1.0f);
+        }
+
+        ImGui::TreePop();
+      }
+    }
+    ImGui::PopID();
+  }
+
+  // buttons
+  if (ImGui::CollapsingHeader("Buttons", ImGuiTreeNodeFlags_DefaultOpen)) {
+    ImGui::PushID("Buttons");
+    int buttonCount = m_data.buttons.count;
+    if (ImGui::InputInt("Count", &buttonCount)) {
+      if (buttonCount < 0) {
+        buttonCount = 0;
+      }
+      if (buttonCount > kMaxButtonCount) {
+        buttonCount = kMaxButtonCount;
+      }
+      m_data.buttons.count = buttonCount;
+    }
+    for (int i = 0; i < buttonCount; ++i) {
+      std::snprintf(label, sizeof(label), "Button %d", i + 1);
+      EditKey(label, &m_buttonKey[i]);
+    }
+    ImGui::PopID();
+  }
+
+  // povs
+  if (ImGui::CollapsingHeader("POVs", ImGuiTreeNodeFlags_DefaultOpen)) {
+    ImGui::PushID("POVs");
+    int povCount = m_data.povs.count;
+    if (ImGui::InputInt("Count", &povCount)) {
+      if (povCount < 0) {
+        povCount = 0;
+      }
+      if (povCount > HAL_kMaxJoystickPOVs) {
+        povCount = HAL_kMaxJoystickPOVs;
+      }
+      m_data.povs.count = povCount;
+    }
+    for (int i = 0; i < povCount; ++i) {
+      std::snprintf(label, sizeof(label), "POV %d", i);
+      if (ImGui::TreeNodeEx(label, ImGuiTreeNodeFlags_DefaultOpen)) {
+        EditKey("  0 deg", &m_povConfig[i].key0);
+        EditKey(" 45 deg", &m_povConfig[i].key45);
+        EditKey(" 90 deg", &m_povConfig[i].key90);
+        EditKey("135 deg", &m_povConfig[i].key135);
+        EditKey("180 deg", &m_povConfig[i].key180);
+        EditKey("225 deg", &m_povConfig[i].key225);
+        EditKey("270 deg", &m_povConfig[i].key270);
+        EditKey("315 deg", &m_povConfig[i].key315);
+        ImGui::TreePop();
+      }
+    }
+    ImGui::PopID();
+  }
+
+  ImGui::PopItemWidth();
+}
+
+static inline bool IsKeyDown(ImGuiIO& io, int key) {
+  return key >= 0 && key < IM_ARRAYSIZE(ImGuiIO::KeysDown) && io.KeysDown[key];
+}
+
+void KeyboardJoystick::Update() {
+  ImGuiIO& io = ImGui::GetIO();
+
+  if (m_data.axes.count > 0 || m_data.buttons.count > 0 ||
+      m_data.povs.count > 0) {
+    m_present = true;
+  }
+
+  // axes
+  for (int i = 0; i < m_data.axes.count; ++i) {
+    auto& config = m_axisConfig[i];
+    float& axisValue = m_data.axes.axes[i];
+    // increase/decrease while key held down (to saturation); decay back to 0
+    if (IsKeyDown(io, config.incKey)) {
+      axisValue += config.keyRate;
+      if (axisValue > config.maxAbsValue) {
+        axisValue = config.maxAbsValue;
+      }
+    } else if (axisValue > 0) {
+      if (axisValue < config.decayRate) {
+        axisValue = 0;
+      } else {
+        axisValue -= config.decayRate;
+      }
+    }
+
+    if (IsKeyDown(io, config.decKey)) {
+      axisValue -= config.keyRate;
+      if (axisValue < -config.maxAbsValue) {
+        axisValue = -config.maxAbsValue;
+      }
+    } else if (axisValue < 0) {
+      if (axisValue > -config.decayRate) {
+        axisValue = 0;
+      } else {
+        axisValue += config.decayRate;
+      }
+    }
+  }
+
+  // buttons
+  m_data.buttons.buttons = 0;
+  m_anyButtonPressed = false;
+  for (int i = 0; i < m_data.buttons.count; ++i) {
+    if (IsKeyDown(io, m_buttonKey[i])) {
+      m_data.buttons.buttons |= 1u << i;
+      m_anyButtonPressed = true;
+    }
+  }
+
+  // povs
+  for (int i = 0; i < m_data.povs.count; ++i) {
+    auto& config = m_povConfig[i];
+    auto& povValue = m_data.povs.povs[i];
+    povValue = -1;
+    if (IsKeyDown(io, config.key0)) {
+      povValue = 0;
+    } else if (IsKeyDown(io, config.key45)) {
+      povValue = 45;
+    } else if (IsKeyDown(io, config.key90)) {
+      povValue = 90;
+    } else if (IsKeyDown(io, config.key135)) {
+      povValue = 135;
+    } else if (IsKeyDown(io, config.key180)) {
+      povValue = 180;
+    } else if (IsKeyDown(io, config.key225)) {
+      povValue = 225;
+    } else if (IsKeyDown(io, config.key270)) {
+      povValue = 270;
+    } else if (IsKeyDown(io, config.key315)) {
+      povValue = 315;
+    }
+  }
+
+  // try to find matching GUID
+  for (auto&& joy : gRobotJoysticks) {
+    if (m_guid == joy.guid) {
+      joy.sys = this;
+      joy.guid.clear();
+      break;
+    }
+  }
+
+  // update desc
+  m_data.desc.axisCount = m_data.axes.count;
+  m_data.desc.buttonCount = m_data.buttons.count;
+  m_data.desc.povCount = m_data.povs.count;
+}
+
+void KeyboardJoystick::ClearKey(int key) {
+  for (auto&& config : m_axisConfig) {
+    if (config.incKey == key) {
+      config.incKey = -1;
+    }
+    if (config.decKey == key) {
+      config.decKey = -1;
+    }
+  }
+  for (auto&& buttonKey : m_buttonKey) {
+    if (buttonKey == key) {
+      buttonKey = -1;
+    }
+  }
+  for (auto&& config : m_povConfig) {
+    if (config.key0 == key) {
+      config.key0 = -1;
+    }
+    if (config.key45 == key) {
+      config.key45 = -1;
+    }
+    if (config.key90 == key) {
+      config.key90 = -1;
+    }
+    if (config.key135 == key) {
+      config.key135 = -1;
+    }
+    if (config.key180 == key) {
+      config.key180 = -1;
+    }
+    if (config.key225 == key) {
+      config.key225 = -1;
+    }
+    if (config.key270 == key) {
+      config.key270 = -1;
+    }
+    if (config.key315 == key) {
+      config.key315 = -1;
+    }
+  }
+}
+
+void KeyboardJoystick::ReadIni(std::string_view name, std::string_view value) {
+  if (wpi::starts_with(name, "axis")) {
+    name.remove_prefix(4);
+    if (name == "Count") {
+      if (auto v = wpi::parse_integer<int>(value, 10)) {
+        m_data.axes.count = (std::min)(v.value(), HAL_kMaxJoystickAxes);
+      }
+      return;
+    }
+
+    auto index = wpi::consume_integer<unsigned int>(&name, 10).value_or(
+        HAL_kMaxJoystickAxes);
+    if (index >= HAL_kMaxJoystickAxes) {
+      return;
+    }
+    if (name == "incKey") {
+      if (auto v = wpi::parse_integer<int>(value, 10)) {
+        m_axisConfig[index].incKey = v.value();
+      }
+    } else if (name == "decKey") {
+      if (auto v = wpi::parse_integer<int>(value, 10)) {
+        m_axisConfig[index].decKey = v.value();
+      }
+    } else if (name == "keyRate") {
+      if (auto v = wpi::parse_float<float>(value)) {
+        m_axisConfig[index].keyRate = v.value();
+      }
+    } else if (name == "decayRate") {
+      if (auto v = wpi::parse_float<float>(value)) {
+        m_axisConfig[index].decayRate = v.value();
+      }
+    } else if (name == "maxAbsValue") {
+      if (auto v = wpi::parse_float<float>(value)) {
+        m_axisConfig[index].maxAbsValue = v.value();
+      }
+    }
+  } else if (wpi::starts_with(name, "button")) {
+    name.remove_prefix(6);
+    if (name == "Count") {
+      if (auto v = wpi::parse_integer<int>(value, 10)) {
+        m_data.buttons.count = (std::min)(v.value(), kMaxButtonCount);
+      }
+      return;
+    }
+
+    auto index =
+        wpi::parse_integer<unsigned int>(name, 10).value_or(kMaxButtonCount);
+    if (index >= kMaxButtonCount) {
+      return;
+    }
+    int v = wpi::parse_integer<int>(value, 10).value_or(-1);
+    if (v < 0 || v >= IM_ARRAYSIZE(ImGuiIO::KeysDown)) {
+      return;
+    }
+    m_buttonKey[index] = v;
+  } else if (wpi::starts_with(name, "pov")) {
+    name.remove_prefix(3);
+    if (name == "Count") {
+      if (auto v = wpi::parse_integer<int>(value, 10)) {
+        m_data.povs.count = (std::min)(v.value(), HAL_kMaxJoystickPOVs);
+      }
+      return;
+    }
+
+    auto index = wpi::consume_integer<unsigned int>(&name, 10).value_or(
+        HAL_kMaxJoystickPOVs);
+    if (index >= HAL_kMaxJoystickPOVs) {
+      return;
+    }
+    int v = wpi::parse_integer<int>(value, 10).value_or(-1);
+    if (v < 0 || v >= IM_ARRAYSIZE(ImGuiIO::KeysDown)) {
+      return;
+    }
+    if (name == "key0") {
+      m_povConfig[index].key0 = v;
+    } else if (name == "key45") {
+      m_povConfig[index].key45 = v;
+    } else if (name == "key90") {
+      m_povConfig[index].key90 = v;
+    } else if (name == "key135") {
+      m_povConfig[index].key135 = v;
+    } else if (name == "key180") {
+      m_povConfig[index].key180 = v;
+    } else if (name == "key225") {
+      m_povConfig[index].key225 = v;
+    } else if (name == "key270") {
+      m_povConfig[index].key270 = v;
+    } else if (name == "key315") {
+      m_povConfig[index].key315 = v;
+    }
+  }
+}
+
+void KeyboardJoystick::WriteIni(ImGuiTextBuffer* out_buf) const {
+  out_buf->appendf("axisCount=%d\nbuttonCount=%d\npovCount=%d\n",
+                   m_data.axes.count, m_data.buttons.count, m_data.povs.count);
+  for (int i = 0; i < m_data.axes.count; ++i) {
+    auto& c = m_axisConfig[i];
+    out_buf->appendf(
+        "axis%dincKey=%d\naxis%ddecKey=%d\naxis%dkeyRate=%f\n"
+        "axis%ddecayRate=%f\naxis%dmaxAbsValue=%f\n",
+        i, c.incKey, i, c.decKey, i, c.keyRate, i, c.decayRate, i,
+        c.maxAbsValue);
+  }
+  for (int i = 0; i < m_data.buttons.count; ++i) {
+    out_buf->appendf("button%d=%d\n", i, m_buttonKey[i]);
+  }
+  for (int i = 0; i < m_data.povs.count; ++i) {
+    auto& c = m_povConfig[i];
+    out_buf->appendf(
+        "pov%dkey0=%d\npov%dkey45=%d\npov%dkey90=%d\npov%dkey135=%d\n"
+        "pov%dkey180=%d\npov%dkey225=%d\npov%dkey270=%d\npov%dkey315=%d\n",
+        i, c.key0, i, c.key45, i, c.key90, i, c.key135, i, c.key180, i,
+        c.key225, i, c.key270, i, c.key315);
+  }
+}
+
+GlfwKeyboardJoystick::GlfwKeyboardJoystick(int index, bool noDefaults)
+    : KeyboardJoystick{index} {
+  if (noDefaults) {
+    return;
+  }
+  // set up a default keyboard config for 0, 1, and 2
+  if (index == 0) {
+    m_data.axes.count = 3;
+    m_axisConfig[0].incKey = GLFW_KEY_D;
+    m_axisConfig[0].decKey = GLFW_KEY_A;
+    m_axisConfig[1].incKey = GLFW_KEY_S;
+    m_axisConfig[1].decKey = GLFW_KEY_W;
+    m_axisConfig[2].incKey = GLFW_KEY_R;
+    m_axisConfig[2].decKey = GLFW_KEY_E;
+    m_axisConfig[2].keyRate = 0.01f;
+    m_axisConfig[2].decayRate = 0;  // works like a throttle
+    m_data.buttons.count = 4;
+    m_buttonKey[0] = GLFW_KEY_Z;
+    m_buttonKey[1] = GLFW_KEY_X;
+    m_buttonKey[2] = GLFW_KEY_C;
+    m_buttonKey[3] = GLFW_KEY_V;
+    m_data.povs.count = 1;
+    m_povConfig[0].key0 = GLFW_KEY_KP_8;
+    m_povConfig[0].key45 = GLFW_KEY_KP_9;
+    m_povConfig[0].key90 = GLFW_KEY_KP_6;
+    m_povConfig[0].key135 = GLFW_KEY_KP_3;
+    m_povConfig[0].key180 = GLFW_KEY_KP_2;
+    m_povConfig[0].key225 = GLFW_KEY_KP_1;
+    m_povConfig[0].key270 = GLFW_KEY_KP_4;
+    m_povConfig[0].key315 = GLFW_KEY_KP_7;
+  } else if (index == 1) {
+    m_data.axes.count = 2;
+    m_axisConfig[0].incKey = GLFW_KEY_L;
+    m_axisConfig[0].decKey = GLFW_KEY_J;
+    m_axisConfig[1].incKey = GLFW_KEY_K;
+    m_axisConfig[1].decKey = GLFW_KEY_I;
+    m_data.buttons.count = 4;
+    m_buttonKey[0] = GLFW_KEY_M;
+    m_buttonKey[1] = GLFW_KEY_COMMA;
+    m_buttonKey[2] = GLFW_KEY_PERIOD;
+    m_buttonKey[3] = GLFW_KEY_SLASH;
+  } else if (index == 2) {
+    m_data.axes.count = 2;
+    m_axisConfig[0].incKey = GLFW_KEY_RIGHT;
+    m_axisConfig[0].decKey = GLFW_KEY_LEFT;
+    m_axisConfig[1].incKey = GLFW_KEY_DOWN;
+    m_axisConfig[1].decKey = GLFW_KEY_UP;
+    m_data.buttons.count = 6;
+    m_buttonKey[0] = GLFW_KEY_INSERT;
+    m_buttonKey[1] = GLFW_KEY_HOME;
+    m_buttonKey[2] = GLFW_KEY_PAGE_UP;
+    m_buttonKey[3] = GLFW_KEY_DELETE;
+    m_buttonKey[4] = GLFW_KEY_END;
+    m_buttonKey[5] = GLFW_KEY_PAGE_DOWN;
+  }
+}
+
+const char* GlfwKeyboardJoystick::GetKeyName(int key) const {
+  if (key < 0) {
+    return "(None)";
+  }
+  const char* name = glfwGetKeyName(key, 0);
+  if (name) {
+    return name;
+  }
+  // glfwGetKeyName sometimes doesn't have these keys
+  switch (key) {
+    case GLFW_KEY_RIGHT:
+      return "Right";
+    case GLFW_KEY_LEFT:
+      return "Left";
+    case GLFW_KEY_DOWN:
+      return "Down";
+    case GLFW_KEY_UP:
+      return "Up";
+    case GLFW_KEY_INSERT:
+      return "Insert";
+    case GLFW_KEY_HOME:
+      return "Home";
+    case GLFW_KEY_PAGE_UP:
+      return "PgUp";
+    case GLFW_KEY_DELETE:
+      return "Delete";
+    case GLFW_KEY_END:
+      return "End";
+    case GLFW_KEY_PAGE_DOWN:
+      return "PgDn";
+  }
+  return "(Unknown)";
+}
+
+void RobotJoystick::Update() {
+  Clear();
+  if (sys) {
+    sys->GetData(&data, useGamepad);
+  }
 }
 
 void RobotJoystick::SetHAL(int i) {
+  if (!gZeroDisconnectedJoysticks && (!sys || !sys->IsPresent())) {
+    return;
+  }
   // set at HAL level
-  HALSIM_SetJoystickDescriptor(i, &desc);
-  HALSIM_SetJoystickAxes(i, &axes);
-  HALSIM_SetJoystickButtons(i, &buttons);
-  HALSIM_SetJoystickPOVs(i, &povs);
+  HALSIM_SetJoystickDescriptor(i, &data.desc);
+  HALSIM_SetJoystickAxes(i, &data.axes);
+  HALSIM_SetJoystickButtons(i, &data.buttons);
+  HALSIM_SetJoystickPOVs(i, &data.povs);
 }
 
 void RobotJoystick::GetHAL(int i) {
-  HALSIM_GetJoystickDescriptor(i, &desc);
-  HALSIM_GetJoystickAxes(i, &axes);
-  HALSIM_GetJoystickButtons(i, &buttons);
-  HALSIM_GetJoystickPOVs(i, &povs);
+  HALSIM_GetJoystickDescriptor(i, &data.desc);
+  HALSIM_GetJoystickAxes(i, &data.axes);
+  HALSIM_GetJoystickButtons(i, &data.buttons);
+  HALSIM_GetJoystickPOVs(i, &data.povs);
 }
 
 static void DriverStationExecute() {
@@ -394,7 +1138,7 @@
       if (!source || source->axisCount != axisCount ||
           source->buttonCount != buttonCount || source->povCount != povCount) {
         source.reset();
-        source = std::make_unique<JoystickSource>(i);
+        source = std::make_unique<JoystickModel>(i);
       }
     } else {
       source.reset();
@@ -405,23 +1149,37 @@
 
   bool disableDS = IsDSDisabled();
   if (disableDS && !prevDisableDS) {
-    HALSimGui::SetWindowVisibility("System Joysticks", HALSimGui::kDisabled);
+    if (auto win = HALSimGui::manager.GetWindow("System Joysticks")) {
+      win->SetVisibility(glass::Window::kDisabled);
+    }
   } else if (!disableDS && prevDisableDS) {
-    HALSimGui::SetWindowVisibility("System Joysticks", HALSimGui::kShow);
+    if (auto win = HALSimGui::manager.GetWindow("System Joysticks")) {
+      win->SetVisibility(glass::Window::kShow);
+    }
   }
   prevDisableDS = disableDS;
-  if (disableDS) return;
+  if (disableDS) {
+    return;
+  }
 
   double curTime = glfwGetTime();
 
   // update system joysticks
+  gNumGlfwJoysticks = 0;
   for (int i = 0; i <= GLFW_JOYSTICK_LAST; ++i) {
-    gSystemJoysticks[i].Update(i);
-    if (gSystemJoysticks[i].present) gNumSystemJoysticks = i + 1;
+    gGlfwJoysticks[i]->Update();
+    if (gGlfwJoysticks[i]->IsPresent()) {
+      gNumGlfwJoysticks = i + 1;
+    }
+  }
+  for (auto&& joy : gKeyboardJoysticks) {
+    joy->Update();
   }
 
   // update robot joysticks
-  for (auto&& joy : gRobotJoysticks) joy.Update();
+  for (auto&& joy : gRobotJoysticks) {
+    joy.Update();
+  }
 
   bool isEnabled = HALSIM_GetDriverStationEnabled();
   bool isAuto = HALSIM_GetDriverStationAutonomous();
@@ -429,16 +1187,38 @@
 
   // Robot state
   {
+    // DS hotkeys
+    bool enableHotkey = false;
+    bool disableHotkey = false;
+    if (gUseEnableDisableHotkeys) {
+      ImGuiIO& io = ImGui::GetIO();
+      if (io.KeysDown[GLFW_KEY_ENTER] || io.KeysDown[GLFW_KEY_KP_ENTER]) {
+        disableHotkey = true;
+      } else if (io.KeysDown[GLFW_KEY_LEFT_BRACKET] &&
+                 io.KeysDown[GLFW_KEY_RIGHT_BRACKET] &&
+                 io.KeysDown[GLFW_KEY_BACKSLASH]) {
+        enableHotkey = true;
+      }
+    }
+    if (gUseEstopHotkey) {
+      ImGuiIO& io = ImGui::GetIO();
+      if (io.KeysDown[GLFW_KEY_SPACE]) {
+        HALSIM_SetDriverStationEnabled(false);
+      }
+    }
+
     ImGui::SetNextWindowPos(ImVec2{5, 20}, ImGuiCond_FirstUseEver);
     ImGui::Begin("Robot State", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
-    if (ImGui::Selectable("Disabled", !isEnabled))
+    if (ImGui::Selectable("Disabled", !isEnabled) || disableHotkey) {
       HALSIM_SetDriverStationEnabled(false);
+    }
     if (ImGui::Selectable("Autonomous", isEnabled && isAuto && !isTest)) {
       HALSIM_SetDriverStationAutonomous(true);
       HALSIM_SetDriverStationTest(false);
       HALSIM_SetDriverStationEnabled(true);
     }
-    if (ImGui::Selectable("Teleoperated", isEnabled && !isAuto && !isTest)) {
+    if (ImGui::Selectable("Teleoperated", isEnabled && !isAuto && !isTest) ||
+        enableHotkey) {
       HALSIM_SetDriverStationAutonomous(false);
       HALSIM_SetDriverStationTest(false);
       HALSIM_SetDriverStationEnabled(true);
@@ -452,7 +1232,9 @@
   }
 
   // Update HAL
-  for (int i = 0; i < HAL_kMaxJoysticks; ++i) gRobotJoysticks[i].SetHAL(i);
+  for (int i = 0; i < HAL_kMaxJoysticks; ++i) {
+    gRobotJoysticks[i].SetHAL(i);
+  }
 
   // Send new data every 20 ms (may be slower depending on GUI refresh rate)
   static double lastNewDataTime = 0.0;
@@ -462,106 +1244,97 @@
   }
 }
 
-static void DisplayFMS() {
-  bool fmsAttached = HALSIM_GetDriverStationFmsAttached();
-  bool dsAttached = HALSIM_GetDriverStationDsAttached();
-  static const char* stations[] = {"Red 1",  "Red 2",  "Red 3",
-                                   "Blue 1", "Blue 2", "Blue 3"};
-  int allianceStationId = HALSIM_GetDriverStationAllianceStationId();
+FMSSimModel::FMSSimModel() {
+  m_fmsAttached.SetDigital(true);
+  m_dsAttached.SetDigital(true);
+  m_estop.SetDigital(true);
+  m_enabled.SetDigital(true);
+  m_test.SetDigital(true);
+  m_autonomous.SetDigital(true);
+}
+
+void FMSSimModel::Update() {
+  bool enabled = HALSIM_GetDriverStationEnabled();
+  m_fmsAttached.SetValue(HALSIM_GetDriverStationFmsAttached());
+  m_dsAttached.SetValue(HALSIM_GetDriverStationDsAttached());
+  m_allianceStationId.SetValue(HALSIM_GetDriverStationAllianceStationId());
+  m_estop.SetValue(HALSIM_GetDriverStationEStop());
+  m_enabled.SetValue(enabled);
+  m_test.SetValue(HALSIM_GetDriverStationTest());
+  m_autonomous.SetValue(HALSIM_GetDriverStationAutonomous());
+
   double matchTime = HALSIM_GetDriverStationMatchTime();
-  HAL_MatchInfo matchInfo;
-  HALSIM_GetMatchInfo(&matchInfo);
+  if (m_matchTimeEnabled && !IsDSDisabled()) {
+    int32_t status = 0;
+    double curTime = HAL_GetFPGATime(&status) * 1.0e-6;
+    if (m_startMatchTime == 0.0) {
+      m_startMatchTime = curTime;
+    }
+    if (enabled) {
+      matchTime = curTime - m_startMatchTime;
+      HALSIM_SetDriverStationMatchTime(matchTime);
+    } else {
+      if (m_prevTime == 0.0) {
+        m_prevTime = curTime;
+      }
+      m_startMatchTime += (curTime - m_prevTime);
+    }
+    m_prevTime = curTime;
+  } else {
+    m_startMatchTime = 0.0;
+    m_prevTime = 0.0;
+  }
+  m_matchTime.SetValue(matchTime);
+}
 
-  if (IsDSDisabled()) {
-    if (!HALSIM_GetDriverStationEnabled())
-      ImGui::Text("Robot State: Disabled");
-    else if (HALSIM_GetDriverStationTest())
-      ImGui::Text("Robot State: Test");
-    else if (HALSIM_GetDriverStationAutonomous())
-      ImGui::Text("Robot State: Autonomous");
-    else
-      ImGui::Text("Robot State: Teleoperated");
+bool FMSSimModel::IsReadOnly() {
+  return IsDSDisabled();
+}
 
-    ImGui::Text("FMS Attached: %s", fmsAttached ? "Yes" : "No");
-    ImGui::Text("DS Attached: %s", dsAttached ? "Yes" : "No");
-    ImGui::Text("Alliance Station: %s", stations[allianceStationId]);
-    ImGui::Text("Match Time: %.1f", matchTime);
-    ImGui::Text("Game Specific: %s", matchInfo.gameSpecificMessage);
-    return;
+static void DisplaySystemJoystick(SystemJoystick& joy, int i) {
+  char label[64];
+  std::snprintf(label, sizeof(label), "%d: %s", i, joy.GetName());
+
+  // highlight if any buttons pressed
+  bool anyButtonPressed = joy.IsAnyButtonPressed();
+  if (anyButtonPressed) {
+    ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255));
+  }
+  ImGui::Selectable(label, false,
+                    joy.IsPresent() ? ImGuiSelectableFlags_None
+                                    : ImGuiSelectableFlags_Disabled);
+  if (anyButtonPressed) {
+    ImGui::PopStyleColor();
   }
 
-  double curTime = glfwGetTime();
-
-  // FMS Attached
-  if (ImGui::Checkbox("FMS Attached", &fmsAttached))
-    HALSIM_SetDriverStationFmsAttached(fmsAttached);
-
-  // DS Attached
-  if (ImGui::Checkbox("DS Attached", &dsAttached))
-    HALSIM_SetDriverStationDsAttached(dsAttached);
-
-  // Alliance Station
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
-  if (ImGui::Combo("Alliance Station", &allianceStationId, stations, 6))
-    HALSIM_SetDriverStationAllianceStationId(
-        static_cast<HAL_AllianceStationID>(allianceStationId));
-
-  // Match Time
-  static bool matchTimeEnabled = true;
-  ImGui::Checkbox("Match Time Enabled", &matchTimeEnabled);
-
-  static double startMatchTime = 0.0;
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
-  if (ImGui::InputDouble("Match Time", &matchTime, 0, 0, "%.1f",
-                         ImGuiInputTextFlags_EnterReturnsTrue)) {
-    HALSIM_SetDriverStationMatchTime(matchTime);
-    startMatchTime = curTime - matchTime;
-  } else if (!HALSIM_GetDriverStationEnabled() || HALSIM_IsTimingPaused()) {
-    startMatchTime = curTime - matchTime;
-  } else if (matchTimeEnabled) {
-    HALSIM_SetDriverStationMatchTime(curTime - startMatchTime);
-  }
-  ImGui::SameLine();
-  if (ImGui::Button("Reset")) {
-    HALSIM_SetDriverStationMatchTime(0.0);
-    startMatchTime = curTime;
-  }
-
-  // Game Specific Message
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
-  if (ImGui::InputText("Game Specific",
-                       reinterpret_cast<char*>(matchInfo.gameSpecificMessage),
-                       sizeof(matchInfo.gameSpecificMessage),
-                       ImGuiInputTextFlags_EnterReturnsTrue)) {
-    matchInfo.gameSpecificMessageSize =
-        std::strlen(reinterpret_cast<char*>(matchInfo.gameSpecificMessage));
-    HALSIM_SetMatchInfo(&matchInfo);
+  // drag and drop sources are the low level joysticks
+  if (ImGui::BeginDragDropSource()) {
+    SystemJoystick* joyPtr = &joy;
+    ImGui::SetDragDropPayload("Joystick", &joyPtr, sizeof(joyPtr));  // NOLINT
+    ImGui::Text("%d: %s", i, joy.GetName());
+    ImGui::EndDragDropSource();
   }
 }
 
 static void DisplaySystemJoysticks() {
   ImGui::Text("(Drag and drop to Joysticks)");
-  int numShowJoysticks = gNumSystemJoysticks < 6 ? 6 : gNumSystemJoysticks;
+  int numShowJoysticks = gNumGlfwJoysticks < 6 ? 6 : gNumGlfwJoysticks;
   for (int i = 0; i < numShowJoysticks; ++i) {
-    auto& joy = gSystemJoysticks[i];
-    wpi::SmallString<128> label;
-    wpi::raw_svector_ostream os(label);
-    os << wpi::format("%d: %s", i, joy.name);
-
-    // highlight if any buttons pressed
-    if (joy.anyButtonPressed)
-      ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255));
-    ImGui::Selectable(label.c_str(), false,
-                      joy.present ? ImGuiSelectableFlags_None
-                                  : ImGuiSelectableFlags_Disabled);
-    if (joy.anyButtonPressed) ImGui::PopStyleColor();
-
-    // drag and drop sources are the low level joysticks
-    if (ImGui::BeginDragDropSource()) {
-      SystemJoystick* joyPtr = &joy;
-      ImGui::SetDragDropPayload("Joystick", &joyPtr, sizeof(joyPtr));
-      ImGui::Text("%d: %s", i, joy.name);
-      ImGui::EndDragDropSource();
+    DisplaySystemJoystick(*gGlfwJoysticks[i], i);
+  }
+  for (size_t i = 0; i < gKeyboardJoysticks.size(); ++i) {
+    auto joy = gKeyboardJoysticks[i].get();
+    DisplaySystemJoystick(*joy, i + GLFW_JOYSTICK_LAST + 1);
+    if (ImGui::BeginPopupContextItem()) {
+      char buf[64];
+      std::snprintf(buf, sizeof(buf), "%s Settings", joy->GetName());
+      if (ImGui::MenuItem(buf)) {
+        if (auto win = DriverStationGui::dsManager.GetWindow(buf)) {
+          win->SetVisible(true);
+        }
+        ImGui::CloseCurrentPopup();
+      }
+      ImGui::EndPopup();
     }
   }
 }
@@ -579,9 +1352,9 @@
     if (!disableDS && joy.sys) {
       ImGui::Selectable(label, false);
       if (ImGui::BeginDragDropSource()) {
-        ImGui::SetDragDropPayload("Joystick", &joy.sys, sizeof(joy.sys));
-        ImGui::Text("%d: %s", static_cast<int>(joy.sys - gSystemJoysticks),
-                    joy.sys->name);
+        ImGui::SetDragDropPayload("Joystick", &joy.sys,
+                                  sizeof(joy.sys));  // NOLINT
+        ImGui::Text("%d: %s", joy.sys->GetIndex(), joy.sys->GetName());
         ImGui::EndDragDropSource();
       }
     } else {
@@ -590,16 +1363,20 @@
     if (!disableDS && ImGui::BeginDragDropTarget()) {
       if (const ImGuiPayload* payload =
               ImGui::AcceptDragDropPayload("Joystick")) {
-        IM_ASSERT(payload->DataSize == sizeof(SystemJoystick*));
+        IM_ASSERT(payload->DataSize == sizeof(SystemJoystick*));  // NOLINT
         SystemJoystick* payload_sys =
             *static_cast<SystemJoystick* const*>(payload->Data);
         // clear it from the other joysticks
         for (auto&& joy2 : gRobotJoysticks) {
-          if (joy2.sys == payload_sys) joy2.sys = nullptr;
+          if (joy2.sys == payload_sys) {
+            joy2.sys = nullptr;
+          }
         }
         joy.sys = payload_sys;
         joy.guid.clear();
-        joy.useGamepad = false;
+        std::string_view name{payload_sys->GetName()};
+        joy.useGamepad =
+            wpi::starts_with(name, "Xbox") || wpi::contains(name, "pad");
       }
       ImGui::EndDragDropTarget();
     }
@@ -612,53 +1389,58 @@
     auto& joy = gRobotJoysticks[i];
     auto source = gJoystickSources[i].get();
 
-    if (disableDS) joy.GetHAL(i);
+    if (disableDS) {
+      joy.GetHAL(i);
+    }
 
-    if ((disableDS && joy.desc.type != 0) || (joy.sys && joy.sys->present)) {
+    if ((disableDS && joy.data.desc.type != 0) ||
+        (joy.sys && joy.sys->IsPresent())) {
       // update GUI display
       ImGui::PushID(i);
       if (disableDS) {
-        ImGui::Text("%s", joy.desc.name);
-        ImGui::Text("Gamepad: %s", joy.desc.isXbox ? "Yes" : "No");
+        ImGui::Text("%s", joy.data.desc.name);
+        ImGui::Text("Gamepad: %s", joy.data.desc.isXbox ? "Yes" : "No");
       } else {
-        ImGui::Text("%d: %s", static_cast<int>(joy.sys - gSystemJoysticks),
-                    joy.sys->name);
+        ImGui::Text("%d: %s", joy.sys->GetIndex(), joy.sys->GetName());
 
-        if (joy.sys->isGamepad) ImGui::Checkbox("Map gamepad", &joy.useGamepad);
+        if (joy.sys->IsGamepad()) {
+          ImGui::Checkbox("Map gamepad", &joy.useGamepad);
+        }
       }
 
-      for (int j = 0; j < joy.axes.count; ++j) {
+      for (int j = 0; j < joy.data.axes.count; ++j) {
         if (source && source->axes[j]) {
           char label[64];
           std::snprintf(label, sizeof(label), "Axis[%d]", j);
           ImGui::Selectable(label);
           source->axes[j]->EmitDrag();
           ImGui::SameLine();
-          ImGui::Text(": %.3f", joy.axes.axes[j]);
+          ImGui::Text(": %.3f", joy.data.axes.axes[j]);
         } else {
-          ImGui::Text("Axis[%d]: %.3f", j, joy.axes.axes[j]);
+          ImGui::Text("Axis[%d]: %.3f", j, joy.data.axes.axes[j]);
         }
       }
 
-      for (int j = 0; j < joy.povs.count; ++j) {
+      for (int j = 0; j < joy.data.povs.count; ++j) {
         if (source && source->povs[j]) {
           char label[64];
           std::snprintf(label, sizeof(label), "POVs[%d]", j);
           ImGui::Selectable(label);
           source->povs[j]->EmitDrag();
           ImGui::SameLine();
-          ImGui::Text(": %d", joy.povs.povs[j]);
+          ImGui::Text(": %d", joy.data.povs.povs[j]);
         } else {
-          ImGui::Text("POVs[%d]: %d", j, joy.povs.povs[j]);
+          ImGui::Text("POVs[%d]: %d", j, joy.data.povs.povs[j]);
         }
       }
 
       // show buttons as multiple lines of LED indicators, 8 per line
       static const ImU32 color = IM_COL32(255, 255, 102, 255);
       wpi::SmallVector<int, 64> buttons;
-      buttons.resize(joy.buttons.count);
-      for (int j = 0; j < joy.buttons.count; ++j)
+      buttons.resize(joy.data.buttons.count);
+      for (int j = 0; j < joy.data.buttons.count; ++j) {
         buttons[j] = joy.IsButtonPressed(j) ? 1 : -1;
+      }
       DrawLEDSources(buttons.data(), source ? source->buttons : nullptr,
                      buttons.size(), 8, &color);
       ImGui::PopID();
@@ -670,15 +1452,25 @@
   ImGui::Columns(1);
 }
 
-static void DriverStationOptionMenu() {
+void DSManager::DisplayMenu() {
   if (gDSSocketConnected && *gDSSocketConnected) {
     ImGui::MenuItem("Turn off DS (real DS connected)", nullptr, true, false);
   } else {
     ImGui::MenuItem("Turn off DS", nullptr, &gDisableDS);
+    ImGui::MenuItem("Zero disconnected joysticks", nullptr,
+                    &gZeroDisconnectedJoysticks);
+    ImGui::MenuItem("Enable on []\\ combo, Disable on Enter", nullptr,
+                    &gUseEnableDisableHotkeys);
+    ImGui::MenuItem("Disable on Spacebar", nullptr, &gUseEstopHotkey);
+  }
+  ImGui::Separator();
+
+  for (auto&& window : m_windows) {
+    window->DisplayMenuItem();
   }
 }
 
-void DriverStationGui::Initialize() {
+static void DriverStationInitialize() {
   // hook ini handler to save joystick settings
   ImGuiSettingsHandler iniHandler;
   iniHandler.TypeName = "Joystick";
@@ -688,6 +1480,14 @@
   iniHandler.WriteAllFn = JoystickWriteAll;
   ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
 
+  // hook ini handler to save keyboard settings
+  iniHandler.TypeName = "KeyboardJoystick";
+  iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
+  iniHandler.ReadOpenFn = KeyboardJoystickReadOpen;
+  iniHandler.ReadLineFn = KeyboardJoystickReadLine;
+  iniHandler.WriteAllFn = KeyboardJoystickWriteAll;
+  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
+
   // hook ini handler to save DS settings
   iniHandler.TypeName = "DriverStation";
   iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
@@ -695,18 +1495,58 @@
   iniHandler.ReadLineFn = DriverStationReadLine;
   iniHandler.WriteAllFn = DriverStationWriteAll;
   ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
+}
 
-  HALSimGui::AddExecute(DriverStationExecute);
-  HALSimGui::AddWindow("FMS", DisplayFMS, ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::AddWindow("System Joysticks", DisplaySystemJoysticks,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::AddWindow("Joysticks", DisplayJoysticks,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::AddOptionMenu(DriverStationOptionMenu);
+void DriverStationGui::GlobalInit() {
+  // set up system joysticks (both GLFW and keyboard)
+  for (int i = 0; i <= GLFW_JOYSTICK_LAST; ++i) {
+    gGlfwJoysticks.emplace_back(std::make_unique<GlfwSystemJoystick>(i));
+  }
+  for (int i = 0; i < 4; ++i) {
+    gKeyboardJoysticks.emplace_back(std::make_unique<GlfwKeyboardJoystick>(i));
+  }
 
-  HALSimGui::SetDefaultWindowPos("FMS", 5, 540);
-  HALSimGui::SetDefaultWindowPos("System Joysticks", 5, 385);
-  HALSimGui::SetDefaultWindowPos("Joysticks", 250, 465);
+  dsManager.GlobalInit();
+
+  wpi::gui::AddInit(DriverStationInitialize);
+
+  gFMSModel = std::make_unique<FMSSimModel>();
+
+  wpi::gui::AddEarlyExecute(DriverStationExecute);
+  wpi::gui::AddEarlyExecute([] { gFMSModel->Update(); });
+  if (auto win = dsManager.AddWindow("FMS", [] {
+        DisplayFMS(gFMSModel.get(), &gFMSModel->m_matchTimeEnabled);
+      })) {
+    win->DisableRenamePopup();
+    win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+    win->SetDefaultPos(5, 540);
+  }
+  if (auto win =
+          dsManager.AddWindow("System Joysticks", DisplaySystemJoysticks)) {
+    win->DisableRenamePopup();
+    win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+    win->SetDefaultPos(5, 350);
+  }
+  if (auto win = dsManager.AddWindow("Joysticks", DisplayJoysticks)) {
+    win->DisableRenamePopup();
+    win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+    win->SetDefaultPos(250, 465);
+  }
+  int i = 0;
+  for (auto&& joy : gKeyboardJoysticks) {
+    char label[64];
+    std::snprintf(label, sizeof(label), "%s Settings", joy->GetName());
+    if (auto win = dsManager.AddWindow(
+            label, [j = joy.get()] { j->SettingsDisplay(); })) {
+      win->SetVisible(false);
+      win->DisableRenamePopup();
+      win->SetDefaultPos(10 + 310 * i++, 50);
+      if (i > 3) {
+        i = 0;
+      }
+      win->SetDefaultSize(300, 560);
+    }
+  }
 }
 
 void DriverStationGui::SetDSSocketExtension(void* data) {
diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h
index 5bf25b5..51f7554 100644
--- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h
+++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h
@@ -1,18 +1,28 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <glass/WindowManager.h>
+
+#include <string_view>
+
 namespace halsimgui {
 
+class DSManager : public glass::WindowManager {
+ public:
+  explicit DSManager(std::string_view iniName) : WindowManager{iniName} {}
+
+  void DisplayMenu() override;
+};
+
 class DriverStationGui {
  public:
-  static void Initialize();
+  static void GlobalInit();
   static void SetDSSocketExtension(void* data);
+
+  static DSManager dsManager;
 };
 
 }  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp
deleted file mode 100644
index 5b169f0..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp
+++ /dev/null
@@ -1,286 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "EncoderGui.h"
-
-#include <limits>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/EncoderData.h>
-#include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-
-struct EncoderInfo : public NameInfo, public OpenInfo {
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value) {
-    if (NameInfo::ReadIni(name, value)) return true;
-    if (OpenInfo::ReadIni(name, value)) return true;
-    return false;
-  }
-  void WriteIni(ImGuiTextBuffer* out) {
-    NameInfo::WriteIni(out);
-    OpenInfo::WriteIni(out);
-  }
-};
-
-class EncoderSource {
- public:
-  EncoderSource(const wpi::Twine& id, int32_t index, int channelA, int channelB)
-      : distancePerPulse(id + " Dist/Count"),
-        count(id + " Count"),
-        period(id + " Period"),
-        direction(id + " Direction"),
-        distance(id + " Distance"),
-        rate(id + " Rate"),
-        m_index{index},
-        m_channelA{channelA},
-        m_channelB{channelB},
-        m_distancePerPulseCallback{
-            HALSIM_RegisterEncoderDistancePerPulseCallback(
-                index, DistancePerPulseCallbackFunc, this, true)},
-        m_countCallback{HALSIM_RegisterEncoderCountCallback(
-            index, CountCallbackFunc, this, true)},
-        m_periodCallback{HALSIM_RegisterEncoderPeriodCallback(
-            index, PeriodCallbackFunc, this, true)},
-        m_directionCallback{HALSIM_RegisterEncoderDirectionCallback(
-            index, DirectionCallbackFunc, this, true)} {
-    direction.SetDigital(true);
-  }
-
-  EncoderSource(int32_t index, int channelA, int channelB)
-      : EncoderSource("Encoder[" + wpi::Twine(channelA) + wpi::Twine(',') +
-                          wpi::Twine(channelB) + wpi::Twine(']'),
-                      index, channelA, channelB) {}
-
-  explicit EncoderSource(int32_t index)
-      : EncoderSource(index, HALSIM_GetEncoderDigitalChannelA(index),
-                      HALSIM_GetEncoderDigitalChannelB(index)) {}
-
-  ~EncoderSource() {
-    if (m_distancePerPulseCallback != 0)
-      HALSIM_CancelEncoderDistancePerPulseCallback(m_index,
-                                                   m_distancePerPulseCallback);
-    if (m_countCallback != 0)
-      HALSIM_CancelEncoderCountCallback(m_index, m_countCallback);
-    if (m_periodCallback != 0)
-      HALSIM_CancelEncoderCountCallback(m_index, m_periodCallback);
-    if (m_directionCallback != 0)
-      HALSIM_CancelEncoderCountCallback(m_index, m_directionCallback);
-  }
-
-  void SetName(const wpi::Twine& name) {
-    if (name.str().empty()) {
-      distancePerPulse.SetName("");
-      count.SetName("");
-      period.SetName("");
-      direction.SetName("");
-      distance.SetName("");
-      rate.SetName("");
-    } else {
-      distancePerPulse.SetName(name + " Distance/Count");
-      count.SetName(name + " Count");
-      period.SetName(name + " Period");
-      direction.SetName(name + " Direction");
-      distance.SetName(name + " Distance");
-      rate.SetName(name + " Rate");
-    }
-  }
-
-  int32_t GetIndex() const { return m_index; }
-  int GetChannelA() const { return m_channelA; }
-  int GetChannelB() const { return m_channelB; }
-
-  GuiDataSource distancePerPulse;
-  GuiDataSource count;
-  GuiDataSource period;
-  GuiDataSource direction;
-  GuiDataSource distance;
-  GuiDataSource rate;
-
- private:
-  static void DistancePerPulseCallbackFunc(const char*, void* param,
-                                           const HAL_Value* value) {
-    if (value->type == HAL_DOUBLE) {
-      auto self = static_cast<EncoderSource*>(param);
-      double distPerPulse = value->data.v_double;
-      self->distancePerPulse.SetValue(distPerPulse);
-      self->distance.SetValue(self->count.GetValue() * distPerPulse);
-      double period = self->period.GetValue();
-      if (period == 0)
-        self->rate.SetValue(std::numeric_limits<double>::infinity());
-      else if (period == std::numeric_limits<double>::infinity())
-        self->rate.SetValue(0);
-      else
-        self->rate.SetValue(static_cast<float>(distPerPulse / period));
-    }
-  }
-
-  static void CountCallbackFunc(const char*, void* param,
-                                const HAL_Value* value) {
-    if (value->type == HAL_INT) {
-      auto self = static_cast<EncoderSource*>(param);
-      double count = value->data.v_int;
-      self->count.SetValue(count);
-      self->distance.SetValue(count * self->distancePerPulse.GetValue());
-    }
-  }
-
-  static void PeriodCallbackFunc(const char*, void* param,
-                                 const HAL_Value* value) {
-    if (value->type == HAL_DOUBLE) {
-      auto self = static_cast<EncoderSource*>(param);
-      double period = value->data.v_double;
-      self->period.SetValue(period);
-      if (period == 0)
-        self->rate.SetValue(std::numeric_limits<double>::infinity());
-      else if (period == std::numeric_limits<double>::infinity())
-        self->rate.SetValue(0);
-      else
-        self->rate.SetValue(
-            static_cast<float>(self->distancePerPulse.GetValue() / period));
-    }
-  }
-
-  static void DirectionCallbackFunc(const char*, void* param,
-                                    const HAL_Value* value) {
-    if (value->type == HAL_BOOLEAN) {
-      static_cast<EncoderSource*>(param)->direction.SetValue(
-          value->data.v_boolean);
-    }
-  }
-
-  int32_t m_index;
-  int m_channelA;
-  int m_channelB;
-  int32_t m_distancePerPulseCallback;
-  int32_t m_countCallback;
-  int32_t m_periodCallback;
-  int32_t m_directionCallback;
-};
-
-}  // namespace
-
-static IniSaver<EncoderInfo> gEncoders{"Encoder"};  // indexed by channel A
-static std::vector<std::unique_ptr<EncoderSource>> gEncoderSources;
-
-static void UpdateEncoderSources() {
-  for (int i = 0, iend = gEncoderSources.size(); i < iend; ++i) {
-    auto& source = gEncoderSources[i];
-    if (HALSIM_GetEncoderInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<EncoderSource>(i);
-        source->SetName(gEncoders[source->GetChannelA()].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayEncoders() {
-  bool hasAny = false;
-  ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
-  for (int i = 0, iend = gEncoderSources.size(); i < iend; ++i) {
-    if (auto source = gEncoderSources[i].get()) {
-      hasAny = true;
-      if (auto simDevice = HALSIM_GetEncoderSimDevice(i)) {
-        ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
-        ImGui::Text("%s", HALSIM_GetSimDeviceName(simDevice));
-        ImGui::PopStyleColor();
-      } else {
-        int chA = source->GetChannelA();
-        int chB = source->GetChannelB();
-
-        // build header name
-        auto& info = gEncoders[chA];
-        char name[128];
-        info.GetLabel(name, sizeof(name), "Encoder", chA, chB);
-
-        // header
-        bool open = ImGui::CollapsingHeader(
-            name, gEncoders[chA].IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0);
-        info.SetOpen(open);
-
-        // context menu to change name
-        if (info.PopupEditName(chA)) {
-          source->SetName(info.GetName());
-        }
-
-        if (open) {
-          ImGui::PushID(i);
-          // distance per pulse
-          double distancePerPulse = source->distancePerPulse.GetValue();
-          source->distancePerPulse.LabelText("Dist/Count", "%.6f",
-                                             distancePerPulse);
-
-          // count
-          int count = source->count.GetValue();
-          if (ImGui::InputInt("##input", &count))
-            HALSIM_SetEncoderCount(i, count);
-          ImGui::SameLine();
-          if (ImGui::Button("Reset")) HALSIM_SetEncoderCount(i, 0);
-          ImGui::SameLine();
-          ImGui::Selectable("Count");
-          source->count.EmitDrag();
-
-          // max period
-          double maxPeriod = HALSIM_GetEncoderMaxPeriod(i);
-          ImGui::LabelText("Max Period", "%.6f", maxPeriod);
-
-          // period
-          double period = source->period.GetValue();
-          if (source->period.InputDouble("Period", &period, 0, 0, "%.6g"))
-            HALSIM_SetEncoderPeriod(i, period);
-
-          // reverse direction
-          ImGui::LabelText(
-              "Reverse Direction", "%s",
-              HALSIM_GetEncoderReverseDirection(i) ? "true" : "false");
-
-          // direction
-          static const char* options[] = {"reverse", "forward"};
-          int direction = source->direction.GetValue() ? 1 : 0;
-          if (source->direction.Combo("Direction", &direction, options, 2))
-            HALSIM_SetEncoderDirection(i, direction);
-
-          // distance
-          double distance = source->distance.GetValue();
-          if (source->distance.InputDouble("Distance", &distance, 0, 0, "%.6g"))
-            HALSIM_SetEncoderDistance(i, distance);
-
-          // rate
-          double rate = source->rate.GetValue();
-          if (source->rate.InputDouble("Rate", &rate, 0, 0, "%.6g"))
-            HALSIM_SetEncoderRate(i, rate);
-
-          ImGui::PopID();
-        }
-      }
-    }
-  }
-  ImGui::PopItemWidth();
-  if (!hasAny) ImGui::Text("No encoders");
-}
-
-void EncoderGui::Initialize() {
-  gEncoders.Initialize();
-  gEncoderSources.resize(HAL_GetNumEncoders());
-  HALSimGui::AddExecute(UpdateEncoderSources);
-  HALSimGui::AddWindow("Encoders", DisplayEncoders,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("Encoders", 5, 250);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.h b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.h
deleted file mode 100644
index b7c9dbf..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class EncoderGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp
new file mode 100644
index 0000000..56e8ea2
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp
@@ -0,0 +1,264 @@
+// 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 "EncoderSimGui.h"
+
+#include <glass/hardware/Encoder.h>
+#include <stdint.h>
+
+#include <limits>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <fmt/format.h>
+#include <hal/Ports.h>
+#include <hal/simulation/EncoderData.h>
+#include <hal/simulation/SimDeviceData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+
+class EncoderSimModel : public glass::EncoderModel {
+ public:
+  EncoderSimModel(std::string_view id, int32_t index, int channelA,
+                  int channelB)
+      : m_distancePerPulse(fmt::format("{} Dist/Count", id)),
+        m_count(fmt::format("{} Count", id)),
+        m_period(fmt::format("{} Period", id)),
+        m_direction(fmt::format("{} Direction", id)),
+        m_distance(fmt::format("{} Distance", id)),
+        m_rate(fmt::format("{} Rate", id)),
+        m_index{index},
+        m_channelA{channelA},
+        m_channelB{channelB},
+        m_distancePerPulseCallback{
+            HALSIM_RegisterEncoderDistancePerPulseCallback(
+                index, DistancePerPulseCallbackFunc, this, true)},
+        m_countCallback{HALSIM_RegisterEncoderCountCallback(
+            index, CountCallbackFunc, this, true)},
+        m_periodCallback{HALSIM_RegisterEncoderPeriodCallback(
+            index, PeriodCallbackFunc, this, true)},
+        m_directionCallback{HALSIM_RegisterEncoderDirectionCallback(
+            index, DirectionCallbackFunc, this, true)} {
+    m_direction.SetDigital(true);
+  }
+
+  EncoderSimModel(int32_t index, int channelA, int channelB)
+      : EncoderSimModel(fmt::format("Encoder[{},{}]", channelA, channelB),
+                        index, channelA, channelB) {}
+
+  explicit EncoderSimModel(int32_t index)
+      : EncoderSimModel(index, HALSIM_GetEncoderDigitalChannelA(index),
+                        HALSIM_GetEncoderDigitalChannelB(index)) {}
+
+  ~EncoderSimModel() override {
+    if (m_distancePerPulseCallback != 0) {
+      HALSIM_CancelEncoderDistancePerPulseCallback(m_index,
+                                                   m_distancePerPulseCallback);
+    }
+    if (m_countCallback != 0) {
+      HALSIM_CancelEncoderCountCallback(m_index, m_countCallback);
+    }
+    if (m_periodCallback != 0) {
+      HALSIM_CancelEncoderCountCallback(m_index, m_periodCallback);
+    }
+    if (m_directionCallback != 0) {
+      HALSIM_CancelEncoderCountCallback(m_index, m_directionCallback);
+    }
+  }
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetEncoderInitialized(m_index); }
+
+  int32_t GetIndex() const { return m_index; }
+
+  const char* GetSimDevice() const override {
+    if (auto simDevice = HALSIM_GetEncoderSimDevice(m_index)) {
+      return HALSIM_GetSimDeviceName(simDevice);
+    } else {
+      return nullptr;
+    }
+  }
+
+  int GetChannelA() const override { return m_channelA; }
+  int GetChannelB() const override { return m_channelB; }
+
+  glass::DataSource* GetDistancePerPulseData() override {
+    return &m_distancePerPulse;
+  }
+  glass::DataSource* GetCountData() override { return &m_count; }
+  glass::DataSource* GetPeriodData() override { return &m_period; }
+  glass::DataSource* GetDirectionData() override { return &m_direction; }
+  glass::DataSource* GetDistanceData() override { return &m_distance; }
+  glass::DataSource* GetRateData() override { return &m_rate; }
+
+  double GetMaxPeriod() override { return HALSIM_GetEncoderMaxPeriod(m_index); }
+  bool GetReverseDirection() override {
+    return HALSIM_GetEncoderReverseDirection(m_index);
+  }
+
+  void SetDistancePerPulse(double val) override {
+    HALSIM_SetEncoderDistancePerPulse(m_index, val);
+  }
+  void SetCount(int val) override { HALSIM_SetEncoderCount(m_index, val); }
+  void SetPeriod(double val) override { HALSIM_SetEncoderPeriod(m_index, val); }
+  void SetDirection(bool val) override {
+    HALSIM_SetEncoderDirection(m_index, val);
+  }
+  void SetDistance(double val) override {
+    HALSIM_SetEncoderDistance(m_index, val);
+  }
+  void SetRate(double val) override { HALSIM_SetEncoderRate(m_index, val); }
+
+  void SetMaxPeriod(double val) override {
+    HALSIM_SetEncoderMaxPeriod(m_index, val);
+  }
+  void SetReverseDirection(bool val) override {
+    HALSIM_SetEncoderReverseDirection(m_index, val);
+  }
+
+ private:
+  static void DistancePerPulseCallbackFunc(const char*, void* param,
+                                           const HAL_Value* value) {
+    if (value->type == HAL_DOUBLE) {
+      auto self = static_cast<EncoderSimModel*>(param);
+      double distPerPulse = value->data.v_double;
+      self->m_distancePerPulse.SetValue(distPerPulse);
+      self->m_distance.SetValue(self->m_count.GetValue() * distPerPulse);
+      double period = self->m_period.GetValue();
+      if (period == 0) {
+        self->m_rate.SetValue(std::numeric_limits<double>::infinity());
+      } else if (period == std::numeric_limits<double>::infinity()) {
+        self->m_rate.SetValue(0);
+      } else {
+        self->m_rate.SetValue(static_cast<float>(distPerPulse / period));
+      }
+    }
+  }
+
+  static void CountCallbackFunc(const char*, void* param,
+                                const HAL_Value* value) {
+    if (value->type == HAL_INT) {
+      auto self = static_cast<EncoderSimModel*>(param);
+      double count = value->data.v_int;
+      self->m_count.SetValue(count);
+      self->m_distance.SetValue(count * self->m_distancePerPulse.GetValue());
+    }
+  }
+
+  static void PeriodCallbackFunc(const char*, void* param,
+                                 const HAL_Value* value) {
+    if (value->type == HAL_DOUBLE) {
+      auto self = static_cast<EncoderSimModel*>(param);
+      double period = value->data.v_double;
+      self->m_period.SetValue(period);
+      if (period == 0) {
+        self->m_rate.SetValue(std::numeric_limits<double>::infinity());
+      } else if (period == std::numeric_limits<double>::infinity()) {
+        self->m_rate.SetValue(0);
+      } else {
+        self->m_rate.SetValue(
+            static_cast<float>(self->m_distancePerPulse.GetValue() / period));
+      }
+    }
+  }
+
+  static void DirectionCallbackFunc(const char*, void* param,
+                                    const HAL_Value* value) {
+    if (value->type == HAL_BOOLEAN) {
+      static_cast<EncoderSimModel*>(param)->m_direction.SetValue(
+          value->data.v_boolean);
+    }
+  }
+
+  glass::DataSource m_distancePerPulse;
+  glass::DataSource m_count;
+  glass::DataSource m_period;
+  glass::DataSource m_direction;
+  glass::DataSource m_distance;
+  glass::DataSource m_rate;
+
+  int32_t m_index;
+  int m_channelA;
+  int m_channelB;
+  int32_t m_distancePerPulseCallback;
+  int32_t m_countCallback;
+  int32_t m_periodCallback;
+  int32_t m_directionCallback;
+};
+
+class EncodersSimModel : public glass::EncodersModel {
+ public:
+  EncodersSimModel() : m_models(HAL_GetNumEncoders()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachEncoder(
+      wpi::function_ref<void(glass::EncoderModel& model, int index)> func)
+      override;
+
+ private:
+  std::vector<std::unique_ptr<EncoderSimModel>> m_models;
+};
+}  // namespace
+
+void EncodersSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetEncoderInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<EncoderSimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void EncodersSimModel::ForEachEncoder(
+    wpi::function_ref<void(glass::EncoderModel& model, int index)> func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool EncodersAnyInitialized() {
+  static const int32_t num = HAL_GetNumEncoders();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetEncoderInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void EncoderSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "Encoders", EncodersAnyInitialized,
+      [] { return std::make_unique<EncodersSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(5, 250);
+        return glass::MakeFunctionView(
+            [=] { DisplayEncoders(static_cast<EncodersSimModel*>(model)); });
+      });
+}
+
+glass::EncodersModel& EncoderSimGui::GetEncodersModel() {
+  static auto model = HALSimGui::halProvider.GetModel("Encoders");
+  assert(model);
+  return *static_cast<glass::EncodersModel*>(model);
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h
new file mode 100644
index 0000000..f27d180
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h
@@ -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.
+
+#pragma once
+
+namespace glass {
+class EncodersModel;
+}  // namespace glass
+
+namespace halsimgui {
+
+class EncoderSimGui {
+ public:
+  static void Initialize();
+  static glass::EncodersModel& GetEncodersModel();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp b/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp
deleted file mode 100644
index b5bf92f..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "ExtraGuiWidgets.h"
-
-#include "GuiDataSource.h"
-
-namespace halsimgui {
-
-void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues,
-                    int cols, const ImU32* colors, float size, float spacing,
-                    const LEDConfig& config) {
-  if (numValues == 0 || cols < 1) return;
-  if (size == 0) size = ImGui::GetFontSize() / 2.0;
-  if (spacing == 0) spacing = ImGui::GetFontSize() / 3.0;
-
-  int rows = (numValues + cols - 1) / cols;
-  float inc = size + spacing;
-
-  ImDrawList* drawList = ImGui::GetWindowDrawList();
-  const ImVec2 p = ImGui::GetCursorScreenPos();
-
-  float sized2 = size / 2;
-  float ystart, yinc;
-  if (config.start & 1) {
-    // lower
-    ystart = p.y + sized2 + inc * (rows - 1);
-    yinc = -inc;
-  } else {
-    // upper
-    ystart = p.y + sized2;
-    yinc = inc;
-  }
-
-  float xstart, xinc;
-  if (config.start & 2) {
-    // right
-    xstart = p.x + sized2 + inc * (cols - 1);
-    xinc = -inc;
-  } else {
-    // left
-    xstart = p.x + sized2;
-    xinc = inc;
-  }
-
-  float x = xstart, y = ystart;
-  int rowcol = 1;  // row for row-major, column for column-major
-  for (int i = 0; i < numValues; ++i) {
-    if (config.order == LEDConfig::RowMajor) {
-      if (i >= (rowcol * cols)) {
-        ++rowcol;
-        if (config.serpentine) {
-          x -= xinc;
-          xinc = -xinc;
-        } else {
-          x = xstart;
-        }
-        y += yinc;
-      }
-    } else {
-      if (i >= (rowcol * rows)) {
-        ++rowcol;
-        if (config.serpentine) {
-          y -= yinc;
-          yinc = -yinc;
-        } else {
-          y = ystart;
-        }
-        x += xinc;
-      }
-    }
-    if (values[i] > 0)
-      drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size),
-                              colors[values[i] - 1]);
-    else if (values[i] < 0)
-      drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size),
-                        colors[-values[i] - 1], 0.0f, 0, 1.0);
-    if (sources) {
-      ImGui::SetCursorScreenPos(ImVec2(x - sized2, y - sized2));
-      if (sources[i]) {
-        ImGui::PushID(i);
-        ImGui::Selectable("", false, 0, ImVec2(inc, inc));
-        sources[i]->EmitDrag();
-        ImGui::PopID();
-      } else {
-        ImGui::Dummy(ImVec2(inc, inc));
-      }
-    }
-    if (config.order == LEDConfig::RowMajor) {
-      x += xinc;
-    } else {
-      y += yinc;
-    }
-  }
-
-  if (!sources) ImGui::Dummy(ImVec2(inc * cols, inc * rows));
-}
-
-void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors,
-              float size, float spacing, const LEDConfig& config) {
-  DrawLEDSources(values, nullptr, numValues, cols, colors, size, spacing,
-                 config);
-}
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp b/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp
deleted file mode 100644
index b1e4f0c..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp
+++ /dev/null
@@ -1,652 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "Field2D.h"
-
-#include <cmath>
-
-#include <hal/SimDevice.h>
-
-#define IMGUI_DEFINE_MATH_OPERATORS
-#include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
-#include <imgui_internal.h>
-#include <units/angle.h>
-#include <units/length.h>
-#include <wpi/Path.h>
-#include <wpi/SmallString.h>
-#include <wpi/json.h>
-#include <wpi/raw_istream.h>
-#include <wpi/raw_ostream.h>
-#include <wpigui.h>
-
-#include "HALSimGui.h"
-#include "SimDeviceGui.h"
-#include "portable-file-dialogs.h"
-
-using namespace halsimgui;
-
-namespace gui = wpi::gui;
-
-namespace {
-
-// Per-frame field data (not persistent)
-struct FieldFrameData {
-  // in window coordinates
-  ImVec2 imageMin;
-  ImVec2 imageMax;
-  ImVec2 min;
-  ImVec2 max;
-
-  float scale;  // scaling from field units to screen units
-};
-
-class FieldInfo {
- public:
-  static constexpr float kDefaultWidth = 15.98f;
-  static constexpr float kDefaultHeight = 8.21f;
-
-  std::unique_ptr<pfd::open_file> m_fileOpener;
-  float m_width = kDefaultWidth;
-  float m_height = kDefaultHeight;
-
-  void Reset();
-  void LoadImage();
-  void LoadJson(const wpi::Twine& jsonfile);
-  FieldFrameData GetFrameData() const;
-  void Draw(ImDrawList* drawList, const ImVec2& windowPos,
-            const FieldFrameData& frameData) const;
-
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out) const;
-
- private:
-  bool LoadImageImpl(const char* fn);
-
-  std::string m_filename;
-  gui::Texture m_texture;
-  int m_imageWidth = 0;
-  int m_imageHeight = 0;
-  int m_top = 0;
-  int m_left = 0;
-  int m_bottom = -1;
-  int m_right = -1;
-};
-
-// Per-frame robot data (not persistent)
-struct RobotFrameData {
-  // in window coordinates
-  ImVec2 center;
-  ImVec2 corners[4];
-  ImVec2 arrow[3];
-
-  // scaled width/2 and length/2, in screen units
-  float width2;
-  float length2;
-};
-
-class RobotInfo {
- public:
-  static constexpr float kDefaultWidth = 0.6858f;
-  static constexpr float kDefaultLength = 0.8204f;
-
-  std::unique_ptr<pfd::open_file> m_fileOpener;
-  float m_width = kDefaultWidth;
-  float m_length = kDefaultLength;
-
-  void Reset();
-  void LoadImage();
-  void UpdateFromSimDevice();
-  void SetPosition(double x, double y);
-  // set and get rotation in radians
-  void SetRotation(double rot);
-  double GetRotation() const {
-    return units::convert<units::degrees, units::radians>(m_rot);
-  }
-  RobotFrameData GetFrameData(const FieldFrameData& ffd) const;
-  void Draw(ImDrawList* drawList, const ImVec2& windowPos,
-            const RobotFrameData& frameData, int hit, float hitRadius) const;
-
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out) const;
-
- private:
-  bool LoadImageImpl(const char* fn);
-
-  std::string m_filename;
-  gui::Texture m_texture;
-
-  HAL_SimDeviceHandle m_devHandle = 0;
-  hal::SimDouble m_xHandle;
-  hal::SimDouble m_yHandle;
-  hal::SimDouble m_rotHandle;
-
-  double m_x = 0;
-  double m_y = 0;
-  double m_rot = 0;
-};
-
-}  // namespace
-
-static FieldInfo gField;
-static RobotInfo gRobot;
-static int gDragRobot = 0;
-static ImVec2 gDragInitialOffset;
-static double gDragInitialAngle;
-
-// read/write settings to ini file
-static void* Field2DReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                             const char* name) {
-  if (name == wpi::StringRef{"Field"}) return &gField;
-  if (name == wpi::StringRef{"Robot"}) return &gRobot;
-  return nullptr;
-}
-
-static void Field2DReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                            void* entry, const char* lineStr) {
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  if (entry == &gField)
-    gField.ReadIni(name, value);
-  else if (entry == &gRobot)
-    gRobot.ReadIni(name, value);
-}
-
-static void Field2DWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                            ImGuiTextBuffer* out_buf) {
-  gField.WriteIni(out_buf);
-  gRobot.WriteIni(out_buf);
-}
-
-void FieldInfo::Reset() {
-  m_texture = gui::Texture{};
-  m_filename.clear();
-  m_imageWidth = 0;
-  m_imageHeight = 0;
-  m_top = 0;
-  m_left = 0;
-  m_bottom = -1;
-  m_right = -1;
-}
-
-void FieldInfo::LoadImage() {
-  if (m_fileOpener && m_fileOpener->ready(0)) {
-    auto result = m_fileOpener->result();
-    if (!result.empty()) {
-      if (wpi::StringRef(result[0]).endswith(".json")) {
-        LoadJson(result[0]);
-      } else {
-        LoadImageImpl(result[0].c_str());
-        m_top = 0;
-        m_left = 0;
-        m_bottom = -1;
-        m_right = -1;
-      }
-    }
-    m_fileOpener.reset();
-  }
-  if (!m_texture && !m_filename.empty()) {
-    if (!LoadImageImpl(m_filename.c_str())) m_filename.clear();
-  }
-}
-
-void FieldInfo::LoadJson(const wpi::Twine& jsonfile) {
-  std::error_code ec;
-  wpi::raw_fd_istream f(jsonfile, ec);
-  if (ec) {
-    wpi::errs() << "GUI: could not open field JSON file\n";
-    return;
-  }
-
-  // parse file
-  wpi::json j;
-  try {
-    j = wpi::json::parse(f);
-  } catch (const wpi::json::parse_error& e) {
-    wpi::errs() << "GUI: JSON: could not parse: " << e.what() << '\n';
-  }
-
-  // top level must be an object
-  if (!j.is_object()) {
-    wpi::errs() << "GUI: JSON: does not contain a top object\n";
-    return;
-  }
-
-  // image filename
-  std::string image;
-  try {
-    image = j.at("field-image").get<std::string>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "GUI: JSON: could not read field-image: " << e.what()
-                << '\n';
-    return;
-  }
-
-  // corners
-  int top, left, bottom, right;
-  try {
-    top = j.at("field-corners").at("top-left").at(1).get<int>();
-    left = j.at("field-corners").at("top-left").at(0).get<int>();
-    bottom = j.at("field-corners").at("bottom-right").at(1).get<int>();
-    right = j.at("field-corners").at("bottom-right").at(0).get<int>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "GUI: JSON: could not read field-corners: " << e.what()
-                << '\n';
-    return;
-  }
-
-  // size
-  float width;
-  float height;
-  try {
-    width = j.at("field-size").at(0).get<float>();
-    height = j.at("field-size").at(1).get<float>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "GUI: JSON: could not read field-size: " << e.what() << '\n';
-    return;
-  }
-
-  // units for size
-  std::string unit;
-  try {
-    unit = j.at("field-unit").get<std::string>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "GUI: JSON: could not read field-unit: " << e.what() << '\n';
-    return;
-  }
-
-  // convert size units to meters
-  if (unit == "foot" || unit == "feet") {
-    width = units::convert<units::feet, units::meters>(width);
-    height = units::convert<units::feet, units::meters>(height);
-  }
-
-  // the image filename is relative to the json file
-  wpi::SmallString<128> pathname;
-  jsonfile.toVector(pathname);
-  wpi::sys::path::remove_filename(pathname);
-  wpi::sys::path::append(pathname, image);
-
-  // load field image
-  if (!LoadImageImpl(pathname.c_str())) return;
-
-  // save to field info
-  m_filename = pathname.str();
-  m_top = top;
-  m_left = left;
-  m_bottom = bottom;
-  m_right = right;
-  m_width = width;
-  m_height = height;
-}
-
-bool FieldInfo::LoadImageImpl(const char* fn) {
-  wpi::outs() << "GUI: loading field image '" << fn << "'\n";
-  auto texture = gui::Texture::CreateFromFile(fn);
-  if (!texture) {
-    wpi::errs() << "GUI: could not read field image\n";
-    return false;
-  }
-  m_texture = std::move(texture);
-  m_imageWidth = m_texture.GetWidth();
-  m_imageHeight = m_texture.GetHeight();
-  m_filename = fn;
-  return true;
-}
-
-FieldFrameData FieldInfo::GetFrameData() const {
-  FieldFrameData ffd;
-
-  // get window content region
-  ffd.imageMin = ImGui::GetWindowContentRegionMin();
-  ffd.imageMax = ImGui::GetWindowContentRegionMax();
-
-  // fit the image into the window
-  if (m_texture && m_imageHeight != 0 && m_imageWidth != 0)
-    gui::MaxFit(&ffd.imageMin, &ffd.imageMax, m_imageWidth, m_imageHeight);
-
-  ImVec2 min = ffd.imageMin;
-  ImVec2 max = ffd.imageMax;
-
-  // size down the box by the image corners (if any)
-  if (m_bottom > 0 && m_right > 0) {
-    min.x += m_left * (max.x - min.x) / m_imageWidth;
-    min.y += m_top * (max.y - min.y) / m_imageHeight;
-    max.x -= (m_imageWidth - m_right) * (max.x - min.x) / m_imageWidth;
-    max.y -= (m_imageHeight - m_bottom) * (max.y - min.y) / m_imageHeight;
-  }
-
-  // draw the field "active area" as a yellow boundary box
-  gui::MaxFit(&min, &max, m_width, m_height);
-
-  ffd.min = min;
-  ffd.max = max;
-  ffd.scale = (max.x - min.x) / m_width;
-  return ffd;
-}
-
-void FieldInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos,
-                     const FieldFrameData& ffd) const {
-  if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
-    drawList->AddImage(m_texture, windowPos + ffd.imageMin,
-                       windowPos + ffd.imageMax);
-  }
-
-  // draw the field "active area" as a yellow boundary box
-  drawList->AddRect(windowPos + ffd.min, windowPos + ffd.max,
-                    IM_COL32(255, 255, 0, 255));
-}
-
-bool FieldInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name == "image") {
-    m_filename = value;
-  } else if (name == "top") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_top = num;
-  } else if (name == "left") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_left = num;
-  } else if (name == "bottom") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_bottom = num;
-  } else if (name == "right") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_right = num;
-  } else if (name == "width") {
-    std::sscanf(value.data(), "%f", &m_width);
-  } else if (name == "height") {
-    std::sscanf(value.data(), "%f", &m_height);
-  } else {
-    return false;
-  }
-  return true;
-}
-
-void FieldInfo::WriteIni(ImGuiTextBuffer* out) const {
-  out->appendf(
-      "[Field2D][Field]\nimage=%s\ntop=%d\nleft=%d\nbottom=%d\nright=%d\nwidth="
-      "%f\nheight=%f\n\n",
-      m_filename.c_str(), m_top, m_left, m_bottom, m_right, m_width, m_height);
-}
-
-void RobotInfo::Reset() {
-  m_texture = gui::Texture{};
-  m_filename.clear();
-}
-
-void RobotInfo::LoadImage() {
-  if (m_fileOpener && m_fileOpener->ready(0)) {
-    auto result = m_fileOpener->result();
-    if (!result.empty()) LoadImageImpl(result[0].c_str());
-    m_fileOpener.reset();
-  }
-  if (!m_texture && !m_filename.empty()) {
-    if (!LoadImageImpl(m_filename.c_str())) m_filename.clear();
-  }
-}
-
-bool RobotInfo::LoadImageImpl(const char* fn) {
-  wpi::outs() << "GUI: loading robot image '" << fn << "'\n";
-  auto texture = gui::Texture::CreateFromFile(fn);
-  if (!texture) {
-    wpi::errs() << "GUI: could not read robot image\n";
-    return false;
-  }
-  m_texture = std::move(texture);
-  m_filename = fn;
-  return true;
-}
-
-void RobotInfo::UpdateFromSimDevice() {
-  if (m_devHandle == 0) m_devHandle = HALSIM_GetSimDeviceHandle("Field2D");
-  if (m_devHandle == 0) return;
-
-  if (!m_xHandle) m_xHandle = HALSIM_GetSimValueHandle(m_devHandle, "x");
-  if (m_xHandle) m_x = m_xHandle.Get();
-
-  if (!m_yHandle) m_yHandle = HALSIM_GetSimValueHandle(m_devHandle, "y");
-  if (m_yHandle) m_y = m_yHandle.Get();
-
-  if (!m_rotHandle) m_rotHandle = HALSIM_GetSimValueHandle(m_devHandle, "rot");
-  if (m_rotHandle) m_rot = m_rotHandle.Get();
-}
-
-void RobotInfo::SetPosition(double x, double y) {
-  m_x = x;
-  m_y = y;
-  if (m_xHandle) m_xHandle.Set(x);
-  if (m_yHandle) m_yHandle.Set(y);
-}
-
-void RobotInfo::SetRotation(double rot) {
-  double rotDegrees = units::convert<units::radians, units::degrees>(rot);
-  // force to -180 to +180 range
-  rotDegrees = rotDegrees + std::ceil((-rotDegrees - 180) / 360) * 360;
-  m_rot = rotDegrees;
-  if (m_rotHandle) m_rotHandle.Set(rotDegrees);
-}
-
-RobotFrameData RobotInfo::GetFrameData(const FieldFrameData& ffd) const {
-  RobotFrameData rfd;
-  float width2 = ffd.scale * m_width / 2;
-  float length2 = ffd.scale * m_length / 2;
-
-  // (0,0) origin is bottom left
-  ImVec2 center(ffd.min.x + ffd.scale * m_x, ffd.max.y - ffd.scale * m_y);
-
-  // build rotated points around center
-  double rot = GetRotation();
-  float cos_a = std::cos(-rot);
-  float sin_a = std::sin(-rot);
-
-  rfd.corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a);
-  rfd.corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a);
-  rfd.corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a);
-  rfd.corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a);
-  rfd.arrow[0] =
-      center + ImRotate(ImVec2(-length2 / 2, -width2 / 2), cos_a, sin_a);
-  rfd.arrow[1] = center + ImRotate(ImVec2(length2 / 2, 0), cos_a, sin_a);
-  rfd.arrow[2] =
-      center + ImRotate(ImVec2(-length2 / 2, width2 / 2), cos_a, sin_a);
-
-  rfd.center = center;
-  rfd.width2 = width2;
-  rfd.length2 = length2;
-  return rfd;
-}
-
-void RobotInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos,
-                     const RobotFrameData& rfd, int hit,
-                     float hitRadius) const {
-  if (m_texture) {
-    drawList->AddImageQuad(
-        m_texture, windowPos + rfd.corners[0], windowPos + rfd.corners[1],
-        windowPos + rfd.corners[2], windowPos + rfd.corners[3]);
-  } else {
-    drawList->AddQuad(windowPos + rfd.corners[0], windowPos + rfd.corners[1],
-                      windowPos + rfd.corners[2], windowPos + rfd.corners[3],
-                      IM_COL32(255, 0, 0, 255), 4.0);
-    drawList->AddTriangle(windowPos + rfd.arrow[0], windowPos + rfd.arrow[1],
-                          windowPos + rfd.arrow[2], IM_COL32(0, 255, 0, 255),
-                          4.0);
-  }
-
-  if (hit > 0) {
-    if (hit == 1) {
-      drawList->AddCircle(windowPos + rfd.center, hitRadius,
-                          IM_COL32(0, 255, 0, 255));
-    } else {
-      drawList->AddCircle(windowPos + rfd.corners[hit - 2], hitRadius,
-                          IM_COL32(0, 255, 0, 255));
-    }
-  }
-}
-
-bool RobotInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name == "image") {
-    m_filename = value;
-  } else if (name == "width") {
-    std::sscanf(value.data(), "%f", &m_width);
-  } else if (name == "length") {
-    std::sscanf(value.data(), "%f", &m_length);
-  } else {
-    return false;
-  }
-  return true;
-}
-
-void RobotInfo::WriteIni(ImGuiTextBuffer* out) const {
-  out->appendf("[Field2D][Robot]\nimage=%s\nwidth=%f\nlength=%f\n\n",
-               m_filename.c_str(), m_width, m_length);
-}
-
-static void OptionMenuField2D() {
-  if (ImGui::BeginMenu("2D Field View")) {
-    if (ImGui::MenuItem("Choose field image...")) {
-      gField.m_fileOpener = std::make_unique<pfd::open_file>(
-          "Choose field image", "",
-          std::vector<std::string>{"Image File",
-                                   "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
-                                   "*.hdr *.pic *.ppm *.pgm",
-                                   "PathWeaver JSON File", "*.json"});
-    }
-    if (ImGui::MenuItem("Reset field image")) {
-      gField.Reset();
-    }
-    if (ImGui::MenuItem("Choose robot image...")) {
-      gRobot.m_fileOpener = std::make_unique<pfd::open_file>(
-          "Choose robot image", "",
-          std::vector<std::string>{"Image File",
-                                   "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
-                                   "*.hdr *.pic *.ppm *.pgm"});
-    }
-    if (ImGui::MenuItem("Reset robot image")) {
-      gRobot.Reset();
-    }
-    ImGui::EndMenu();
-  }
-}
-
-static void DisplayField2DSettings() {
-  ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
-  ImGui::InputFloat("Field Width", &gField.m_width);
-  ImGui::InputFloat("Field Height", &gField.m_height);
-  // ImGui::InputInt("Field Top", &gField.m_top);
-  // ImGui::InputInt("Field Left", &gField.m_left);
-  // ImGui::InputInt("Field Right", &gField.m_right);
-  // ImGui::InputInt("Field Bottom", &gField.m_bottom);
-  ImGui::InputFloat("Robot Width", &gRobot.m_width);
-  ImGui::InputFloat("Robot Length", &gRobot.m_length);
-  ImGui::PopItemWidth();
-}
-
-static void DisplayField2D() {
-  // load images
-  gField.LoadImage();
-  gRobot.LoadImage();
-
-  // get robot coordinates from SimDevice
-  gRobot.UpdateFromSimDevice();
-
-  FieldFrameData ffd = gField.GetFrameData();
-  RobotFrameData rfd = gRobot.GetFrameData(ffd);
-
-  ImVec2 windowPos = ImGui::GetWindowPos();
-
-  // for dragging to work, there needs to be a button (otherwise the window is
-  // dragged)
-  ImVec2 contentSize =
-      ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin();
-  if (contentSize.x <= 0 || contentSize.y <= 0) return;
-  ImGui::InvisibleButton("field", contentSize);
-
-  // allow dragging the robot around
-  ImVec2 cursor = ImGui::GetIO().MousePos - windowPos;
-
-  int hit = 0;
-  float hitRadius = (std::min)(rfd.width2, rfd.length2) / 2;
-  // only allow initiation of dragging when invisible button is hovered; this
-  // prevents the window resize handles from simultaneously activating the drag
-  // functionality
-  if (ImGui::IsItemHovered()) {
-    float hitRadiusSquared = hitRadius * hitRadius;
-    // it's within the hit radius of the center?
-    if (gui::GetDistSquared(cursor, rfd.center) < hitRadiusSquared)
-      hit = 1;
-    else if (gui::GetDistSquared(cursor, rfd.corners[0]) < hitRadiusSquared)
-      hit = 2;
-    else if (gui::GetDistSquared(cursor, rfd.corners[1]) < hitRadiusSquared)
-      hit = 3;
-    else if (gui::GetDistSquared(cursor, rfd.corners[2]) < hitRadiusSquared)
-      hit = 4;
-    else if (gui::GetDistSquared(cursor, rfd.corners[3]) < hitRadiusSquared)
-      hit = 5;
-    if (hit > 0 && ImGui::IsMouseClicked(0)) {
-      if (hit == 1) {
-        gDragRobot = hit;
-        gDragInitialOffset = cursor - rfd.center;
-      } else {
-        gDragRobot = hit;
-        ImVec2 off = cursor - rfd.center;
-        gDragInitialAngle = std::atan2(off.y, off.x) + gRobot.GetRotation();
-      }
-    }
-  }
-
-  if (gDragRobot > 0 && ImGui::IsMouseDown(0)) {
-    if (gDragRobot == 1) {
-      ImVec2 newPos = cursor - gDragInitialOffset;
-      gRobot.SetPosition(
-          (std::clamp(newPos.x, ffd.min.x, ffd.max.x) - ffd.min.x) / ffd.scale,
-          (ffd.max.y - std::clamp(newPos.y, ffd.min.y, ffd.max.y)) / ffd.scale);
-      rfd = gRobot.GetFrameData(ffd);
-    } else {
-      ImVec2 off = cursor - rfd.center;
-      gRobot.SetRotation(gDragInitialAngle - std::atan2(off.y, off.x));
-    }
-    hit = gDragRobot;  // keep it highlighted
-  } else {
-    gDragRobot = 0;
-  }
-
-  // draw
-  auto drawList = ImGui::GetWindowDrawList();
-  gField.Draw(drawList, windowPos, ffd);
-  gRobot.Draw(drawList, windowPos, rfd, hit, hitRadius);
-}
-
-void Field2D::Initialize() {
-  // hook ini handler to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = "Field2D";
-  iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
-  iniHandler.ReadOpenFn = Field2DReadOpen;
-  iniHandler.ReadLineFn = Field2DReadLine;
-  iniHandler.WriteAllFn = Field2DWriteAll;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-
-  HALSimGui::AddOptionMenu(OptionMenuField2D);
-
-  HALSimGui::AddWindow("2D Field Settings", DisplayField2DSettings,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetWindowVisibility("2D Field Settings", HALSimGui::kHide);
-  HALSimGui::SetDefaultWindowPos("2D Field Settings", 200, 150);
-
-  HALSimGui::AddWindow("2D Field View", DisplayField2D);
-  HALSimGui::SetWindowVisibility("2D Field View", HALSimGui::kHide);
-  HALSimGui::SetDefaultWindowPos("2D Field View", 200, 200);
-  HALSimGui::SetDefaultWindowSize("2D Field View", 400, 200);
-  HALSimGui::SetWindowPadding("2D Field View", 0, 0);
-
-  // SimDeviceGui::Hide("Field2D");
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/Field2D.h b/simulation/halsim_gui/src/main/native/cpp/Field2D.h
deleted file mode 100644
index 52218f3..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/Field2D.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class Field2D {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp b/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp
deleted file mode 100644
index df967c1..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "GuiDataSource.h"
-
-#include <wpi/StringMap.h>
-
-using namespace halsimgui;
-
-wpi::sig::Signal<const char*, GuiDataSource*> GuiDataSource::sourceCreated;
-
-static wpi::StringMap<GuiDataSource*> gSources;
-
-GuiDataSource::GuiDataSource(const wpi::Twine& id) : m_id{id.str()} {
-  gSources.try_emplace(m_id, this);
-  sourceCreated(m_id.c_str(), this);
-}
-
-GuiDataSource::GuiDataSource(const wpi::Twine& id, int index)
-    : GuiDataSource{id + wpi::Twine('[') + wpi::Twine(index) +
-                    wpi::Twine(']')} {}
-
-GuiDataSource::GuiDataSource(const wpi::Twine& id, int index, int index2)
-    : GuiDataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(',') +
-                    wpi::Twine(index2) + wpi::Twine(']')} {}
-
-GuiDataSource::~GuiDataSource() {
-  auto it = gSources.find(m_id);
-  if (it == gSources.end()) return;
-  if (it->getValue() == this) gSources.erase(it);
-}
-
-void GuiDataSource::LabelText(const char* label, const char* fmt, ...) const {
-  va_list args;
-  va_start(args, fmt);
-  LabelTextV(label, fmt, args);
-  va_end(args);
-}
-
-// Add a label+text combo aligned to other label+value widgets
-void GuiDataSource::LabelTextV(const char* label, const char* fmt,
-                               va_list args) const {
-  ImGui::PushID(label);
-  ImGui::LabelTextV("##input", fmt, args);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(label);
-  ImGui::PopID();
-  EmitDrag();
-}
-
-bool GuiDataSource::Combo(const char* label, int* current_item,
-                          const char* const items[], int items_count,
-                          int popup_max_height_in_items) const {
-  ImGui::PushID(label);
-  bool rv = ImGui::Combo("##input", current_item, items, items_count,
-                         popup_max_height_in_items);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(label);
-  EmitDrag();
-  ImGui::PopID();
-  return rv;
-}
-
-bool GuiDataSource::SliderFloat(const char* label, float* v, float v_min,
-                                float v_max, const char* format,
-                                float power) const {
-  ImGui::PushID(label);
-  bool rv = ImGui::SliderFloat("##input", v, v_min, v_max, format, power);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(label);
-  EmitDrag();
-  ImGui::PopID();
-  return rv;
-}
-
-bool GuiDataSource::InputDouble(const char* label, double* v, double step,
-                                double step_fast, const char* format,
-                                ImGuiInputTextFlags flags) const {
-  ImGui::PushID(label);
-  bool rv = ImGui::InputDouble("##input", v, step, step_fast, format, flags);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(label);
-  EmitDrag();
-  ImGui::PopID();
-  return rv;
-}
-
-bool GuiDataSource::InputInt(const char* label, int* v, int step, int step_fast,
-                             ImGuiInputTextFlags flags) const {
-  ImGui::PushID(label);
-  bool rv = ImGui::InputInt("##input", v, step, step_fast, flags);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(label);
-  EmitDrag();
-  ImGui::PopID();
-  return rv;
-}
-
-void GuiDataSource::EmitDrag(ImGuiDragDropFlags flags) const {
-  if (ImGui::BeginDragDropSource(flags)) {
-    auto self = this;
-    ImGui::SetDragDropPayload("DataSource", &self, sizeof(self));
-    ImGui::TextUnformatted(m_name.empty() ? m_id.c_str() : m_name.c_str());
-    ImGui::EndDragDropSource();
-  }
-}
-
-GuiDataSource* GuiDataSource::Find(wpi::StringRef id) {
-  auto it = gSources.find(id);
-  if (it == gSources.end()) return nullptr;
-  return it->getValue();
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp b/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp
new file mode 100644
index 0000000..72b9c29
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp
@@ -0,0 +1,105 @@
+// 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 "HALProvider.h"
+
+#include <glass/Model.h>
+
+#include <algorithm>
+#include <string>
+
+#include <hal/simulation/DriverStationData.h>
+
+using namespace halsimgui;
+
+static bool gDisableOutputsOnDSDisable = true;
+
+bool HALProvider::AreOutputsDisabled() {
+  return gDisableOutputsOnDSDisable && !HALSIM_GetDriverStationEnabled();
+}
+
+void HALProvider::DisplayMenu() {
+  ImGui::MenuItem("Disable outputs on DS disable", nullptr,
+                  &gDisableOutputsOnDSDisable, true);
+  ImGui::Separator();
+
+  for (auto&& viewEntry : m_viewEntries) {
+    bool visible = viewEntry->window && viewEntry->window->IsVisible();
+    bool wasVisible = visible;
+    bool exists = viewEntry->modelEntry->exists();
+    ImGui::MenuItem(viewEntry->name.c_str(), nullptr, &visible,
+                    visible || exists);
+    if (!wasVisible && visible) {
+      Show(viewEntry.get(), viewEntry->window);
+    } else if (wasVisible && !visible && viewEntry->window) {
+      viewEntry->window->SetVisible(false);
+    }
+  }
+}
+
+void HALProvider::Update() {
+  Provider::Update();
+
+  // 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 it = FindViewEntry(id);
+    if (it == m_viewEntries.end() || (*it)->name != id) {
+      continue;
+    }
+    Show(it->get(), window.get());
+  }
+}
+
+glass::Model* HALProvider::GetModel(std::string_view name) {
+  auto it = FindModelEntry(name);
+  if (it == m_modelEntries.end() || (*it)->name != name) {
+    return nullptr;
+  }
+  auto entry = it->get();
+
+  // get or create model
+  if (!entry->model) {
+    entry->model = entry->createModel();
+  }
+  return entry->model.get();
+}
+
+void HALProvider::Show(ViewEntry* entry, glass::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();
+  }
+  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;
+  }
+  entry->window = window;
+
+  // create view
+  auto view = entry->createView(window, entry->modelEntry->model.get());
+  if (!view) {
+    return;
+  }
+  window->SetView(std::move(view));
+
+  entry->window->SetVisible(true);
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp
index 9803494..8d66ac7 100644
--- a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp
@@ -1,343 +1,25 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimGui.h"
 
-#include <algorithm>
-
-#include <hal/simulation/DriverStationData.h>
 #include <imgui.h>
-#include <imgui_internal.h>
-#include <wpi/StringMap.h>
-#include <wpi/raw_ostream.h>
 #include <wpigui.h>
 
 using namespace halsimgui;
 
-namespace gui = wpi::gui;
+glass::MainMenuBar HALSimGui::mainMenu;
+glass::WindowManager HALSimGui::manager{"SimWindow"};
+HALProvider HALSimGui::halProvider{"HALProvider"};
+glass::NetworkTablesProvider HALSimGui::ntProvider{"NTProvider"};
 
-namespace {
-struct WindowInfo {
-  WindowInfo() = default;
-  explicit WindowInfo(const char* name_) : name{name_} {}
-  WindowInfo(const char* name_, std::function<void()> display_,
-             ImGuiWindowFlags flags_)
-      : name{name_}, display{std::move(display_)}, flags{flags_} {}
+void HALSimGui::GlobalInit() {
+  manager.GlobalInit();
+  halProvider.GlobalInit();
+  ntProvider.GlobalInit();
 
-  std::string name;
-  std::function<void()> display;
-  ImGuiWindowFlags flags = 0;
-  bool visible = true;
-  bool enabled = true;
-  ImGuiCond posCond = 0;
-  ImGuiCond sizeCond = 0;
-  ImVec2 pos;
-  ImVec2 size;
-  bool setPadding = false;
-  ImVec2 padding;
-};
-}  // namespace
+  wpi::gui::AddLateExecute([] { mainMenu.Display(); });
 
-static std::vector<WindowInfo> gWindows;
-static wpi::StringMap<int> gWindowMap;   // index into gWindows
-static std::vector<int> gSortedWindows;  // index into gWindows
-static std::vector<std::function<void()>> gOptionMenus;
-static std::vector<std::function<void()>> gMenus;
-static bool gDisableOutputsOnDSDisable = true;
-
-// read/write open state to ini file
-static void* SimWindowsReadOpen(ImGuiContext* ctx,
-                                ImGuiSettingsHandler* handler,
-                                const char* name) {
-  if (wpi::StringRef{name} == "GLOBAL") return &gDisableOutputsOnDSDisable;
-
-  int index = gWindowMap.try_emplace(name, gWindows.size()).first->second;
-  if (index == static_cast<int>(gWindows.size())) {
-    gSortedWindows.push_back(index);
-    gWindows.emplace_back(name);
-    std::sort(gSortedWindows.begin(), gSortedWindows.end(),
-              [](int a, int b) { return gWindows[a].name < gWindows[b].name; });
-  }
-  return &gWindows[index];
+  glass::AddStandardNetworkTablesViews(ntProvider);
 }
-
-static void SimWindowsReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                               void* entry, const char* lineStr) {
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-
-  if (entry == &gDisableOutputsOnDSDisable) {
-    int num;
-    if (value.getAsInteger(10, num)) return;
-    if (name == "disableOutputsOnDS") {
-      gDisableOutputsOnDSDisable = num;
-    }
-    return;
-  }
-
-  auto element = static_cast<WindowInfo*>(entry);
-  if (name == "visible") {
-    int num;
-    if (value.getAsInteger(10, num)) return;
-    element->visible = num;
-  } else if (name == "enabled") {
-    int num;
-    if (value.getAsInteger(10, num)) return;
-    element->enabled = num;
-  }
-}
-
-static void SimWindowsWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                               ImGuiTextBuffer* out_buf) {
-  out_buf->appendf("[SimWindow][GLOBAL]\ndisableOutputsOnDS=%d\n\n",
-                   gDisableOutputsOnDSDisable ? 1 : 0);
-  for (auto&& window : gWindows)
-    out_buf->appendf("[SimWindow][%s]\nvisible=%d\nenabled=%d\n\n",
-                     window.name.c_str(), window.visible ? 1 : 0,
-                     window.enabled ? 1 : 0);
-}
-
-void HALSimGui::Add(std::function<void()> initialize) {
-  gui::AddInit(std::move(initialize));
-}
-
-void HALSimGui::AddExecute(std::function<void()> execute) {
-  gui::AddEarlyExecute(std::move(execute));
-}
-
-void HALSimGui::AddWindow(const char* name, std::function<void()> display,
-                          int flags) {
-  if (display) {
-    int index = gWindowMap.try_emplace(name, gWindows.size()).first->second;
-    if (index < static_cast<int>(gWindows.size())) {
-      if (gWindows[index].display) {
-        wpi::errs() << "halsim_gui: ignoring duplicate window '" << name
-                    << "'\n";
-      } else {
-        gWindows[index].display = display;
-        gWindows[index].flags = flags;
-      }
-      return;
-    }
-    gSortedWindows.push_back(index);
-    gWindows.emplace_back(name, std::move(display),
-                          static_cast<ImGuiWindowFlags>(flags));
-    std::sort(gSortedWindows.begin(), gSortedWindows.end(),
-              [](int a, int b) { return gWindows[a].name < gWindows[b].name; });
-  }
-}
-
-void HALSimGui::AddMainMenu(std::function<void()> menu) {
-  if (menu) gMenus.emplace_back(std::move(menu));
-}
-
-void HALSimGui::AddOptionMenu(std::function<void()> menu) {
-  if (menu) gOptionMenus.emplace_back(std::move(menu));
-}
-
-void HALSimGui::SetWindowVisibility(const char* name,
-                                    WindowVisibility visibility) {
-  auto it = gWindowMap.find(name);
-  if (it == gWindowMap.end()) return;
-  auto& window = gWindows[it->second];
-  switch (visibility) {
-    case kHide:
-      window.visible = false;
-      window.enabled = true;
-      break;
-    case kShow:
-      window.visible = true;
-      window.enabled = true;
-      break;
-    case kDisabled:
-      window.enabled = false;
-      break;
-  }
-}
-
-void HALSimGui::SetDefaultWindowPos(const char* name, float x, float y) {
-  auto it = gWindowMap.find(name);
-  if (it == gWindowMap.end()) return;
-  auto& window = gWindows[it->second];
-  window.posCond = ImGuiCond_FirstUseEver;
-  window.pos = ImVec2{x, y};
-}
-
-void HALSimGui::SetDefaultWindowSize(const char* name, float width,
-                                     float height) {
-  auto it = gWindowMap.find(name);
-  if (it == gWindowMap.end()) return;
-  auto& window = gWindows[it->second];
-  window.sizeCond = ImGuiCond_FirstUseEver;
-  window.size = ImVec2{width, height};
-}
-
-void HALSimGui::SetWindowPadding(const char* name, float x, float y) {
-  auto it = gWindowMap.find(name);
-  if (it == gWindowMap.end()) return;
-  auto& window = gWindows[it->second];
-  window.setPadding = true;
-  window.padding = ImVec2{x, y};
-}
-
-bool HALSimGui::AreOutputsDisabled() {
-  return gDisableOutputsOnDSDisable && !HALSIM_GetDriverStationEnabled();
-}
-
-void HALSimGui::GlobalInit() { gui::CreateContext(); }
-
-bool HALSimGui::Initialize() {
-  gui::AddInit([] {
-    // Hook ini handler to save settings
-    ImGuiSettingsHandler iniHandler;
-    iniHandler.TypeName = "SimWindow";
-    iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
-    iniHandler.ReadOpenFn = SimWindowsReadOpen;
-    iniHandler.ReadLineFn = SimWindowsReadLine;
-    iniHandler.WriteAllFn = SimWindowsWriteAll;
-    ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-  });
-
-  gui::AddWindowScaler([](float windowScale) {
-    // scale default window positions
-    for (auto&& window : gWindows) {
-      if ((window.posCond & ImGuiCond_FirstUseEver) != 0) {
-        window.pos.x *= windowScale;
-        window.pos.y *= windowScale;
-        window.size.x *= windowScale;
-        window.size.y *= windowScale;
-      }
-    }
-  });
-
-  gui::AddLateExecute([] {
-    {
-      ImGui::BeginMainMenuBar();
-
-      if (ImGui::BeginMenu("Options")) {
-        ImGui::MenuItem("Disable outputs on DS disable", nullptr,
-                        &gDisableOutputsOnDSDisable, true);
-        for (auto&& menu : gOptionMenus) {
-          if (menu) menu();
-        }
-        ImGui::EndMenu();
-      }
-
-      gui::EmitViewMenu();
-
-      if (ImGui::BeginMenu("Window")) {
-        for (auto&& windowIndex : gSortedWindows) {
-          auto& window = gWindows[windowIndex];
-          ImGui::MenuItem(window.name.c_str(), nullptr, &window.visible,
-                          window.enabled);
-        }
-        ImGui::EndMenu();
-      }
-
-      for (auto&& menu : gMenus) {
-        if (menu) menu();
-      }
-
-#if 0
-      char str[64];
-      std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)",
-                    1000.0f / ImGui::GetIO().Framerate,
-                    ImGui::GetIO().Framerate);
-      ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x -
-                      10);
-      ImGui::Text("%s", str);
-#endif
-      ImGui::EndMainMenuBar();
-    }
-
-    for (auto&& window : gWindows) {
-      if (window.display && window.visible && window.enabled) {
-        if (window.posCond != 0)
-          ImGui::SetNextWindowPos(window.pos, window.posCond);
-        if (window.sizeCond != 0)
-          ImGui::SetNextWindowSize(window.size, window.sizeCond);
-        if (window.setPadding)
-          ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.padding);
-        if (ImGui::Begin(window.name.c_str(), &window.visible, window.flags))
-          window.display();
-        ImGui::End();
-        if (window.setPadding) ImGui::PopStyleVar();
-      }
-    }
-  });
-
-  if (!gui::Initialize("", 1280, 720)) return false;
-
-  return true;
-}
-
-void HALSimGui::Main(void*) {
-  gui::Main();
-  gui::DestroyContext();
-}
-
-void HALSimGui::Exit(void*) { gui::Exit(); }
-
-extern "C" {
-
-void HALSIMGUI_Add(void* param, void (*initialize)(void*)) {
-  if (initialize) {
-    HALSimGui::Add([=] { initialize(param); });
-  }
-}
-
-void HALSIMGUI_AddExecute(void* param, void (*execute)(void*)) {
-  if (execute) {
-    HALSimGui::AddExecute([=] { execute(param); });
-  }
-}
-
-void HALSIMGUI_AddWindow(const char* name, void* param, void (*display)(void*),
-                         int32_t flags) {
-  if (display) {
-    HALSimGui::AddWindow(
-        name, [=] { display(param); }, flags);
-  }
-}
-
-void HALSIMGUI_AddMainMenu(void* param, void (*menu)(void*)) {
-  if (menu) {
-    HALSimGui::AddMainMenu([=] { menu(param); });
-  }
-}
-
-void HALSIMGUI_AddOptionMenu(void* param, void (*menu)(void*)) {
-  if (menu) {
-    HALSimGui::AddOptionMenu([=] { menu(param); });
-  }
-}
-
-void HALSIMGUI_SetWindowVisibility(const char* name, int32_t visibility) {
-  HALSimGui::SetWindowVisibility(
-      name, static_cast<HALSimGui::WindowVisibility>(visibility));
-}
-
-void HALSIMGUI_SetDefaultWindowPos(const char* name, float x, float y) {
-  HALSimGui::SetDefaultWindowPos(name, x, y);
-}
-
-void HALSIMGUI_SetDefaultWindowSize(const char* name, float width,
-                                    float height) {
-  HALSimGui::SetDefaultWindowSize(name, width, height);
-}
-
-void HALSIMGUI_SetWindowPadding(const char* name, float x, float y) {
-  HALSimGui::SetDefaultWindowSize(name, x, y);
-}
-
-int HALSIMGUI_AreOutputsDisabled(void) {
-  return HALSimGui::AreOutputsDisabled();
-}
-
-}  // extern "C"
diff --git a/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp b/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp
deleted file mode 100644
index 6d01cd3..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp
+++ /dev/null
@@ -1,141 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "IniSaverInfo.h"
-
-#include <cstdio>
-#include <cstring>
-
-#include <imgui_internal.h>
-
-using namespace halsimgui;
-
-void NameInfo::GetName(char* buf, size_t size, const char* defaultName) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s", m_name);
-  } else {
-    std::snprintf(buf, size, "%s", defaultName);
-  }
-}
-
-void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
-                       int index) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s [%d]", m_name, index);
-  } else {
-    std::snprintf(buf, size, "%s[%d]", defaultName, index);
-  }
-}
-
-void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
-                       int index, int index2) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s [%d,%d]", m_name, index, index2);
-  } else {
-    std::snprintf(buf, size, "%s[%d,%d]", defaultName, index, index2);
-  }
-}
-
-void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s###Name%s", m_name, defaultName);
-  } else {
-    std::snprintf(buf, size, "%s###Name%s", defaultName, defaultName);
-  }
-}
-
-void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
-                        int index) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s [%d]###Name%d", m_name, index, index);
-  } else {
-    std::snprintf(buf, size, "%s[%d]###Name%d", defaultName, index, index);
-  }
-}
-
-void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
-                        int index, int index2) const {
-  if (m_name[0] != '\0') {
-    std::snprintf(buf, size, "%s [%d,%d]###Name%d", m_name, index, index2,
-                  index);
-  } else {
-    std::snprintf(buf, size, "%s[%d,%d]###Name%d", defaultName, index, index2,
-                  index);
-  }
-}
-
-bool NameInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name != "name") return false;
-  size_t len = (std::min)(value.size(), sizeof(m_name) - 1);
-  std::memcpy(m_name, value.data(), len);
-  m_name[len] = '\0';
-  return true;
-}
-
-void NameInfo::WriteIni(ImGuiTextBuffer* out) {
-  out->appendf("name=%s\n", m_name);
-}
-
-void NameInfo::PushEditNameId(int index) {
-  char id[64];
-  std::snprintf(id, sizeof(id), "Name%d", index);
-  ImGui::PushID(id);
-}
-
-void NameInfo::PushEditNameId(const char* name) {
-  char id[128];
-  std::snprintf(id, sizeof(id), "Name%s", name);
-  ImGui::PushID(id);
-}
-
-bool NameInfo::PopupEditName(int index) {
-  bool rv = false;
-  char id[64];
-  std::snprintf(id, sizeof(id), "Name%d", index);
-  if (ImGui::BeginPopupContextItem(id)) {
-    ImGui::Text("Edit name:");
-    if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) {
-      ImGui::CloseCurrentPopup();
-      rv = true;
-    }
-    if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
-    ImGui::EndPopup();
-  }
-  return rv;
-}
-
-bool NameInfo::PopupEditName(const char* name) {
-  bool rv = false;
-  char id[128];
-  std::snprintf(id, sizeof(id), "Name%s", name);
-  if (ImGui::BeginPopupContextItem(id)) {
-    ImGui::Text("Edit name:");
-    if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) {
-      ImGui::CloseCurrentPopup();
-      rv = true;
-    }
-    if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
-    ImGui::EndPopup();
-  }
-  return rv;
-}
-
-bool NameInfo::InputTextName(const char* label_id, ImGuiInputTextFlags flags) {
-  return ImGui::InputText(label_id, m_name, sizeof(m_name), flags);
-}
-
-bool OpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name != "open") return false;
-  int num;
-  if (value.getAsInteger(10, num)) return true;
-  m_open = num;
-  return true;
-}
-
-void OpenInfo::WriteIni(ImGuiTextBuffer* out) {
-  out->appendf("open=%d\n", m_open ? 1 : 0);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp b/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp
deleted file mode 100644
index 8203325..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp
+++ /dev/null
@@ -1,322 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "Mechanism2D.h"
-
-#include <cmath>
-#include <string>
-
-#include <hal/SimDevice.h>
-#include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
-
-#define IMGUI_DEFINE_MATH_OPERATORS
-#include <imgui_internal.h>
-#include <wpi/json.h>
-#include <wpi/math>
-#include <wpi/raw_istream.h>
-#include <wpi/raw_ostream.h>
-
-#include "HALSimGui.h"
-#include "portable-file-dialogs.h"
-
-using namespace halsimgui;
-
-static HAL_SimDeviceHandle devHandle = 0;
-static wpi::StringMap<ImColor> colorLookUpTable;
-static std::unique_ptr<pfd::open_file> m_fileOpener;
-static std::string previousJsonLocation = "Not empty";
-namespace {
-struct BodyConfig {
-  std::string name;
-  std::string type = "line";
-  int length = 100;
-  std::string color = "green";
-  int angle = 0;
-  std::vector<BodyConfig> children;
-  int lineWidth = 1;
-};
-}  // namespace
-static std::vector<BodyConfig> bodyConfigVector;
-namespace {
-struct DrawLineStruct {
-  float xEnd;
-  float yEnd;
-  float angle;
-};
-}  // namespace
-static struct NamedColor {
-  const char* name;
-  ImColor value;
-} staticColors[] = {{"white", IM_COL32(255, 255, 255, 255)},
-                    {"silver", IM_COL32(192, 192, 192, 255)},
-                    {"gray", IM_COL32(128, 128, 128, 255)},
-                    {"black", IM_COL32(0, 0, 0, 255)},
-                    {"red", IM_COL32(255, 0, 0, 255)},
-                    {"maroon", IM_COL32(128, 0, 0, 255)},
-                    {"yellow", IM_COL32(255, 255, 0, 255)},
-                    {"olive", IM_COL32(128, 128, 0, 255)},
-                    {"lime", IM_COL32(0, 255, 0, 255)},
-                    {"green", IM_COL32(0, 128, 0, 255)},
-                    {"aqua", IM_COL32(0, 255, 255, 255)},
-                    {"teal", IM_COL32(0, 128, 128, 255)},
-                    {"blue", IM_COL32(0, 0, 255, 255)},
-                    {"navy", IM_COL32(0, 0, 128, 255)},
-                    {"fuchsia", IM_COL32(255, 0, 255, 255)},
-                    {"purple", IM_COL32(128, 0, 128, 255)}};
-
-static void buildColorTable() {
-  for (auto&& namedColor : staticColors) {
-    colorLookUpTable.try_emplace(namedColor.name, namedColor.value);
-  }
-}
-namespace {
-class Mechanism2DInfo {
- public:
-  std::string jsonLocation;
-};
-}  // namespace
-
-static Mechanism2DInfo mechanism2DInfo;
-
-bool ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (name == "jsonLocation") {
-    mechanism2DInfo.jsonLocation = value;
-  } else {
-    return false;
-  }
-  return true;
-}
-
-void WriteIni(ImGuiTextBuffer* out) {
-  out->appendf("[Mechanism2D][Mechanism2D]\njsonLocation=%s\n\n",
-               mechanism2DInfo.jsonLocation.c_str());
-}
-
-// read/write settings to ini file
-static void* Mechanism2DReadOpen(ImGuiContext* ctx,
-                                 ImGuiSettingsHandler* handler,
-                                 const char* name) {
-  if (name == wpi::StringRef{"Mechanism2D"}) return &mechanism2DInfo;
-  return nullptr;
-}
-
-static void Mechanism2DReadLine(ImGuiContext* ctx,
-                                ImGuiSettingsHandler* handler, void* entry,
-                                const char* lineStr) {
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  if (entry == &mechanism2DInfo) ReadIni(name, value);
-}
-
-static void Mechanism2DWriteAll(ImGuiContext* ctx,
-                                ImGuiSettingsHandler* handler,
-                                ImGuiTextBuffer* out_buf) {
-  WriteIni(out_buf);
-}
-
-static void GetJsonFileLocation() {
-  if (m_fileOpener && m_fileOpener->ready(0)) {
-    auto result = m_fileOpener->result();
-    if (!result.empty()) {
-      mechanism2DInfo.jsonLocation = result[0];
-    } else {
-      wpi::errs() << "Can not find json file!!!";
-    }
-  }
-}
-
-DrawLineStruct DrawLine(float startXLocation, float startYLocation, int length,
-                        float angle, ImDrawList* drawList, ImVec2 windowPos,
-                        ImColor color, const BodyConfig& bodyConfig,
-                        const std::string& previousPath) {
-  DrawLineStruct drawLineStruct;
-  drawLineStruct.angle = angle;
-  // Find the current path do the ligament
-  std::string currentPath = previousPath + bodyConfig.name;
-  // Find the angle in radians
-  double radAngle = (drawLineStruct.angle - 90) * wpi::math::pi / 180;
-  // Get the start X and Y location
-  drawLineStruct.xEnd = startXLocation + length * std::cos(radAngle);
-  drawLineStruct.yEnd = startYLocation + length * std::sin(radAngle);
-  // Add the line to the drawList
-  drawList->AddLine(
-      windowPos + ImVec2(startXLocation, startYLocation),
-      windowPos + ImVec2(drawLineStruct.xEnd, drawLineStruct.yEnd), color,
-      bodyConfig.lineWidth);
-  // Return the end X, Y, and angle
-  return drawLineStruct;
-}
-
-static void buildDrawList(float startXLocation, float startYLocation,
-                          ImDrawList* drawList, float previousAngle,
-                          const std::vector<BodyConfig>& subBodyConfigs,
-                          ImVec2 windowPos) {
-  for (BodyConfig const& bodyConfig : subBodyConfigs) {
-    hal::SimDouble angleHandle;
-    hal::SimDouble lengthHandle;
-    float angle = 0;
-    float length = 0;
-    // Get the smallest of width or height
-    double minSize;
-    // Find the min size of the window
-    minSize = ImGui::GetWindowHeight() > ImGui::GetWindowWidth()
-                  ? ImGui::GetWindowWidth()
-                  : ImGui::GetWindowHeight();
-    if (devHandle == 0) devHandle = HALSIM_GetSimDeviceHandle("Mechanism2D");
-    // Get the length
-    if (!lengthHandle)
-      lengthHandle = HALSIM_GetSimValueHandle(
-          devHandle, (bodyConfig.name + "/length").c_str());
-    if (lengthHandle) length = lengthHandle.Get();
-    if (length <= 0) {
-      length = bodyConfig.length;
-    }
-    // Get the angle
-    if (!angleHandle)
-      angleHandle = HALSIM_GetSimValueHandle(
-          devHandle, (bodyConfig.name + "/angle").c_str());
-    if (angleHandle) angle = angleHandle.Get();
-    // Calculate the next angle to go to
-    float angleToGoTo = angle + bodyConfig.angle + previousAngle;
-    // Draw the first line and get the ending coordinates
-
-    DrawLineStruct drawLine =
-        DrawLine(startXLocation, startYLocation, minSize / 100 * length,
-                 angleToGoTo, drawList, windowPos,
-                 colorLookUpTable[bodyConfig.color], bodyConfig, "");
-
-    // If the line has children then draw them with the stating points being the
-    // end of the parent
-    if (!bodyConfig.children.empty()) {
-      buildDrawList(drawLine.xEnd, drawLine.yEnd, drawList, drawLine.angle,
-                    bodyConfig.children, windowPos);
-    }
-  }
-}
-
-static BodyConfig readSubJson(const std::string& name, wpi::json const& body) {
-  BodyConfig c;
-  try {
-    c.name = name + "/" + body.at("name").get<std::string>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "could not read body name: " << e.what() << '\n';
-  }
-  try {
-    c.length = body.at("length").get<int>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "length '" << c.name
-                << "': could not find length path: " << e.what() << '\n';
-  }
-  try {
-    c.color = body.at("color").get<std::string>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "color '" << c.name
-                << "': could not find color path: " << e.what() << '\n';
-  }
-  try {
-    c.angle = body.at("angle").get<int>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "angle '" << c.name
-                << "': could not find angle path: " << e.what() << '\n';
-  }
-  try {
-    c.lineWidth = body.at("lineWidth").get<int>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "lineWidth '" << c.name
-                << "': could not find lineWidth path: " << e.what() << '\n';
-  }
-  try {
-    for (wpi::json const& child : body.at("children")) {
-      c.children.push_back(readSubJson(c.name, child));
-      wpi::outs() << "Reading Child with name " << c.name << '\n';
-    }
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "could not read body: " << e.what() << '\n';
-  }
-  return c;
-}
-
-static void readJson(std::string jFile) {
-  std::error_code ec;
-  std::string name;
-  wpi::raw_fd_istream is(jFile, ec);
-  if (ec) {
-    wpi::errs() << "could not open '" << jFile << "': " << ec.message() << '\n';
-  }
-  // parse file
-  wpi::json j;
-  try {
-    j = wpi::json::parse(is);
-  } catch (const wpi::json::parse_error& e) {
-    wpi::errs() << "byte " << e.byte << ": " << e.what() << '\n';
-  }
-  // top level must be an object
-  if (!j.is_object()) {
-    wpi::errs() << "must be JSON object\n";
-  }
-  try {
-    name = j.at("name").get<std::string>();
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "name '" << name
-                << "': could not find name path: " << e.what() << '\n';
-  }
-  try {
-    for (wpi::json const& body : j.at("body")) {
-      bodyConfigVector.push_back(readSubJson(name, body));
-    }
-  } catch (const wpi::json::exception& e) {
-    wpi::errs() << "could not read body: " << e.what() << '\n';
-  }
-}
-
-static void OptionMenuLocateJson() {
-  if (ImGui::BeginMenu("Mechanism2D")) {
-    if (ImGui::MenuItem("Load Json")) {
-      m_fileOpener = std::make_unique<pfd::open_file>(
-          "Choose Mechanism2D json", "", std::vector<std::string>{"*.json"});
-    }
-    ImGui::EndMenu();
-  }
-}
-
-static void DisplayAssembly2D() {
-  GetJsonFileLocation();
-  if (!mechanism2DInfo.jsonLocation.empty()) {
-    // Only read the json file if it changed
-    if (mechanism2DInfo.jsonLocation != previousJsonLocation) {
-      bodyConfigVector.clear();
-      readJson(mechanism2DInfo.jsonLocation);
-    }
-    previousJsonLocation = mechanism2DInfo.jsonLocation;
-    ImVec2 windowPos = ImGui::GetWindowPos();
-    ImDrawList* drawList = ImGui::GetWindowDrawList();
-    buildDrawList(ImGui::GetWindowWidth() / 2, ImGui::GetWindowHeight(),
-                  drawList, 0, bodyConfigVector, windowPos);
-  }
-}
-
-void Mechanism2D::Initialize() {
-  // hook ini handler to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = "Mechanism2D";
-  iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
-  iniHandler.ReadOpenFn = Mechanism2DReadOpen;
-  iniHandler.ReadLineFn = Mechanism2DReadLine;
-  iniHandler.WriteAllFn = Mechanism2DWriteAll;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-
-  buildColorTable();
-  HALSimGui::AddWindow("Mechanism 2D", DisplayAssembly2D);
-  HALSimGui::SetWindowVisibility("Mechanism 2D", HALSimGui::kHide);
-  HALSimGui::AddOptionMenu(OptionMenuLocateJson);
-  HALSimGui::SetDefaultWindowPos("Mechanism 2D", 200, 200);
-  HALSimGui::SetDefaultWindowSize("Mechanism 2D", 600, 600);
-  HALSimGui::SetWindowPadding("Mechanism 2D", 0, 0);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.h b/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.h
deleted file mode 100644
index 2f291dd..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class Mechanism2D {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp
deleted file mode 100644
index 2d442b5..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp
+++ /dev/null
@@ -1,409 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "NetworkTablesGui.h"
-
-#include <cstdio>
-#include <cstring>
-#include <initializer_list>
-#include <memory>
-#include <vector>
-
-#include <imgui.h>
-#include <networktables/NetworkTableInstance.h>
-#include <networktables/NetworkTableValue.h>
-#include <ntcore_cpp.h>
-#include <wpi/DenseMap.h>
-#include <wpi/Format.h>
-#include <wpi/SmallString.h>
-#include <wpi/StringRef.h>
-#include <wpi/raw_ostream.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-
-using namespace halsimgui;
-
-static NT_EntryListenerPoller gNetworkTablesPoller;
-static wpi::DenseMap<NT_Entry, std::unique_ptr<GuiDataSource>>
-    gNetworkTableSources;
-
-static void UpdateNetworkTableSources() {
-  bool timedOut = false;
-  for (auto&& event :
-       nt::PollEntryListener(gNetworkTablesPoller, 0, &timedOut)) {
-    if (!event.value->IsBoolean() && !event.value->IsDouble()) continue;
-    if (event.flags & NT_NOTIFY_NEW) {
-      auto& source = gNetworkTableSources[event.entry];
-      if (!source)
-        source =
-            std::make_unique<GuiDataSource>(wpi::Twine{"NT:"} + event.name);
-    }
-    if (event.flags & NT_NOTIFY_DELETE) {
-      if (auto& source = gNetworkTableSources[event.entry]) source.reset();
-    }
-    if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) {
-      if (auto& source = gNetworkTableSources[event.entry]) {
-        if (event.value->IsBoolean()) {
-          source->SetValue(event.value->GetBoolean() ? 1 : 0);
-          source->SetDigital(true);
-        } else if (event.value->IsDouble()) {
-          source->SetValue(event.value->GetDouble());
-          source->SetDigital(false);
-        }
-      }
-    }
-  }
-}
-
-static void BooleanArrayToString(wpi::SmallVectorImpl<char>& out,
-                                 wpi::ArrayRef<int> in) {
-  out.clear();
-  wpi::raw_svector_ostream os{out};
-  os << '[';
-  bool first = true;
-  for (auto v : in) {
-    if (!first) os << ',';
-    first = false;
-    if (v)
-      os << "true";
-    else
-      os << "false";
-  }
-  os << ']';
-}
-
-static std::shared_ptr<nt::Value> StringToBooleanArray(wpi::StringRef in) {
-  in = in.trim();
-  if (in.empty())
-    return nt::NetworkTableValue::MakeBooleanArray(
-        std::initializer_list<bool>{});
-  if (in.front() == '[') in = in.drop_front();
-  if (in.back() == ']') in = in.drop_back();
-  in = in.trim();
-
-  wpi::SmallVector<wpi::StringRef, 16> inSplit;
-  wpi::SmallVector<int, 16> out;
-
-  in.split(inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    val = val.trim();
-    if (val.equals_lower("true")) {
-      out.emplace_back(1);
-    } else if (val.equals_lower("false")) {
-      out.emplace_back(0);
-    } else {
-      wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
-                  << "'\n";
-      return nullptr;
-    }
-  }
-
-  return nt::NetworkTableValue::MakeBooleanArray(out);
-}
-
-static void DoubleArrayToString(wpi::SmallVectorImpl<char>& out,
-                                wpi::ArrayRef<double> in) {
-  out.clear();
-  wpi::raw_svector_ostream os{out};
-  os << '[';
-  bool first = true;
-  for (auto v : in) {
-    if (!first) os << ',';
-    first = false;
-    os << wpi::format("%.6f", v);
-  }
-  os << ']';
-}
-
-static std::shared_ptr<nt::Value> StringToDoubleArray(wpi::StringRef in) {
-  in = in.trim();
-  if (in.empty())
-    return nt::NetworkTableValue::MakeBooleanArray(
-        std::initializer_list<bool>{});
-  if (in.front() == '[') in = in.drop_front();
-  if (in.back() == ']') in = in.drop_back();
-  in = in.trim();
-
-  wpi::SmallVector<wpi::StringRef, 16> inSplit;
-  wpi::SmallVector<double, 16> out;
-
-  in.split(inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    val = val.trim();
-    wpi::SmallString<32> valStr = val;
-    double d;
-    if (std::sscanf(valStr.c_str(), "%lf", &d) == 1) {
-      out.emplace_back(d);
-    } else {
-      wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
-                  << "'\n";
-      return nullptr;
-    }
-  }
-
-  return nt::NetworkTableValue::MakeDoubleArray(out);
-}
-
-static void StringArrayToString(wpi::SmallVectorImpl<char>& out,
-                                wpi::ArrayRef<std::string> in) {
-  out.clear();
-  wpi::raw_svector_ostream os{out};
-  os << '[';
-  bool first = true;
-  for (auto&& v : in) {
-    if (!first) os << ',';
-    first = false;
-    os << '"';
-    os.write_escaped(v);
-    os << '"';
-  }
-  os << ']';
-}
-
-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 wpi::StringRef UnescapeString(wpi::StringRef 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 wpi::StringRef{buf.data(), buf.size()};
-}
-
-static std::shared_ptr<nt::Value> StringToStringArray(wpi::StringRef in) {
-  in = in.trim();
-  if (in.empty())
-    return nt::NetworkTableValue::MakeStringArray(
-        std::initializer_list<std::string>{});
-  if (in.front() == '[') in = in.drop_front();
-  if (in.back() == ']') in = in.drop_back();
-  in = in.trim();
-
-  wpi::SmallVector<wpi::StringRef, 16> inSplit;
-  std::vector<std::string> out;
-  wpi::SmallString<32> buf;
-
-  in.split(inSplit, ',', -1, false);
-  for (auto val : inSplit) {
-    val = val.trim();
-    if (val.empty()) continue;
-    if (val.front() != '"' || val.back() != '"') {
-      wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
-                  << "'\n";
-      return nullptr;
-    }
-    out.emplace_back(UnescapeString(val, buf));
-  }
-
-  return nt::NetworkTableValue::MakeStringArray(std::move(out));
-}
-
-static constexpr size_t kTextBufferSize = 4096;
-
-static char* GetTextBuffer(wpi::StringRef 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 DisplayNetworkTables() {
-  static auto inst = nt::NetworkTableInstance::GetDefault();
-
-  if (ImGui::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 : inst.GetConnections()) {
-      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 (ImGui::CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) {
-    static bool first = true;
-    ImGui::Columns(4, "values");
-    if (first) ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth());
-    ImGui::Text("Name");
-    ImGui::NextColumn();
-    ImGui::Text("Value");
-    ImGui::NextColumn();
-    if (first) ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize());
-    ImGui::Text("Flags");
-    ImGui::NextColumn();
-    ImGui::Text("Changed");
-    ImGui::NextColumn();
-    ImGui::Separator();
-    first = false;
-
-    auto info = inst.GetEntryInfo("", 0);
-    std::sort(info.begin(), info.end(),
-              [](const auto& a, const auto& b) { return a.name < b.name; });
-
-    for (auto&& i : info) {
-      if (auto source = gNetworkTableSources[i.entry].get()) {
-        ImGui::Selectable(i.name.c_str());
-        source->EmitDrag();
-      } else {
-        ImGui::Text("%s", i.name.c_str());
-      }
-      ImGui::NextColumn();
-
-      if (auto val = nt::GetEntryValue(i.entry)) {
-        ImGui::PushID(i.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(i.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(i.entry, nt::NetworkTableValue::MakeDouble(v));
-            break;
-          }
-          case NT_STRING: {
-            char* v = GetTextBuffer(val->GetString());
-            if (ImGui::InputText("string", v, kTextBufferSize,
-                                 ImGuiInputTextFlags_EnterReturnsTrue))
-              nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeString(v));
-            break;
-          }
-          case NT_BOOLEAN_ARRAY: {
-            wpi::SmallString<64> buf;
-            BooleanArrayToString(buf, val->GetBooleanArray());
-            char* v = GetTextBuffer(buf);
-            if (ImGui::InputText("boolean[]", v, kTextBufferSize,
-                                 ImGuiInputTextFlags_EnterReturnsTrue)) {
-              if (auto outv = StringToBooleanArray(v))
-                nt::SetEntryValue(i.entry, std::move(outv));
-            }
-            break;
-          }
-          case NT_DOUBLE_ARRAY: {
-            wpi::SmallString<64> buf;
-            DoubleArrayToString(buf, val->GetDoubleArray());
-            char* v = GetTextBuffer(buf);
-            if (ImGui::InputText("double[]", v, kTextBufferSize,
-                                 ImGuiInputTextFlags_EnterReturnsTrue)) {
-              if (auto outv = StringToDoubleArray(v))
-                nt::SetEntryValue(i.entry, std::move(outv));
-            }
-            break;
-          }
-          case NT_STRING_ARRAY: {
-            wpi::SmallString<64> buf;
-            StringArrayToString(buf, val->GetStringArray());
-            char* v = GetTextBuffer(buf);
-            if (ImGui::InputText("string[]", v, kTextBufferSize,
-                                 ImGuiInputTextFlags_EnterReturnsTrue)) {
-              if (auto outv = StringToStringArray(v))
-                nt::SetEntryValue(i.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();
-      }
-      ImGui::NextColumn();
-
-      if ((i.flags & NT_PERSISTENT) != 0)
-        ImGui::Text("Persistent");
-      else if (i.flags != 0)
-        ImGui::Text("%02x", i.flags);
-      ImGui::NextColumn();
-
-      ImGui::Text("%llu",
-                  static_cast<unsigned long long>(  // NOLINT(runtime/int)
-                      i.last_change));
-      ImGui::NextColumn();
-      ImGui::Separator();
-    }
-    ImGui::Columns();
-  }
-}
-
-void NetworkTablesGui::Initialize() {
-  gNetworkTablesPoller =
-      nt::CreateEntryListenerPoller(nt::GetDefaultInstance());
-  nt::AddPolledEntryListener(gNetworkTablesPoller, "",
-                             NT_NOTIFY_LOCAL | NT_NOTIFY_NEW |
-                                 NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
-                                 NT_NOTIFY_IMMEDIATE);
-  HALSimGui::AddExecute(UpdateNetworkTableSources);
-  HALSimGui::AddWindow("NetworkTables", DisplayNetworkTables);
-  HALSimGui::SetDefaultWindowPos("NetworkTables", 250, 277);
-  HALSimGui::SetDefaultWindowSize("NetworkTables", 750, 185);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h
deleted file mode 100644
index f4e5d7b..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class NetworkTablesGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp
new file mode 100644
index 0000000..c17a4f8
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp
@@ -0,0 +1,37 @@
+// 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 "NetworkTablesSimGui.h"
+
+#include <glass/networktables/NetworkTables.h>
+
+#include <wpigui.h>
+
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+static std::unique_ptr<glass::NetworkTablesModel> gNetworkTablesModel;
+static std::unique_ptr<glass::NetworkTablesView> gNetworkTablesView;
+static glass::Window* gNetworkTablesWindow;
+
+void NetworkTablesSimGui::Initialize() {
+  gNetworkTablesModel = std::make_unique<glass::NetworkTablesModel>();
+  gNetworkTablesView =
+      std::make_unique<glass::NetworkTablesView>(gNetworkTablesModel.get());
+  wpi::gui::AddEarlyExecute([] { gNetworkTablesModel->Update(); });
+  gNetworkTablesWindow = HALSimGui::ntProvider.AddWindow(
+      "NetworkTables", [] { gNetworkTablesView->Display(); });
+  if (gNetworkTablesWindow) {
+    gNetworkTablesWindow->SetDefaultPos(250, 277);
+    gNetworkTablesWindow->SetDefaultSize(750, 185);
+    gNetworkTablesWindow->DisableRenamePopup();
+  }
+}
+
+void NetworkTablesSimGui::DisplayMenu() {
+  if (gNetworkTablesWindow) {
+    gNetworkTablesWindow->DisplayMenuItem("NetworkTables View");
+  }
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h
new file mode 100644
index 0000000..d0d02b3
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h
@@ -0,0 +1,15 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+namespace halsimgui {
+
+class NetworkTablesSimGui {
+ public:
+  static void Initialize();
+  static void DisplayMenu();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp
new file mode 100644
index 0000000..bcc510a
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp
@@ -0,0 +1,231 @@
+// 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 "PCMSimGui.h"
+
+#include <glass/hardware/PCM.h>
+#include <glass/other/DeviceTree.h>
+
+#include <cstdio>
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/Value.h>
+#include <hal/simulation/CTREPCMData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+#include "SimDeviceGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(CTREPCMCompressorOn, "Compressor On");
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(CTREPCMClosedLoopEnabled, "Closed Loop");
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(CTREPCMPressureSwitch, "Pressure Switch");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(CTREPCMCompressorCurrent, "Comp Current");
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED2(CTREPCMSolenoidOutput, "Solenoid");
+
+class CompressorSimModel : public glass::CompressorModel {
+ public:
+  explicit CompressorSimModel(int32_t index)
+      : m_index{index},
+        m_running{index},
+        m_enabled{index},
+        m_pressureSwitch{index},
+        m_current{index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetCTREPCMInitialized(m_index); }
+
+  glass::DataSource* GetRunningData() override { return &m_running; }
+  glass::DataSource* GetEnabledData() override { return &m_enabled; }
+  glass::DataSource* GetPressureSwitchData() override {
+    return &m_pressureSwitch;
+  }
+  glass::DataSource* GetCurrentData() override { return &m_current; }
+
+  void SetRunning(bool val) override {
+    HALSIM_SetCTREPCMCompressorOn(m_index, val);
+  }
+  void SetEnabled(bool val) override {
+    HALSIM_SetCTREPCMClosedLoopEnabled(m_index, val);
+  }
+  void SetPressureSwitch(bool val) override {
+    HALSIM_SetCTREPCMPressureSwitch(m_index, val);
+  }
+  void SetCurrent(double val) override {
+    HALSIM_SetCTREPCMCompressorCurrent(m_index, val);
+  }
+
+ private:
+  int32_t m_index;
+  CTREPCMCompressorOnSource m_running;
+  CTREPCMClosedLoopEnabledSource m_enabled;
+  CTREPCMPressureSwitchSource m_pressureSwitch;
+  CTREPCMCompressorCurrentSource m_current;
+};
+
+class SolenoidSimModel : public glass::SolenoidModel {
+ public:
+  SolenoidSimModel(int32_t index, int32_t channel)
+      : m_index{index}, m_channel{channel}, m_output{index, channel} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetCTREPCMInitialized(m_index); }
+
+  glass::DataSource* GetOutputData() override { return &m_output; }
+
+  void SetOutput(bool val) override {
+    HALSIM_SetCTREPCMSolenoidOutput(m_index, m_channel, val);
+  }
+
+ private:
+  int32_t m_index;
+  int32_t m_channel;
+  CTREPCMSolenoidOutputSource m_output;
+};
+
+class PCMSimModel : public glass::PCMModel {
+ public:
+  explicit PCMSimModel(int32_t index)
+      : m_index{index},
+        m_compressor{index},
+        m_solenoids(HAL_GetNumCTRESolenoidChannels()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  CompressorSimModel* GetCompressor() override { return &m_compressor; }
+
+  void ForEachSolenoid(
+      wpi::function_ref<void(glass::SolenoidModel& model, int index)> func)
+      override;
+
+  int GetNumSolenoids() const { return m_solenoidInitCount; }
+
+ private:
+  int32_t m_index;
+  CompressorSimModel m_compressor;
+  std::vector<std::unique_ptr<SolenoidSimModel>> m_solenoids;
+  int m_solenoidInitCount = 0;
+};
+
+class PCMsSimModel : public glass::PCMsModel {
+ public:
+  PCMsSimModel() : m_models(HAL_GetNumCTREPCMModules()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachPCM(
+      wpi::function_ref<void(glass::PCMModel& model, int index)> func) override;
+
+ private:
+  std::vector<std::unique_ptr<PCMSimModel>> m_models;
+};
+}  // namespace
+
+void PCMSimModel::Update() {
+  int32_t numChannels = m_solenoids.size();
+  m_solenoidInitCount = 0;
+  for (int32_t i = 0; i < numChannels; ++i) {
+    auto& model = m_solenoids[i];
+    if (HALSIM_GetCTREPCMInitialized(m_index)) {
+      if (!model) {
+        model = std::make_unique<SolenoidSimModel>(m_index, i);
+      }
+      ++m_solenoidInitCount;
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void PCMSimModel::ForEachSolenoid(
+    wpi::function_ref<void(glass::SolenoidModel& model, int index)> func) {
+  if (m_solenoidInitCount == 0) {
+    return;
+  }
+  int32_t numSolenoids = m_solenoids.size();
+  for (int32_t i = 0; i < numSolenoids; ++i) {
+    if (auto model = m_solenoids[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+void PCMsSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetCTREPCMInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<PCMSimModel>(i);
+      }
+      model->Update();
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void PCMsSimModel::ForEachPCM(
+    wpi::function_ref<void(glass::PCMModel& model, int index)> func) {
+  int32_t numCTREPCMs = m_models.size();
+  for (int32_t i = 0; i < numCTREPCMs; ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool PCMsAnyInitialized() {
+  static const int32_t num = HAL_GetNumCTREPCMModules();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetCTREPCMInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void PCMSimGui::Initialize() {
+  HALSimGui::halProvider.RegisterModel("CTREPCMs", PCMsAnyInitialized, [] {
+    return std::make_unique<PCMsSimModel>();
+  });
+  HALSimGui::halProvider.RegisterView(
+      "Solenoids", "CTREPCMs",
+      [](glass::Model* model) {
+        bool any = false;
+        static_cast<PCMsSimModel*>(model)->ForEachPCM(
+            [&](glass::PCMModel& CTREPCM, int) {
+              if (static_cast<PCMSimModel*>(&CTREPCM)->GetNumSolenoids() > 0) {
+                any = true;
+              }
+            });
+        return any;
+      },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(290, 20);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayPCMsSolenoids(
+              static_cast<PCMsSimModel*>(model),
+              HALSimGui::halProvider.AreOutputsEnabled());
+        });
+      });
+
+  SimDeviceGui::GetDeviceTree().Add(
+      HALSimGui::halProvider.GetModel("CTREPCMs"), [](glass::Model* model) {
+        glass::DisplayCompressorsDevice(
+            static_cast<PCMsSimModel*>(model),
+            HALSimGui::halProvider.AreOutputsEnabled());
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h
new file mode 100644
index 0000000..aef7717
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class PCMSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp
deleted file mode 100644
index 35ba492..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp
+++ /dev/null
@@ -1,141 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "PDPGui.h"
-
-#include <algorithm>
-#include <cstdio>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/PDPData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPTemperature, "PDP Temp");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPVoltage, "PDP Voltage");
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED2(PDPCurrent, "PDP Current");
-struct PDPSource {
-  explicit PDPSource(int32_t index) : temp{index}, voltage{index} {
-    const int numChannels = HAL_GetNumPDPChannels();
-    currents.reserve(numChannels);
-    for (int i = 0; i < numChannels; ++i)
-      currents.emplace_back(std::make_unique<PDPCurrentSource>(index, i));
-  }
-  PDPTemperatureSource temp;
-  PDPVoltageSource voltage;
-  std::vector<std::unique_ptr<PDPCurrentSource>> currents;
-};
-}  // namespace
-
-static IniSaver<NameInfo> gChannels{"PDP"};
-static std::vector<std::unique_ptr<PDPSource>> gPDPSources;
-
-static void UpdatePDPSources() {
-  for (int i = 0, iend = gPDPSources.size(); i < iend; ++i) {
-    auto& source = gPDPSources[i];
-    if (HALSIM_GetPDPInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<PDPSource>(i);
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayPDP() {
-  bool hasAny = false;
-  for (int i = 0, iend = gPDPSources.size(); i < iend; ++i) {
-    if (auto source = gPDPSources[i].get()) {
-      hasAny = true;
-
-      char name[128];
-      std::snprintf(name, sizeof(name), "PDP[%d]", i);
-      if (ImGui::CollapsingHeader(name, ImGuiTreeNodeFlags_DefaultOpen)) {
-        ImGui::PushID(i);
-
-        // temperature
-        double temp = source->temp.GetValue();
-        ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-        if (source->temp.InputDouble("Temp", &temp, 0, 0, "%.3f"))
-          HALSIM_SetPDPTemperature(i, temp);
-
-        // voltage
-        double volts = source->voltage.GetValue();
-        ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-        if (source->voltage.InputDouble("Voltage", &volts, 0, 0, "%.3f"))
-          HALSIM_SetPDPVoltage(i, volts);
-
-        // channel currents; show as two columns laid out like PDP
-        const int numChannels = source->currents.size();
-        ImGui::Text("Channel Current (A)");
-        ImGui::Columns(2, "channels", false);
-        float maxWidth = ImGui::GetFontSize() * 13;
-        for (int left = 0, right = numChannels - 1; left < right;
-             ++left, --right) {
-          double val;
-
-          ImGui::PushID(left);
-          auto& leftInfo = gChannels[i * numChannels + left];
-          leftInfo.GetLabel(name, sizeof(name), "", left);
-          val = source->currents[left]->GetValue();
-          ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-          if (source->currents[left]->InputDouble(name, &val, 0, 0, "%.3f"))
-            HALSIM_SetPDPCurrent(i, left, val);
-          float leftWidth = ImGui::GetItemRectSize().x;
-          if (leftInfo.PopupEditName(left)) {
-            source->currents[left]->SetName(leftInfo.GetName());
-          }
-          ImGui::PopID();
-          ImGui::NextColumn();
-
-          ImGui::PushID(right);
-          auto& rightInfo = gChannels[i * numChannels + right];
-          rightInfo.GetLabel(name, sizeof(name), "", right);
-          val = source->currents[right]->GetValue();
-          ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-          if (source->currents[right]->InputDouble(name, &val, 0, 0, "%.3f"))
-            HALSIM_SetPDPCurrent(i, right, val);
-          float rightWidth = ImGui::GetItemRectSize().x;
-          if (rightInfo.PopupEditName(right)) {
-            source->currents[right]->SetName(rightInfo.GetName());
-          }
-          ImGui::PopID();
-          ImGui::NextColumn();
-
-          float width =
-              (std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4;
-          if (width > maxWidth) maxWidth = width;
-        }
-        ImGui::Columns(1);
-        ImGui::Dummy(ImVec2(maxWidth, 0));
-        ImGui::PopID();
-      }
-    }
-  }
-  if (!hasAny) ImGui::Text("No PDPs");
-}
-
-void PDPGui::Initialize() {
-  gChannels.Initialize();
-  gPDPSources.resize(HAL_GetNumPDPModules());
-  HALSimGui::AddExecute(UpdatePDPSources);
-  HALSimGui::AddWindow("PDP", DisplayPDP);
-  // hide it by default
-  HALSimGui::SetWindowVisibility("PDP", HALSimGui::kHide);
-  HALSimGui::SetDefaultWindowPos("PDP", 245, 155);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPGui.h b/simulation/halsim_gui/src/main/native/cpp/PDPGui.h
deleted file mode 100644
index aa53a3c..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class PDPGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp
deleted file mode 100644
index 59d7a93..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "PWMGui.h"
-
-#include <cstdio>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/AddressableLEDData.h>
-#include <hal/simulation/PWMData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PWMSpeed, "PWM");
-}  // namespace
-
-static IniSaver<NameInfo> gPWM{"PWM"};
-static std::vector<std::unique_ptr<PWMSpeedSource>> gPWMSources;
-
-static void UpdatePWMSources() {
-  static const int numPWM = HAL_GetNumPWMChannels();
-  if (static_cast<size_t>(numPWM) != gPWMSources.size())
-    gPWMSources.resize(numPWM);
-
-  for (int i = 0; i < numPWM; ++i) {
-    auto& source = gPWMSources[i];
-    if (HALSIM_GetPWMInitialized(i)) {
-      if (!source) {
-        source = std::make_unique<PWMSpeedSource>(i);
-        source->SetName(gPWM[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayPWMs() {
-  bool hasOutputs = false;
-  static const int numPWM = HAL_GetNumPWMChannels();
-  static const int numLED = HAL_GetNumAddressableLEDs();
-  static auto ledMap = std::make_unique<int[]>(numPWM);
-
-  std::memset(ledMap.get(), 0, numPWM * sizeof(ledMap[0]));
-
-  for (int i = 0; i < numLED; ++i) {
-    if (HALSIM_GetAddressableLEDInitialized(i)) {
-      int channel = HALSIM_GetAddressableLEDOutputPort(i);
-      if (channel >= 0 && channel < numPWM) ledMap[channel] = i + 1;
-    }
-  }
-
-  bool first = true;
-  ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
-  for (int i = 0; i < numPWM; ++i) {
-    if (auto source = gPWMSources[i].get()) {
-      ImGui::PushID(i);
-      hasOutputs = true;
-
-      if (!first)
-        ImGui::Separator();
-      else
-        first = false;
-
-      auto& info = gPWM[i];
-      char label[128];
-      info.GetLabel(label, sizeof(label), "PWM", i);
-      if (ledMap[i] > 0) {
-        ImGui::LabelText(label, "LED[%d]", ledMap[i] - 1);
-      } else {
-        float val = HALSimGui::AreOutputsDisabled() ? 0 : HALSIM_GetPWMSpeed(i);
-        source->LabelText(label, "%0.3f", val);
-      }
-      if (info.PopupEditName(i)) {
-        source->SetName(info.GetName());
-      }
-      ImGui::PopID();
-    }
-  }
-  ImGui::PopItemWidth();
-  if (!hasOutputs) ImGui::Text("No PWM outputs");
-}
-
-void PWMGui::Initialize() {
-  gPWM.Initialize();
-  HALSimGui::AddExecute(UpdatePWMSources);
-  HALSimGui::AddWindow("PWM Outputs", DisplayPWMs,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("PWM Outputs", 910, 20);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMGui.h b/simulation/halsim_gui/src/main/native/cpp/PWMGui.h
deleted file mode 100644
index 211eaba..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PWMGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class PWMGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp
new file mode 100644
index 0000000..3cb5e1a
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp
@@ -0,0 +1,119 @@
+// 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 "PWMSimGui.h"
+
+#include <glass/hardware/PWM.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/AddressableLEDData.h>
+#include <hal/simulation/PWMData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PWMSpeed, "PWM");
+
+class PWMSimModel : public glass::PWMModel {
+ public:
+  explicit PWMSimModel(int32_t index) : m_index{index}, m_speed{m_index} {}
+
+  void Update() override {}
+
+  bool Exists() override { return HALSIM_GetPWMInitialized(m_index); }
+
+  void SetAddressableLED(int led) { m_led = led; }
+  int GetAddressableLED() const override { return m_led; }
+
+  glass::DataSource* GetSpeedData() override { return &m_speed; }
+
+  void SetSpeed(double val) override { HALSIM_SetPWMSpeed(m_index, val); }
+
+ private:
+  int32_t m_index;
+  int m_led = -1;
+  PWMSpeedSource m_speed;
+};
+
+class PWMsSimModel : public glass::PWMsModel {
+ public:
+  PWMsSimModel() : m_sources(HAL_GetNumPWMChannels()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachPWM(
+      wpi::function_ref<void(glass::PWMModel& model, int index)> func) override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<PWMSimModel>> m_sources;
+};
+}  // namespace
+
+void PWMsSimModel::Update() {
+  const int32_t numPWM = m_sources.size();
+  for (int32_t i = 0; i < numPWM; ++i) {
+    auto& model = m_sources[i];
+    if (HALSIM_GetPWMInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<PWMSimModel>(i);
+      }
+      model->SetAddressableLED(-1);
+    } else {
+      model.reset();
+    }
+  }
+
+  static const int32_t numLED = HAL_GetNumAddressableLEDs();
+  for (int32_t i = 0; i < numLED; ++i) {
+    if (HALSIM_GetAddressableLEDInitialized(i)) {
+      int32_t channel = HALSIM_GetAddressableLEDOutputPort(i);
+      if (channel >= 0 && channel < numPWM && m_sources[channel]) {
+        m_sources[channel]->SetAddressableLED(i);
+      }
+    }
+  }
+}
+
+void PWMsSimModel::ForEachPWM(
+    wpi::function_ref<void(glass::PWMModel& model, int index)> func) {
+  const int32_t numPWM = m_sources.size();
+  for (int32_t i = 0; i < numPWM; ++i) {
+    if (auto model = m_sources[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool PWMsAnyInitialized() {
+  static const int32_t num = HAL_GetNumPWMChannels();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetPWMInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void PWMSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "PWM Outputs", PWMsAnyInitialized,
+      [] { return std::make_unique<PWMsSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(910, 20);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayPWMs(static_cast<PWMsSimModel*>(model),
+                             HALSimGui::halProvider.AreOutputsEnabled());
+        });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h
new file mode 100644
index 0000000..d947643
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class PWMSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
deleted file mode 100644
index ecaec63..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
+++ /dev/null
@@ -1,894 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "PlotGui.h"
-
-#include <algorithm>
-#include <atomic>
-#include <cstdio>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#define IMGUI_DEFINE_MATH_OPERATORS
-#include <hal/simulation/MockHooks.h>
-#include <imgui.h>
-#include <imgui_internal.h>
-#include <implot.h>
-#include <wpi/Signal.h>
-#include <wpi/SmallVector.h>
-#include <wpi/timestamp.h>
-#include <wpi/raw_ostream.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaverInfo.h"
-#include "IniSaverVector.h"
-
-using namespace halsimgui;
-
-namespace {
-struct PlotSeriesRef {
-  size_t plotIndex;
-  size_t seriesIndex;
-};
-
-class PlotSeries : public NameInfo, public OpenInfo {
- public:
-  explicit PlotSeries(wpi::StringRef id);
-  explicit PlotSeries(GuiDataSource* source, int yAxis = 0);
-
-  const std::string& GetId() const { return m_id; }
-
-  void CheckSource();
-
-  void SetSource(GuiDataSource* source);
-  GuiDataSource* GetSource() const { return m_source; }
-
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out);
-
-  bool EmitPlot(double now, size_t i, size_t plotIndex);
-  bool EmitSettings(size_t i, size_t plotIndex);
-  bool EmitSettingsDetail(size_t i);
-  void EmitDragDropPayload(size_t i, size_t plotIndex);
-
-  void GetLabel(char* buf, size_t size) const;
-
-  int GetYAxis() const { return m_yAxis; }
-  void SetYAxis(int yAxis) { m_yAxis = yAxis; }
-
- private:
-  bool IsDigital() const {
-    return m_digital == kDigital ||
-           (m_digital == kAuto && m_source && m_source->IsDigital());
-  }
-  void AppendValue(double value);
-
-  // source linkage
-  GuiDataSource* m_source = nullptr;
-  wpi::sig::ScopedConnection m_sourceCreatedConn;
-  wpi::sig::ScopedConnection m_newValueConn;
-  std::string m_id;
-
-  // user settings
-  int m_yAxis = 0;
-  ImVec4 m_color = IMPLOT_AUTO_COL;
-  int m_marker = 0;
-
-  enum Digital { kAuto, kDigital, kAnalog };
-  int m_digital = 0;
-  int m_digitalBitHeight = 8;
-  int m_digitalBitGap = 4;
-
-  // value storage
-  static constexpr int kMaxSize = 2000;
-  static constexpr double kTimeGap = 0.05;
-  std::atomic<int> m_size = 0;
-  std::atomic<int> m_offset = 0;
-  ImPlotPoint m_data[kMaxSize];
-};
-
-class Plot : public NameInfo, public OpenInfo {
- public:
-  Plot();
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out);
-
-  void GetLabel(char* buf, size_t size, int index) const;
-  void GetName(char* buf, size_t size, int index) const;
-
-  void DragDropTarget(size_t i, bool inPlot);
-  void EmitPlot(double now, size_t i);
-  void EmitSettings(size_t i);
-
-  std::vector<std::unique_ptr<PlotSeries>> m_series;
-
- private:
-  void EmitSettingsLimits(int axis);
-
-  bool m_visible = true;
-  unsigned int m_plotFlags = ImPlotFlags_Default;
-  bool m_lockPrevX = false;
-  bool m_paused = false;
-  float m_viewTime = 10;
-  int m_height = 300;
-  struct PlotRange {
-    double min = 0;
-    double max = 1;
-    bool lockMin = false;
-    bool lockMax = false;
-    bool apply = false;
-  };
-  PlotRange m_axisRange[3];
-  ImPlotRange m_xaxisRange;  // read from plot, used for lockPrevX
-};
-}  // namespace
-
-static IniSaverVector<Plot> gPlots{"Plot"};
-
-PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) {
-  if (GuiDataSource* source = GuiDataSource::Find(id)) {
-    SetSource(source);
-    return;
-  }
-  CheckSource();
-}
-
-PlotSeries::PlotSeries(GuiDataSource* source, int yAxis) : m_yAxis(yAxis) {
-  SetSource(source);
-}
-
-void PlotSeries::CheckSource() {
-  if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
-    m_source = nullptr;
-    m_sourceCreatedConn = GuiDataSource::sourceCreated.connect_connection(
-        [this](const char* id, GuiDataSource* source) {
-          if (m_id == id) {
-            SetSource(source);
-            m_sourceCreatedConn.disconnect();
-          }
-        });
-  }
-}
-
-void PlotSeries::SetSource(GuiDataSource* source) {
-  m_source = source;
-  m_id = source->GetId();
-
-  // add initial value
-  m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
-
-  m_newValueConn = source->valueChanged.connect_connection(
-      [this](double value) { AppendValue(value); });
-}
-
-void PlotSeries::AppendValue(double value) {
-  double time = wpi::Now() * 1.0e-6;
-  if (IsDigital()) {
-    if (m_size < kMaxSize) {
-      m_data[m_size] = ImPlotPoint{time, value};
-      ++m_size;
-    } else {
-      m_data[m_offset] = ImPlotPoint{time, value};
-      m_offset = (m_offset + 1) % kMaxSize;
-    }
-  } else {
-    // as an analog graph draws linear lines in between each value,
-    // insert duplicate value if "long" time between updates so it
-    // looks appropriately flat
-    if (m_size < kMaxSize) {
-      if (m_size > 0) {
-        if ((time - m_data[m_size - 1].x) > kTimeGap) {
-          m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y};
-          ++m_size;
-        }
-      }
-      m_data[m_size] = ImPlotPoint{time, value};
-      ++m_size;
-    } else {
-      if (m_offset == 0) {
-        if ((time - m_data[kMaxSize - 1].x) > kTimeGap) {
-          m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y};
-          ++m_offset;
-        }
-      } else {
-        if ((time - m_data[m_offset - 1].x) > kTimeGap) {
-          m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y};
-          m_offset = (m_offset + 1) % kMaxSize;
-        }
-      }
-      m_data[m_offset] = ImPlotPoint{time, value};
-      m_offset = (m_offset + 1) % kMaxSize;
-    }
-  }
-}
-
-bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (NameInfo::ReadIni(name, value)) return true;
-  if (OpenInfo::ReadIni(name, value)) return true;
-  if (name == "yAxis") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_yAxis = num;
-    return true;
-  } else if (name == "color") {
-    unsigned int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_color = ImColor(num);
-    return true;
-  } else if (name == "marker") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_marker = num;
-    return true;
-  } else if (name == "digital") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_digital = num;
-    return true;
-  } else if (name == "digitalBitHeight") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_digitalBitHeight = num;
-    return true;
-  } else if (name == "digitalBitGap") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_digitalBitGap = num;
-    return true;
-  }
-  return false;
-}
-
-void PlotSeries::WriteIni(ImGuiTextBuffer* out) {
-  NameInfo::WriteIni(out);
-  OpenInfo::WriteIni(out);
-  out->appendf(
-      "yAxis=%d\ncolor=%u\nmarker=%d\ndigital=%d\n"
-      "digitalBitHeight=%d\ndigitalBitGap=%d\n",
-      m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker, m_digital,
-      m_digitalBitHeight, m_digitalBitGap);
-}
-
-void PlotSeries::GetLabel(char* buf, size_t size) const {
-  const char* name = GetName();
-  if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName();
-  if (name[0] == '\0') name = m_id.c_str();
-  std::snprintf(buf, size, "%s###%s", name, m_id.c_str());
-}
-
-bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) {
-  CheckSource();
-
-  char label[128];
-  GetLabel(label, sizeof(label));
-
-  int size = m_size;
-  int offset = m_offset;
-
-  // need to have last value at current time, so need to create fake last value
-  // we handle the offset logic ourselves to avoid wrap issues with size + 1
-  struct GetterData {
-    double now;
-    ImPlotPoint* data;
-    int size;
-    int offset;
-  };
-  GetterData getterData = {now, m_data, size, offset};
-  auto getter = [](void* data, int idx) {
-    auto d = static_cast<GetterData*>(data);
-    if (idx == d->size)
-      return ImPlotPoint{
-          d->now, d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
-    if (d->offset + idx < d->size)
-      return d->data[d->offset + idx];
-    else
-      return d->data[d->offset + idx - d->size];
-  };
-
-  if (m_color.w == IMPLOT_AUTO_COL.w) m_color = ImPlot::GetColormapColor(i);
-  ImPlot::PushStyleColor(ImPlotCol_Line, m_color);
-  if (IsDigital()) {
-    ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
-    ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
-    ImPlot::PlotDigital(label, getter, &getterData, size + 1);
-    ImPlot::PopStyleVar();
-    ImPlot::PopStyleVar();
-  } else {
-    ImPlot::SetPlotYAxis(m_yAxis);
-    ImPlot::SetNextMarkerStyle(m_marker - 1);
-    ImPlot::PlotLine(label, getter, &getterData, size + 1);
-  }
-  ImPlot::PopStyleColor();
-
-  // DND source for PlotSeries
-  if (ImPlot::BeginLegendDragDropSource(label)) {
-    EmitDragDropPayload(i, plotIndex);
-    ImPlot::EndLegendDragDropSource();
-  }
-
-  // Plot-specific variant of IniSaverInfo::PopupEditName() that also
-  // allows editing of other settings
-  bool rv = false;
-  if (ImPlot::BeginLegendPopup(label)) {
-    if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
-    ImGui::Text("Edit name:");
-    if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) {
-      ImGui::CloseCurrentPopup();
-    }
-    rv = EmitSettingsDetail(i);
-    ImPlot::EndLegendPopup();
-  }
-
-  return rv;
-}
-
-void PlotSeries::EmitDragDropPayload(size_t i, size_t plotIndex) {
-  PlotSeriesRef ref = {plotIndex, i};
-  ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
-  const char* name = GetName();
-  if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName();
-  if (name[0] == '\0') name = m_id.c_str();
-  ImGui::TextUnformatted(name);
-}
-
-static void MovePlotSeries(size_t fromPlotIndex, size_t fromSeriesIndex,
-                           size_t toPlotIndex, size_t toSeriesIndex,
-                           int yAxis = -1) {
-  if (fromPlotIndex == toPlotIndex) {
-    // need to handle this specially as the index of the old location changes
-    if (fromSeriesIndex != toSeriesIndex) {
-      auto& plotSeries = gPlots[fromPlotIndex].m_series;
-      auto val = std::move(plotSeries[fromSeriesIndex]);
-      // only set Y-axis if actually set
-      if (yAxis != -1) val->SetYAxis(yAxis);
-      plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val));
-      plotSeries.erase(plotSeries.begin() + fromSeriesIndex +
-                       (fromSeriesIndex > toSeriesIndex ? 1 : 0));
-    }
-  } else {
-    auto& fromPlot = gPlots[fromPlotIndex];
-    auto& toPlot = gPlots[toPlotIndex];
-    // always set Y-axis if moving plots
-    fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
-    toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex,
-                           std::move(fromPlot.m_series[fromSeriesIndex]));
-    fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex);
-  }
-}
-
-bool PlotSeries::EmitSettings(size_t i, size_t plotIndex) {
-  char label[128];
-  GetLabel(label, sizeof(label));
-
-  bool open = ImGui::CollapsingHeader(
-      label, IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0);
-
-  // DND source for PlotSeries
-  if (ImGui::BeginDragDropSource()) {
-    EmitDragDropPayload(i, plotIndex);
-    ImGui::EndDragDropSource();
-  }
-
-  // If another PlotSeries is dropped, move it before this series
-  if (ImGui::BeginDragDropTarget()) {
-    if (const ImGuiPayload* payload =
-            ImGui::AcceptDragDropPayload("PlotSeries")) {
-      auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
-      MovePlotSeries(ref->plotIndex, ref->seriesIndex, plotIndex, i);
-    }
-  }
-
-  SetOpen(open);
-  PopupEditName(i);
-  if (!open) return false;
-
-  return EmitSettingsDetail(i);
-}
-
-bool PlotSeries::EmitSettingsDetail(size_t i) {
-  if (ImGui::Button("Delete")) {
-    return true;
-  }
-
-  // Line color
-  {
-    ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
-    ImGui::SameLine();
-    if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i);
-  }
-
-  // Digital
-  {
-    static const char* const options[] = {"Auto", "Digital", "Analog"};
-    ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
-    ImGui::Combo("Digital", &m_digital, options,
-                 sizeof(options) / sizeof(options[0]));
-  }
-
-  if (IsDigital()) {
-    // Bit Height
-    {
-      ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-      ImGui::InputInt("Bit Height", &m_digitalBitHeight);
-    }
-
-    // Bit Gap
-    {
-      ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-      ImGui::InputInt("Bit Gap", &m_digitalBitGap);
-    }
-  } else {
-    // Y-axis
-    {
-      ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
-      static const char* const options[] = {"1", "2", "3"};
-      ImGui::Combo("Y-Axis", &m_yAxis, options, 3);
-    }
-
-    // Marker
-    {
-      static const char* const options[] = {
-          "None", "Circle", "Square", "Diamond", "Up",      "Down",
-          "Left", "Right",  "Cross",  "Plus",    "Asterisk"};
-      ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
-      ImGui::Combo("Marker", &m_marker, options,
-                   sizeof(options) / sizeof(options[0]));
-    }
-  }
-
-  return false;
-}
-
-Plot::Plot() {
-  for (int i = 0; i < 3; ++i) {
-    m_axisRange[i] = PlotRange{};
-  }
-}
-
-bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) {
-  if (NameInfo::ReadIni(name, value)) return true;
-  if (OpenInfo::ReadIni(name, value)) return true;
-  if (name == "visible") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_visible = num != 0;
-    return true;
-  } else if (name == "lockPrevX") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_lockPrevX = num != 0;
-    return true;
-  } else if (name == "legend") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    if (num == 0)
-      m_plotFlags &= ~ImPlotFlags_Legend;
-    else
-      m_plotFlags |= ImPlotFlags_Legend;
-    return true;
-  } else if (name == "yaxis2") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    if (num == 0)
-      m_plotFlags &= ~ImPlotFlags_YAxis2;
-    else
-      m_plotFlags |= ImPlotFlags_YAxis2;
-    return true;
-  } else if (name == "yaxis3") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    if (num == 0)
-      m_plotFlags &= ~ImPlotFlags_YAxis3;
-    else
-      m_plotFlags |= ImPlotFlags_YAxis3;
-    return true;
-  } else if (name == "viewTime") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_viewTime = num / 1000.0;
-    return true;
-  } else if (name == "height") {
-    int num;
-    if (value.getAsInteger(10, num)) return true;
-    m_height = num;
-    return true;
-  } else if (name.startswith("y")) {
-    auto [yAxisStr, yName] = name.split('_');
-    int yAxis;
-    if (yAxisStr.substr(1).getAsInteger(10, yAxis)) return false;
-    if (yAxis < 0 || yAxis > 3) return false;
-    if (yName == "min") {
-      int num;
-      if (value.getAsInteger(10, num)) return true;
-      m_axisRange[yAxis].min = num / 1000.0;
-      return true;
-    } else if (yName == "max") {
-      int num;
-      if (value.getAsInteger(10, num)) return true;
-      m_axisRange[yAxis].max = num / 1000.0;
-      return true;
-    } else if (yName == "lockMin") {
-      int num;
-      if (value.getAsInteger(10, num)) return true;
-      m_axisRange[yAxis].lockMin = num != 0;
-      return true;
-    } else if (yName == "lockMax") {
-      int num;
-      if (value.getAsInteger(10, num)) return true;
-      m_axisRange[yAxis].lockMax = num != 0;
-      return true;
-    }
-  }
-  return false;
-}
-
-void Plot::WriteIni(ImGuiTextBuffer* out) {
-  NameInfo::WriteIni(out);
-  OpenInfo::WriteIni(out);
-  out->appendf(
-      "visible=%d\nlockPrevX=%d\nlegend=%d\nyaxis2=%d\nyaxis3=%d\n"
-      "viewTime=%d\nheight=%d\n",
-      m_visible ? 1 : 0, m_lockPrevX ? 1 : 0,
-      (m_plotFlags & ImPlotFlags_Legend) ? 1 : 0,
-      (m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
-      (m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
-      static_cast<int>(m_viewTime * 1000), m_height);
-  for (int i = 0; i < 3; ++i) {
-    out->appendf("y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n", i,
-                 static_cast<int>(m_axisRange[i].min * 1000), i,
-                 static_cast<int>(m_axisRange[i].max * 1000), i,
-                 m_axisRange[i].lockMin ? 1 : 0, i,
-                 m_axisRange[i].lockMax ? 1 : 0);
-  }
-}
-
-void Plot::GetLabel(char* buf, size_t size, int index) const {
-  const char* name = NameInfo::GetName();
-  if (name[0] != '\0') {
-    std::snprintf(buf, size, "%s##Plot%d", name, index);
-  } else {
-    std::snprintf(buf, size, "Plot %d##Plot%d", index, index);
-  }
-}
-
-void Plot::GetName(char* buf, size_t size, int index) const {
-  const char* name = NameInfo::GetName();
-  if (name[0] != '\0') {
-    std::snprintf(buf, size, "%s", name);
-  } else {
-    std::snprintf(buf, size, "Plot %d", index);
-  }
-}
-
-void Plot::DragDropTarget(size_t i, bool inPlot) {
-  if (!ImGui::BeginDragDropTarget()) return;
-  // handle dragging onto a specific Y axis
-  int yAxis = -1;
-  if (inPlot) {
-    for (int y = 0; y < 3; ++y) {
-      if (ImPlot::IsPlotYAxisHovered(y)) {
-        yAxis = y;
-        break;
-      }
-    }
-  }
-  if (const ImGuiPayload* payload =
-          ImGui::AcceptDragDropPayload("DataSource")) {
-    auto source = *static_cast<GuiDataSource**>(payload->Data);
-    // don't add duplicates unless it's onto a different Y axis
-    auto it =
-        std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) {
-          return elem->GetId() == source->GetId() &&
-                 (yAxis == -1 || elem->GetYAxis() == yAxis);
-        });
-    if (it == m_series.end()) {
-      m_series.emplace_back(
-          std::make_unique<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
-    }
-  } else if (const ImGuiPayload* payload =
-                 ImGui::AcceptDragDropPayload("PlotSeries")) {
-    auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
-    MovePlotSeries(ref->plotIndex, ref->seriesIndex, i, m_series.size(), yAxis);
-  } else if (const ImGuiPayload* payload =
-                 ImGui::AcceptDragDropPayload("Plot")) {
-    auto fromPlotIndex = *static_cast<const size_t*>(payload->Data);
-    if (i != fromPlotIndex) {
-      auto val = std::move(gPlots[fromPlotIndex]);
-      gPlots.insert(gPlots.begin() + i, std::move(val));
-      gPlots.erase(gPlots.begin() + fromPlotIndex +
-                   (fromPlotIndex > i ? 1 : 0));
-    }
-  }
-}
-
-void Plot::EmitPlot(double now, size_t i) {
-  if (!m_visible) return;
-
-  bool lockX = (i != 0 && m_lockPrevX);
-
-  if (!lockX && ImGui::Button(m_paused ? "Resume" : "Pause"))
-    m_paused = !m_paused;
-
-  char label[128];
-  GetLabel(label, sizeof(label), i);
-
-  if (lockX) {
-    ImPlot::SetNextPlotLimitsX(gPlots[i - 1].m_xaxisRange.Min,
-                               gPlots[i - 1].m_xaxisRange.Max,
-                               ImGuiCond_Always);
-  } else {
-    // also force-pause plots if overall timing is paused
-    ImPlot::SetNextPlotLimitsX(now - m_viewTime, now,
-                               (m_paused || HALSIM_IsTimingPaused())
-                                   ? ImGuiCond_Once
-                                   : ImGuiCond_Always);
-  }
-
-  ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_Default,
-                               ImPlotAxisFlags_Auxiliary,
-                               ImPlotAxisFlags_Auxiliary};
-  for (int i = 0; i < 3; ++i) {
-    ImPlot::SetNextPlotLimitsY(
-        m_axisRange[i].min, m_axisRange[i].max,
-        m_axisRange[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i);
-    m_axisRange[i].apply = false;
-    if (m_axisRange[i].lockMin) yFlags[i] |= ImPlotAxisFlags_LockMin;
-    if (m_axisRange[i].lockMax) yFlags[i] |= ImPlotAxisFlags_LockMax;
-  }
-
-  if (ImPlot::BeginPlot(label, nullptr, nullptr, ImVec2(-1, m_height),
-                        m_plotFlags, ImPlotAxisFlags_Default, yFlags[0],
-                        yFlags[1], yFlags[2])) {
-    for (size_t j = 0; j < m_series.size(); ++j) {
-      if (m_series[j]->EmitPlot(now, j, i)) {
-        m_series.erase(m_series.begin() + j);
-      }
-    }
-    DragDropTarget(i, true);
-    m_xaxisRange = ImPlot::GetPlotLimits().X;
-    ImPlot::EndPlot();
-  }
-}
-
-void Plot::EmitSettingsLimits(int axis) {
-  ImGui::Indent();
-  ImGui::PushID(axis);
-
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
-  ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f");
-  ImGui::SameLine();
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
-  ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f");
-  ImGui::SameLine();
-  if (ImGui::Button("Apply")) m_axisRange[axis].apply = true;
-
-  ImGui::TextUnformatted("Lock Axis");
-  ImGui::SameLine();
-  ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin);
-  ImGui::SameLine();
-  ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax);
-
-  ImGui::PopID();
-  ImGui::Unindent();
-}
-
-// Delete button (X in circle), based on ImGui::CloseButton()
-static bool DeleteButton(ImGuiID id, const ImVec2& pos) {
-  ImGuiContext& g = *GImGui;
-  ImGuiWindow* window = g.CurrentWindow;
-
-  // We intentionally allow interaction when clipped so that a mechanical
-  // Alt,Right,Validate sequence close a window. (this isn't the regular
-  // behavior of buttons, but it doesn't affect the user much because navigation
-  // tends to keep items visible).
-  const ImRect bb(
-      pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
-  bool is_clipped = !ImGui::ItemAdd(bb, id);
-
-  bool hovered, held;
-  bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
-  if (is_clipped) return pressed;
-
-  // Render
-  ImU32 col =
-      ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
-  ImVec2 center = bb.GetCenter();
-  if (hovered)
-    window->DrawList->AddCircleFilled(
-        center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12);
-
-  ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text);
-  window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f),
-                              cross_col, 12);
-  float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f;
-  center -= ImVec2(0.5f, 0.5f);
-  window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent),
-                            center + ImVec2(-cross_extent, -cross_extent),
-                            cross_col, 1.0f);
-  window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent),
-                            center + ImVec2(-cross_extent, +cross_extent),
-                            cross_col, 1.0f);
-
-  return pressed;
-}
-
-void Plot::EmitSettings(size_t i) {
-  char label[128];
-  GetLabel(label, sizeof(label), i);
-
-  bool open = ImGui::CollapsingHeader(
-      label, ImGuiTreeNodeFlags_AllowItemOverlap |
-                 ImGuiTreeNodeFlags_ClipLabelForTrailingButton |
-                 (IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0));
-
-  {
-    // Create a small overlapping delete button
-    ImGuiWindow* window = ImGui::GetCurrentWindow();
-    ImGuiContext& g = *GImGui;
-    ImGuiItemHoveredDataBackup last_item_backup;
-    ImGuiID id = window->GetID(label);
-    float button_size = g.FontSize;
-    float button_x = ImMax(window->DC.LastItemRect.Min.x,
-                           window->DC.LastItemRect.Max.x -
-                               g.Style.FramePadding.x * 2.0f - button_size);
-    float button_y = window->DC.LastItemRect.Min.y;
-    if (DeleteButton(window->GetID(reinterpret_cast<void*>(
-                         static_cast<intptr_t>(id) + 1)),
-                     ImVec2(button_x, button_y))) {
-      gPlots.erase(gPlots.begin() + i);
-      return;
-    }
-    last_item_backup.Restore();
-  }
-
-  // DND source for Plot
-  if (ImGui::BeginDragDropSource()) {
-    ImGui::SetDragDropPayload("Plot", &i, sizeof(i));
-    char name[64];
-    GetName(name, sizeof(name), i);
-    ImGui::TextUnformatted(name);
-    ImGui::EndDragDropSource();
-  }
-  DragDropTarget(i, false);
-  SetOpen(open);
-  PopupEditName(i);
-  if (!open) return;
-  ImGui::PushID(i);
-#if 0
-  if (ImGui::Button("Move Up") && i > 0) {
-    std::swap(gPlots[i - 1], gPlots[i]);
-    ImGui::PopID();
-    return;
-  }
-  ImGui::SameLine();
-  if (ImGui::Button("Move Down") && i < (gPlots.size() - 1)) {
-    std::swap(gPlots[i], gPlots[i + 1]);
-    ImGui::PopID();
-    return;
-  }
-  ImGui::SameLine();
-  if (ImGui::Button("Delete")) {
-    gPlots.erase(gPlots.begin() + i);
-    ImGui::PopID();
-    return;
-  }
-#endif
-  ImGui::Checkbox("Visible", &m_visible);
-  ImGui::CheckboxFlags("Show Legend", &m_plotFlags, ImPlotFlags_Legend);
-  if (i != 0) ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
-  ImGui::TextUnformatted("Primary Y-Axis");
-  EmitSettingsLimits(0);
-  ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2);
-  if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) EmitSettingsLimits(1);
-  ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
-  if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) EmitSettingsLimits(2);
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
-  ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
-  ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
-  if (ImGui::InputInt("Height", &m_height, 10)) {
-    if (m_height < 0) m_height = 0;
-  }
-
-  ImGui::Indent();
-  for (size_t j = 0; j < m_series.size(); ++j) {
-    ImGui::PushID(j);
-    if (m_series[j]->EmitSettings(j, i)) {
-      m_series.erase(m_series.begin() + j);
-    }
-    ImGui::PopID();
-  }
-  ImGui::Unindent();
-
-  ImGui::PopID();
-}
-
-static void DisplayPlot() {
-  if (gPlots.empty()) {
-    ImGui::Text("No Plots");
-    return;
-  }
-  double now = wpi::Now() * 1.0e-6;
-  for (size_t i = 0; i < gPlots.size(); ++i) {
-    ImGui::PushID(i);
-    gPlots[i].EmitPlot(now, i);
-    ImGui::PopID();
-  }
-  ImGui::Text("(Right double click for more settings)");
-}
-
-static void DisplayPlotSettings() {
-  if (ImGui::Button("Add new plot")) {
-    gPlots.emplace_back();
-  }
-  for (size_t i = 0; i < gPlots.size(); ++i) {
-    gPlots[i].EmitSettings(i);
-  }
-}
-
-static void* PlotSeries_ReadOpen(ImGuiContext* ctx,
-                                 ImGuiSettingsHandler* handler,
-                                 const char* name) {
-  wpi::StringRef plotIndexStr, id;
-  std::tie(plotIndexStr, id) = wpi::StringRef{name}.split(',');
-  unsigned int plotIndex;
-  if (plotIndexStr.getAsInteger(10, plotIndex)) return nullptr;
-  if (plotIndex >= gPlots.size()) gPlots.resize(plotIndex + 1);
-  auto& plot = gPlots[plotIndex];
-  auto it = std::find_if(
-      plot.m_series.begin(), plot.m_series.end(),
-      [&](const auto& elem) { return elem && elem->GetId() == id; });
-  if (it != plot.m_series.end()) return it->get();
-  return plot.m_series.emplace_back(std::make_unique<PlotSeries>(id)).get();
-}
-
-static void PlotSeries_ReadLine(ImGuiContext* ctx,
-                                ImGuiSettingsHandler* handler, void* entry,
-                                const char* lineStr) {
-  auto element = static_cast<PlotSeries*>(entry);
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  element->ReadIni(name, value);
-}
-
-static void PlotSeries_WriteAll(ImGuiContext* ctx,
-                                ImGuiSettingsHandler* handler,
-                                ImGuiTextBuffer* out_buf) {
-  for (size_t i = 0; i < gPlots.size(); ++i) {
-    for (const auto& series : gPlots[i].m_series) {
-      out_buf->appendf("[PlotSeries][%d,%s]\n", static_cast<int>(i),
-                       series->GetId().c_str());
-      series->WriteIni(out_buf);
-      out_buf->append("\n");
-    }
-  }
-}
-
-void PlotGui::Initialize() {
-  gPlots.Initialize();
-
-  // hook ini handler for PlotSeries to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = "PlotSeries";
-  iniHandler.TypeHash = ImHashStr("PlotSeries");
-  iniHandler.ReadOpenFn = PlotSeries_ReadOpen;
-  iniHandler.ReadLineFn = PlotSeries_ReadLine;
-  iniHandler.WriteAllFn = PlotSeries_WriteAll;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-
-  // HALSimGui::AddExecute([] { ImPlot::ShowDemoWindow(); });
-  HALSimGui::AddWindow("Plot", DisplayPlot);
-  HALSimGui::SetDefaultWindowPos("Plot", 600, 75);
-  HALSimGui::SetDefaultWindowSize("Plot", 300, 200);
-
-  HALSimGui::AddWindow("Plot Settings", DisplayPlotSettings);
-  HALSimGui::SetDefaultWindowPos("Plot Settings", 902, 75);
-  HALSimGui::SetDefaultWindowSize("Plot Settings", 120, 200);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PlotGui.h b/simulation/halsim_gui/src/main/native/cpp/PlotGui.h
deleted file mode 100644
index 6bbd337..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/PlotGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class PlotGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.cpp
new file mode 100644
index 0000000..c511345
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.cpp
@@ -0,0 +1,137 @@
+// 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 "PowerDistributionSimGui.h"
+
+#include <glass/hardware/PowerDistribution.h>
+
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/PowerDistributionData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PowerDistributionTemperature,
+                                    "Power Distribution Temp");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PowerDistributionVoltage,
+                                    "Power Distribution Voltage");
+HALSIMGUI_DATASOURCE_DOUBLE_INDEXED2(PowerDistributionCurrent,
+                                     "Power Distribution Current");
+
+class PowerDistributionSimModel : public glass::PowerDistributionModel {
+ public:
+  explicit PowerDistributionSimModel(int32_t index)
+      : m_index{index}, m_temp{index}, m_voltage{index} {
+    // TODO make this better
+    const int numChannels = HAL_GetNumREVPDHChannels();
+    m_currents.reserve(numChannels);
+    for (int i = 0; i < numChannels; ++i) {
+      m_currents.emplace_back(
+          std::make_unique<PowerDistributionCurrentSource>(index, i));
+    }
+  }
+
+  void Update() override {}
+
+  bool Exists() override {
+    return HALSIM_GetPowerDistributionInitialized(m_index);
+  }
+
+  int GetNumChannels() const override { return m_currents.size(); }
+
+  glass::DataSource* GetTemperatureData() override { return &m_temp; }
+  glass::DataSource* GetVoltageData() override { return &m_voltage; }
+  glass::DataSource* GetCurrentData(int channel) override {
+    return m_currents[channel].get();
+  }
+
+  void SetTemperature(double val) override {
+    HALSIM_SetPowerDistributionTemperature(m_index, val);
+  }
+  void SetVoltage(double val) override {
+    HALSIM_SetPowerDistributionVoltage(m_index, val);
+  }
+  void SetCurrent(int channel, double val) override {
+    HALSIM_SetPowerDistributionCurrent(m_index, channel, val);
+  }
+
+ private:
+  int32_t m_index;
+  PowerDistributionTemperatureSource m_temp;
+  PowerDistributionVoltageSource m_voltage;
+  std::vector<std::unique_ptr<PowerDistributionCurrentSource>> m_currents;
+};
+
+class PowerDistributionsSimModel : public glass::PowerDistributionsModel {
+ public:
+  PowerDistributionsSimModel() : m_models(HAL_GetNumREVPDHModules()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachPowerDistribution(
+      wpi::function_ref<void(glass::PowerDistributionModel& model, int index)>
+          func) override;
+
+ private:
+  std::vector<std::unique_ptr<PowerDistributionSimModel>> m_models;
+};
+}  // namespace
+
+void PowerDistributionsSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetPowerDistributionInitialized(i)) {
+      if (!model) {
+        model = std::make_unique<PowerDistributionSimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void PowerDistributionsSimModel::ForEachPowerDistribution(
+    wpi::function_ref<void(glass::PowerDistributionModel& model, int index)>
+        func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool PowerDistributionsAnyInitialized() {
+  static const int32_t num = HAL_GetNumREVPDHModules();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetPowerDistributionInitialized(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void PowerDistributionSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "PowerDistributions", PowerDistributionsAnyInitialized,
+      [] { return std::make_unique<PowerDistributionsSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetDefaultPos(245, 155);
+        return glass::MakeFunctionView([=] {
+          DisplayPowerDistributions(
+              static_cast<PowerDistributionsSimModel*>(model));
+        });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.h b/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.h
new file mode 100644
index 0000000..10a78cc
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/PowerDistributionSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class PowerDistributionSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp
deleted file mode 100644
index 9171678..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp
+++ /dev/null
@@ -1,120 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "RelayGui.h"
-
-#include <cstdio>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/RelayData.h>
-#include <imgui.h>
-
-#include "ExtraGuiWidgets.h"
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayForward, "RelayFwd");
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayReverse, "RelayRev");
-}  // namespace
-
-static IniSaver<NameInfo> gRelays{"Relay"};
-static std::vector<std::unique_ptr<RelayForwardSource>> gRelayForwardSources;
-static std::vector<std::unique_ptr<RelayReverseSource>> gRelayReverseSources;
-
-static void UpdateRelaySources() {
-  for (int i = 0, iend = gRelayForwardSources.size(); i < iend; ++i) {
-    auto& source = gRelayForwardSources[i];
-    if (HALSIM_GetRelayInitializedForward(i)) {
-      if (!source) {
-        source = std::make_unique<RelayForwardSource>(i);
-        source->SetName(gRelays[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-  for (int i = 0, iend = gRelayReverseSources.size(); i < iend; ++i) {
-    auto& source = gRelayReverseSources[i];
-    if (HALSIM_GetRelayInitializedReverse(i)) {
-      if (!source) {
-        source = std::make_unique<RelayReverseSource>(i);
-        source->SetName(gRelays[i].GetName());
-      }
-    } else {
-      source.reset();
-    }
-  }
-}
-
-static void DisplayRelays() {
-  bool hasOutputs = false;
-  bool first = true;
-  for (int i = 0, iend = gRelayForwardSources.size(); i < iend; ++i) {
-    auto forwardSource = gRelayForwardSources[i].get();
-    auto reverseSource = gRelayReverseSources[i].get();
-
-    if (forwardSource || reverseSource) {
-      hasOutputs = true;
-
-      if (!first)
-        ImGui::Separator();
-      else
-        first = false;
-
-      bool forward = false;
-      bool reverse = false;
-      if (!HALSimGui::AreOutputsDisabled()) {
-        if (forwardSource) forward = forwardSource->GetValue();
-        if (reverseSource) reverse = reverseSource->GetValue();
-      }
-
-      auto& info = gRelays[i];
-      info.PushEditNameId(i);
-      if (info.HasName())
-        ImGui::Text("%s [%d]", info.GetName(), i);
-      else
-        ImGui::Text("Relay[%d]", i);
-      ImGui::PopID();
-      if (info.PopupEditName(i)) {
-        if (forwardSource) forwardSource->SetName(info.GetName());
-        if (reverseSource) reverseSource->SetName(info.GetName());
-      }
-      ImGui::SameLine();
-
-      // show forward and reverse as LED indicators
-      static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
-                                     IM_COL32(255, 0, 0, 255),
-                                     IM_COL32(128, 128, 128, 255)};
-      int values[2] = {reverseSource ? (reverse ? 2 : -2) : -3,
-                       forwardSource ? (forward ? 1 : -1) : -3};
-      GuiDataSource* sources[2] = {reverseSource, forwardSource};
-      ImGui::PushID(i);
-      DrawLEDSources(values, sources, 2, 2, colors);
-      ImGui::PopID();
-    }
-  }
-  if (!hasOutputs) ImGui::Text("No relays");
-}
-
-void RelayGui::Initialize() {
-  gRelays.Initialize();
-  int numRelays = HAL_GetNumRelayHeaders();
-  gRelayForwardSources.resize(numRelays);
-  gRelayReverseSources.resize(numRelays);
-  HALSimGui::AddExecute(UpdateRelaySources);
-  HALSimGui::AddWindow("Relays", DisplayRelays,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("Relays", 180, 20);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/RelayGui.h b/simulation/halsim_gui/src/main/native/cpp/RelayGui.h
deleted file mode 100644
index ccc2fb6..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/RelayGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class RelayGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp
new file mode 100644
index 0000000..3815617
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp
@@ -0,0 +1,118 @@
+// 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 "RelaySimGui.h"
+
+#include <glass/hardware/Relay.h>
+
+#include <memory>
+#include <vector>
+
+#include <hal/Ports.h>
+#include <hal/simulation/RelayData.h>
+#include <wpigui.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayForward, "RelayFwd");
+HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayReverse, "RelayRev");
+
+class RelaySimModel : public glass::RelayModel {
+ public:
+  explicit RelaySimModel(int32_t index)
+      : m_index{index}, m_forward{index}, m_reverse{index} {}
+
+  void Update() override {}
+
+  bool Exists() override {
+    return HALSIM_GetRelayInitializedForward(m_index) ||
+           HALSIM_GetRelayInitializedReverse(m_index);
+  }
+
+  glass::DataSource* GetForwardData() override {
+    return HALSIM_GetRelayInitializedForward(m_index) ? &m_forward : nullptr;
+  }
+  glass::DataSource* GetReverseData() override {
+    return HALSIM_GetRelayInitializedReverse(m_index) ? &m_reverse : nullptr;
+  }
+
+  void SetForward(bool val) override { HALSIM_SetRelayForward(m_index, val); }
+  void SetReverse(bool val) override { HALSIM_SetRelayReverse(m_index, val); }
+
+ private:
+  int32_t m_index;
+  RelayForwardSource m_forward;
+  RelayReverseSource m_reverse;
+};
+
+class RelaysSimModel : public glass::RelaysModel {
+ public:
+  RelaysSimModel() : m_models(HAL_GetNumRelayHeaders()) {}
+
+  void Update() override;
+
+  bool Exists() override { return true; }
+
+  void ForEachRelay(wpi::function_ref<void(glass::RelayModel& model, int index)>
+                        func) override;
+
+ private:
+  // indexed by channel
+  std::vector<std::unique_ptr<RelaySimModel>> m_models;
+};
+}  // namespace
+
+void RelaysSimModel::Update() {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    auto& model = m_models[i];
+    if (HALSIM_GetRelayInitializedForward(i) ||
+        HALSIM_GetRelayInitializedReverse(i)) {
+      if (!model) {
+        model = std::make_unique<RelaySimModel>(i);
+      }
+    } else {
+      model.reset();
+    }
+  }
+}
+
+void RelaysSimModel::ForEachRelay(
+    wpi::function_ref<void(glass::RelayModel& model, int index)> func) {
+  for (int32_t i = 0, iend = static_cast<int32_t>(m_models.size()); i < iend;
+       ++i) {
+    if (auto model = m_models[i].get()) {
+      func(*model, i);
+    }
+  }
+}
+
+static bool RelayAnyInitialized() {
+  static const int32_t num = HAL_GetNumRelayHeaders();
+  for (int32_t i = 0; i < num; ++i) {
+    if (HALSIM_GetRelayInitializedForward(i) ||
+        HALSIM_GetRelayInitializedReverse(i)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void RelaySimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "Relays", RelayAnyInitialized,
+      [] { return std::make_unique<RelaysSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(180, 20);
+        return glass::MakeFunctionView([=] {
+          glass::DisplayRelays(static_cast<RelaysSimModel*>(model),
+                               HALSimGui::halProvider.AreOutputsEnabled());
+        });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h
new file mode 100644
index 0000000..cb27057
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class RelaySimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp
deleted file mode 100644
index b4f8909..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp
+++ /dev/null
@@ -1,170 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "RoboRioGui.h"
-
-#include <memory>
-
-#include <hal/simulation/RoboRioData.h>
-#include <imgui.h>
-
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInVoltage, "Rio Input Voltage");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInCurrent, "Rio Input Current");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage6V, "Rio 6V Voltage");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent6V, "Rio 6V Current");
-HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive6V, "Rio 6V Active");
-HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults6V, "Rio 6V Faults");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage5V, "Rio 5V Voltage");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent5V, "Rio 5V Current");
-HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive5V, "Rio 5V Active");
-HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults5V, "Rio 5V Faults");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage3V3, "Rio 3.3V Voltage");
-HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent3V3, "Rio 3.3V Current");
-HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive3V3, "Rio 3.3V Active");
-HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults3V3, "Rio 3.3V Faults");
-struct RoboRioSource {
-  RoboRioVInVoltageSource vInVoltage;
-  RoboRioVInCurrentSource vInCurrent;
-  RoboRioUserVoltage6VSource userVoltage6V;
-  RoboRioUserCurrent6VSource userCurrent6V;
-  RoboRioUserActive6VSource userActive6V;
-  RoboRioUserFaults6VSource userFaults6V;
-  RoboRioUserVoltage5VSource userVoltage5V;
-  RoboRioUserCurrent5VSource userCurrent5V;
-  RoboRioUserActive5VSource userActive5V;
-  RoboRioUserFaults5VSource userFaults5V;
-  RoboRioUserVoltage3V3Source userVoltage3V3;
-  RoboRioUserCurrent3V3Source userCurrent3V3;
-  RoboRioUserActive3V3Source userActive3V3;
-  RoboRioUserFaults3V3Source userFaults3V3;
-};
-}  // namespace
-
-static std::unique_ptr<RoboRioSource> gRioSource;
-
-static void UpdateRoboRioSources() {
-  if (!gRioSource) gRioSource = std::make_unique<RoboRioSource>();
-}
-
-static void DisplayRoboRio() {
-  ImGui::Button("User Button");
-  HALSIM_SetRoboRioFPGAButton(ImGui::IsItemActive());
-
-  ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
-
-  if (ImGui::CollapsingHeader("RoboRIO Input")) {
-    {
-      double val = gRioSource->vInVoltage.GetValue();
-      if (gRioSource->vInVoltage.InputDouble("Voltage (V)", &val))
-        HALSIM_SetRoboRioVInVoltage(val);
-    }
-
-    {
-      double val = gRioSource->vInCurrent.GetValue();
-      if (gRioSource->vInCurrent.InputDouble("Current (A)", &val))
-        HALSIM_SetRoboRioVInCurrent(val);
-    }
-  }
-
-  if (ImGui::CollapsingHeader("6V Rail")) {
-    {
-      double val = gRioSource->userVoltage6V.GetValue();
-      if (gRioSource->userVoltage6V.InputDouble("Voltage (V)", &val))
-        HALSIM_SetRoboRioUserVoltage6V(val);
-    }
-
-    {
-      double val = gRioSource->userCurrent6V.GetValue();
-      if (gRioSource->userCurrent6V.InputDouble("Current (A)", &val))
-        HALSIM_SetRoboRioUserCurrent6V(val);
-    }
-
-    {
-      static const char* options[] = {"inactive", "active"};
-      int val = gRioSource->userActive6V.GetValue() ? 1 : 0;
-      if (gRioSource->userActive6V.Combo("Active", &val, options, 2))
-        HALSIM_SetRoboRioUserActive6V(val);
-    }
-
-    {
-      int val = gRioSource->userFaults6V.GetValue();
-      if (gRioSource->userFaults6V.InputInt("Faults", &val))
-        HALSIM_SetRoboRioUserFaults6V(val);
-    }
-  }
-
-  if (ImGui::CollapsingHeader("5V Rail")) {
-    {
-      double val = gRioSource->userVoltage5V.GetValue();
-      if (gRioSource->userVoltage5V.InputDouble("Voltage (V)", &val))
-        HALSIM_SetRoboRioUserVoltage5V(val);
-    }
-
-    {
-      double val = gRioSource->userCurrent5V.GetValue();
-      if (gRioSource->userCurrent5V.InputDouble("Current (A)", &val))
-        HALSIM_SetRoboRioUserCurrent5V(val);
-    }
-
-    {
-      static const char* options[] = {"inactive", "active"};
-      int val = gRioSource->userActive5V.GetValue() ? 1 : 0;
-      if (gRioSource->userActive5V.Combo("Active", &val, options, 2))
-        HALSIM_SetRoboRioUserActive5V(val);
-    }
-
-    {
-      int val = gRioSource->userFaults5V.GetValue();
-      if (gRioSource->userFaults5V.InputInt("Faults", &val))
-        HALSIM_SetRoboRioUserFaults5V(val);
-    }
-  }
-
-  if (ImGui::CollapsingHeader("3.3V Rail")) {
-    {
-      double val = gRioSource->userVoltage3V3.GetValue();
-      if (gRioSource->userVoltage3V3.InputDouble("Voltage (V)", &val))
-        HALSIM_SetRoboRioUserVoltage3V3(val);
-    }
-
-    {
-      double val = gRioSource->userCurrent3V3.GetValue();
-      if (gRioSource->userCurrent3V3.InputDouble("Current (A)", &val))
-        HALSIM_SetRoboRioUserCurrent3V3(val);
-    }
-
-    {
-      static const char* options[] = {"inactive", "active"};
-      int val = HALSIM_GetRoboRioUserActive3V3() ? 1 : 0;
-      if (gRioSource->userActive3V3.Combo("Active", &val, options, 2))
-        HALSIM_SetRoboRioUserActive3V3(val);
-    }
-
-    {
-      int val = gRioSource->userFaults3V3.GetValue();
-      if (gRioSource->userFaults3V3.InputInt("Faults", &val))
-        HALSIM_SetRoboRioUserFaults3V3(val);
-    }
-  }
-
-  ImGui::PopItemWidth();
-}
-
-void RoboRioGui::Initialize() {
-  HALSimGui::AddExecute(UpdateRoboRioSources);
-  HALSimGui::AddWindow("RoboRIO", DisplayRoboRio,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  // hide it by default
-  HALSimGui::SetWindowVisibility("RoboRIO", HALSimGui::kHide);
-  HALSimGui::SetDefaultWindowPos("RoboRIO", 5, 125);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h
deleted file mode 100644
index 603abf0..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class RoboRioGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp
new file mode 100644
index 0000000..ba13a2a
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp
@@ -0,0 +1,144 @@
+// 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 "RoboRioSimGui.h"
+
+#include <glass/hardware/RoboRio.h>
+
+#include <memory>
+
+#include <hal/simulation/RoboRioData.h>
+
+#include "HALDataSource.h"
+#include "HALSimGui.h"
+
+using namespace halsimgui;
+
+namespace {
+HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioFPGAButton, "Rio User Button");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInVoltage, "Rio Input Voltage");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInCurrent, "Rio Input Current");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage6V, "Rio 6V Voltage");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent6V, "Rio 6V Current");
+HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive6V, "Rio 6V Active");
+HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults6V, "Rio 6V Faults");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage5V, "Rio 5V Voltage");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent5V, "Rio 5V Current");
+HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive5V, "Rio 5V Active");
+HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults5V, "Rio 5V Faults");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage3V3, "Rio 3.3V Voltage");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent3V3, "Rio 3.3V Current");
+HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive3V3, "Rio 3.3V Active");
+HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults3V3, "Rio 3.3V Faults");
+HALSIMGUI_DATASOURCE_DOUBLE(RoboRioBrownoutVoltage, "Rio Brownout Voltage");
+
+class RoboRioUser6VRailSimModel : public glass::RoboRioRailModel {
+ public:
+  void Update() override {}
+  bool Exists() override { return true; }
+  glass::DataSource* GetVoltageData() override { return &m_voltage; }
+  glass::DataSource* GetCurrentData() override { return &m_current; }
+  glass::DataSource* GetActiveData() override { return &m_active; }
+  glass::DataSource* GetFaultsData() override { return &m_faults; }
+
+  void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage6V(val); }
+  void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent6V(val); }
+  void SetActive(bool val) override { HALSIM_SetRoboRioUserActive6V(val); }
+  void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults6V(val); }
+
+ private:
+  RoboRioUserVoltage6VSource m_voltage;
+  RoboRioUserCurrent6VSource m_current;
+  RoboRioUserActive6VSource m_active;
+  RoboRioUserFaults6VSource m_faults;
+};
+
+class RoboRioUser5VRailSimModel : public glass::RoboRioRailModel {
+ public:
+  void Update() override {}
+  bool Exists() override { return true; }
+  glass::DataSource* GetVoltageData() override { return &m_voltage; }
+  glass::DataSource* GetCurrentData() override { return &m_current; }
+  glass::DataSource* GetActiveData() override { return &m_active; }
+  glass::DataSource* GetFaultsData() override { return &m_faults; }
+
+  void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage5V(val); }
+  void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent5V(val); }
+  void SetActive(bool val) override { HALSIM_SetRoboRioUserActive5V(val); }
+  void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults5V(val); }
+
+ private:
+  RoboRioUserVoltage5VSource m_voltage;
+  RoboRioUserCurrent5VSource m_current;
+  RoboRioUserActive5VSource m_active;
+  RoboRioUserFaults5VSource m_faults;
+};
+
+class RoboRioUser3V3RailSimModel : public glass::RoboRioRailModel {
+ public:
+  void Update() override {}
+  bool Exists() override { return true; }
+  glass::DataSource* GetVoltageData() override { return &m_voltage; }
+  glass::DataSource* GetCurrentData() override { return &m_current; }
+  glass::DataSource* GetActiveData() override { return &m_active; }
+  glass::DataSource* GetFaultsData() override { return &m_faults; }
+
+  void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage3V3(val); }
+  void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent3V3(val); }
+  void SetActive(bool val) override { HALSIM_SetRoboRioUserActive3V3(val); }
+  void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults3V3(val); }
+
+ private:
+  RoboRioUserVoltage3V3Source m_voltage;
+  RoboRioUserCurrent3V3Source m_current;
+  RoboRioUserActive3V3Source m_active;
+  RoboRioUserFaults3V3Source m_faults;
+};
+
+class RoboRioSimModel : public glass::RoboRioModel {
+ public:
+  void Update() override {}
+
+  bool Exists() override { return true; }
+
+  glass::RoboRioRailModel* GetUser6VRail() override { return &m_user6VRail; }
+  glass::RoboRioRailModel* GetUser5VRail() override { return &m_user5VRail; }
+  glass::RoboRioRailModel* GetUser3V3Rail() override { return &m_user3V3Rail; }
+
+  glass::DataSource* GetUserButton() override { return &m_userButton; }
+  glass::DataSource* GetVInVoltageData() override { return &m_vInVoltage; }
+  glass::DataSource* GetVInCurrentData() override { return &m_vInCurrent; }
+  glass::DataSource* GetBrownoutVoltage() override {
+    return &m_brownoutVoltage;
+  }
+
+  void SetUserButton(bool val) override { HALSIM_SetRoboRioFPGAButton(val); }
+  void SetVInVoltage(double val) override { HALSIM_SetRoboRioVInVoltage(val); }
+  void SetVInCurrent(double val) override { HALSIM_SetRoboRioVInCurrent(val); }
+  void SetBrownoutVoltage(double val) override {
+    HALSIM_SetRoboRioBrownoutVoltage(val);
+  }
+
+ private:
+  RoboRioFPGAButtonSource m_userButton;
+  RoboRioVInVoltageSource m_vInVoltage;
+  RoboRioVInCurrentSource m_vInCurrent;
+  RoboRioUser6VRailSimModel m_user6VRail;
+  RoboRioUser5VRailSimModel m_user5VRail;
+  RoboRioUser3V3RailSimModel m_user3V3Rail;
+  RoboRioBrownoutVoltageSource m_brownoutVoltage;
+};
+}  // namespace
+
+void RoboRioSimGui::Initialize() {
+  HALSimGui::halProvider.Register(
+      "RoboRIO", [] { return true; },
+      [] { return std::make_unique<RoboRioSimModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(5, 125);
+        return glass::MakeFunctionView(
+            [=] { DisplayRoboRio(static_cast<RoboRioSimModel*>(model)); });
+      });
+}
diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h
new file mode 100644
index 0000000..27999b5
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h
@@ -0,0 +1,14 @@
+// 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
+
+namespace halsimgui {
+
+class RoboRioSimGui {
+ public:
+  static void Initialize();
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp
index 2802000..62e2896 100644
--- a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp
@@ -1,59 +1,40 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "SimDeviceGui.h"
 
+#include <glass/other/DeviceTree.h>
 #include <stdint.h>
 
-#include <functional>
-#include <memory>
-#include <vector>
-
+#include <fmt/format.h>
 #include <hal/SimDevice.h>
 #include <hal/simulation/SimDeviceData.h>
-#include <imgui.h>
 #include <wpi/DenseMap.h>
+#include <wpi/StringExtras.h>
 
-#include "GuiDataSource.h"
+#include "HALDataSource.h"
 #include "HALSimGui.h"
-#include "IniSaverInfo.h"
-#include "IniSaverString.h"
 
 using namespace halsimgui;
 
 namespace {
-
-struct ElementInfo : public NameInfo, public OpenInfo {
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value) {
-    if (NameInfo::ReadIni(name, value)) return true;
-    if (OpenInfo::ReadIni(name, value)) return true;
-    return false;
-  }
-  void WriteIni(ImGuiTextBuffer* out) {
-    NameInfo::WriteIni(out);
-    OpenInfo::WriteIni(out);
-  }
-  bool visible = true;  // not saved
-};
-
-class SimValueSource : public GuiDataSource {
+class SimValueSource : public glass::DataSource {
  public:
   explicit SimValueSource(HAL_SimValueHandle handle, const char* device,
                           const char* name)
-      : GuiDataSource(wpi::Twine{device} + wpi::Twine{'-'} + name),
+      : DataSource(fmt::format("{}-{}", device, name)),
         m_callback{HALSIM_RegisterSimValueChangedCallback(
             handle, this, CallbackFunc, true)} {}
-  ~SimValueSource() {
-    if (m_callback != 0) HALSIM_CancelSimValueChangedCallback(m_callback);
+  ~SimValueSource() override {
+    if (m_callback != 0) {
+      HALSIM_CancelSimValueChangedCallback(m_callback);
+    }
   }
 
  private:
   static void CallbackFunc(const char*, void* param, HAL_SimValueHandle,
-                           HAL_Bool, const HAL_Value* value) {
+                           int32_t, const HAL_Value* value) {
     auto source = static_cast<SimValueSource*>(param);
     if (value->type == HAL_BOOLEAN) {
       source->SetValue(value->data.v_boolean);
@@ -67,226 +48,144 @@
   int32_t m_callback;
 };
 
+class SimDevicesModel : public glass::Model {
+ public:
+  void Update() override;
+  bool Exists() override { return true; }
+
+  glass::DataSource* GetSource(HAL_SimValueHandle handle) {
+    return m_sources[handle].get();
+  }
+
+ private:
+  wpi::DenseMap<HAL_SimValueHandle, std::unique_ptr<SimValueSource>> m_sources;
+};
 }  // namespace
 
-static std::vector<std::function<void()>> gDeviceExecutors;
-static IniSaverString<ElementInfo> gElements{"Device"};
-static wpi::DenseMap<HAL_SimValueHandle, std::unique_ptr<SimValueSource>>
-    gSimValueSources;
+static SimDevicesModel* gSimDevicesModel;
+static bool gSimDevicesShowPrefix = false;
 
-static void UpdateSimValueSources() {
+void SimDevicesModel::Update() {
   HALSIM_EnumerateSimDevices(
-      "", nullptr, [](const char* name, void*, HAL_SimDeviceHandle handle) {
+      "", this, [](const char* name, void* self, HAL_SimDeviceHandle handle) {
+        struct Data {
+          SimDevicesModel* self;
+          const char* device;
+        } data = {static_cast<SimDevicesModel*>(self), name};
         HALSIM_EnumerateSimValues(
-            handle, const_cast<char*>(name),
-            [](const char* name, void* deviceV, HAL_SimValueHandle handle,
-               HAL_Bool readonly, const HAL_Value* value) {
-              auto device = static_cast<const char*>(deviceV);
-              auto& source = gSimValueSources[handle];
+            handle, &data,
+            [](const char* name, void* dataV, HAL_SimValueHandle handle,
+               int32_t direction, const HAL_Value* value) {
+              auto data = static_cast<Data*>(dataV);
+              auto& source = data->self->m_sources[handle];
               if (!source) {
-                source = std::make_unique<SimValueSource>(handle, device, name);
+                source = std::make_unique<SimValueSource>(handle, data->device,
+                                                          name);
               }
             });
       });
 }
 
-void SimDeviceGui::Hide(const char* name) { gElements[name].visible = false; }
+static void DisplaySimValue(const char* name, void* data,
+                            HAL_SimValueHandle handle, int32_t direction,
+                            const HAL_Value* value) {
+  auto model = static_cast<SimDevicesModel*>(data);
 
-void SimDeviceGui::Add(std::function<void()> execute) {
-  if (execute) gDeviceExecutors.emplace_back(std::move(execute));
-}
+  HAL_Value valueCopy = *value;
 
-bool SimDeviceGui::StartDevice(const char* label, ImGuiTreeNodeFlags flags) {
-  auto& element = gElements[label];
-  if (!element.visible) return false;
-
-  char name[128];
-  element.GetLabel(name, sizeof(name), label);
-
-  bool open = ImGui::CollapsingHeader(
-      name, flags | (element.IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0));
-  element.SetOpen(open);
-  element.PopupEditName(label);
-
-  if (open) ImGui::PushID(label);
-  return open;
-}
-
-void SimDeviceGui::FinishDevice() { ImGui::PopID(); }
-
-static bool DisplayValueImpl(const char* name, bool readonly, HAL_Value* value,
-                             const char** options, int32_t numOptions) {
-  // read-only
-  if (readonly) {
-    switch (value->type) {
-      case HAL_BOOLEAN:
-        ImGui::LabelText(name, "%s", value->data.v_boolean ? "true" : "false");
-        break;
-      case HAL_DOUBLE:
-        ImGui::LabelText(name, "%.6f", value->data.v_double);
-        break;
-      case HAL_ENUM: {
-        int current = value->data.v_enum;
-        if (current < 0 || current >= numOptions)
-          ImGui::LabelText(name, "%d (unknown)", current);
-        else
-          ImGui::LabelText(name, "%s", options[current]);
-        break;
-      }
-      case HAL_INT:
-        ImGui::LabelText(name, "%d", static_cast<int>(value->data.v_int));
-        break;
-      case HAL_LONG:
-        ImGui::LabelText(name, "%lld",
-                         static_cast<long long int>(  // NOLINT(runtime/int)
-                             value->data.v_long));
-        break;
-      default:
-        break;
-    }
-    return false;
-  }
-
-  // writable
   switch (value->type) {
     case HAL_BOOLEAN: {
-      static const char* boolOptions[] = {"false", "true"};
-      int val = value->data.v_boolean ? 1 : 0;
-      if (ImGui::Combo(name, &val, boolOptions, 2)) {
-        value->data.v_boolean = val;
-        return true;
+      bool v = value->data.v_boolean;
+      if (glass::DeviceBoolean(name, direction == HAL_SimValueOutput, &v,
+                               model->GetSource(handle))) {
+        valueCopy.data.v_boolean = v ? 1 : 0;
+        HAL_SetSimValue(handle, valueCopy);
       }
       break;
     }
-    case HAL_DOUBLE: {
-      if (ImGui::InputDouble(name, &value->data.v_double, 0, 0, "%.6f",
-                             ImGuiInputTextFlags_EnterReturnsTrue))
-        return true;
+    case HAL_DOUBLE:
+      if (glass::DeviceDouble(name, direction == HAL_SimValueOutput,
+                              &valueCopy.data.v_double,
+                              model->GetSource(handle))) {
+        HAL_SetSimValue(handle, valueCopy);
+      }
       break;
-    }
     case HAL_ENUM: {
-      int current = value->data.v_enum;
-      if (ImGui::Combo(name, &current, options, numOptions)) {
-        value->data.v_enum = current;
-        return true;
+      int32_t numOptions = 0;
+      const char** options = HALSIM_GetSimValueEnumOptions(handle, &numOptions);
+      if (glass::DeviceEnum(name, direction == HAL_SimValueOutput,
+                            &valueCopy.data.v_enum, options, numOptions,
+                            model->GetSource(handle))) {
+        HAL_SetSimValue(handle, valueCopy);
       }
       break;
     }
-    case HAL_INT: {
-      if (ImGui::InputScalar(name, ImGuiDataType_S32, &value->data.v_int,
-                             nullptr, nullptr, nullptr,
-                             ImGuiInputTextFlags_EnterReturnsTrue))
-        return true;
+    case HAL_INT:
+      if (glass::DeviceInt(name, direction == HAL_SimValueOutput,
+                           &valueCopy.data.v_int, model->GetSource(handle))) {
+        HAL_SetSimValue(handle, valueCopy);
+      }
       break;
-    }
-    case HAL_LONG: {
-      if (ImGui::InputScalar(name, ImGuiDataType_S64, &value->data.v_long,
-                             nullptr, nullptr, nullptr,
-                             ImGuiInputTextFlags_EnterReturnsTrue))
-        return true;
+    case HAL_LONG:
+      if (glass::DeviceLong(name, direction == HAL_SimValueOutput,
+                            &valueCopy.data.v_long, model->GetSource(handle))) {
+        HAL_SetSimValue(handle, valueCopy);
+      }
       break;
-    }
     default:
       break;
   }
-  return false;
 }
 
-static bool DisplayValueSourceImpl(const char* name, bool readonly,
-                                   HAL_Value* value,
-                                   const GuiDataSource* source,
-                                   const char** options, int32_t numOptions) {
-  if (!source)
-    return DisplayValueImpl(name, readonly, value, options, numOptions);
-  ImGui::PushID(name);
-  bool rv = DisplayValueImpl("", readonly, value, options, numOptions);
-  ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
-  ImGui::Selectable(name);
-  source->EmitDrag();
-  ImGui::PopID();
-  return rv;
-}
-
-bool SimDeviceGui::DisplayValue(const char* name, bool readonly,
-                                HAL_Value* value, const char** options,
-                                int32_t numOptions) {
-  return DisplayValueSource(name, readonly, value, nullptr, options,
-                            numOptions);
-}
-
-bool SimDeviceGui::DisplayValueSource(const char* name, bool readonly,
-                                      HAL_Value* value,
-                                      const GuiDataSource* source,
-                                      const char** options,
-                                      int32_t numOptions) {
-  ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f);
-  return DisplayValueSourceImpl(name, readonly, value, source, options,
-                                numOptions);
-}
-
-static void SimDeviceDisplayValue(const char* name, void*,
-                                  HAL_SimValueHandle handle, HAL_Bool readonly,
-                                  const HAL_Value* value) {
-  int32_t numOptions = 0;
-  const char** options = nullptr;
-
-  if (value->type == HAL_ENUM)
-    options = HALSIM_GetSimValueEnumOptions(handle, &numOptions);
-
-  HAL_Value valueCopy = *value;
-  if (DisplayValueSourceImpl(name, readonly, &valueCopy,
-                             gSimValueSources[handle].get(), options,
-                             numOptions))
-    HAL_SetSimValue(handle, valueCopy);
-}
-
-static void SimDeviceDisplayDevice(const char* name, void*,
-                                   HAL_SimDeviceHandle handle) {
-  auto it = gElements.find(name);
-  if (it != gElements.end() && !it->second.visible) return;
-
-  if (SimDeviceGui::StartDevice(name)) {
-    ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.5f);
-    HALSIM_EnumerateSimValues(handle, nullptr, SimDeviceDisplayValue);
-    ImGui::PopItemWidth();
-    SimDeviceGui::FinishDevice();
+static void DisplaySimDevice(const char* name, void* data,
+                             HAL_SimDeviceHandle handle) {
+  std::string_view id{name};
+  if (!gSimDevicesShowPrefix) {
+    // only show "Foo" portion of "Accel:Foo"
+    std::string_view type;
+    std::tie(type, id) = wpi::split(id, ':');
+    if (id.empty()) {
+      id = type;
+    }
   }
-}
-
-static void DisplayDeviceTree() {
-  for (auto&& execute : gDeviceExecutors) {
-    if (execute) execute();
+  if (glass::BeginDevice(id.data())) {
+    HALSIM_EnumerateSimValues(handle, data, DisplaySimValue);
+    glass::EndDevice();
   }
-  HALSIM_EnumerateSimDevices("", nullptr, SimDeviceDisplayDevice);
 }
 
 void SimDeviceGui::Initialize() {
-  gElements.Initialize();
-  HALSimGui::AddExecute(UpdateSimValueSources);
-  HALSimGui::AddWindow("Other Devices", DisplayDeviceTree);
-  HALSimGui::SetDefaultWindowPos("Other Devices", 1025, 20);
-  HALSimGui::SetDefaultWindowSize("Other Devices", 250, 695);
+  HALSimGui::halProvider.Register(
+      "Other Devices", [] { return true; },
+      [] { return std::make_unique<glass::DeviceTreeModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->SetDefaultPos(1025, 20);
+        win->SetDefaultSize(250, 695);
+        win->DisableRenamePopup();
+        return glass::MakeFunctionView([=] {
+          if (ImGui::BeginPopupContextItem()) {
+            ImGui::Checkbox("Show prefix", &gSimDevicesShowPrefix);
+            ImGui::EndPopup();
+          }
+          static_cast<glass::DeviceTreeModel*>(model)->Display();
+        });
+      });
+  HALSimGui::halProvider.ShowDefault("Other Devices");
+
+  auto model = std::make_unique<SimDevicesModel>();
+  gSimDevicesModel = model.get();
+  GetDeviceTree().Add(std::move(model), [](glass::Model* model) {
+    HALSIM_EnumerateSimDevices("", static_cast<SimDevicesModel*>(model),
+                               DisplaySimDevice);
+  });
 }
 
-extern "C" {
-
-void HALSIMGUI_DeviceTreeAdd(void* param, void (*execute)(void*)) {
-  if (execute) SimDeviceGui::Add([=] { execute(param); });
+glass::DataSource* SimDeviceGui::GetValueSource(HAL_SimValueHandle handle) {
+  return gSimDevicesModel->GetSource(handle);
 }
 
-void HALSIMGUI_DeviceTreeHide(const char* name) { SimDeviceGui::Hide(name); }
-
-HAL_Bool HALSIMGUI_DeviceTreeDisplayValue(const char* name, HAL_Bool readonly,
-                                          struct HAL_Value* value,
-                                          const char** options,
-                                          int32_t numOptions) {
-  return SimDeviceGui::DisplayValue(name, readonly, value, options, numOptions);
+glass::DeviceTreeModel& SimDeviceGui::GetDeviceTree() {
+  static auto model = HALSimGui::halProvider.GetModel("Other Devices");
+  assert(model);
+  return *static_cast<glass::DeviceTreeModel*>(model);
 }
-
-HAL_Bool HALSIMGUI_DeviceTreeStartDevice(const char* label, int32_t flags) {
-  return SimDeviceGui::StartDevice(label, flags);
-}
-
-void HALSIMGUI_DeviceTreeFinishDevice(void) { SimDeviceGui::FinishDevice(); }
-
-}  // extern "C"
diff --git a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp
deleted file mode 100644
index b0f802e..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#include "SolenoidGui.h"
-
-#include <cstdio>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#include <hal/Ports.h>
-#include <hal/simulation/PCMData.h>
-#include <imgui.h>
-#include <wpi/SmallVector.h>
-
-#include "ExtraGuiWidgets.h"
-#include "GuiDataSource.h"
-#include "HALSimGui.h"
-#include "IniSaver.h"
-#include "IniSaverInfo.h"
-
-using namespace halsimgui;
-
-namespace {
-HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED2(PCMSolenoidOutput, "Solenoid");
-struct PCMSource {
-  explicit PCMSource(int numChannels) : solenoids(numChannels) {}
-  std::vector<std::unique_ptr<PCMSolenoidOutputSource>> solenoids;
-  int initCount = 0;
-};
-}  // namespace
-
-static IniSaver<OpenInfo> gPCMs{"PCM"};
-static IniSaver<NameInfo> gSolenoids{"Solenoid"};
-static std::vector<PCMSource> gPCMSources;
-
-static void UpdateSolenoidSources() {
-  for (int i = 0, iend = gPCMSources.size(); i < iend; ++i) {
-    auto& pcmSource = gPCMSources[i];
-    int numChannels = pcmSource.solenoids.size();
-    pcmSource.initCount = 0;
-    for (int j = 0; j < numChannels; ++j) {
-      auto& source = pcmSource.solenoids[j];
-      if (HALSIM_GetPCMSolenoidInitialized(i, j)) {
-        if (!source) {
-          source = std::make_unique<PCMSolenoidOutputSource>(i, j);
-          source->SetName(gSolenoids[i * numChannels + j].GetName());
-        }
-        ++pcmSource.initCount;
-      } else {
-        source.reset();
-      }
-    }
-  }
-}
-
-static void DisplaySolenoids() {
-  bool hasOutputs = false;
-  for (int i = 0, iend = gPCMSources.size(); i < iend; ++i) {
-    auto& pcmSource = gPCMSources[i];
-    if (pcmSource.initCount == 0) continue;
-    hasOutputs = true;
-
-    int numChannels = pcmSource.solenoids.size();
-    wpi::SmallVector<int, 16> channels;
-    channels.resize(numChannels);
-    for (int j = 0; j < numChannels; ++j) {
-      if (pcmSource.solenoids[j]) {
-        channels[j] = (!HALSimGui::AreOutputsDisabled() &&
-                       pcmSource.solenoids[j]->GetValue())
-                          ? 1
-                          : -1;
-      } else {
-        channels[j] = -2;
-      }
-    }
-
-    char name[128];
-    std::snprintf(name, sizeof(name), "PCM[%d]", i);
-    auto& pcmInfo = gPCMs[i];
-    bool open = ImGui::CollapsingHeader(
-        name, pcmInfo.IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0);
-    pcmInfo.SetOpen(open);
-    ImGui::SetItemAllowOverlap();
-    ImGui::SameLine();
-
-    // show channels as LED indicators
-    static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
-                                   IM_COL32(128, 128, 128, 255)};
-    DrawLEDs(channels.data(), channels.size(), channels.size(), colors);
-
-    if (open) {
-      ImGui::PushID(i);
-      ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
-      for (int j = 0; j < numChannels; ++j) {
-        if (!pcmSource.solenoids[j]) continue;
-        auto& info = gSolenoids[i * numChannels + j];
-        info.GetLabel(name, sizeof(name), "Solenoid", j);
-        ImGui::PushID(j);
-        pcmSource.solenoids[j]->LabelText(name, "%s",
-                                          channels[j] == 1 ? "On" : "Off");
-        if (info.PopupEditName(j)) {
-          pcmSource.solenoids[j]->SetName(info.GetName());
-        }
-        ImGui::PopID();
-      }
-      ImGui::PopItemWidth();
-      ImGui::PopID();
-    }
-  }
-  if (!hasOutputs) ImGui::Text("No solenoids");
-}
-
-void SolenoidGui::Initialize() {
-  gPCMs.Initialize();
-  gSolenoids.Initialize();
-  const int numModules = HAL_GetNumPCMModules();
-  const int numChannels = HAL_GetNumSolenoidChannels();
-  gPCMSources.reserve(numModules);
-  for (int i = 0; i < numModules; ++i) gPCMSources.emplace_back(numChannels);
-
-  HALSimGui::AddExecute(UpdateSolenoidSources);
-  HALSimGui::AddWindow("Solenoids", DisplaySolenoids,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("Solenoids", 290, 20);
-}
diff --git a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h
deleted file mode 100644
index 35905cf..0000000
--- a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-class SolenoidGui {
- public:
-  static void Initialize();
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp b/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp
index 18c76b0..1928063 100644
--- a/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp
@@ -1,12 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "TimingGui.h"
 
+#include <glass/Model.h>
+#include <glass/View.h>
+
 #include <cstdio>
 #include <cstring>
 #include <vector>
@@ -20,20 +20,33 @@
 
 using namespace halsimgui;
 
+namespace {
+class TimingModel : public glass::Model {
+ public:
+  void Update() override {}
+  bool Exists() override { return true; }
+};
+}  // namespace
+
 static void DisplayTiming() {
   int32_t status = 0;
   uint64_t curTime = HAL_GetFPGATime(&status);
 
-  if (ImGui::Button("Run")) HALSIM_ResumeTiming();
+  if (ImGui::Button("Run")) {
+    HALSIM_ResumeTiming();
+  }
   ImGui::SameLine();
-  if (ImGui::Button("Pause")) HALSIM_PauseTiming();
+  if (ImGui::Button("Pause")) {
+    HALSIM_PauseTiming();
+  }
   ImGui::SameLine();
   ImGui::PushButtonRepeat(true);
   if (ImGui::Button("Step")) {
     HALSIM_PauseTiming();
     uint64_t nextTimeout = HALSIM_GetNextNotifierTimeout();
-    if (nextTimeout != UINT64_MAX)
+    if (nextTimeout != UINT64_MAX) {
       HALSIM_StepTimingAsync(nextTimeout - curTime);
+    }
   }
   ImGui::PopButtonRepeat();
   ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
@@ -46,16 +59,26 @@
     notifiers.resize(num);
     HALSIM_GetNotifierInfo(notifiers.data(), notifiers.size());
   }
-  if (num > 0) ImGui::Separator();
+  if (num > 0) {
+    ImGui::Separator();
+  }
   ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
-  for (int32_t i = 0; i < num; ++i)
+  for (int32_t i = 0; i < num; ++i) {
     ImGui::LabelText(notifiers[i].name, "%.3f",
                      notifiers[i].timeout / 1000000.0);
+  }
   ImGui::PopItemWidth();
 }
 
 void TimingGui::Initialize() {
-  HALSimGui::AddWindow("Timing", DisplayTiming,
-                       ImGuiWindowFlags_AlwaysAutoResize);
-  HALSimGui::SetDefaultWindowPos("Timing", 5, 150);
+  HALSimGui::halProvider.Register(
+      "Timing", [] { return true; },
+      [] { return std::make_unique<TimingModel>(); },
+      [](glass::Window* win, glass::Model* model) {
+        win->DisableRenamePopup();
+        win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
+        win->SetDefaultPos(5, 150);
+        return glass::MakeFunctionView(DisplayTiming);
+      });
+  HALSimGui::halProvider.ShowDefault("Timing");
 }
diff --git a/simulation/halsim_gui/src/main/native/cpp/TimingGui.h b/simulation/halsim_gui/src/main/native/cpp/TimingGui.h
index 49f33cc..e87b791 100644
--- a/simulation/halsim_gui/src/main/native/cpp/TimingGui.h
+++ b/simulation/halsim_gui/src/main/native/cpp/TimingGui.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_gui/src/main/native/cpp/main.cpp b/simulation/halsim_gui/src/main/native/cpp/main.cpp
index a904580..6df6f35 100644
--- a/simulation/halsim_gui/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_gui/src/main/native/cpp/main.cpp
@@ -1,76 +1,121 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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/Context.h>
+#include <glass/other/Plot.h>
+
+#include <cstdio>
+#include <string_view>
 
 #include <hal/Extensions.h>
 #include <hal/Main.h>
-#include <wpi/StringRef.h>
-#include <wpi/raw_ostream.h>
+#include <imgui.h>
+#include <wpigui.h>
 
-#include "AccelerometerGui.h"
+#include "AccelerometerSimGui.h"
 #include "AddressableLEDGui.h"
-#include "AnalogGyroGui.h"
-#include "AnalogInputGui.h"
-#include "AnalogOutGui.h"
-#include "CompressorGui.h"
-#include "DIOGui.h"
+#include "AnalogGyroSimGui.h"
+#include "AnalogInputSimGui.h"
+#include "AnalogOutputSimGui.h"
+#include "DIOSimGui.h"
 #include "DriverStationGui.h"
-#include "EncoderGui.h"
-#include "Field2D.h"
+#include "EncoderSimGui.h"
 #include "HALSimGui.h"
-#include "Mechanism2D.h"
-#include "NetworkTablesGui.h"
-#include "PDPGui.h"
-#include "PWMGui.h"
-#include "PlotGui.h"
-#include "RelayGui.h"
-#include "RoboRioGui.h"
+#include "NetworkTablesSimGui.h"
+#include "PCMSimGui.h"
+#include "PWMSimGui.h"
+#include "PowerDistributionSimGui.h"
+#include "RelaySimGui.h"
+#include "RoboRioSimGui.h"
 #include "SimDeviceGui.h"
-#include "SolenoidGui.h"
 #include "TimingGui.h"
 
 using namespace halsimgui;
 
+namespace gui = wpi::gui;
+
+static glass::PlotProvider gPlotProvider{"Plot"};
+
 extern "C" {
 #if defined(WIN32) || defined(_WIN32)
 __declspec(dllexport)
 #endif
     int HALSIM_InitExtension(void) {
-  HALSimGui::GlobalInit();
-  HALSimGui::Add(AccelerometerGui::Initialize);
-  HALSimGui::Add(AddressableLEDGui::Initialize);
-  HALSimGui::Add(AnalogGyroGui::Initialize);
-  HALSimGui::Add(AnalogInputGui::Initialize);
-  HALSimGui::Add(AnalogOutGui::Initialize);
-  HALSimGui::Add(CompressorGui::Initialize);
-  HALSimGui::Add(DriverStationGui::Initialize);
-  HALSimGui::Add(DIOGui::Initialize);
-  HALSimGui::Add(EncoderGui::Initialize);
-  HALSimGui::Add(Field2D::Initialize);
-  HALSimGui::Add(Mechanism2D::Initialize);
-  HALSimGui::Add(NetworkTablesGui::Initialize);
-  HALSimGui::Add(PDPGui::Initialize);
-  HALSimGui::Add(PlotGui::Initialize);
-  HALSimGui::Add(PWMGui::Initialize);
-  HALSimGui::Add(RelayGui::Initialize);
-  HALSimGui::Add(RoboRioGui::Initialize);
-  HALSimGui::Add(SimDeviceGui::Initialize);
-  HALSimGui::Add(SolenoidGui::Initialize);
-  HALSimGui::Add(TimingGui::Initialize);
+  std::puts("Simulator GUI Initializing.");
 
-  wpi::outs() << "Simulator GUI Initializing.\n";
-  if (!HALSimGui::Initialize()) return 0;
+  gui::CreateContext();
+  glass::CreateContext();
+  HALSimGui::GlobalInit();
+  DriverStationGui::GlobalInit();
+  gPlotProvider.GlobalInit();
+
+  // These need to initialize first
+  gui::AddInit(EncoderSimGui::Initialize);
+  gui::AddInit(SimDeviceGui::Initialize);
+
+  gui::AddInit(AccelerometerSimGui::Initialize);
+  gui::AddInit(AddressableLEDGui::Initialize);
+  gui::AddInit(AnalogGyroSimGui::Initialize);
+  gui::AddInit(AnalogInputSimGui::Initialize);
+  gui::AddInit(AnalogOutputSimGui::Initialize);
+  gui::AddInit(DIOSimGui::Initialize);
+  gui::AddInit(NetworkTablesSimGui::Initialize);
+  gui::AddInit(PCMSimGui::Initialize);
+  gui::AddInit(PowerDistributionSimGui::Initialize);
+  gui::AddInit(PWMSimGui::Initialize);
+  gui::AddInit(RelaySimGui::Initialize);
+  gui::AddInit(RoboRioSimGui::Initialize);
+  gui::AddInit(TimingGui::Initialize);
+
+  HALSimGui::mainMenu.AddMainMenu([] {
+    if (ImGui::BeginMenu("Hardware")) {
+      HALSimGui::halProvider.DisplayMenu();
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("NetworkTables")) {
+      NetworkTablesSimGui::DisplayMenu();
+      ImGui::Separator();
+      HALSimGui::ntProvider.DisplayMenu();
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("DS")) {
+      DriverStationGui::dsManager.DisplayMenu();
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("Plot")) {
+      bool paused = gPlotProvider.IsPaused();
+      if (ImGui::MenuItem("Pause All Plots", nullptr, &paused)) {
+        gPlotProvider.SetPaused(paused);
+      }
+      ImGui::Separator();
+      gPlotProvider.DisplayMenu();
+      ImGui::EndMenu();
+    }
+    if (ImGui::BeginMenu("Window")) {
+      HALSimGui::manager.DisplayMenu();
+      ImGui::EndMenu();
+    }
+  });
+
+  if (!gui::Initialize("Robot Simulation", 1280, 720)) {
+    return 0;
+  }
   HAL_RegisterExtensionListener(
       nullptr, [](void*, const char* name, void* data) {
-        if (wpi::StringRef{name} == "ds_socket") {
+        if (std::string_view{name} == "ds_socket") {
           DriverStationGui::SetDSSocketExtension(data);
         }
       });
-  HAL_SetMain(nullptr, HALSimGui::Main, HALSimGui::Exit);
-  wpi::outs() << "Simulator GUI Initialized!\n";
+  HAL_SetMain(
+      nullptr,
+      [](void*) {
+        gui::Main();
+        glass::DestroyContext();
+        gui::DestroyContext();
+      },
+      [](void*) { gui::Exit(); });
+  std::puts("Simulator GUI Initialized!");
 
   return 0;
 }
diff --git a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h b/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h
deleted file mode 100644
index 91d7301..0000000
--- a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h
+++ /dev/null
@@ -1,89 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <imgui.h>
-
-namespace halsimgui {
-
-class GuiDataSource;
-
-/**
- * DrawLEDs() configuration for 2D arrays.
- */
-struct LEDConfig {
-  /**
-   * Whether the major order is serpentined (e.g. the first row goes left to
-   * right, the second row right to left, the third row left to right, and so
-   * on).
-   */
-  bool serpentine = false;
-
-  /**
-   * The input array order (row-major or column-major).
-   */
-  enum Order { RowMajor = 0, ColumnMajor } order = RowMajor;
-
-  /**
-   * The starting location of the array (0 location).
-   */
-  enum Start {
-    UpperLeft = 0,
-    LowerLeft,
-    UpperRight,
-    LowerRight
-  } start = UpperLeft;
-};
-
-/**
- * Draw a 2D array of LEDs.
- *
- * Values are indices into colors array.  Positive values are filled (lit),
- * negative values are unfilled (dark / border only).  The actual color index
- * is the absolute value of the value - 1.  0 values are not drawn at all
- * (an empty space is left).
- *
- * @param values values array
- * @param numValues size of values array
- * @param cols number of columns
- * @param colors colors array
- * @param size size of each LED (both horizontal and vertical);
- *             if 0, defaults to 1/2 of font size
- * @param spacing spacing between each LED (both horizontal and vertical);
- *                if 0, defaults to 1/3 of font size
- * @param config 2D array configuration
- */
-void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors,
-              float size = 0.0f, float spacing = 0.0f,
-              const LEDConfig& config = LEDConfig{});
-
-/**
- * Draw a 2D array of LEDs.
- *
- * Values are indices into colors array.  Positive values are filled (lit),
- * negative values are unfilled (dark / border only).  The actual color index
- * is the absolute value of the value - 1.  0 values are not drawn at all
- * (an empty space is left).
- *
- * @param values values array
- * @param sources sources array
- * @param numValues size of values and sources arrays
- * @param cols number of columns
- * @param colors colors array
- * @param size size of each LED (both horizontal and vertical);
- *             if 0, defaults to 1/2 of font size
- * @param spacing spacing between each LED (both horizontal and vertical);
- *                if 0, defaults to 1/3 of font size
- * @param config 2D array configuration
- */
-void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues,
-                    int cols, const ImU32* colors, float size = 0.0f,
-                    float spacing = 0.0f,
-                    const LEDConfig& config = LEDConfig{});
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/GuiDataSource.h b/simulation/halsim_gui/src/main/native/include/HALDataSource.h
similarity index 69%
rename from simulation/halsim_gui/src/main/native/include/GuiDataSource.h
rename to simulation/halsim_gui/src/main/native/include/HALDataSource.h
index 117d6ea..c04f358 100644
--- a/simulation/halsim_gui/src/main/native/include/GuiDataSource.h
+++ b/simulation/halsim_gui/src/main/native/include/HALDataSource.h
@@ -1,91 +1,24 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <atomic>
-#include <string>
-
-#include <imgui.h>
-#include <wpi/Signal.h>
-#include <wpi/StringRef.h>
-#include <wpi/Twine.h>
-#include <wpi/spinlock.h>
-
-namespace halsimgui {
-
-/**
- * A data source.
- */
-class GuiDataSource {
- public:
-  explicit GuiDataSource(const wpi::Twine& id);
-  GuiDataSource(const wpi::Twine& id, int index);
-  GuiDataSource(const wpi::Twine& id, int index, int index2);
-  ~GuiDataSource();
-
-  GuiDataSource(const GuiDataSource&) = delete;
-  GuiDataSource& operator=(const GuiDataSource&) = delete;
-
-  const char* GetId() const { return m_id.c_str(); }
-
-  void SetName(const wpi::Twine& name) { m_name = name.str(); }
-  const char* GetName() const { return m_name.c_str(); }
-
-  void SetDigital(bool digital) { m_digital = digital; }
-  bool IsDigital() const { return m_digital; }
-
-  void SetValue(double value) {
-    m_value = value;
-    valueChanged(value);
-  }
-  double GetValue() const { return m_value; }
-
-  // drag source helpers
-  void LabelText(const char* label, const char* fmt, ...) const;
-  void LabelTextV(const char* label, const char* fmt, va_list args) const;
-  bool Combo(const char* label, int* current_item, const char* const items[],
-             int items_count, int popup_max_height_in_items = -1) const;
-  bool SliderFloat(const char* label, float* v, float v_min, float v_max,
-                   const char* format = "%.3f", float power = 1.0f) const;
-  bool InputDouble(const char* label, double* v, double step = 0.0,
-                   double step_fast = 0.0, const char* format = "%.6f",
-                   ImGuiInputTextFlags flags = 0) const;
-  bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100,
-                ImGuiInputTextFlags flags = 0) const;
-  void EmitDrag(ImGuiDragDropFlags flags = 0) const;
-
-  wpi::sig::SignalBase<wpi::spinlock, double> valueChanged;
-
-  static GuiDataSource* Find(wpi::StringRef id);
-
-  static wpi::sig::Signal<const char*, GuiDataSource*> sourceCreated;
-
- private:
-  std::string m_id;
-  std::string m_name;
-  bool m_digital = false;
-  std::atomic<double> m_value = 0;
-};
-
-}  // namespace halsimgui
+#include <glass/DataSource.h>
 
 #define HALSIMGUI_DATASOURCE(cbname, id, TYPE, vtype)                         \
-  class cbname##Source : public ::halsimgui::GuiDataSource {                  \
+  class cbname##Source : public ::glass::DataSource {                         \
    public:                                                                    \
     cbname##Source()                                                          \
-        : GuiDataSource(id),                                                  \
+        : DataSource(id),                                                     \
           m_callback{                                                         \
               HALSIM_Register##cbname##Callback(CallbackFunc, this, true)} {  \
       SetDigital(HAL_##TYPE == HAL_BOOLEAN);                                  \
     }                                                                         \
                                                                               \
     ~cbname##Source() {                                                       \
-      if (m_callback != 0) HALSIM_Cancel##cbname##Callback(m_callback);       \
+      if (m_callback != 0)                                                    \
+        HALSIM_Cancel##cbname##Callback(m_callback);                          \
     }                                                                         \
                                                                               \
    private:                                                                   \
@@ -108,10 +41,10 @@
   HALSIMGUI_DATASOURCE(cbname, id, INT, int)
 
 #define HALSIMGUI_DATASOURCE_INDEXED(cbname, id, TYPE, vtype)                 \
-  class cbname##Source : public ::halsimgui::GuiDataSource {                  \
+  class cbname##Source : public ::glass::DataSource {                         \
    public:                                                                    \
     explicit cbname##Source(int32_t index, int channel = -1)                  \
-        : GuiDataSource(id, channel < 0 ? index : channel),                   \
+        : DataSource(id, channel < 0 ? index : channel),                      \
           m_index{index},                                                     \
           m_channel{channel < 0 ? index : channel},                           \
           m_callback{HALSIM_Register##cbname##Callback(index, CallbackFunc,   \
@@ -147,10 +80,10 @@
   HALSIMGUI_DATASOURCE_INDEXED(cbname, id, DOUBLE, double)
 
 #define HALSIMGUI_DATASOURCE_INDEXED2(cbname, id, TYPE, vtype)                \
-  class cbname##Source : public ::halsimgui::GuiDataSource {                  \
+  class cbname##Source : public ::glass::DataSource {                         \
    public:                                                                    \
     explicit cbname##Source(int32_t index, int32_t channel)                   \
-        : GuiDataSource(id, index, channel),                                  \
+        : DataSource(id, index, channel),                                     \
           m_index{index},                                                     \
           m_channel{channel},                                                 \
           m_callback{HALSIM_Register##cbname##Callback(                       \
diff --git a/simulation/halsim_gui/src/main/native/include/HALProvider.h b/simulation/halsim_gui/src/main/native/include/HALProvider.h
new file mode 100644
index 0000000..e3098db
--- /dev/null
+++ b/simulation/halsim_gui/src/main/native/include/HALProvider.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 <glass/Model.h>
+#include <glass/Provider.h>
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace halsimgui {
+
+class HALProvider : public glass::Provider<> {
+ public:
+  explicit HALProvider(std::string_view iniName) : Provider{iniName} {}
+
+  void DisplayMenu() override;
+
+  glass::Model* GetModel(std::string_view name);
+
+  /**
+   * Returns true if outputs are disabled.
+   *
+   * @return true if outputs are disabled, false otherwise.
+   */
+  static bool AreOutputsDisabled();
+
+  /**
+   * Returns true if outputs are enabled.
+   *
+   * @return true if outputs are enabled, false otherwise.
+   */
+  static bool AreOutputsEnabled() { return !AreOutputsDisabled(); }
+
+ private:
+  void Update() override;
+
+  void Show(ViewEntry* entry, glass::Window* window) override;
+};
+
+}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/HALSimGui.h b/simulation/halsim_gui/src/main/native/include/HALSimGui.h
index 4a4c912..33ff242 100644
--- a/simulation/halsim_gui/src/main/native/include/HALSimGui.h
+++ b/simulation/halsim_gui/src/main/native/include/HALSimGui.h
@@ -1,150 +1,26 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
-#ifdef __cplusplus
-#include <functional>
-#endif
+#include <glass/MainMenuBar.h>
+#include <glass/WindowManager.h>
+#include <glass/networktables/NetworkTablesProvider.h>
 
-extern "C" {
-
-void HALSIMGUI_Add(void* param, void (*initialize)(void*));
-void HALSIMGUI_AddExecute(void* param, void (*execute)(void*));
-void HALSIMGUI_AddWindow(const char* name, void* param, void (*display)(void*),
-                         int32_t flags);
-void HALSIMGUI_AddMainMenu(void* param, void (*menu)(void*));
-void HALSIMGUI_AddOptionMenu(void* param, void (*menu)(void*));
-void HALSIMGUI_SetWindowVisibility(const char* name, int32_t visibility);
-void HALSIMGUI_SetDefaultWindowPos(const char* name, float x, float y);
-void HALSIMGUI_SetDefaultWindowSize(const char* name, float width,
-                                    float height);
-void HALSIMGUI_SetWindowPadding(const char* name, float x, float y);
-int HALSIMGUI_AreOutputsDisabled(void);
-
-}  // extern "C"
-
-#ifdef __cplusplus
+#include "HALProvider.h"
 
 namespace halsimgui {
 
 class HALSimGui {
  public:
   static void GlobalInit();
-  static bool Initialize();
-  static void Main(void*);
-  static void Exit(void*);
 
-  /**
-   * Adds feature to GUI.  The initialize function is called once, immediately
-   * after the GUI (both GLFW and Dear ImGui) are initialized.
-   *
-   * @param initialize initialization function
-   * @param execute frame execution function
-   */
-  static void Add(std::function<void()> initialize);
+  static glass::MainMenuBar mainMenu;
+  static glass::WindowManager manager;
 
-  /**
-   * Adds per-frame executor to GUI.  The passed function is called on each
-   * Dear ImGui frame prior to window and menu functions.
-   *
-   * @param execute frame execution function
-   */
-  static void AddExecute(std::function<void()> execute);
-
-  /**
-   * Adds window to GUI.  The display function is called from within a
-   * ImGui::Begin()/End() block.  While windows can be created within the
-   * execute function passed to AddExecute(), using this function ensures the
-   * windows are consistently integrated with the rest of the GUI.
-   *
-   * On each Dear ImGui frame, AddExecute() functions are always called prior
-   * to AddWindow display functions.  Note that windows may be shaded or
-   * completely hidden, in which case this function will not be called.
-   * It's important to perform any processing steps that must be performed
-   * every frame in the AddExecute() function.
-   *
-   * @param name name of the window (title bar)
-   * @param display window contents display function
-   * @param flags Dear ImGui window flags
-   */
-  static void AddWindow(const char* name, std::function<void()> display,
-                        int flags = 0);
-
-  /**
-   * Adds to GUI's main menu bar.  The menu function is called from within a
-   * ImGui::BeginMainMenuBar()/EndMainMenuBar() block.  Usually it's only
-   * appropriate to create a menu with ImGui::BeginMenu()/EndMenu() inside of
-   * this function.
-   *
-   * On each Dear ImGui frame, AddExecute() functions are always called prior
-   * to AddMainMenu menu functions.
-   *
-   * @param menu menu display function
-   */
-  static void AddMainMenu(std::function<void()> menu);
-
-  /**
-   * Adds to GUI's option menu.  The menu function is called from within a
-   * ImGui::BeginMenu()/EndMenu() block.  Usually it's only appropriate to
-   * create menu items inside of this function.
-   *
-   * On each Dear ImGui frame, AddExecute() functions are always called prior
-   * to AddMainMenu menu functions.
-   *
-   * @param menu menu display function
-   */
-  static void AddOptionMenu(std::function<void()> menu);
-
-  enum WindowVisibility { kHide = 0, kShow, kDisabled };
-
-  /**
-   * Sets visibility of window added with AddWindow().
-   *
-   * @param name window name (same as name passed to AddWindow())
-   * @param visibility 0=hide, 1=show, 2=disabled (force-hide)
-   */
-  static void SetWindowVisibility(const char* name,
-                                  WindowVisibility visibility);
-
-  /**
-   * Sets default position of window added with AddWindow().
-   *
-   * @param name window name (same as name passed to AddWindow())
-   * @param x x location of upper left corner
-   * @param y y location of upper left corner
-   */
-  static void SetDefaultWindowPos(const char* name, float x, float y);
-
-  /**
-   * Sets default size of window added with AddWindow().
-   *
-   * @param name window name (same as name passed to AddWindow())
-   * @param width width
-   * @param height height
-   */
-  static void SetDefaultWindowSize(const char* name, float width, float height);
-
-  /**
-   * Sets internal padding of window added with AddWindow().
-   * @param name window name (same as name passed to AddWindow())
-   * @param x horizontal padding
-   * @param y vertical padding
-   */
-  static void SetWindowPadding(const char* name, float x, float y);
-
-  /**
-   * Returns true if outputs are disabled.
-   *
-   * @return true if outputs are disabled, false otherwise.
-   */
-  static bool AreOutputsDisabled();
+  static HALProvider halProvider;
+  static glass::NetworkTablesProvider ntProvider;
 };
 
 }  // namespace halsimgui
-
-#endif  // __cplusplus
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaver.h b/simulation/halsim_gui/src/main/native/include/IniSaver.h
deleted file mode 100644
index a36fa8b..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaver.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <imgui.h>
-#include <imgui_internal.h>
-#include <wpi/DenseMap.h>
-
-namespace halsimgui {
-
-template <typename Info>
-class IniSaver {
- public:
-  explicit IniSaver(const char* typeName) : m_typeName(typeName) {}
-  void Initialize();
-
-  // pass through useful functions to map
-  Info& operator[](int index) { return m_map[index]; }
-
-  auto begin() { return m_map.begin(); }
-  auto end() { return m_map.end(); }
-  auto find(int index) { return m_map.find(index); }
-
-  auto begin() const { return m_map.begin(); }
-  auto end() const { return m_map.end(); }
-  auto find(int index) const { return m_map.find(index); }
-
- private:
-  static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                        const char* name);
-  static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       void* entry, const char* lineStr);
-  static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       ImGuiTextBuffer* out_buf);
-
-  const char* m_typeName;
-  wpi::DenseMap<int, Info> m_map;
-};
-
-}  // namespace halsimgui
-
-#include "IniSaver.inl"
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaver.inl b/simulation/halsim_gui/src/main/native/include/IniSaver.inl
deleted file mode 100644
index 007ad5a..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaver.inl
+++ /dev/null
@@ -1,56 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-template <typename Info>
-void IniSaver<Info>::Initialize() {
-  // hook ini handler to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = m_typeName;
-  iniHandler.TypeHash = ImHashStr(m_typeName);
-  iniHandler.ReadOpenFn = ReadOpen;
-  iniHandler.ReadLineFn = ReadLine;
-  iniHandler.WriteAllFn = WriteAll;
-  iniHandler.UserData = this;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-}
-
-template <typename Info>
-void* IniSaver<Info>::ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                               const char* name) {
-  auto self = static_cast<IniSaver*>(handler->UserData);
-  int num;
-  if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr;
-  return &self->m_map[num];
-}
-
-template <typename Info>
-void IniSaver<Info>::ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                              void* entry, const char* lineStr) {
-  auto element = static_cast<Info*>(entry);
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  element->ReadIni(name, value);
-}
-
-template <typename Info>
-void IniSaver<Info>::WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                              ImGuiTextBuffer* out_buf) {
-  auto self = static_cast<IniSaver*>(handler->UserData);
-  for (auto&& it : self->m_map) {
-    out_buf->appendf("[%s][%d]\n", self->m_typeName, it.first);
-    it.second.WriteIni(out_buf);
-    out_buf->append("\n");
-  }
-}
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h b/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h
deleted file mode 100644
index 25fbe58..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <imgui.h>
-#include <wpi/StringRef.h>
-
-namespace halsimgui {
-
-class NameInfo {
- public:
-  NameInfo() { m_name[0] = '\0'; }
-
-  bool HasName() const { return m_name[0] != '\0'; }
-  const char* GetName() const { return m_name; }
-  void GetName(char* buf, size_t size, const char* defaultName) const;
-  void GetName(char* buf, size_t size, const char* defaultName,
-               int index) const;
-  void GetName(char* buf, size_t size, const char* defaultName, int index,
-               int index2) const;
-  void GetLabel(char* buf, size_t size, const char* defaultName) const;
-  void GetLabel(char* buf, size_t size, const char* defaultName,
-                int index) const;
-  void GetLabel(char* buf, size_t size, const char* defaultName, int index,
-                int index2) const;
-
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out);
-  void PushEditNameId(int index);
-  void PushEditNameId(const char* name);
-  bool PopupEditName(int index);
-  bool PopupEditName(const char* name);
-  bool InputTextName(const char* label_id, ImGuiInputTextFlags flags = 0);
-
- private:
-  char m_name[64];
-};
-
-class OpenInfo {
- public:
-  OpenInfo() = default;
-  explicit OpenInfo(bool open) : m_open(open) {}
-
-  bool IsOpen() const { return m_open; }
-  void SetOpen(bool open) { m_open = open; }
-  bool ReadIni(wpi::StringRef name, wpi::StringRef value);
-  void WriteIni(ImGuiTextBuffer* out);
-
- private:
-  bool m_open = false;
-};
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverString.h b/simulation/halsim_gui/src/main/native/include/IniSaverString.h
deleted file mode 100644
index 206d695..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaverString.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <imgui.h>
-#include <imgui_internal.h>
-#include <wpi/StringMap.h>
-#include <wpi/StringRef.h>
-
-namespace halsimgui {
-
-template <typename Info>
-class IniSaverString {
- public:
-  explicit IniSaverString(const char* typeName) : m_typeName(typeName) {}
-  void Initialize();
-
-  // pass through useful functions to map
-  Info& operator[](wpi::StringRef key) { return m_map[key]; }
-
-  auto begin() { return m_map.begin(); }
-  auto end() { return m_map.end(); }
-  auto find(wpi::StringRef key) { return m_map.find(key); }
-
-  auto begin() const { return m_map.begin(); }
-  auto end() const { return m_map.end(); }
-  auto find(wpi::StringRef key) const { return m_map.find(key); }
-
- private:
-  static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                        const char* name);
-  static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       void* entry, const char* lineStr);
-  static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       ImGuiTextBuffer* out_buf);
-
-  const char* m_typeName;
-  wpi::StringMap<Info> m_map;
-};
-
-}  // namespace halsimgui
-
-#include "IniSaverString.inl"
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverString.inl b/simulation/halsim_gui/src/main/native/include/IniSaverString.inl
deleted file mode 100644
index 5ac7dc2..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaverString.inl
+++ /dev/null
@@ -1,57 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-template <typename Info>
-void IniSaverString<Info>::Initialize() {
-  // hook ini handler to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = m_typeName;
-  iniHandler.TypeHash = ImHashStr(m_typeName);
-  iniHandler.ReadOpenFn = ReadOpen;
-  iniHandler.ReadLineFn = ReadLine;
-  iniHandler.WriteAllFn = WriteAll;
-  iniHandler.UserData = this;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-}
-
-template <typename Info>
-void* IniSaverString<Info>::ReadOpen(ImGuiContext* ctx,
-                                     ImGuiSettingsHandler* handler,
-                                     const char* name) {
-  auto self = static_cast<IniSaverString*>(handler->UserData);
-  return &self->m_map[name];
-}
-
-template <typename Info>
-void IniSaverString<Info>::ReadLine(ImGuiContext* ctx,
-                                    ImGuiSettingsHandler* handler, void* entry,
-                                    const char* lineStr) {
-  auto element = static_cast<Info*>(entry);
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  element->ReadIni(name, value);
-}
-
-template <typename Info>
-void IniSaverString<Info>::WriteAll(ImGuiContext* ctx,
-                                    ImGuiSettingsHandler* handler,
-                                    ImGuiTextBuffer* out_buf) {
-  auto self = static_cast<IniSaverString*>(handler->UserData);
-  for (auto&& it : self->m_map) {
-    out_buf->appendf("[%s][%s]\n", self->m_typeName, it.getKey().data());
-    it.second.WriteIni(out_buf);
-    out_buf->append("\n");
-  }
-}
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.h b/simulation/halsim_gui/src/main/native/include/IniSaverVector.h
deleted file mode 100644
index 0816933..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaverVector.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <vector>
-
-#include <imgui.h>
-#include <imgui_internal.h>
-
-namespace halsimgui {
-
-template <typename Info>
-class IniSaverVector : public std::vector<Info> {
- public:
-  explicit IniSaverVector(const char* typeName) : m_typeName(typeName) {}
-  void Initialize();
-
- private:
-  static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                        const char* name);
-  static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       void* entry, const char* lineStr);
-  static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
-                       ImGuiTextBuffer* out_buf);
-
-  const char* m_typeName;
-};
-
-}  // namespace halsimgui
-
-#include "IniSaverVector.inl"
diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl b/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl
deleted file mode 100644
index b2979bc..0000000
--- a/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl
+++ /dev/null
@@ -1,60 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-namespace halsimgui {
-
-template <typename Info>
-void IniSaverVector<Info>::Initialize() {
-  // hook ini handler to save settings
-  ImGuiSettingsHandler iniHandler;
-  iniHandler.TypeName = m_typeName;
-  iniHandler.TypeHash = ImHashStr(m_typeName);
-  iniHandler.ReadOpenFn = ReadOpen;
-  iniHandler.ReadLineFn = ReadLine;
-  iniHandler.WriteAllFn = WriteAll;
-  iniHandler.UserData = this;
-  ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
-}
-
-template <typename Info>
-void* IniSaverVector<Info>::ReadOpen(ImGuiContext* ctx,
-                                     ImGuiSettingsHandler* handler,
-                                     const char* name) {
-  auto self = static_cast<IniSaverVector*>(handler->UserData);
-  unsigned int num;
-  if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr;
-  if (num >= self->size()) self->resize(num + 1);
-  return &(*self)[num];
-}
-
-template <typename Info>
-void IniSaverVector<Info>::ReadLine(ImGuiContext* ctx,
-                                    ImGuiSettingsHandler* handler, void* entry,
-                                    const char* lineStr) {
-  auto element = static_cast<Info*>(entry);
-  wpi::StringRef line{lineStr};
-  auto [name, value] = line.split('=');
-  name = name.trim();
-  value = value.trim();
-  element->ReadIni(name, value);
-}
-
-template <typename Info>
-void IniSaverVector<Info>::WriteAll(ImGuiContext* ctx,
-                                    ImGuiSettingsHandler* handler,
-                                    ImGuiTextBuffer* out_buf) {
-  auto self = static_cast<IniSaverVector*>(handler->UserData);
-  for (size_t i = 0; i < self->size(); ++i) {
-    out_buf->appendf("[%s][%d]\n", self->m_typeName, static_cast<int>(i));
-    (*self)[i].WriteIni(out_buf);
-    out_buf->append("\n");
-  }
-}
-
-}  // namespace halsimgui
diff --git a/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h b/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h
index 8c24cc9..84f432c 100644
--- a/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h
+++ b/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h
@@ -1,109 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2019-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <stdint.h>
+#include <hal/SimDevice.h>
 
-#include <hal/Types.h>
-#include <hal/Value.h>
-
-#ifdef __cplusplus
-#include <functional>
-
-#include <imgui.h>
-#endif
-
-extern "C" {
-
-void HALSIMGUI_DeviceTreeHide(const char* name);
-void HALSIMGUI_DeviceTreeAdd(void* param, void (*execute)(void*));
-HAL_Bool HALSIMGUI_DeviceTreeDisplayValue(const char* name, HAL_Bool readonly,
-                                          struct HAL_Value* value,
-                                          const char** options,
-                                          int32_t numOptions);
-HAL_Bool HALSIMGUI_DeviceTreeStartDevice(const char* label, int32_t flags);
-void HALSIMGUI_DeviceTreeFinishDevice(void);
-
-}  // extern "C"
-
-#ifdef __cplusplus
+namespace glass {
+class DataSource;
+class DeviceTreeModel;
+}  // namespace glass
 
 namespace halsimgui {
 
-class GuiDataSource;
-
 class SimDeviceGui {
  public:
   static void Initialize();
-
-  /**
-   * Hides device on tree.
-   *
-   * @param name device name
-   */
-  static void Hide(const char* name);
-
-  /**
-   * Adds device to tree.  The execute function is called from within the
-   * device tree window context on every frame, so it should implement an
-   * TreeNodeEx() block for each device to display.
-   *
-   * @param execute execute function
-   */
-  static void Add(std::function<void()> execute);
-
-  /**
-   * Displays device value formatted the same way as SimDevice device values.
-   *
-   * @param name value name
-   * @param readonly prevent value from being modified by the user
-   * @param value value contents (modified in place)
-   * @param options options array for enum values
-   * @param numOptions size of options array for enum values
-   * @return True if value was modified by the user
-   */
-  static bool DisplayValue(const char* name, bool readonly, HAL_Value* value,
-                           const char** options = nullptr,
-                           int32_t numOptions = 0);
-
-  /**
-   * Displays device value formatted the same way as SimDevice device values.
-   *
-   * @param name value name
-   * @param readonly prevent value from being modified by the user
-   * @param value value contents (modified in place)
-   * @param source data source (may be nullptr)
-   * @param options options array for enum values
-   * @param numOptions size of options array for enum values
-   * @return True if value was modified by the user
-   */
-  static bool DisplayValueSource(const char* name, bool readonly,
-                                 HAL_Value* value, const GuiDataSource* source,
-                                 const char** options = nullptr,
-                                 int32_t numOptions = 0);
-
-  /**
-   * Wraps ImGui::CollapsingHeader() to provide consistency and open
-   * persistence.  As with the ImGui function, returns true if the tree node
-   * is expanded.  If returns true, call StopDevice() to finish the block.
-   *
-   * @param label label
-   * @param flags ImGuiTreeNodeFlags flags
-   * @return True if expanded
-   */
-  static bool StartDevice(const char* label, ImGuiTreeNodeFlags flags = 0);
-
-  /**
-   * Finish a device block started with StartDevice().
-   */
-  static void FinishDevice();
+  static glass::DataSource* GetValueSource(HAL_SimValueHandle handle);
+  static glass::DeviceTreeModel& GetDeviceTree();
 };
 
 }  // namespace halsimgui
-
-#endif  // __cplusplus
diff --git a/simulation/halsim_gui/src/test/native/cpp/main.cpp b/simulation/halsim_gui/src/test/native/cpp/main.cpp
index c6b6c58..6aea19a 100644
--- a/simulation/halsim_gui/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_gui/src/test/native/cpp/main.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2015-2019 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <hal/HALBase.h>
 
diff --git a/simulation/halsim_ws_client/CMakeLists.txt b/simulation/halsim_ws_client/CMakeLists.txt
index d869f28..5bc99db 100644
--- a/simulation/halsim_ws_client/CMakeLists.txt
+++ b/simulation/halsim_ws_client/CMakeLists.txt
@@ -4,7 +4,7 @@
 
 file(GLOB halsim_ws_client_src src/main/native/cpp/*.cpp)
 
-add_library(halsim_ws_client MODULE ${halsim_ws_client_src})
+add_library(halsim_ws_client SHARED ${halsim_ws_client_src})
 wpilib_target_warnings(halsim_ws_client)
 set_target_properties(halsim_ws_client PROPERTIES DEBUG_POSTFIX "d")
 target_link_libraries(halsim_ws_client PUBLIC hal halsim_ws_core)
diff --git a/simulation/halsim_ws_client/README.md b/simulation/halsim_ws_client/README.md
new file mode 100644
index 0000000..c5111be
--- /dev/null
+++ b/simulation/halsim_ws_client/README.md
@@ -0,0 +1,13 @@
+# HAL WebSockets Client
+
+This is an extension that provides a client version of a WebSockets API for transmitting robot hardware interface state over a network.  See the [Robot Hardware Interface WebSockets API specification](../halsim_ws_core/doc/hardware_ws_api.md) for more details on the protocol.
+
+## Configuration
+
+The WebSockets client has a number of configuration options available through environment variables.
+
+``HALSIMWS_HOST``: The host to connect to.  Defaults to localhost.
+
+``HALSIMWS_PORT``: The port number to connect to.  Defaults to 3300.
+
+``HALSIMWS_URI``: The URI path to connect to.  Defaults to ``"/wpilibws"``.
diff --git a/simulation/halsim_ws_client/build.gradle b/simulation/halsim_ws_client/build.gradle
index c0137a4..7d32586 100644
--- a/simulation/halsim_ws_client/build.gradle
+++ b/simulation/halsim_ws_client/build.gradle
@@ -28,7 +28,6 @@
                 }
 
                 lib project: ":simulation:halsim_ws_core", library: "halsim_ws_core", linkage: "static"
-
             }
         }
     }
diff --git a/simulation/halsim_ws_client/src/dev/native/cpp/main.cpp b/simulation/halsim_ws_client/src/dev/native/cpp/main.cpp
index 1efcefc..cf32a3f 100644
--- a/simulation/halsim_ws_client/src/dev/native/cpp/main.cpp
+++ b/simulation/halsim_ws_client/src/dev/native/cpp/main.cpp
@@ -1,13 +1,10 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <iostream>
 #include <thread>
 
+#include <fmt/core.h>
 #include <hal/DriverStation.h>
 #include <hal/HALBase.h>
 #include <hal/Main.h>
@@ -24,9 +21,9 @@
   while (cycleCount < 100) {
     std::this_thread::sleep_for(std::chrono::milliseconds(100));
     cycleCount++;
-    std::cout << "Count: " << cycleCount << std::endl;
+    fmt::print("Count: {}\n", cycleCount);
   }
 
-  std::cout << "DONE" << std::endl;
+  fmt::print("DONE\n");
   HAL_ExitMain();
 }
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
index 0b40a61..9c23885 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWS.cpp
@@ -1,14 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimWS.h"
 
+#include <cstdio>
+
+#include <fmt/format.h>
 #include <wpi/SmallString.h>
-#include <wpi/raw_ostream.h>
 #include <wpi/uv/util.h>
 
 #include "HALSimWSClientConnection.h"
@@ -25,7 +24,7 @@
       m_providers(providers),
       m_simDevicesProvider(simDevicesProvider) {
   m_loop.error.connect([](uv::Error err) {
-    wpi::errs() << "HALSim WS Client libuv Error: " << err.str() << "\n";
+    fmt::print(stderr, "HALSim WS Client libuv Error: {}\n", err.str());
   });
 
   m_tcp_client = uv::Tcp::Create(m_loop);
@@ -42,26 +41,26 @@
   }
 
   const char* host = std::getenv("HALSIMWS_HOST");
-  if (host != NULL) {
+  if (host != nullptr) {
     m_host = host;
   } else {
     m_host = "localhost";
   }
 
   const char* port = std::getenv("HALSIMWS_PORT");
-  if (port != NULL) {
+  if (port != nullptr) {
     try {
       m_port = std::stoi(port);
     } catch (const std::invalid_argument& err) {
-      wpi::errs() << "Error decoding HALSIMWS_PORT (" << err.what() << ")\n";
+      fmt::print(stderr, "Error decoding HALSIMWS_PORT ({})\n", err.what());
       return false;
     }
   } else {
-    m_port = 8080;
+    m_port = 3300;
   }
 
   const char* uri = std::getenv("HALSIMWS_URI");
-  if (uri != NULL) {
+  if (uri != nullptr) {
     m_uri = uri;
   } else {
     m_uri = "/wpilibws";
@@ -86,13 +85,12 @@
         m_connect_timer->Start(uv::Timer::Time(kTcpConnectAttemptTimeout));
       });
 
-  m_tcp_client->closed.connect(
-      []() { wpi::outs() << "TCP connection closed\n"; });
+  m_tcp_client->closed.connect([]() { std::puts("TCP connection closed"); });
 
   // Set up the connection timer
-  wpi::outs() << "HALSimWS Initialized\n";
-  wpi::outs() << "Will attempt to connect to ws://" << m_host << ":" << m_port
-              << m_uri << "\n";
+  std::puts("HALSimWS Initialized");
+  fmt::print("Will attempt to connect to ws://{}:{}{}\n", m_host, m_port,
+             m_uri);
 
   // Set up the timer to attempt connection
   m_connect_timer->timeout.connect([this] { AttemptConnect(); });
@@ -105,7 +103,7 @@
 void HALSimWS::AttemptConnect() {
   m_connect_attempts++;
 
-  wpi::outs() << "Connection Attempt " << m_connect_attempts << "\n";
+  fmt::print("Connection Attempt {}\n", m_connect_attempts);
 
   struct sockaddr_in dest;
   uv::NameToAddr(m_host, m_port, &dest);
@@ -170,6 +168,6 @@
       provider->OnNetValueChanged(msg.at("data"));
     }
   } catch (wpi::json::exception& e) {
-    wpi::errs() << "Error with incoming message: " << e.what() << "\n";
+    fmt::print(stderr, "Error with incoming message: {}\n", e.what());
   }
 }
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClient.cpp b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClient.cpp
index f019e27..dfa54d2 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClient.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClient.cpp
@@ -1,22 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimWSClient.h"
 
 #include <WSProviderContainer.h>
+#include <WSProvider_AddressableLED.h>
 #include <WSProvider_Analog.h>
+#include <WSProvider_BuiltInAccelerometer.h>
 #include <WSProvider_DIO.h>
 #include <WSProvider_DriverStation.h>
 #include <WSProvider_Encoder.h>
 #include <WSProvider_Joystick.h>
+#include <WSProvider_PCM.h>
 #include <WSProvider_PWM.h>
 #include <WSProvider_Relay.h>
 #include <WSProvider_RoboRIO.h>
 #include <WSProvider_SimDevice.h>
+#include <WSProvider_Solenoid.h>
 #include <WSProvider_dPWM.h>
 #include <wpi/EventLoopRunner.h>
 
@@ -36,16 +37,20 @@
       providers.Add(key, provider);
     };
 
+    HALSimWSProviderAddressableLED::Initialize(registerFunc);
     HALSimWSProviderAnalogIn::Initialize(registerFunc);
     HALSimWSProviderAnalogOut::Initialize(registerFunc);
+    HALSimWSProviderBuiltInAccelerometer::Initialize(registerFunc);
     HALSimWSProviderDIO::Initialize(registerFunc);
     HALSimWSProviderDigitalPWM::Initialize(registerFunc);
     HALSimWSProviderDriverStation::Initialize(registerFunc);
     HALSimWSProviderEncoder::Initialize(registerFunc);
     HALSimWSProviderJoystick::Initialize(registerFunc);
+    HALSimWSProviderPCM::Initialize(registerFunc);
     HALSimWSProviderPWM::Initialize(registerFunc);
     HALSimWSProviderRelay::Initialize(registerFunc);
     HALSimWSProviderRoboRIO::Initialize(registerFunc);
+    HALSimWSProviderSolenoid::Initialize(registerFunc);
 
     simDevices.Initialize(loop);
 
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
index 5cfec5c..22dadb1 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/HALSimWSClientConnection.cpp
@@ -1,13 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimWSClientConnection.h"
 
-#include <wpi/raw_ostream.h>
+#include <cstdio>
+
+#include <fmt/format.h>
 #include <wpi/raw_uv_ostream.h>
 
 #include "HALSimWS.h"
@@ -20,29 +19,29 @@
   // Get a shared pointer to ourselves
   auto self = this->shared_from_this();
 
-  auto ws =
-      wpi::WebSocket::CreateClient(*m_stream, m_client->GetTargetUri(),
-                                   wpi::Twine{m_client->GetTargetHost()} + ":" +
-                                       wpi::Twine{m_client->GetTargetPort()});
+  auto ws = wpi::WebSocket::CreateClient(
+      *m_stream, m_client->GetTargetUri(),
+      fmt::format("{}:{}", m_client->GetTargetHost(),
+                  m_client->GetTargetPort()));
 
   ws->SetData(self);
 
   m_websocket = ws.get();
 
   // Hook up events
-  m_websocket->open.connect_extended([this](auto conn, wpi::StringRef) {
+  m_websocket->open.connect_extended([this](auto conn, auto) {
     conn.disconnect();
 
     if (!m_client->RegisterWebsocket(shared_from_this())) {
-      wpi::errs() << "Unable to register websocket\n";
+      std::fputs("Unable to register websocket\n", stderr);
       return;
     }
 
     m_ws_connected = true;
-    wpi::outs() << "HALSimWS: WebSocket Connected\n";
+    std::puts("HALSimWS: WebSocket Connected");
   });
 
-  m_websocket->text.connect([this](wpi::StringRef msg, bool) {
+  m_websocket->text.connect([this](auto msg, bool) {
     if (!m_ws_connected) {
       return;
     }
@@ -53,7 +52,7 @@
     } catch (const wpi::json::parse_error& e) {
       std::string err("JSON parse failed: ");
       err += e.what();
-      wpi::errs() << err << "\n";
+      fmt::print(stderr, "{}\n", err);
       m_websocket->Fail(1003, err);
       return;
     }
@@ -61,9 +60,9 @@
     m_client->OnNetValueChanged(j);
   });
 
-  m_websocket->closed.connect([this](uint16_t, wpi::StringRef) {
+  m_websocket->closed.connect([this](uint16_t, auto) {
     if (m_ws_connected) {
-      wpi::outs() << "HALSimWS: Websocket Disconnected\n";
+      std::puts("HALSimWS: Websocket Disconnected");
       m_ws_connected = false;
 
       m_client->CloseWebsocket(shared_from_this());
@@ -93,8 +92,8 @@
                                   }
 
                                   if (err) {
-                                    wpi::errs() << err.str() << "\n";
-                                    wpi::errs().flush();
+                                    fmt::print(stderr, "{}\n", err.str());
+                                    std::fflush(stderr);
                                   }
                                 });
   });
diff --git a/simulation/halsim_ws_client/src/main/native/cpp/main.cpp b/simulation/halsim_ws_client/src/main/native/cpp/main.cpp
index 1112e61..8e2e2fa 100644
--- a/simulation/halsim_ws_client/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_ws_client/src/main/native/cpp/main.cpp
@@ -1,14 +1,11 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <cstdio>
 #include <memory>
 
 #include <hal/Extensions.h>
-#include <wpi/raw_ostream.h>
 
 #include "HALSimWSClient.h"
 
@@ -22,7 +19,7 @@
 #endif
 
     int HALSIM_InitExtension(void) {
-  wpi::outs() << "HALSim WS Client Extension Initializing\n";
+  std::puts("HALSim WS Client Extension Initializing");
 
   HAL_OnShutdown(nullptr, [](void*) { gClient.reset(); });
 
@@ -31,7 +28,7 @@
     return -1;
   }
 
-  wpi::outs() << "HALSim WS Client Extension Initialized\n";
+  std::puts("HALSim WS Client Extension Initialized");
   return 0;
 }
 
diff --git a/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h b/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
index e126ac1..9563482 100644
--- a/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
+++ b/simulation/halsim_ws_client/src/main/native/include/HALSimWS.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -44,8 +41,8 @@
 
   void OnNetValueChanged(const wpi::json& msg);
 
-  wpi::StringRef GetTargetHost() const { return m_host; }
-  wpi::StringRef GetTargetUri() const { return m_uri; }
+  const std::string& GetTargetHost() const { return m_host; }
+  const std::string& GetTargetUri() const { return m_uri; }
   int GetTargetPort() const { return m_port; }
   wpi::uv::Loop& GetLoop() { return m_loop; }
 
diff --git a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClient.h b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClient.h
index 5812140..f048b40 100644
--- a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClient.h
+++ b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClient.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
index 1bd23e9..dddd885 100644
--- a/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
+++ b/simulation/halsim_ws_client/src/main/native/include/HALSimWSClientConnection.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_ws_core/README.md b/simulation/halsim_ws_core/README.md
new file mode 100644
index 0000000..ae15e0e
--- /dev/null
+++ b/simulation/halsim_ws_core/README.md
@@ -0,0 +1,5 @@
+# HAL WebSockets Core
+
+This is the common WebSockets implementation shared by the [HAL WebSockets Client](../halsim_ws_client/) and [HAL WebSockets Server](../halsim_ws_server/) extensions.  It is not a standalone extension.
+
+These extensions provide a WebSockets API for transmitting robot hardware interface state over a network and implement the [Robot Hardware Interface WebSockets API specification](doc/hardware_ws_api.md).  See the specification for more details on the protocol.
diff --git a/simulation/halsim_ws_core/build.gradle b/simulation/halsim_ws_core/build.gradle
index ccf02ce..8e1f7ab 100644
--- a/simulation/halsim_ws_core/build.gradle
+++ b/simulation/halsim_ws_core/build.gradle
@@ -23,6 +23,7 @@
     apply from: "${rootDir}/shared/googletest.gradle"
 
     apply from: "${rootDir}/shared/config.gradle"
+    apply from: "${rootDir}/shared/plugins/publish.gradle"
 
     model {
         components {
@@ -41,7 +42,6 @@
                 binaries.all {
                     project(':hal').addHalDependency(it, 'shared')
                     lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
-
                 }
                 appendDebugPathToBinaries(binaries)
             }
diff --git a/simulation/halsim_ws_core/doc/hardware_ws_api.md b/simulation/halsim_ws_core/doc/hardware_ws_api.md
new file mode 100644
index 0000000..b2b40fc
--- /dev/null
+++ b/simulation/halsim_ws_core/doc/hardware_ws_api.md
@@ -0,0 +1,342 @@
+# Robot Hardware Interface WebSockets API Specification
+
+- [Summary](#summary)
+- [Motivation](#motivation)
+- [References](#references)
+- [Design](#design)
+  - [WebSockets Protocol Configuration](#websockets-protocol-configuration)
+  - [Text Data Frames](#text-data-frames)
+  - [Robot Program Behavior](#robot-program-behavior)
+  - [Hardware Behavior](#hardware-behavior)
+  - [Hardware Messages](#hardware-messages)
+  - [CAN Messages](#can-messages)
+  - [RoboRIO Messages](#roborio-messages)
+  - [Other Device Messages ("SimDevice")](#other-device-messages-simdevice)
+
+## Summary
+
+A WebSockets API for transmitting robot hardware interface state over a network.  In typical use, one end of the connection is a robot program running in a desktop environment, and the other end of the connection is either a simulation engine, physical robot, or simulation dashboard GUI.
+
+## Motivation
+
+Provide a standard interface for 3rd party software to easily interface with a robot program running on a desktop computer.  Currently this is possible in WPILib only by writing a custom C++ simulation plugin.  Providing a standard text-based network interface to the "hardware" interface layer of a robot program lowers the barrier to entry for this type of development and enables unique capabilities such as direct control of a simple physical robot over a wireless link.
+
+## References
+
+- [RFC 6455](https://tools.ietf.org/html/rfc6455) The WebSocket Protocol
+- [RFC 7159](https://tools.ietf.org/html/rfc7159) The JavaScript Object Notation (JSON) Data Interchange Format
+
+## Design
+
+The messages in the protocol are based around typical representations of electronic physical devices, rather than higher level abstractions.  As such, the messages e.g. represent an analog input as a voltage, rather than a “potentiometer” device.  However, the “SimDevice” message can be used to communicate a higher level of abstraction device such as a gyro.  What this means is that simulation engines are responsible for implementing the mapping from their system knowledge (e.g. a joint angle) into an electronic value (e.g. an analog voltage).  See the Trades section for more discussion.
+
+### WebSockets Protocol Configuration
+
+Binary WebSocket frames are not used.  Text WebSocket frames are JSON messages for human readability and ease of debugging.
+
+Both clients and servers shall support unsecure connections (``ws:``) and may support secure connections (``wss:``).  In a trusted network environment (e.g. a robot network), clients that support secure connections should fall back to an unsecure connection if a secure connection is not available.
+
+The resource name for the websockets connection is ``/wpilibws``.  Servers shall reject a second connection to the same resource location as a currently active connection, but may support multiple connections via additional resource names; the resource name is used to prevent duplicate client connections (such as when a web browser is used).  Servers may support multiplexed HTTP file serving on the same port.
+
+The unsecure standard server port number shall be 3300.
+
+### Text Data Frames
+
+Each WebSockets text data frame shall consist of a single JSON object (“message“).
+
+Each message shall be a JSON object with three keys: a ``"type"`` key and lowercase string value describing the type of message, a ``"device"`` key and string value identifying the device, and a ``"data"`` key containing the message data as a JSON object.
+
+The contents of the data object depends on the message type; see the sections for each message for details for the standard message types.  The contents of the data object shall be transmitted as deltas (e.g. the message should only contain the values actually being changed).  Clients and servers are free to ignore data values they don’t find useful, and/or transmit additional data values not specified here.  Clients and servers shall ignore data values they don’t understand.
+
+Data keys have a prefix of either ``"<"``, ``">"``, or ``"<>"``.  This indicates whether the data value is an output from robot code (``"<"``), an input to the robot code (``">"``), or both (``"<>"``).
+
+Clients and servers shall ignore JSON messages that:
+
+* are not objects
+* have no ``"type"`` key, ``"device"`` key, or ``"data"`` key
+* have a ``"type"`` or ``"device"`` value that is not a string
+* have a ``"data"`` value that is not an object
+* have a ``"type"`` value that the client or server does not recognize
+
+### Robot Program Behavior
+
+The robot program may operate as either a client or a server.  Generally, the robot program only pays attention to data values with ``">"`` or ``"<>"`` prefixes in received messages.
+
+Upon initial connection, the robot program shall send a message for every initialized device in the program with the current state of that device (both input and output values).  When a robot program removes a device, it shall send a message for that device with ``"<init"`` = false.
+
+For example, if a robot program has an analog input configured for port 1, it will send a message upon initial connection with type ``"AI"``, device ``"1"``, ``"<init"`` true, and ``">voltage"`` set to an indeterminate value.  The remote "hardware" would send messages with type ``"AI"``, device ``"1"``, and ``">voltage"`` set to the (simulated or real) voltage.  When the robot program reads the voltage, it will read the last received ``">voltage"`` value.
+
+The initial state includes joystick and driver station state.
+
+### “Hardware“ Behavior
+
+The “hardware“ (which might be a full-fledged 3D simulation engine, a physical robot, or an interactive GUI) is responsible for mapping the robot program’s inputs and outputs into the real (or virtual) world.  For example, a robot program’s Analog Input 1 might show up as simply ``Analog Input #1`` on a GUI, connect to analog input port #1 on a physical robot, or map to a virtual potentiometer in a 3D simulation engine.
+
+### Hardware Messages
+
+| Type value              | Description                | Device value              |
+| ----------------------- | -------------------------- | ------------------------- |
+| [``"Accel"``][]         | Accelerometer              | Arbitrary device name     |
+| [``"AI"``][]            | Analog input               | Port index, e.g. "1", "2" |
+| [``"DIO"``][]           | Digital input/output       | Port index, e.g. "1", "2" |
+| [``"dPWM"``][]          | Duty cycle output          | Arbitrary device number   |
+| [``"DriverStation"``][] | Driver station / FMS state | Blank                     |
+| [``"DutyCycle"``][]     | Duty cycle input           | Arbitrary device name     |
+| [``"Encoder"``][]       | Quadrature encoder         | Arbitrary device number   |
+| [``"Gyro"``][]          | Gyro                       | Arbitrary device name     |
+| [``"Joystick"``][]      | Joystick data              | Joystick number           |
+| [``"PWM"``][]           | PWM output                 | Port index, e.g. "1", "2" |
+| [``"Relay"``][]         | Relay output               | Port index, e.g. "1", "2" |
+| [``"Solenoid"``][]      | Solenoid output            | Module +Port index, e.g. "0,1", "2,5" |
+
+#### Accelerometer ("Accel")
+
+[``"Accel"``]:#accelerometer-accel
+
+A 3-axis accelerometer.
+
+C++/Java implementation note: these are created as either BuiltInAccelerometer or SimDevice nodes where the device name is prefixed by ``"Accel:"``, for example ``"Accel:ADXL362[1]"``.  The BuiltInAccelerometer uses a device name of ``"BuiltInAccel"``.
+
+| Data Key     | Type    | Description                                          |
+| ------------ | ------- | ---------------------------------------------------- |
+| ``"<init"``  | Boolean | If accelerometer is initialized in the robot program |
+| ``"<range"`` | Float   | Desired range in G’s                                 |
+| ``">x"``     | Float   | Acceleration in G’s                                  |
+| ``">y"``     | Float   | Acceleration in G’s                                  |
+| ``">z"``     | Float   | Acceleration in G’s                                  |
+
+#### Analog Input ("AI")
+
+[``"AI"``]:#analog-input-ai
+
+The basic analog input just reads a voltage.
+
+| Data Key       | Type    | Description                                         |
+| -------------- | ------- | --------------------------------------------------- |
+| ``"<init"``    | Boolean | If analog input is initialized in the robot program |
+| ``">voltage"`` | Float   | Input voltage, in volts                             |
+
+#### Digital Input/Output ("DIO")
+
+[``"DIO"``]:#digital-inputoutput-dio
+
+| Data Key      | Type    | Description                                |
+| ------------- | ------- | ------------------------------------------ |
+| ``"<init"``   | Boolean | If DIO is initialized in the robot program |
+| ``"<input"``  | Boolean | True if input, false if output             |
+| ``"<>value"`` | Boolean | Input or output state                      |
+
+#### Duty Cycle Output ("dPWM")
+
+[``"dPWM"``]:#duty-cycle-output-dpwm
+
+| Data Key          | Type    | Description                                   |
+| ----------------- | ------- | --------------------------------------------- |
+| ``"<init"``       | Boolean | If output is initialized in the robot program |
+| ``"<duty_cycle"`` | Float   | Duty cycle % (0.0 to 1.0)                     |
+| ``"<dio_pin"``    | Integer | DIO pin number                                |
+
+#### Driver Station ("DriverStation")
+
+[``"DriverStation"``]:#driver-station-driverstation
+
+| Data Key          | Type    | Description                                      |
+| ----------------- | ------- | ------------------------------------------------ |
+| ``">new_data"``   | Boolean | One shot.  If set to true in a message, notifies the robot program that new DS and Joystick data is available. |
+| ``">enabled"``    | Boolean | True to enable the robot program |
+| ``">autonomous"`` | Boolean | True for autonomous mode; false for teleoperated mode |
+| ``">test"``       | Boolean | True for test mode; false for other modes |
+| ``">estop"``      | Boolean | True to emergency stop (no motor outputs) |
+| ``">fms"``        | Boolean | True if the DS is connected to a Field Management System (FMS) |
+| ``">ds"``         | Boolean | True if a DS application is connected |
+| ``">station"``    | String  | Station color and number; supported values are ``"red1"``, ``"red2"``, ``"red3"``, ``"blue1"``, ``"blue2"``, ``"blue3"``. |
+| ``">match_time"`` | Float   | Match time countdown, in seconds, for each match period (e.g. for 15 second period, starts at 15 and counts down to 0).  If not in a match, -1. |
+| ``">game_data"``  | String  | Game-specific data; arbitrary string contents |
+
+#### Duty Cycle Input ("DutyCycle")
+
+[``"DutyCycle"``]:#duty-cycle-input-dutycycle
+
+Duty Cycle inputs are commonly used for absolute encoders.  The position is accumulated over multiple rotations.
+
+C++/Java implementation note: these are created as SimDevice nodes where the device name is prefixed by ``"DutyCycle:"``, for example ``"DutyCycle:DutyCycleEncoder[1]"``.
+
+| Data Key         | Type    | Description                      |
+| ---------------- | ------- | -------------------------------- |
+| ``">connected"`` | Boolean | True if the encoder is connected |
+| ``">position"``  | Float   | The position in rotations        |
+
+#### Quadrature Encoder ("Encoder")
+
+[``"Encoder"``]:#quadrature-encoder-encoder
+
+A relative encoder.  For absolute encoders, use ``"DutyCycle"``.
+
+| Data Key              | Type    | Description                                         |
+| --------------------- | ------- | --------------------------------------------------- |
+| ``"<init"``           | Boolean | If encoder is initialized in the robot program      |
+| ``"<channel_a"``      | Integer | Digital channel number for “A” phase                |
+| ``"<channel_b"``      | Integer | Digital channel number for “B” phase                |
+| ``"<samples_to_avg"`` | Integer | Number of samples to average for period measurement |
+| ``">count"``          | Integer | Accumulated count (pulses)                          |
+| ``">period"``         | Float   | Period between pulses in seconds                    |
+
+#### Gyro ("Gyro")
+
+[``"Gyro"``]:#gyro-gyro
+
+A single axis or 3-axis gyro.  Single axis gyros only use the X angle parameter.
+
+C++/Java implementation note: these are created as SimDevice nodes where the device name is prefixed by ``"Gyro:"``, for example ``"Gyro:ADXRS450[1]"``.
+
+| Data Key          | Type    | Description                                               |
+| ----------------- | ------- | --------------------------------------------------------- |
+| ``"<init"``       | Boolean | If gyro is initialized in the robot program               |
+| ``"<range"``      | Float   | Gyro range in degrees/second (optional)                   |
+| ``">connected"``  | Boolean | True if the gyro is connected                             |
+| ``">angle_x"``    | Float   | The gyro angle in degrees                                 |
+| ``">angle_y"``    | Float   | The gyro angle in degrees                                 |
+| ``">angle_z"``    | Float   | The gyro angle in degrees                                 |
+| ``">rate_x"``     | Float   | The current gyro angular rate of change in degrees/second |
+| ``">rate_y"``     | Float   | The current gyro angular rate of change in degrees/second |
+| ``">rate_z"``     | Float   | The current gyro angular rate of change in degrees/second |
+
+#### Joystick Data ("Joystick")
+
+[``"Joystick"``]:#joystick-data-joystick
+
+Joystick data is an input to the robot program and should be updated for each input joystick on a periodic basis.  To enable synchronous updates of joystick and driver station state, joystick data is not made visible to the robot program until a DriverStation message with ``">new_data"`` set to true is received.
+
+| Data Key            | Type             | Description |
+| ------------------- | ---------------- | --- |
+| ``">axes"``         | Array of float   | One array element per axis; value is -1 to 1 range |
+| ``">povs"``         | Array of integer | One array element per POV; value is angle in degrees of the POV (e.g. 0, 90, 315) if pressed, or -1 if the POV is not pressed |
+| ``">buttons"``      | Array of boolean | One array element per button; true if button is pressed, false if button is released |
+| ``"<rumble_left"``  | Float            | Left rumble, value is 0-1 range |
+| ``"<rumble_right"`` | Float            | Right rumble, value is 0-1 range |
+
+#### PWM Output ("PWM")
+
+[``"PWM"``]:#pwm-output-pwm
+
+PWMs may be used to control either speed controllers or servos.  Typically only one of either ``"<speed"`` (for a speed controller) or ``"<position"`` (for a servo) is used for a given PWM.
+
+| Data Key        | Type    | Description                                |
+| --------------- | ------- | ------------------------------------------ |
+| ``"<init"``     | Boolean | If PWM is initialized in the robot program |
+| ``"<speed"``    | Float   | Speed, -1.0 to 1.0 range                   |
+| ``"<position"`` | Float   | Servo position, 0.0 to 1.0 range           |
+
+#### Relay Output ("Relay")
+
+[``"Relay"``]:#relay-output-relay
+
+| Data Key        | Type    | Description                                                    |
+| --------------- | ------- | -------------------------------------------------------------- |
+| ``"<init_fwd"`` | Boolean | If relay forward direction is initialized in the robot program |
+| ``"<init_rev"`` | Boolean | If relay reverse direction is initialized in the robot program |
+| ``"<fwd"``      | Boolean | True if forward direction is enabled                           |
+| ``"<rev"``      | Boolean | True if reverse direction is enabled                           |
+
+#### Solenoid Output ("Solenoid")
+
+[``"Solenoid"``]:#solenoid-output-solenoid
+
+Solenoids are used to control pneumatic pistons
+
+| Data Key        | Type    | Description                                     |
+| --------------- | ------- | ----------------------------------------------- |
+| ``"<init"``     | Boolean | If Solenoid is initialized in the robot program |
+| ``"<output"``   | Boolean | The state of the solenoid                       |
+
+### CAN Messages
+
+CAN messages all use a device value of ``"DeviceType[Number]"``, where the DeviceType is the vendor-specific CAN device type (motor controller class) name and Number is the CAN device number (the user-visible number passed to the device constructor).
+
+Many of the CAN messages use the same data key/values as other standard messages.  They are separately namespaced to make it easier for implementations to separate them from main robot controller messages.
+
+C++/Java implementation note: these are created as SimDevice nodes where the device name is prefixed by the message name and ``":"``, for example ``"CANMotor:Controller[1]"``.
+
+#### CANMotor
+
+Only one of ``"supplyCurrent"`` or ``"motorCurrent"`` should be sent by the hardware; the other value should be set to zero.  If ``"busVoltage"`` is not simulated it should also be set to zero.
+
+| Data Key             | Type             | Description                                      |
+| -------------------- | ---------------- | ------------------------------------------------ |
+| ``"<percentOutput"`` | Integer or Float | Percent output (-1 to 1 range)                   |
+| ``">supplyCurrent"`` | Float            | The supply current in amps as simulated/measured |
+| ``">motorCurrent"``  | Float            | The motor current in amps as simulated/measured  |
+| ``">busVoltage"``    | Float            | The bus voltage as simulated/measured            |
+
+#### CANEncoder
+
+A relative encoder (typically quadrature).  For absolute encoders, use ``"CANDutyCycle"``.
+
+| Data Key        | Type  | Description                      |
+| --------------- | ----- | -------------------------------- |
+| ``">position"`` | Float | Relative position, in rotations  |
+| ``">velocity"`` | Float | Velocity in rotations per second |
+
+#### CANGyro
+
+Uses the same keys as [``"Gyro"``][].
+
+#### CANAccel
+
+Uses the same keys as [``"Accel"``][].
+
+#### CANAIn
+
+Uses the same keys as [``"AI"``][].
+
+The device value may have a suffix for multiple inputs on a single CAN device; ``"-"`` followed by the input name or number is recommended but not required.
+
+#### CANDIO
+
+Uses the same keys as [``"DIO"``][].  This is commonly used for limit switches.
+
+The device value may have a suffix for multiple inputs on a single CAN device; ``"-"`` followed by the input name or number is recommended but not required.
+
+#### CANDutyCycle
+
+Uses the same keys as [``"DutyCycle"``][].
+
+The device value may have a suffix for multiple inputs on a single CAN device; ``"-"`` followed by the input name or number is recommended but not required.
+
+### RoboRIO Messages
+
+These messages are specific to the RoboRIO and will not likely be found in other simulators or hardware devices except in a very limited capacity (e.g. Vin voltage).
+
+| Type value        | Description         | Device value |
+| ----------------- | ------------------- | ------------ |
+| [``"RoboRIO"``][] | RoboRIO information | Blank        |
+
+#### RoboRIO
+
+[``"RoboRIO"``]:#roborio
+
+The RoboRIO.
+
+| Data Key           | Type    | Description                                 |
+| ------------------ | ------- | ------------------------------------------- |
+| ``">fpga_button"`` | Boolean | FPGA button state                           |
+| ``">vin_voltage"`` | Float   | Vin rail voltage                            |
+| ``">vin_current"`` | Float   | Vin rail current                            |
+| ``">6v_voltage"``  | Float   | 6V rail voltage                             |
+| ``">6v_current"``  | Float   | 6V rail current                             |
+| ``">6v_active"``   | Boolean | True if 6V rail active, false if inactive   |
+| ``">6v_faults"``   | Integer | Number of faults on 6V rail                 |
+| ``">5v_voltage"``  | Float   | 5V rail voltage                             |
+| ``">5v_current"``  | Float   | 5V rail current                             |
+| ``">5v_active"``   | Boolean | True if 5V rail active, false if inactive   |
+| ``">5v_faults"``   | Integer | Number of faults on 5V rail                 |
+| ``">3v3_voltage"`` | Float   | 3.3V rail voltage                           |
+| ``">3v3_current"`` | Float   | 3.3V rail current                           |
+| ``">3v3_active"``  | Boolean | True if 3.3V rail active, false if inactive |
+| ``">3v3_faults"``  | Integer | Number of faults on 3.3V rail               |
+
+### Other Device Messages ("SimDevice")
+
+[``"SimDevice"``]:#other-device-messages-simdevice
+
+A device type of ``"SimDevice"`` may be used for extending the protocol for arbitrary complex devices.  The device value is an arbitrary string, generally ``"DeviceName[Port/Index]"``, and the data keys are arbitrary and device-dependent.
diff --git a/simulation/halsim_ws_core/doc/wpilib-ws.yaml b/simulation/halsim_ws_core/doc/wpilib-ws.yaml
new file mode 100644
index 0000000..b4ddb04
--- /dev/null
+++ b/simulation/halsim_ws_core/doc/wpilib-ws.yaml
@@ -0,0 +1,516 @@
+asyncapi: 2.0.0
+info:
+  title: WPILib WebSocket Remote Endpoint API
+  version: "1.0.0"
+  description: |
+    API to route WPILib HAL calls over WebSockets.
+  license:
+    name: WPILib BSD
+
+channels:
+  wpilibws:
+    description: General channel for WPILib WebSocket messages
+    publish:
+      operationId: wpilibwsPublish
+      message:
+        $ref: "#/components/messages/wpilibwsMsg"
+    subscribe:
+      operationId: wpilibwsSubscribe
+      message:
+        $ref: "#/components/messages/wpilibwsMsg"
+
+components:
+  messages:
+    wpilibwsMsg:
+      title: WPILib WebSocket Message
+      summary: Message envelope. Note that the "data" field contains a diff of the current state of a particular device. E.g. If only the "value" changes for a DIO device, then only the "<>value" field will be sent.
+      contentType: application/json
+      examples:
+      - payload:
+          type: PWM
+          device: "1"
+          data:
+            "<speed": 0.5
+      - payload:
+          type: DIO
+          device: "3"
+          data:
+            "<init": true
+      payload:
+        type: object
+        oneOf:
+            - $ref: "#/components/schemas/accelData"
+            - $ref: "#/components/schemas/aiData"
+            - $ref: "#/components/schemas/dioData"
+            - $ref: "#/components/schemas/dpwmData"
+            - $ref: "#/components/schemas/driverstationData"
+            - $ref: "#/components/schemas/dutycycleData"
+            - $ref: "#/components/schemas/encoderData"
+            - $ref: "#/components/schemas/gyroData"
+            - $ref: "#/components/schemas/joystickData"
+            - $ref: "#/components/schemas/pwmData"
+            - $ref: "#/components/schemas/relayData"
+            - $ref: "#/components/schemas/solenoidData"
+            - $ref: "#/components/schemas/roborioData"
+
+  schemas:
+    accelData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Accel
+        device:
+          type: string
+          description: Arbitrary device name
+        data:
+          type: object
+          description: "Accelerometer Data (type: Accelerometer, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If accelerometer is initialized in the robot program"
+            <range:
+              type: number
+              description: "Desired range in G’s"
+            ">x":
+              type: number
+              description: "Acceleration in G’s "
+            ">y":
+              type: number
+              description: "Acceleration in G’s "
+            ">z":
+              type: number
+              description: "Acceleration in G’s "
+
+    aiData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: AI
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Analog Input Data (type: AI, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If analog input is initialized in the robot program"
+            ">voltage":
+              type: number
+              description: "Input voltage, in volts"
+
+    dioData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: DIO
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Digital Input/Output Data (type: DIO, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If DIO is initialized in the robot program"
+            <input:
+              type: boolean
+              description: "True if input, false if output"
+            <>value:
+              type: boolean
+              description: "Input or output state"
+
+    dpwmData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: dPWM
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Duty Cycle Output Data (type: dPWM, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If output is initialized in the robot program"
+            <duty_cycle:
+              type: number
+              description: "Duty cycle %"
+              minimum: 0.0
+              maximum: 1.0
+            <dio_pin:
+              type: integer
+              description: "DIO pin number"
+
+    driverstationData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: DriverStation
+        device:
+          type: string
+          description: Should be left blank
+        data:
+          type: object
+          description: "Driver Station Data (type: DriverStation)"
+          properties:
+            ">new_data":
+              type: boolean
+              description: "One shot.  If set to true in a message, notifies the robot program that new DS and Joystick data is available."
+            ">enabled":
+              type: boolean
+              description: "True to enable the robot program"
+            ">autonomous":
+              type: boolean
+              description: "True for autonomous mode; false for teleoperated mode"
+            ">test":
+              type: boolean
+              description: "True for test mode; false for other modes"
+            ">estop":
+              type: boolean
+              description: "True to emergency stop (no motor outputs)"
+            ">fms":
+              type: boolean
+              description: "True if the DS is connected to a Field Management System (FMS)"
+            ">ds":
+              type: boolean
+              description: "True if a DS application is connected"
+            ">station":
+              type: string
+              description: "Station color and number; supported values are 'red1', 'red2', 'red3', 'blue1', 'blue2', 'blue3'."
+            ">match_time":
+              type: number
+              description: "Match time countdown, in seconds, for each match period (e.g. for 15 second period, starts at 15 and counts down to 0).  If not in a match, -1."
+            ">game_data":
+              type: string
+              description: "Game-specific data; arbitrary string contents"
+
+    dutycycleData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: DutyCycle
+        device:
+          type: string
+          description: Arbitrary device name
+        data:
+          type: object
+          description: "Duty Cycle Input Data (type: DutyCycle, device: channel number)"
+          properties:
+            ">connected":
+              type: boolean
+              description: "True if the encoder is connected"
+            ">position":
+              type: number
+              description: "The position in rotations"
+
+    encoderData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Encoder
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Quadrature Encoder Data (type: Encoder, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If encoder is initialized in the robot program"
+            <channel_a:
+              type: integer
+              description: "Digital channel number for 'A' phase"
+            <channel_b:
+              type: integer
+              description: "Digital channel number for 'B' phase"
+            <samples_to_avg:
+              type: integer
+              description: "Number of samples to average for period measurement"
+            ">count":
+              type: integer
+              description: "Accumulated count (pulses)"
+            ">period":
+              type: number
+              description: "Period between pulses in seconds"
+
+    gyroData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Gyro
+        device:
+          type: string
+          description: Arbitrary device name
+        data:
+          type: object
+          description: "Gyro Data (type: Gyro, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If gyro is initialized in the robot program"
+            <range:
+              type: number
+              description: "Gyro range in degrees/second (optional)"
+            ">connected":
+              type: boolean
+              description: "True if the gyro is connected"
+            ">angle_x":
+              type: number
+              description: "The gyro angle in degrees"
+            ">angle_y":
+              type: number
+              description: "The gyro angle in degrees"
+            ">angle_z":
+              type: number
+              description: "The gyro angle in degrees"
+            ">rate_x":
+              type: number
+              description: "The current gyro angular rate of change in degrees/second"
+            ">rate_y":
+              type: number
+              description: "The current gyro angular rate of change in degrees/second"
+            ">rate_z":
+              type: number
+              description: "The current gyro angular rate of change in degrees/second"
+
+    joystickData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Joystick
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Joystick Data (type: Joystick, device: channel number)"
+          properties:
+            ">axes":
+              type: array
+              items:
+                type: number
+                description: "Value of an individual axis on this joystick"
+                minimum: -1.0
+                maximum: 1.0
+            ">povs":
+              type: array
+              description: "One array element per POV; value is a"
+              items:
+                type: integer
+                description: "State of all POV switches on this joystick; an angle in degrees of the POV (e.g. 0, 90, 315) if pressed, or -1 if the POV is not pressed"
+            ">buttons":
+              type: array
+              description: State of all buttons on this joystick
+              items:
+                type: boolean
+                description: Pressed state of an individual button
+            <rumble_left:
+              type: number
+              description: "Left rumble"
+              minimum: 0.0
+              maximum: 1.0
+            <rumble_right:
+              type: number
+              description: "Right rumble"
+              minimum: 0.0
+              maximum: 1.0
+
+    pwmData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: PWM
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "PWM Output Data (type: PWM, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If PWM is initialized in the robot program"
+            <speed:
+              type: number
+              description: "Speed"
+              minimum: -1.0
+              maximum: 1.0
+            <position:
+              type: number
+              description: "Servo position"
+              minimum: 0.0
+              maximum: 1.0
+
+    relayData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Relay
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Relay Output Data (type: Relay, device: channel number)"
+          properties:
+            <init_fwd:
+              type: boolean
+              description: "If relay forward direction is initialized in the robot program"
+            <init_rev:
+              type: boolean
+              description: "If relay reverse direction is initialized in the robot program"
+            <fwd:
+              type: boolean
+              description: "True if forward direction is enabled"
+            <rev:
+              type: boolean
+              description: "True if reverse direction is enabled"
+
+    solenoidData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: Solenoid
+        device:
+          type: string
+          description: Device Identifier (usually channel)
+        data:
+          type: object
+          description: "Solenoid Data (type: Solenoid, device: channel number)"
+          properties:
+            <init:
+              type: boolean
+              description: "If Solenoid is initialized in the robot program"
+            <output:
+              type: boolean
+              description: "The state of the solenoid"
+
+    roborioData:
+      type: object
+      required:
+      - type
+      - device
+      properties:
+        type:
+          type: string
+          description: Device Type (e.g. DIO/AI/PWM/Encoder etc)
+          const: RoboRIO
+        device:
+          type: string
+          description: Should be left blank
+        data:
+          type: object
+          description: "RoboRIO Data (type: RoboRIO)"
+          properties:
+            ">fpga_button":
+              type: boolean
+              description: "FPGA button state"
+            ">vin_voltage":
+              type: number
+              description: "Vin rail voltage"
+            ">vin_current":
+              type: number
+              description: "Vin rail current"
+            ">6v_voltage":
+              type: number
+              description: "6V rail voltage"
+            ">6v_current":
+              type: number
+              description: "6V rail current"
+            ">6v_active":
+              type: boolean
+              description: "True if 6V rail active, false if inactive"
+            ">6v_faults":
+              type: integer
+              description: "Number of faults on 6V rail"
+            ">5v_voltage":
+              type: number
+              description: "5V rail voltage"
+            ">5v_current":
+              type: number
+              description: "5V rail current"
+            ">5v_active":
+              type: boolean
+              description: "True if 5V rail active, false if inactive"
+            ">5v_faults":
+              type: integer
+              description: "Number of faults on 5V rail"
+            ">3v3_voltage":
+              type: number
+              description: "3.3V rail voltage"
+            ">3v3_current":
+              type: number
+              description: "3.3V rail current"
+            ">3v3_active":
+              type: boolean
+              description: "True if 3.3V rail active, false if inactive"
+            ">3v3_faults":
+              type: integer
+              description: "Number of faults on 3.3V rail"
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSBaseProvider.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSBaseProvider.cpp
index 39057a1..16d92fa 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSBaseProvider.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSBaseProvider.cpp
@@ -1,16 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSBaseProvider.h"
 
+#include <utility>
+
 namespace wpilibws {
 
-HALSimWSBaseProvider::HALSimWSBaseProvider(const std::string& key,
-                                           const std::string& type)
+HALSimWSBaseProvider::HALSimWSBaseProvider(std::string_view key,
+                                           std::string_view type)
     : m_key(key), m_type(type) {}
 
 void HALSimWSBaseProvider::OnNetValueChanged(const wpi::json& json) {
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSHalProviders.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSHalProviders.cpp
index bc78202..4d6c024 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSHalProviders.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSHalProviders.cpp
@@ -1,12 +1,11 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSHalProviders.h"
 
+#include <fmt/format.h>
+
 namespace wpilibws {
 
 void HALSimWSHalProvider::OnNetworkConnected(
@@ -19,7 +18,9 @@
   RegisterCallbacks();
 }
 
-void HALSimWSHalProvider::OnNetworkDisconnected() { CancelCallbacks(); }
+void HALSimWSHalProvider::OnNetworkDisconnected() {
+  CancelCallbacks();
+}
 
 void HALSimWSHalProvider::ProcessHalCallback(const wpi::json& payload) {
   auto ws = m_ws.lock();
@@ -31,10 +32,10 @@
 }
 
 HALSimWSHalChanProvider::HALSimWSHalChanProvider(int32_t channel,
-                                                 const std::string& key,
-                                                 const std::string& type)
+                                                 std::string_view key,
+                                                 std::string_view type)
     : HALSimWSHalProvider(key, type), m_channel(channel) {
-  m_deviceId = std::to_string(channel);
+  m_deviceId = fmt::format("{}", channel);
 }
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_AddressableLED.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_AddressableLED.cpp
new file mode 100644
index 0000000..4cac7f5
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_AddressableLED.cpp
@@ -0,0 +1,78 @@
+// 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 "WSProvider_AddressableLED.h"
+
+#include <hal/Ports.h>
+#include <hal/simulation/AddressableLEDData.h>
+
+#define REGISTER(halsim, jsonid, ctype, haltype)                          \
+  HALSIM_RegisterAddressableLED##halsim##Callback(                        \
+      m_channel,                                                          \
+      [](const char* name, void* param, const struct HAL_Value* value) {  \
+        static_cast<HALSimWSProviderAddressableLED*>(param)               \
+            ->ProcessHalCallback(                                         \
+                {{jsonid, static_cast<ctype>(value->data.v_##haltype)}}); \
+      },                                                                  \
+      this, true)
+namespace wpilibws {
+void HALSimWSProviderAddressableLED::Initialize(
+    WSRegisterFunc webRegisterFunc) {
+  CreateProviders<HALSimWSProviderAddressableLED>(
+      "AddressableLED", HAL_GetNumAddressableLEDs(), webRegisterFunc);
+}
+
+HALSimWSProviderAddressableLED::~HALSimWSProviderAddressableLED() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderAddressableLED::RegisterCallbacks() {
+  m_initCbKey = REGISTER(Initialized, "<init", bool, boolean);
+  m_outputPortCbKey = REGISTER(OutputPort, "<output_port", int32_t, int);
+  m_lengthCbKey = REGISTER(Length, "<length", int32_t, int);
+  m_runningCbKey = REGISTER(Running, "<running", bool, boolean);
+
+  m_dataCbKey = HALSIM_RegisterAddressableLEDDataCallback(
+      0,
+      [](const char* name, void* param, const unsigned char* buffer,
+         unsigned int count) {
+        auto provider = static_cast<HALSimWSProviderAddressableLED*>(param);
+
+        size_t numLeds = count / sizeof(HAL_AddressableLEDData);
+        const HAL_AddressableLEDData* data =
+            reinterpret_cast<const HAL_AddressableLEDData*>(buffer);
+
+        std::vector<wpi::json> jsonData;
+
+        for (size_t i = 0; i < numLeds; ++i) {
+          jsonData.push_back(
+              {{"r", data[i].r}, {"g", data[i].g}, {"b", data[i].b}});
+        }
+
+        wpi::json payload;
+        payload[">data"] = jsonData;
+
+        provider->ProcessHalCallback(payload);
+      },
+      this);
+}
+
+void HALSimWSProviderAddressableLED::CancelCallbacks() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderAddressableLED::DoCancelCallbacks() {
+  HALSIM_CancelAddressableLEDInitializedCallback(m_channel, m_initCbKey);
+  HALSIM_CancelAddressableLEDOutputPortCallback(m_channel, m_outputPortCbKey);
+  HALSIM_CancelAddressableLEDLengthCallback(m_channel, m_lengthCbKey);
+  HALSIM_CancelAddressableLEDRunningCallback(m_channel, m_runningCbKey);
+  HALSIM_CancelAddressableLEDDataCallback(m_channel, m_dataCbKey);
+
+  m_initCbKey = 0;
+  m_outputPortCbKey = 0;
+  m_lengthCbKey = 0;
+  m_runningCbKey = 0;
+  m_dataCbKey = 0;
+}
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Analog.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Analog.cpp
index 91a8fa9..be19a1d 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Analog.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Analog.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_Analog.h"
 
@@ -45,7 +42,9 @@
                                             webRegisterFunc);
 }
 
-HALSimWSProviderAnalogIn::~HALSimWSProviderAnalogIn() { DoCancelCallbacks(); }
+HALSimWSProviderAnalogIn::~HALSimWSProviderAnalogIn() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderAnalogIn::RegisterCallbacks() {
   m_initCbKey = REGISTER_AIN(Initialized, "<init", bool, boolean);
@@ -66,7 +65,9 @@
       REGISTER_AIN_ACCUM(Deadband, "<accum_deadband", int32_t, int);
 }
 
-void HALSimWSProviderAnalogIn::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderAnalogIn::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderAnalogIn::DoCancelCallbacks() {
   // Cancel callbacks
@@ -112,7 +113,9 @@
                                              webRegisterFunc);
 }
 
-HALSimWSProviderAnalogOut::~HALSimWSProviderAnalogOut() { CancelCallbacks(); }
+HALSimWSProviderAnalogOut::~HALSimWSProviderAnalogOut() {
+  CancelCallbacks();
+}
 
 void HALSimWSProviderAnalogOut::RegisterCallbacks() {
   m_initCbKey = REGISTER_AOUT(Initialized, "<init", bool, boolean);
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_BuiltInAccelerometer.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_BuiltInAccelerometer.cpp
new file mode 100644
index 0000000..fefbcd5
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_BuiltInAccelerometer.cpp
@@ -0,0 +1,94 @@
+// 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 "WSProvider_BuiltInAccelerometer.h"
+
+#include <hal/Ports.h>
+#include <hal/simulation/AccelerometerData.h>
+
+#define REGISTER(halsim, jsonid, ctype, haltype)                          \
+  HALSIM_RegisterAccelerometer##halsim##Callback(                         \
+      0,                                                                  \
+      [](const char* name, void* param, const struct HAL_Value* value) {  \
+        static_cast<HALSimWSProviderBuiltInAccelerometer*>(param)         \
+            ->ProcessHalCallback(                                         \
+                {{jsonid, static_cast<ctype>(value->data.v_##haltype)}}); \
+      },                                                                  \
+      this, true)
+namespace wpilibws {
+HALSimWSProviderBuiltInAccelerometer::HALSimWSProviderBuiltInAccelerometer()
+    : HALSimWSHalProvider("Accel/BuiltInAccel", "Accel") {
+  m_deviceId = "BuiltInAccel";
+}
+
+void HALSimWSProviderBuiltInAccelerometer::Initialize(
+    WSRegisterFunc webRegisterFunc) {
+  webRegisterFunc("Accel/BuiltInAccel",
+                  std::make_unique<HALSimWSProviderBuiltInAccelerometer>());
+}
+
+HALSimWSProviderBuiltInAccelerometer::~HALSimWSProviderBuiltInAccelerometer() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderBuiltInAccelerometer::RegisterCallbacks() {
+  m_activeCbKey = REGISTER(Active, "<init", bool, boolean);
+  m_rangeCbKey = HALSIM_RegisterAccelerometerRangeCallback(
+      0,
+      [](const char* name, void* param, const struct HAL_Value* value) {
+        double range;
+        switch (value->data.v_int) {
+          case 0:
+            range = 2;
+            break;
+          case 1:
+            range = 4;
+            break;
+          case 2:
+          default:
+            range = 8;
+            break;
+        }
+        static_cast<HALSimWSProviderBuiltInAccelerometer*>(param)
+            ->ProcessHalCallback({{"<range", range}});
+      },
+      this, true);
+  m_xCbKey = REGISTER(X, ">x", double, double);
+  m_yCbKey = REGISTER(Y, ">y", double, double);
+  m_zCbKey = REGISTER(Z, ">z", double, double);
+}
+
+void HALSimWSProviderBuiltInAccelerometer::CancelCallbacks() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderBuiltInAccelerometer::DoCancelCallbacks() {
+  HALSIM_CancelAccelerometerActiveCallback(0, m_activeCbKey);
+  HALSIM_CancelAccelerometerRangeCallback(0, m_rangeCbKey);
+  HALSIM_CancelAccelerometerXCallback(0, m_xCbKey);
+  HALSIM_CancelAccelerometerYCallback(0, m_yCbKey);
+  HALSIM_CancelAccelerometerZCallback(0, m_zCbKey);
+
+  m_activeCbKey = 0;
+  m_rangeCbKey = 0;
+  m_xCbKey = 0;
+  m_yCbKey = 0;
+  m_zCbKey = 0;
+}
+
+void HALSimWSProviderBuiltInAccelerometer::OnNetValueChanged(
+    const wpi::json& json) {
+  wpi::json::const_iterator it;
+  if ((it = json.find(">x")) != json.end()) {
+    HALSIM_SetAccelerometerX(0, it.value());
+  }
+  if ((it = json.find(">y")) != json.end()) {
+    HALSIM_SetAccelerometerY(0, it.value());
+  }
+  if ((it = json.find(">z")) != json.end()) {
+    HALSIM_SetAccelerometerZ(0, it.value());
+  }
+}
+
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DIO.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DIO.cpp
index 88fb29f..405a2df 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DIO.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DIO.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_DIO.h"
 
@@ -26,7 +23,9 @@
                                        webRegisterFunc);
 }
 
-HALSimWSProviderDIO::~HALSimWSProviderDIO() { DoCancelCallbacks(); }
+HALSimWSProviderDIO::~HALSimWSProviderDIO() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderDIO::RegisterCallbacks() {
   m_initCbKey = REGISTER(Initialized, "<init", bool, boolean);
@@ -35,7 +34,9 @@
   m_inputCbKey = REGISTER(IsInput, "<input", bool, boolean);
 }
 
-void HALSimWSProviderDIO::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderDIO::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderDIO::DoCancelCallbacks() {
   HALSIM_CancelDIOInitializedCallback(m_channel, m_initCbKey);
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
index b3cec6d..6fbc3d7 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_DriverStation.cpp
@@ -1,14 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_DriverStation.h"
 
 #include <algorithm>
 #include <atomic>
+#include <string_view>
 
 #include <hal/DriverStation.h>
 #include <hal/Extensions.h>
@@ -35,7 +33,7 @@
     registered = true;
     HAL_RegisterExtensionListener(
         nullptr, [](void*, const char* name, void* data) {
-          if (wpi::StringRef{name} == "ds_socket") {
+          if (std::string_view{name} == "ds_socket") {
             gDSSocketConnected = static_cast<std::atomic<bool>*>(data);
           }
         });
@@ -92,10 +90,12 @@
       },
       this, true);
 
-  m_matchTimeCbKey = REGISTER(MatchTime, "<match_time", double, double);
+  m_matchTimeCbKey = REGISTER(MatchTime, ">match_time", double, double);
 }
 
-void HALSimWSProviderDriverStation::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderDriverStation::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderDriverStation::DoCancelCallbacks() {
   HALSIM_CancelDriverStationEnabledCallback(m_enabledCbKey);
@@ -121,7 +121,9 @@
 
 void HALSimWSProviderDriverStation::OnNetValueChanged(const wpi::json& json) {
   // ignore if DS connected
-  if (gDSSocketConnected && *gDSSocketConnected) return;
+  if (gDSSocketConnected && *gDSSocketConnected) {
+    return;
+  }
 
   wpi::json::const_iterator it;
   if ((it = json.find(">enabled")) != json.end()) {
@@ -160,6 +162,14 @@
     }
   }
 
+  if ((it = json.find(">match_time")) != json.end()) {
+    HALSIM_SetDriverStationMatchTime(it.value());
+  }
+  if ((it = json.find(">game_data")) != json.end()) {
+    HALSIM_SetGameSpecificMessage(
+        it.value().get_ref<const std::string&>().c_str());
+  }
+
   // Only notify usercode if we get the new data message
   if ((it = json.find(">new_data")) != json.end()) {
     HALSIM_NotifyDriverStationNewData();
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Encoder.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Encoder.cpp
index 58d47d0..73f05dc 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Encoder.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Encoder.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_Encoder.h"
 
@@ -26,7 +23,9 @@
                                            webRegisterFunc);
 }
 
-HALSimWSProviderEncoder::~HALSimWSProviderEncoder() { DoCancelCallbacks(); }
+HALSimWSProviderEncoder::~HALSimWSProviderEncoder() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderEncoder::RegisterCallbacks() {
   // Special case for initialization since we will need to send
@@ -49,15 +48,35 @@
         provider->ProcessHalCallback(payload);
       },
       this, true);
-  m_countCbKey = REGISTER(Count, ">count", int32_t, int);
+  m_countCbKey = HALSIM_RegisterEncoderCountCallback(
+      m_channel,
+      [](const char* name, void* param, const struct HAL_Value* value) {
+        auto provider = static_cast<HALSimWSProviderEncoder*>(param);
+        provider->ProcessHalCallback(
+            {{">count", static_cast<int32_t>(value->data.v_int +
+                                             provider->m_countOffset)}});
+      },
+      this, true);
   m_periodCbKey = REGISTER(Period, ">period", double, double);
-  m_resetCbKey = REGISTER(Reset, "<reset", bool, boolean);
+  m_resetCbKey = HALSIM_RegisterEncoderResetCallback(
+      m_channel,
+      [](const char* name, void* param, const struct HAL_Value* value) {
+        auto provider = static_cast<HALSimWSProviderEncoder*>(param);
+        bool reset = static_cast<bool>(value->data.v_boolean);
+        if (reset) {
+          provider->m_countOffset +=
+              HALSIM_GetEncoderCount(provider->m_channel);
+        }
+      },
+      this, true);
   m_reverseDirectionCbKey =
       REGISTER(ReverseDirection, "<reverse_direction", bool, boolean);
   m_samplesCbKey = REGISTER(SamplesToAverage, "<samples_to_avg", int32_t, int);
 }
 
-void HALSimWSProviderEncoder::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderEncoder::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderEncoder::DoCancelCallbacks() {
   HALSIM_CancelEncoderInitializedCallback(m_channel, m_initCbKey);
@@ -79,7 +98,8 @@
 void HALSimWSProviderEncoder::OnNetValueChanged(const wpi::json& json) {
   wpi::json::const_iterator it;
   if ((it = json.find(">count")) != json.end()) {
-    HALSIM_SetEncoderCount(m_channel, it.value());
+    HALSIM_SetEncoderCount(m_channel,
+                           static_cast<int32_t>(it.value()) - m_countOffset);
   }
   if ((it = json.find(">period")) != json.end()) {
     HALSIM_SetEncoderPeriod(m_channel, it.value());
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Joystick.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Joystick.cpp
index 6f3f27b..3c16026 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Joystick.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Joystick.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_Joystick.h"
 
@@ -21,7 +18,9 @@
                                             webregisterFunc);
 }
 
-HALSimWSProviderJoystick::~HALSimWSProviderJoystick() { DoCancelCallbacks(); }
+HALSimWSProviderJoystick::~HALSimWSProviderJoystick() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderJoystick::RegisterCallbacks() {
   m_dsNewDataCbKey = HALSIM_RegisterDriverStationNewDataCallback(
@@ -57,16 +56,28 @@
           buttonsValues.push_back(((buttons.buttons >> i) & 0x1) == 1);
         }
 
+        // Rumble data
+        int64_t outputs = 0;
+        int32_t leftRumble = 0;
+        int32_t rightRumble = 0;
+        HALSIM_GetJoystickOutputs(provider->GetChannel(), &outputs, &leftRumble,
+                                  &rightRumble);
+
         payload[">axes"] = axesValues;
         payload[">povs"] = povsValues;
         payload[">buttons"] = buttonsValues;
+        payload["<outputs"] = outputs;
+        payload["<rumble_left"] = leftRumble;
+        payload["<rumble_right"] = rightRumble;
 
         provider->ProcessHalCallback(payload);
       },
       this, true);
 }
 
-void HALSimWSProviderJoystick::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderJoystick::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderJoystick::DoCancelCallbacks() {
   HALSIM_CancelDriverStationNewDataCallback(m_dsNewDataCbKey);
@@ -76,13 +87,16 @@
 
 void HALSimWSProviderJoystick::OnNetValueChanged(const wpi::json& json) {
   // ignore if DS connected
-  if (gDSSocketConnected && *gDSSocketConnected) return;
+  if (gDSSocketConnected && *gDSSocketConnected) {
+    return;
+  }
 
   wpi::json::const_iterator it;
   if ((it = json.find(">axes")) != json.end()) {
     HAL_JoystickAxes axes{};
     axes.count =
-        std::min(it.value().size(), (wpi::json::size_type)HAL_kMaxJoystickAxes);
+        std::min(it.value().size(),
+                 static_cast<wpi::json::size_type>(HAL_kMaxJoystickAxes));
     for (int i = 0; i < axes.count; i++) {
       axes.axes[i] = it.value()[i];
     }
@@ -92,10 +106,11 @@
 
   if ((it = json.find(">buttons")) != json.end()) {
     HAL_JoystickButtons buttons{};
-    buttons.count = std::min(it.value().size(), (wpi::json::size_type)32);
+    buttons.count =
+        std::min(it.value().size(), static_cast<wpi::json::size_type>(32));
     for (int i = 0; i < buttons.count; i++) {
       if (it.value()[i]) {
-        buttons.buttons |= 1 << (i - 1);
+        buttons.buttons |= 1 << i;
       }
     }
 
@@ -105,7 +120,8 @@
   if ((it = json.find(">povs")) != json.end()) {
     HAL_JoystickPOVs povs{};
     povs.count =
-        std::min(it.value().size(), (wpi::json::size_type)HAL_kMaxJoystickPOVs);
+        std::min(it.value().size(),
+                 static_cast<wpi::json::size_type>(HAL_kMaxJoystickPOVs));
     for (int i = 0; i < povs.count; i++) {
       povs.povs[i] = it.value()[i];
     }
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PCM.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PCM.cpp
new file mode 100644
index 0000000..b7eab57
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PCM.cpp
@@ -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.
+
+#include "WSProvider_PCM.h"
+
+#include <hal/Ports.h>
+#include <hal/simulation/CTREPCMData.h>
+
+#define REGISTER_CTREPCM(halsim, jsonid, ctype, haltype)                 \
+  HALSIM_RegisterCTREPCM##halsim##Callback(                              \
+      m_channel,                                                         \
+      [](const char* name, void* param, const struct HAL_Value* value) { \
+        static_cast<HALSimWSProviderPCM*>(param)->ProcessHalCallback(    \
+            {{jsonid, static_cast<ctype>(value->data.v_##haltype)}});    \
+      },                                                                 \
+      this, true)
+namespace wpilibws {
+void HALSimWSProviderPCM::Initialize(WSRegisterFunc webRegisterFunc) {
+  CreateProviders<HALSimWSProviderPCM>("CTREPCM", HAL_GetNumCTREPCMModules(),
+                                       webRegisterFunc);
+}
+
+HALSimWSProviderPCM::~HALSimWSProviderPCM() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderPCM::RegisterCallbacks() {
+  m_initCbKey = REGISTER_CTREPCM(Initialized, "<init", bool, boolean);
+  m_onCbKey = REGISTER_CTREPCM(CompressorOn, ">on", bool, boolean);
+  m_closedLoopCbKey =
+      REGISTER_CTREPCM(ClosedLoopEnabled, "<closed_loop", bool, boolean);
+  m_pressureSwitchCbKey =
+      REGISTER_CTREPCM(PressureSwitch, ">pressure_switch", bool, boolean);
+  m_currentCbKey =
+      REGISTER_CTREPCM(CompressorCurrent, ">current", double, double);
+}
+
+void HALSimWSProviderPCM::CancelCallbacks() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderPCM::DoCancelCallbacks() {
+  HALSIM_CancelCTREPCMInitializedCallback(m_channel, m_initCbKey);
+  HALSIM_CancelCTREPCMCompressorOnCallback(m_channel, m_onCbKey);
+  HALSIM_CancelCTREPCMClosedLoopEnabledCallback(m_channel, m_closedLoopCbKey);
+  HALSIM_CancelCTREPCMPressureSwitchCallback(m_channel, m_pressureSwitchCbKey);
+  HALSIM_CancelCTREPCMCompressorCurrentCallback(m_channel, m_currentCbKey);
+
+  m_initCbKey = 0;
+  m_onCbKey = 0;
+  m_closedLoopCbKey = 0;
+  m_pressureSwitchCbKey = 0;
+  m_currentCbKey = 0;
+}
+
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
index cd0f4f4..5792ef5 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_PWM.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_PWM.h"
 
@@ -25,7 +22,9 @@
                                        webRegisterFunc);
 }
 
-HALSimWSProviderPWM::~HALSimWSProviderPWM() { DoCancelCallbacks(); }
+HALSimWSProviderPWM::~HALSimWSProviderPWM() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderPWM::RegisterCallbacks() {
   m_initCbKey = REGISTER(Initialized, "<init", bool, boolean);
@@ -36,7 +35,9 @@
   m_zeroLatchCbKey = REGISTER(ZeroLatch, "<zero_latch", bool, boolean);
 }
 
-void HALSimWSProviderPWM::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderPWM::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderPWM::DoCancelCallbacks() {
   HALSIM_CancelPWMInitializedCallback(m_channel, m_initCbKey);
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Relay.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Relay.cpp
index e51658f..4bb7e55 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Relay.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Relay.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_Relay.h"
 
@@ -25,7 +22,9 @@
                                          webRegisterFunc);
 }
 
-HALSimWSProviderRelay::~HALSimWSProviderRelay() { DoCancelCallbacks(); }
+HALSimWSProviderRelay::~HALSimWSProviderRelay() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderRelay::RegisterCallbacks() {
   m_initFwdCbKey = REGISTER(InitializedForward, "<init_fwd", bool, boolean);
@@ -34,7 +33,9 @@
   m_revCbKey = REGISTER(Reverse, "<rev", bool, boolean);
 }
 
-void HALSimWSProviderRelay::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderRelay::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderRelay::DoCancelCallbacks() {
   HALSIM_CancelRelayInitializedForwardCallback(m_channel, m_initFwdCbKey);
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_RoboRIO.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_RoboRIO.cpp
index 42e2cc8..c2998cb 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_RoboRIO.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_RoboRIO.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_RoboRIO.h"
 
@@ -24,7 +21,9 @@
   CreateSingleProvider<HALSimWSProviderRoboRIO>("RoboRIO", webRegisterFunc);
 }
 
-HALSimWSProviderRoboRIO::~HALSimWSProviderRoboRIO() { DoCancelCallbacks(); }
+HALSimWSProviderRoboRIO::~HALSimWSProviderRoboRIO() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderRoboRIO::RegisterCallbacks() {
   m_fpgaCbKey = REGISTER(FPGAButton, ">fpga_button", bool, boolean);
@@ -47,7 +46,9 @@
   m_3v3FaultsCbKey = REGISTER(UserFaults3V3, ">3v3_faults", int32_t, int);
 }
 
-void HALSimWSProviderRoboRIO::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderRoboRIO::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderRoboRIO::DoCancelCallbacks() {
   HALSIM_CancelRoboRioFPGAButtonCallback(m_fpgaCbKey);
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_SimDevice.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_SimDevice.cpp
index ee8fded..93852b6 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_SimDevice.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_SimDevice.cpp
@@ -1,17 +1,21 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_SimDevice.h"
 
+#include <algorithm>
+#include <cmath>
+
+#include <fmt/format.h>
 #include <hal/Ports.h>
+#include <wpi/StringExtras.h>
 
 namespace wpilibws {
 
-HALSimWSProviderSimDevice::~HALSimWSProviderSimDevice() { CancelCallbacks(); }
+HALSimWSProviderSimDevice::~HALSimWSProviderSimDevice() {
+  CancelCallbacks();
+}
 
 void HALSimWSProviderSimDevice::OnNetworkConnected(
     std::shared_ptr<HALSimBaseWebSocketConnection> ws) {
@@ -69,15 +73,38 @@
           break;
         case HAL_DOUBLE:
           value.data.v_double = it.value();
+          value.data.v_double -= vd->second->doubleOffset;
           break;
-        case HAL_ENUM:
+        case HAL_ENUM: {
+          if (it->is_string()) {
+            auto& options = vd->second->options;
+            auto& str = it.value().get_ref<const std::string&>();
+            auto optionIt =
+                std::find_if(options.begin(), options.end(),
+                             [&](const std::string& v) { return v == str; });
+            if (optionIt != options.end()) {
+              value.data.v_enum = optionIt - options.begin();
+            }
+          } else if (it->is_number()) {
+            auto& values = vd->second->optionValues;
+            double num = it.value();
+            auto valueIt = std::find_if(
+                values.begin(), values.end(),
+                [&](double v) { return std::fabs(v - num) < 1e-4; });
+            if (valueIt != values.end()) {
+              value.data.v_enum = valueIt - values.begin();
+            }
+          }
           value.data.v_enum = it.value();
           break;
+        }
         case HAL_INT:
           value.data.v_int = it.value();
+          value.data.v_int -= vd->second->intOffset;
           break;
         case HAL_LONG:
           value.data.v_long = it.value();
+          value.data.v_long -= vd->second->intOffset;
           break;
         default:
           break;
@@ -88,15 +115,51 @@
   }
 }
 
+void HALSimWSProviderSimDevice::OnValueCreatedStatic(
+    const char* name, void* param, HAL_SimValueHandle handle, int32_t direction,
+    const struct HAL_Value* value) {
+  (reinterpret_cast<HALSimWSProviderSimDevice*>(param))
+      ->OnValueCreated(name, handle, direction, value);
+}
+
 void HALSimWSProviderSimDevice::OnValueCreated(const char* name,
                                                HAL_SimValueHandle handle,
-                                               HAL_Bool readonly,
+                                               int32_t direction,
                                                const struct HAL_Value* value) {
-  wpi::Twine key = wpi::Twine(readonly ? "<" : "<>") + name;
+  const char* prefix = "";
+  if (name[0] != '<' && name[0] != '>') {
+    switch (direction) {
+      case HAL_SimValueInput:
+        prefix = ">";
+        break;
+      case HAL_SimValueOutput:
+        prefix = "<";
+        break;
+      case HAL_SimValueBidir:
+        prefix = "<>";
+        break;
+      default:
+        break;
+    }
+  }
+  std::string key = fmt::format("{}{}", prefix, name);
   auto data = std::make_unique<SimDeviceValueData>();
   data->device = this;
   data->handle = handle;
-  data->key = key.str();
+  data->key = key;
+  if (value->type == HAL_ENUM) {
+    int32_t numOptions = 0;
+
+    const char** options = HALSIM_GetSimValueEnumOptions(handle, &numOptions);
+    data->options.reserve(numOptions);
+    for (int32_t i = 0; i < numOptions; ++i) {
+      data->options.emplace_back(options[i]);
+    }
+
+    const double* values =
+        HALSIM_GetSimValueEnumDoubleValues(handle, &numOptions);
+    data->optionValues.assign(values, values + numOptions);
+  }
   data->valueType = value->type;
 
   auto param = data.get();
@@ -109,7 +172,14 @@
   int32_t cbKey = HALSIM_RegisterSimValueChangedCallback(
       handle, param, HALSimWSProviderSimDevice::OnValueChangedStatic, true);
 
-  m_simValueChangedCbKeys[key.str()] = cbKey;
+  m_simValueChangedCbKeys[key] = cbKey;
+}
+
+void HALSimWSProviderSimDevice::OnValueChangedStatic(
+    const char* name, void* param, HAL_SimValueHandle handle, int32_t direction,
+    const struct HAL_Value* value) {
+  auto valueData = (reinterpret_cast<SimDeviceValueData*>(param));
+  valueData->device->OnValueChanged(valueData, value);
 }
 
 void HALSimWSProviderSimDevice::OnValueChanged(SimDeviceValueData* valueData,
@@ -121,16 +191,25 @@
         ProcessHalCallback({{valueData->key, value->data.v_boolean}});
         break;
       case HAL_DOUBLE:
-        ProcessHalCallback({{valueData->key, value->data.v_double}});
+        ProcessHalCallback(
+            {{valueData->key, value->data.v_double + valueData->doubleOffset}});
         break;
-      case HAL_ENUM:
-        ProcessHalCallback({{valueData->key, value->data.v_enum}});
+      case HAL_ENUM: {
+        int v = value->data.v_enum;
+        if (v >= 0 && v < static_cast<int>(valueData->optionValues.size())) {
+          ProcessHalCallback({{valueData->key, valueData->optionValues[v]}});
+        } else if (v >= 0 && v < static_cast<int>(valueData->options.size())) {
+          ProcessHalCallback({{valueData->key, valueData->options[v]}});
+        }
         break;
+      }
       case HAL_INT:
-        ProcessHalCallback({{valueData->key, value->data.v_int}});
+        ProcessHalCallback(
+            {{valueData->key, value->data.v_int + valueData->intOffset}});
         break;
       case HAL_LONG:
-        ProcessHalCallback({{valueData->key, value->data.v_long}});
+        ProcessHalCallback(
+            {{valueData->key, value->data.v_long + valueData->intOffset}});
         break;
       default:
         break;
@@ -138,23 +217,61 @@
   }
 }
 
+void HALSimWSProviderSimDevice::OnValueResetStatic(
+    const char* name, void* param, HAL_SimValueHandle handle, int32_t direction,
+    const struct HAL_Value* value) {
+  auto valueData = (reinterpret_cast<SimDeviceValueData*>(param));
+  valueData->device->OnValueReset(valueData, value);
+}
+
+void HALSimWSProviderSimDevice::OnValueReset(SimDeviceValueData* valueData,
+                                             const struct HAL_Value* value) {
+  switch (value->type) {
+    case HAL_BOOLEAN:
+    case HAL_ENUM:
+      break;
+    case HAL_DOUBLE:
+      valueData->doubleOffset += value->data.v_double;
+      break;
+    case HAL_INT:
+      valueData->intOffset += value->data.v_int;
+      break;
+    case HAL_LONG:
+      valueData->intOffset += value->data.v_long;
+      break;
+    default:
+      break;
+  }
+}
+
 void HALSimWSProviderSimDevice::ProcessHalCallback(const wpi::json& payload) {
   auto ws = m_ws.lock();
   if (ws) {
     wpi::json netValue = {
-        {"type", "SimDevices"}, {"device", m_deviceId}, {"data", payload}};
+        {"type", m_type}, {"device", m_deviceId}, {"data", payload}};
     ws->OnSimValueChanged(netValue);
   }
 }
 
-HALSimWSProviderSimDevices::~HALSimWSProviderSimDevices() { CancelCallbacks(); }
+HALSimWSProviderSimDevices::~HALSimWSProviderSimDevices() {
+  CancelCallbacks();
+}
 
 void HALSimWSProviderSimDevices::DeviceCreatedCallback(
     const char* name, HAL_SimDeviceHandle handle) {
-  auto key = (wpi::Twine("SimDevices/") + name).str();
-  auto dev = std::make_shared<HALSimWSProviderSimDevice>(
-      handle, key, wpi::Twine(name).str());
-  m_providers.Add(key, dev);
+  // Map "Accel:Foo" -> type=Accel, device=Foo
+  auto [type, id] = wpi::split(name, ':');
+  std::shared_ptr<HALSimWSProviderSimDevice> dev;
+  if (id.empty()) {
+    auto key = fmt::format("SimDevice/{}", type);
+    dev = std::make_shared<HALSimWSProviderSimDevice>(handle, key, "SimDevice",
+                                                      type);
+    m_providers.Add(key, dev);
+  } else {
+    auto key = fmt::format("{}/{}", type, id);
+    dev = std::make_shared<HALSimWSProviderSimDevice>(handle, key, type, id);
+    m_providers.Add(key, dev);
+  }
 
   if (m_ws) {
     m_exec->Call([this, dev]() { dev->OnNetworkConnected(GetWSConnection()); });
@@ -170,7 +287,7 @@
   m_deviceCreatedCbKey = HALSIM_RegisterSimDeviceCreatedCallback(
       "", this, HALSimWSProviderSimDevices::DeviceCreatedCallbackStatic, 1);
   m_deviceFreedCbKey = HALSIM_RegisterSimDeviceFreedCallback(
-      "", this, HALSimWSProviderSimDevices::DeviceFreedCallbackStatic);
+      "", this, HALSimWSProviderSimDevices::DeviceFreedCallbackStatic, false);
 
   m_exec = UvExecFn::Create(loop, [](auto out, LoopFn func) {
     func();
@@ -191,6 +308,8 @@
   m_ws = hws;
 }
 
-void HALSimWSProviderSimDevices::OnNetworkDisconnected() { m_ws = nullptr; }
+void HALSimWSProviderSimDevices::OnNetworkDisconnected() {
+  m_ws = nullptr;
+}
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Solenoid.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Solenoid.cpp
new file mode 100644
index 0000000..a26a4e5
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_Solenoid.cpp
@@ -0,0 +1,67 @@
+// 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 "WSProvider_Solenoid.h"
+
+#include <fmt/format.h>
+#include <hal/Ports.h>
+#include <hal/simulation/CTREPCMData.h>
+
+#define REGISTER_SOLENOID(halsim, jsonid, ctype, haltype)                  \
+  HALSIM_RegisterCTREPCMSolenoid##halsim##Callback(                        \
+      m_pcmIndex, m_solenoidIndex,                                         \
+      [](const char* name, void* param, const struct HAL_Value* value) {   \
+        static_cast<HALSimWSProviderSolenoid*>(param)->ProcessHalCallback( \
+            {{jsonid, static_cast<ctype>(value->data.v_##haltype)}});      \
+      },                                                                   \
+      this, true)
+
+namespace wpilibws {
+void HALSimWSProviderSolenoid::Initialize(WSRegisterFunc webRegisterFunc) {
+  for (int32_t CTREPCMIndex = 0; CTREPCMIndex < HAL_GetNumCTREPCMModules();
+       ++CTREPCMIndex) {
+    for (int32_t solenoidIndex = 0;
+         solenoidIndex < HAL_GetNumCTRESolenoidChannels(); ++solenoidIndex) {
+      auto key = fmt::format("Solenoid/{},{}", CTREPCMIndex, solenoidIndex);
+      auto ptr = std::make_unique<HALSimWSProviderSolenoid>(
+          CTREPCMIndex, solenoidIndex, key, "Solenoid");
+      webRegisterFunc(key, std::move(ptr));
+    }
+  }
+}
+
+HALSimWSProviderSolenoid::HALSimWSProviderSolenoid(int32_t CTREPCMChannel,
+                                                   int32_t solenoidChannel,
+                                                   const std::string& key,
+                                                   const std::string& type)
+    : HALSimWSHalProvider(key, type),
+      m_pcmIndex(CTREPCMChannel),
+      m_solenoidIndex(solenoidChannel) {
+  m_deviceId = fmt::format("{},{}", m_pcmIndex, solenoidChannel);
+}
+
+HALSimWSProviderSolenoid::~HALSimWSProviderSolenoid() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderSolenoid::RegisterCallbacks() {
+  // m_initCbKey = REGISTER_SOLENOID(Initialized, "<init", bool, boolean);
+  m_outputCbKey = REGISTER_SOLENOID(Output, "<output", bool, boolean);
+}
+
+void HALSimWSProviderSolenoid::CancelCallbacks() {
+  DoCancelCallbacks();
+}
+
+void HALSimWSProviderSolenoid::DoCancelCallbacks() {
+  // HALSIM_CancelCTREPCMSolenoidInitializedCallback(m_pcmIndex,
+  // m_solenoidIndex,
+  //                                                 m_initCbKey);
+  HALSIM_CancelCTREPCMSolenoidOutputCallback(m_pcmIndex, m_solenoidIndex,
+                                             m_outputCbKey);
+
+  m_initCbKey = 0;
+  m_outputCbKey = 0;
+}
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_dPWM.cpp b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_dPWM.cpp
index ce4fd79..af8a8e4 100644
--- a/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_dPWM.cpp
+++ b/simulation/halsim_ws_core/src/main/native/cpp/WSProvider_dPWM.cpp
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WSProvider_dPWM.h"
 
@@ -36,7 +33,9 @@
   m_pinCbKey = REGISTER(Pin, "<dio_pin", int32_t, int);
 }
 
-void HALSimWSProviderDigitalPWM::CancelCallbacks() { DoCancelCallbacks(); }
+void HALSimWSProviderDigitalPWM::CancelCallbacks() {
+  DoCancelCallbacks();
+}
 
 void HALSimWSProviderDigitalPWM::DoCancelCallbacks() {
   HALSIM_CancelDigitalPWMInitializedCallback(m_channel, m_initCbKey);
diff --git a/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h b/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
index 9f01100..bcf29c2 100644
--- a/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
+++ b/simulation/halsim_ws_core/src/main/native/include/HALSimBaseWebSocketConnection.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSBaseProvider.h b/simulation/halsim_ws_core/src/main/native/include/WSBaseProvider.h
index 46873b7..268c47a 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSBaseProvider.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSBaseProvider.h
@@ -1,15 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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>
+#include <string_view>
 
 #include <wpi/json.h>
 
@@ -19,9 +17,9 @@
 
 class HALSimWSBaseProvider {
  public:
-  explicit HALSimWSBaseProvider(const std::string& key,
-                                const std::string& type = "");
-  virtual ~HALSimWSBaseProvider() {}
+  explicit HALSimWSBaseProvider(std::string_view key,
+                                std::string_view type = "");
+  virtual ~HALSimWSBaseProvider() = default;
 
   HALSimWSBaseProvider(const HALSimWSBaseProvider&) = delete;
   HALSimWSBaseProvider& operator=(const HALSimWSBaseProvider&) = delete;
@@ -38,8 +36,8 @@
   // network -> sim
   virtual void OnNetValueChanged(const wpi::json& json);
 
-  const std::string GetDeviceType() { return m_type; }
-  const std::string GetDeviceId() { return m_deviceId; }
+  const std::string& GetDeviceType() { return m_type; }
+  const std::string& GetDeviceId() { return m_deviceId; }
 
  protected:
   // sim -> network
@@ -47,7 +45,7 @@
   std::string m_key;
 
   std::string m_type;
-  std::string m_deviceId = "";
+  std::string m_deviceId;
 };
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
index be32607..e1bc1f8 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.h
@@ -1,15 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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>
+#include <string_view>
 
 #include <hal/simulation/NotifyListener.h>
 #include <wpi/json.h>
@@ -19,19 +17,20 @@
 
 namespace wpilibws {
 
-typedef void (*HALCbRegisterIndexedFunc)(int32_t index,
-                                         HAL_NotifyCallback callback,
+using HALCbRegisterIndexedFunc = void (*)(int32_t index,
+                                          HAL_NotifyCallback callback,
+                                          void* param, HAL_Bool initialNotify);
+using HALCbRegisterSingleFunc = void (*)(HAL_NotifyCallback callback,
                                          void* param, HAL_Bool initialNotify);
-typedef void (*HALCbRegisterSingleFunc)(HAL_NotifyCallback callback,
-                                        void* param, HAL_Bool initialNotify);
 
 // provider generates diffs based on values
 class HALSimWSHalProvider : public HALSimWSBaseProvider {
  public:
   using HALSimWSBaseProvider::HALSimWSBaseProvider;
 
-  void OnNetworkConnected(std::shared_ptr<HALSimBaseWebSocketConnection> ws);
-  void OnNetworkDisconnected();
+  void OnNetworkConnected(
+      std::shared_ptr<HALSimBaseWebSocketConnection> ws) override;
+  void OnNetworkDisconnected() override;
 
   void ProcessHalCallback(const wpi::json& payload);
 
@@ -43,8 +42,8 @@
 // provider generates per-channel diffs
 class HALSimWSHalChanProvider : public HALSimWSHalProvider {
  public:
-  explicit HALSimWSHalChanProvider(int32_t channel, const std::string& key,
-                                   const std::string& type);
+  explicit HALSimWSHalChanProvider(int32_t channel, std::string_view key,
+                                   std::string_view type);
 
   int32_t GetChannel() { return m_channel; }
 
@@ -53,16 +52,15 @@
 };
 
 using WSRegisterFunc = std::function<void(
-    const std::string&, std::shared_ptr<HALSimWSBaseProvider>)>;
+    std::string_view, std::shared_ptr<HALSimWSBaseProvider>)>;
 
 template <typename T>
-void CreateProviders(const std::string& prefix, int32_t numChannels,
+void CreateProviders(std::string_view prefix, int32_t numChannels,
                      WSRegisterFunc webRegisterFunc);
 
 template <typename T>
-void CreateSingleProvider(const std::string& key,
-                          WSRegisterFunc webRegisterFunc);
-
-#include "WSHalProviders.inl"
+void CreateSingleProvider(std::string_view key, WSRegisterFunc webRegisterFunc);
 
 }  // namespace wpilibws
+
+#include "WSHalProviders.inc"
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inc b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inc
new file mode 100644
index 0000000..c9ce9d1
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inc
@@ -0,0 +1,34 @@
+// 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_view>
+#include <utility>
+
+#include <fmt/format.h>
+
+#include "WSHalProviders.h"
+
+namespace wpilibws {
+
+template <typename T>
+void CreateProviders(std::string_view prefix, int numChannels,
+                     WSRegisterFunc webRegisterFunc) {
+  for (int32_t i = 0; i < numChannels; i++) {
+    auto key = fmt::format("{}/{}", prefix, i);
+    auto ptr = std::make_unique<T>(i, key, prefix);
+    webRegisterFunc(key, std::move(ptr));
+  }
+}
+
+template <typename T>
+void CreateSingleProvider(std::string_view key,
+                          WSRegisterFunc webRegisterFunc) {
+  auto ptr = std::make_unique<T>(key, key);
+  webRegisterFunc(key, std::move(ptr));
+}
+
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inl b/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inl
deleted file mode 100644
index 6bcdcdd..0000000
--- a/simulation/halsim_ws_core/src/main/native/include/WSHalProviders.inl
+++ /dev/null
@@ -1,29 +0,0 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
-
-#pragma once
-
-#include <memory>
-#include <string>
-#include <utility>
-
-template <typename T>
-void CreateProviders(const std::string& prefix, int numChannels,
-                     WSRegisterFunc webRegisterFunc) {
-  for (int32_t i = 0; i < numChannels; i++) {
-    auto key = (prefix + "/" + wpi::Twine(i)).str();
-    auto ptr = std::make_unique<T>(i, key, prefix);
-    webRegisterFunc(key, std::move(ptr));
-  }
-}
-
-template <typename T>
-void CreateSingleProvider(const std::string& key,
-                          WSRegisterFunc webRegisterFunc) {
-  auto ptr = std::make_unique<T>(key, key);
-  webRegisterFunc(key, std::move(ptr));
-}
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProviderContainer.h b/simulation/halsim_ws_core/src/main/native/include/WSProviderContainer.h
index 7cd1240..7c8f861 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProviderContainer.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProviderContainer.h
@@ -1,15 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <mutex>
 #include <shared_mutex>
+#include <string_view>
 
 #include <wpi/StringMap.h>
 
@@ -22,17 +21,18 @@
   using ProviderPtr = std::shared_ptr<HALSimWSBaseProvider>;
   using IterFn = std::function<void(ProviderPtr)>;
 
-  ProviderContainer() {}
+  ProviderContainer() = default;
 
   ProviderContainer(const ProviderContainer&) = delete;
   ProviderContainer& operator=(const ProviderContainer&) = delete;
 
-  void Add(wpi::StringRef key, std::shared_ptr<HALSimWSBaseProvider> provider) {
+  void Add(std::string_view key,
+           std::shared_ptr<HALSimWSBaseProvider> provider) {
     std::unique_lock lock(m_mutex);
     m_providers[key] = provider;
   }
 
-  void Delete(wpi::StringRef key) {
+  void Delete(std::string_view key) {
     std::unique_lock lock(m_mutex);
     m_providers.erase(key);
   }
@@ -44,7 +44,7 @@
     }
   }
 
-  ProviderPtr Get(wpi::StringRef key) {
+  ProviderPtr Get(std::string_view key) {
     std::shared_lock lock(m_mutex);
     auto fiter = m_providers.find(key);
     return fiter != m_providers.end() ? fiter->second : ProviderPtr();
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_AddressableLED.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_AddressableLED.h
new file mode 100644
index 0000000..3970774
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_AddressableLED.h
@@ -0,0 +1,29 @@
+// 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 "WSHalProviders.h"
+
+namespace wpilibws {
+class HALSimWSProviderAddressableLED : public HALSimWSHalChanProvider {
+ public:
+  static void Initialize(WSRegisterFunc webRegisterFunc);
+
+  using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
+  ~HALSimWSProviderAddressableLED() override;
+
+ protected:
+  void RegisterCallbacks() override;
+  void CancelCallbacks() override;
+  void DoCancelCallbacks();
+
+ private:
+  int32_t m_initCbKey = 0;
+  int32_t m_outputPortCbKey = 0;
+  int32_t m_lengthCbKey = 0;
+  int32_t m_runningCbKey = 0;
+  int32_t m_dataCbKey = 0;
+};
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Analog.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Analog.h
index 8ebc235..ec5771e 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Analog.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Analog.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,13 +15,13 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderAnalogIn();
+  ~HALSimWSProviderAnalogIn() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
  protected:
   void RegisterCallbacks() override;
-  void CancelCallbacks() override;
+  void CancelCallbacks() final;
   void DoCancelCallbacks();
 
  private:
@@ -44,11 +41,11 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderAnalogOut();
+  ~HALSimWSProviderAnalogOut() override;
 
  protected:
   void RegisterCallbacks() override;
-  void CancelCallbacks() override;
+  void CancelCallbacks() final;
   void DoCancelCallbacks();
 
  private:
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_BuiltInAccelerometer.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_BuiltInAccelerometer.h
new file mode 100644
index 0000000..62568e7
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_BuiltInAccelerometer.h
@@ -0,0 +1,32 @@
+// 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 "WSHalProviders.h"
+
+namespace wpilibws {
+class HALSimWSProviderBuiltInAccelerometer : public HALSimWSHalProvider {
+ public:
+  HALSimWSProviderBuiltInAccelerometer();
+  static void Initialize(WSRegisterFunc webRegisterFunc);
+
+  using HALSimWSHalProvider::HALSimWSHalProvider;
+  ~HALSimWSProviderBuiltInAccelerometer() override;
+
+  void OnNetValueChanged(const wpi::json& json) override;
+
+ protected:
+  void RegisterCallbacks() override;
+  void CancelCallbacks() override;
+  void DoCancelCallbacks();
+
+ private:
+  int32_t m_activeCbKey = 0;
+  int32_t m_rangeCbKey = 0;
+  int32_t m_xCbKey = 0;
+  int32_t m_yCbKey = 0;
+  int32_t m_zCbKey = 0;
+};
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_DIO.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_DIO.h
index 8d6da7e..5fc3296 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_DIO.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_DIO.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderDIO();
+  ~HALSimWSProviderDIO() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_DriverStation.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_DriverStation.h
index c61419c..f164a70 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_DriverStation.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_DriverStation.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalProvider::HALSimWSHalProvider;
-  ~HALSimWSProviderDriverStation();
+  ~HALSimWSProviderDriverStation() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Encoder.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Encoder.h
index 882e447..b7601b3 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Encoder.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Encoder.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderEncoder();
+  ~HALSimWSProviderEncoder() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
@@ -34,6 +31,8 @@
   int32_t m_resetCbKey = 0;
   int32_t m_reverseDirectionCbKey = 0;
   int32_t m_samplesCbKey = 0;
+
+  int32_t m_countOffset = 0;
 };
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Joystick.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Joystick.h
index 4b06bee..edabad0 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Joystick.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Joystick.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderJoystick();
+  ~HALSimWSProviderJoystick() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_PCM.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_PCM.h
new file mode 100644
index 0000000..d12d6df
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_PCM.h
@@ -0,0 +1,31 @@
+// 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 "WSHalProviders.h"
+
+namespace wpilibws {
+class HALSimWSProviderPCM : public HALSimWSHalChanProvider {
+ public:
+  static void Initialize(WSRegisterFunc webRegisterFunc);
+
+  using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
+  ~HALSimWSProviderPCM() override;
+
+ protected:
+  void RegisterCallbacks() override;
+  void CancelCallbacks() override;
+  void DoCancelCallbacks();
+
+ private:
+  int32_t m_initCbKey = 0;
+  int32_t m_onCbKey = 0;
+  int32_t m_closedLoopCbKey = 0;
+  int32_t m_pressureSwitchCbKey = 0;
+  int32_t m_currentCbKey = 0;
+};
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_PWM.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_PWM.h
index 19be17a..badb02d 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_PWM.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_PWM.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderPWM();
+  ~HALSimWSProviderPWM() override;
 
  protected:
   void RegisterCallbacks() override;
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Relay.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Relay.h
index aa3f84f..d712865 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Relay.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Relay.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderRelay();
+  ~HALSimWSProviderRelay() override;
 
  protected:
   void RegisterCallbacks() override;
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_RoboRIO.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_RoboRIO.h
index 9ffc2ed..0b8e44e 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_RoboRIO.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_RoboRIO.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalProvider::HALSimWSHalProvider;
-  ~HALSimWSProviderRoboRIO();
+  ~HALSimWSProviderRoboRIO() override;
 
   void OnNetValueChanged(const wpi::json& json) override;
 
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
index 63e5d22..c4d4347 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_SimDevice.h
@@ -1,14 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <hal/SimDevice.h>
 #include <hal/simulation/SimDeviceData.h>
@@ -27,18 +26,22 @@
   HALSimWSProviderSimDevice* device;
   HAL_SimValueHandle handle;
   std::string key;
+  std::vector<std::string> options;
+  std::vector<double> optionValues;
   HAL_Type valueType;
+  double doubleOffset = 0;
+  int64_t intOffset = 0;
 };
 
 class HALSimWSProviderSimDevice : public HALSimWSBaseProvider {
  public:
-  HALSimWSProviderSimDevice(HAL_SimDeviceHandle handle, const std::string& key,
-                            const std::string& deviceId)
-      : HALSimWSBaseProvider(key, "SimDevices"), m_handle(handle) {
+  HALSimWSProviderSimDevice(HAL_SimDeviceHandle handle, std::string_view key,
+                            std::string_view type, std::string_view deviceId)
+      : HALSimWSBaseProvider(key, type), m_handle(handle) {
     m_deviceId = deviceId;
   }
 
-  ~HALSimWSProviderSimDevice();
+  ~HALSimWSProviderSimDevice() override;
 
   void OnNetworkConnected(
       std::shared_ptr<HALSimBaseWebSocketConnection> ws) override;
@@ -51,23 +54,23 @@
 
  private:
   static void OnValueCreatedStatic(const char* name, void* param,
-                                   HAL_SimValueHandle handle, HAL_Bool readonly,
-                                   const struct HAL_Value* value) {
-    (reinterpret_cast<HALSimWSProviderSimDevice*>(param))
-        ->OnValueCreated(name, handle, readonly, value);
-  }
+                                   HAL_SimValueHandle handle, int32_t direction,
+                                   const struct HAL_Value* value);
   void OnValueCreated(const char* name, HAL_SimValueHandle handle,
-                      HAL_Bool readonly, const struct HAL_Value* value);
+                      int32_t direction, const struct HAL_Value* value);
 
   static void OnValueChangedStatic(const char* name, void* param,
-                                   HAL_SimValueHandle handle, HAL_Bool readonly,
-                                   const struct HAL_Value* value) {
-    auto valueData = (reinterpret_cast<SimDeviceValueData*>(param));
-    valueData->device->OnValueChanged(valueData, value);
-  }
+                                   HAL_SimValueHandle handle, int32_t direction,
+                                   const struct HAL_Value* value);
   void OnValueChanged(SimDeviceValueData* valueData,
                       const struct HAL_Value* value);
 
+  static void OnValueResetStatic(const char* name, void* param,
+                                 HAL_SimValueHandle handle, int32_t direction,
+                                 const struct HAL_Value* value);
+  void OnValueReset(SimDeviceValueData* valueData,
+                    const struct HAL_Value* value);
+
   void CancelCallbacks();
 
   wpi::StringMap<std::unique_ptr<SimDeviceValueData>> m_valueHandles;
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_Solenoid.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Solenoid.h
new file mode 100644
index 0000000..b6bc47a
--- /dev/null
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_Solenoid.h
@@ -0,0 +1,33 @@
+// 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 "WSHalProviders.h"
+
+namespace wpilibws {
+class HALSimWSProviderSolenoid : public HALSimWSHalProvider {
+ public:
+  static void Initialize(WSRegisterFunc webRegisterFunc);
+
+  explicit HALSimWSProviderSolenoid(int32_t pcmChannel, int32_t solenoidChannel,
+                                    const std::string& key,
+                                    const std::string& type);
+  ~HALSimWSProviderSolenoid() override;
+
+ protected:
+  void RegisterCallbacks() override;
+  void CancelCallbacks() override;
+  void DoCancelCallbacks();
+
+ private:
+  int32_t m_pcmIndex = 0;
+  int32_t m_solenoidIndex = 0;
+
+  int32_t m_initCbKey = 0;
+  int32_t m_outputCbKey = 0;
+};
+}  // namespace wpilibws
diff --git a/simulation/halsim_ws_core/src/main/native/include/WSProvider_dPWM.h b/simulation/halsim_ws_core/src/main/native/include/WSProvider_dPWM.h
index 49b7000..0340a59 100644
--- a/simulation/halsim_ws_core/src/main/native/include/WSProvider_dPWM.h
+++ b/simulation/halsim_ws_core/src/main/native/include/WSProvider_dPWM.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -18,7 +15,7 @@
   static void Initialize(WSRegisterFunc webRegisterFunc);
 
   using HALSimWSHalChanProvider::HALSimWSHalChanProvider;
-  ~HALSimWSProviderDigitalPWM();
+  ~HALSimWSProviderDigitalPWM() override;
 
  protected:
   void RegisterCallbacks() override;
diff --git a/simulation/halsim_ws_server/CMakeLists.txt b/simulation/halsim_ws_server/CMakeLists.txt
index d1c7983..e5b55c8 100644
--- a/simulation/halsim_ws_server/CMakeLists.txt
+++ b/simulation/halsim_ws_server/CMakeLists.txt
@@ -4,7 +4,7 @@
 
 file(GLOB halsim_ws_server_src src/main/native/cpp/*.cpp)
 
-add_library(halsim_ws_server MODULE ${halsim_ws_server_src})
+add_library(halsim_ws_server SHARED ${halsim_ws_server_src})
 wpilib_target_warnings(halsim_ws_server)
 set_target_properties(halsim_ws_server PROPERTIES DEBUG_POSTFIX "d")
 target_link_libraries(halsim_ws_server PUBLIC hal halsim_ws_core)
diff --git a/simulation/halsim_ws_server/README.md b/simulation/halsim_ws_server/README.md
new file mode 100644
index 0000000..c75414d
--- /dev/null
+++ b/simulation/halsim_ws_server/README.md
@@ -0,0 +1,15 @@
+# HAL WebSockets Server
+
+This is an extension that provides a server version of a WebSockets API for transmitting robot hardware interface state over a network.  See the [Robot Hardware Interface WebSockets API specification](../halsim_ws_core/doc/hardware_ws_api.md) for more details on the protocol.
+
+## Configuration
+
+The WebSockets server has a number of configuration options available through environment variables.
+
+``HALSIMWS_SYSROOT``: The local directory to serve non-websocket HTTP content from (e.g. HTML files) starting from `/`.  Defaults to the `sim` subdirectory of the current working directory.
+
+``HALSIMWS_USERROOT``: The local directory to serve non-websocket HTTP content from (e.g. HTML files) starting from `/user/`.  Defaults to the `sim/user` subdirectory of the current working directory.
+
+``HALSIMWS_PORT``: The port number to listen at.  Defaults to 3300.
+
+``HALSIMWS_URI``: The URI path to use for WebSockets connections.  Defaults to ``"/wpilibws"``.
diff --git a/simulation/halsim_ws_server/build.gradle b/simulation/halsim_ws_server/build.gradle
index 6c6be48..16cb24e 100644
--- a/simulation/halsim_ws_server/build.gradle
+++ b/simulation/halsim_ws_server/build.gradle
@@ -59,11 +59,10 @@
             lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
             lib library: pluginName, linkage: 'shared'
             if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
-                nativeUtils.useRequiredLibrary(it, 'netcomm_shared', 'chipobject_shared', 'visa_shared', 'ni_runtime_shared')
+                nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
             }
         }
     }
-
 }
 
 tasks.withType(RunTestExecutable) {
diff --git a/simulation/halsim_ws_server/src/dev/native/cpp/main.cpp b/simulation/halsim_ws_server/src/dev/native/cpp/main.cpp
index c98c5e4..d215df6 100644
--- a/simulation/halsim_ws_server/src/dev/native/cpp/main.cpp
+++ b/simulation/halsim_ws_server/src/dev/native/cpp/main.cpp
@@ -1,18 +1,15 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <cstdio>
 #include <iostream>
 #include <thread>
 
+#include <fmt/format.h>
 #include <hal/DriverStation.h>
 #include <hal/HALBase.h>
 #include <hal/Main.h>
-#include <wpi/Format.h>
-#include <wpi/raw_ostream.h>
 
 extern "C" int HALSIM_InitExtension(void);
 
@@ -28,9 +25,9 @@
   while (cycleCount < 1000) {
     std::this_thread::sleep_for(std::chrono::milliseconds(100));
     cycleCount++;
-    std::cout << "Count: " << cycleCount << std::endl;
+    fmt::print("Count: {}\n", cycleCount);
   }
 
-  std::cout << "DONE" << std::endl;
+  std::puts("DONE");
   HAL_ExitMain();
 }
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp b/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
index 73ebaca..3a2e3b2 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/HALSimHttpConnection.cpp
@@ -1,21 +1,20 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimHttpConnection.h"
 
 #include <uv.h>
 
-#include <wpi/FileSystem.h>
+#include <string_view>
+
+#include <fmt/format.h>
 #include <wpi/MimeTypes.h>
-#include <wpi/Path.h>
 #include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
 #include <wpi/UrlParser.h>
+#include <wpi/fs.h>
 #include <wpi/raw_istream.h>
-#include <wpi/raw_ostream.h>
 #include <wpi/raw_uv_ostream.h>
 #include <wpi/uv/Request.h>
 
@@ -23,7 +22,7 @@
 
 using namespace wpilibws;
 
-bool HALSimHttpConnection::IsValidWsUpgrade(wpi::StringRef protocol) {
+bool HALSimHttpConnection::IsValidWsUpgrade(std::string_view protocol) {
   if (m_request.GetUrl() != m_server->GetServerUri()) {
     MySendError(404, "invalid websocket address");
     return false;
@@ -33,7 +32,7 @@
 }
 
 void HALSimHttpConnection::ProcessWsUpgrade() {
-  m_websocket->open.connect_extended([this](auto conn, wpi::StringRef) {
+  m_websocket->open.connect_extended([this](auto conn, auto) {
     conn.disconnect();  // one-shot
 
     if (!m_server->RegisterWebsocket(shared_from_this())) {
@@ -44,11 +43,11 @@
 
     Log(200);
     m_isWsConnected = true;
-    wpi::errs() << "HALWebSim: websocket connected\n";
+    std::fputs("HALWebSim: websocket connected\n", stderr);
   });
 
   // parse incoming JSON, dispatch to parent
-  m_websocket->text.connect([this](wpi::StringRef msg, bool) {
+  m_websocket->text.connect([this](auto msg, bool) {
     if (!m_isWsConnected) {
       return;
     }
@@ -65,10 +64,10 @@
     m_server->OnNetValueChanged(j);
   });
 
-  m_websocket->closed.connect([this](uint16_t, wpi::StringRef) {
+  m_websocket->closed.connect([this](uint16_t, auto) {
     // unset the global, allow another websocket to connect
     if (m_isWsConnected) {
-      wpi::errs() << "HALWebSim: websocket disconnected\n";
+      std::fputs("HALWebSim: websocket disconnected\n", stderr);
       m_isWsConnected = false;
 
       m_server->CloseWebsocket(shared_from_this());
@@ -95,47 +94,36 @@
                                   }
 
                                   if (err) {
-                                    wpi::errs() << err.str() << "\n";
-                                    wpi::errs().flush();
+                                    fmt::print(stderr, "{}\n", err.str());
+                                    std::fflush(stderr);
                                   }
                                 });
   });
 }
 
-void HALSimHttpConnection::SendFileResponse(int code,
-                                            const wpi::Twine& codeText,
-                                            const wpi::Twine& contentType,
-                                            const wpi::Twine& filename,
-                                            const wpi::Twine& extraHeader) {
-  // open file
-  int infd;
-  if (wpi::sys::fs::openFileForRead(filename, infd)) {
-    MySendError(404, "error opening file");
-    return;
-  }
+void HALSimHttpConnection::SendFileResponse(int code, std::string_view codeText,
+                                            std::string_view contentType,
+                                            std::string_view filename,
+                                            std::string_view extraHeader) {
+  std::error_code ec;
 
-  // get status (to get file size)
-  wpi::sys::fs::file_status status;
-  if (wpi::sys::fs::status(infd, status)) {
+  // get file size
+  auto size = fs::file_size(filename, ec);
+  if (ec) {
     MySendError(404, "error getting file size");
-    wpi::sys::fs::file_t file = uv_get_osfhandle(infd);
-    wpi::sys::fs::closeFile(file);
     return;
   }
 
-  uv_os_fd_t outfd;
-  int err = uv_fileno(m_stream.GetRawHandle(), &outfd);
-  if (err < 0) {
-    m_stream.GetLoopRef().ReportError(err);
-    MySendError(404, "error getting fd");
-    wpi::sys::fs::file_t file = uv_get_osfhandle(infd);
-    wpi::sys::fs::closeFile(file);
+  // open file
+  wpi::raw_fd_istream is{filename, ec, true};
+  if (ec) {
+    MySendError(404, "error opening file");
     return;
   }
 
   wpi::SmallVector<uv::Buffer, 4> toSend;
   wpi::raw_uv_ostream os{toSend, 4096};
-  BuildHeader(os, code, codeText, contentType, status.getSize(), extraHeader);
+  BuildHeader(os, code, codeText, contentType, size, extraHeader);
   SendData(os.bufs(), false);
 
   Log(code);
@@ -144,11 +132,10 @@
   wpi::SmallVector<uv::Buffer, 4> bodyData;
   wpi::raw_uv_ostream bodyOs{bodyData, 4096};
 
-  wpi::raw_fd_istream is{infd, true};
   std::string fileBuf;
   size_t oldSize = 0;
 
-  while (fileBuf.size() < status.getSize()) {
+  while (fileBuf.size() < size) {
     oldSize = fileBuf.size();
     fileBuf.resize(oldSize + 1);
     is.read(&(*fileBuf.begin()) + oldSize, 1);
@@ -171,51 +158,47 @@
     return;
   }
 
-  wpi::StringRef path;
-  if (url.HasPath()) path = url.GetPath();
+  std::string_view path;
+  if (url.HasPath()) {
+    path = url.GetPath();
+  }
 
-  if (m_request.GetMethod() == wpi::HTTP_GET && path.startswith("/") &&
-      !path.contains("..")) {
+  if (m_request.GetMethod() == wpi::HTTP_GET && wpi::starts_with(path, '/') &&
+      !wpi::contains(path, "..") && !wpi::contains(path, "//")) {
     // convert to fs native representation
-    wpi::SmallVector<char, 32> nativePath;
-    wpi::sys::path::native(path, nativePath);
-
-    if (path.startswith("/user/")) {
-      std::string prefix = (wpi::sys::path::get_separator() + "user" +
-                            wpi::sys::path::get_separator())
-                               .str();
-      wpi::sys::path::replace_path_prefix(nativePath, prefix,
-                                          m_server->GetWebrootUser());
+    fs::path nativePath;
+    if (wpi::starts_with(path, "/user/")) {
+      nativePath =
+          fs::path{m_server->GetWebrootSys()} /
+          fs::path{wpi::drop_front(path, 6), fs::path::format::generic_format};
     } else {
-      wpi::sys::path::replace_path_prefix(nativePath,
-                                          wpi::sys::path::get_separator(),
-                                          m_server->GetWebrootSys());
+      nativePath =
+          fs::path{m_server->GetWebrootSys()} /
+          fs::path{wpi::drop_front(path, 1), fs::path::format::generic_format};
     }
 
-    if (wpi::sys::fs::is_directory(nativePath)) {
-      wpi::sys::path::append(nativePath, "index.html");
+    if (fs::is_directory(nativePath)) {
+      nativePath.append("index.html");
     }
 
-    if (!wpi::sys::fs::exists(nativePath) ||
-        wpi::sys::fs::is_directory(nativePath)) {
-      MySendError(404, "Resource '" + path + "' not found");
+    if (!fs::exists(nativePath) || fs::is_directory(nativePath)) {
+      MySendError(404, fmt::format("Resource '{}' not found", path));
     } else {
-      auto contentType = wpi::MimeTypeFromPath(wpi::Twine(nativePath).str());
-      SendFileResponse(200, "OK", contentType, nativePath);
+      auto contentType = wpi::MimeTypeFromPath(nativePath.string());
+      SendFileResponse(200, "OK", contentType, nativePath.string());
     }
   } else {
     MySendError(404, "Resource not found");
   }
 }
 
-void HALSimHttpConnection::MySendError(int code, const wpi::Twine& message) {
+void HALSimHttpConnection::MySendError(int code, std::string_view message) {
   Log(code);
   SendError(code, message);
 }
 
 void HALSimHttpConnection::Log(int code) {
   auto method = wpi::http_method_str(m_request.GetMethod());
-  wpi::errs() << method << " " << m_request.GetUrl() << " HTTP/"
-              << m_request.GetMajor() << "." << m_request.GetMinor() << " "
-              << code << "\n";
+  fmt::print(stderr, "{} {} HTTP/{}.{} {}\n", method, m_request.GetUrl(),
+             m_request.GetMajor(), m_request.GetMinor(), code);
 }
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWSServer.cpp b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWSServer.cpp
index 2bc846b..9ecccb0 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWSServer.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWSServer.cpp
@@ -1,22 +1,23 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimWSServer.h"
 
 #include <WSProviderContainer.h>
+#include <WSProvider_AddressableLED.h>
 #include <WSProvider_Analog.h>
+#include <WSProvider_BuiltInAccelerometer.h>
 #include <WSProvider_DIO.h>
 #include <WSProvider_DriverStation.h>
 #include <WSProvider_Encoder.h>
 #include <WSProvider_Joystick.h>
+#include <WSProvider_PCM.h>
 #include <WSProvider_PWM.h>
 #include <WSProvider_Relay.h>
 #include <WSProvider_RoboRIO.h>
 #include <WSProvider_SimDevice.h>
+#include <WSProvider_Solenoid.h>
 #include <WSProvider_dPWM.h>
 
 using namespace wpilibws;
@@ -35,16 +36,20 @@
       providers.Add(key, provider);
     };
 
+    HALSimWSProviderAddressableLED::Initialize(registerFunc);
     HALSimWSProviderAnalogIn::Initialize(registerFunc);
     HALSimWSProviderAnalogOut::Initialize(registerFunc);
+    HALSimWSProviderBuiltInAccelerometer::Initialize(registerFunc);
     HALSimWSProviderDIO::Initialize(registerFunc);
     HALSimWSProviderDigitalPWM::Initialize(registerFunc);
     HALSimWSProviderDriverStation::Initialize(registerFunc);
     HALSimWSProviderEncoder::Initialize(registerFunc);
     HALSimWSProviderJoystick::Initialize(registerFunc);
+    HALSimWSProviderPCM::Initialize(registerFunc);
     HALSimWSProviderPWM::Initialize(registerFunc);
     HALSimWSProviderRelay::Initialize(registerFunc);
     HALSimWSProviderRoboRIO::Initialize(registerFunc);
+    HALSimWSProviderSolenoid::Initialize(registerFunc);
 
     simDevices.Initialize(loop);
 
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
index fe5393d..43e3b08 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/HALSimWeb.cpp
@@ -1,18 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "HALSimWeb.h"
 
-#include <wpi/FileSystem.h>
-#include <wpi/Path.h>
+#include <fmt/format.h>
 #include <wpi/SmallString.h>
-#include <wpi/Twine.h>
 #include <wpi/UrlParser.h>
 #include <wpi/WebSocketServer.h>
+#include <wpi/fs.h>
 #include <wpi/raw_uv_ostream.h>
 #include <wpi/uv/Loop.h>
 #include <wpi/uv/Tcp.h>
@@ -29,7 +25,7 @@
       m_providers(providers),
       m_simDevicesProvider(simDevicesProvider) {
   m_loop.error.connect([](uv::Error err) {
-    wpi::errs() << "HALSim WS Server libuv ERROR: " << err.str() << '\n';
+    fmt::print(stderr, "HALSim WS Server libuv ERROR: {}\n", err.str());
   });
 
   m_server = uv::Tcp::Create(m_loop);
@@ -45,49 +41,40 @@
   }
 
   // determine where to get static content from
-  // wpi::SmallVector<char, 64> tmp;
-  wpi::SmallString<64> tmp;
-
+  fs::path path;
   const char* webroot_sys = std::getenv("HALSIMWS_SYSROOT");
-  if (webroot_sys != NULL) {
-    wpi::StringRef tstr(webroot_sys);
-    tmp.append(tstr);
+  if (webroot_sys != nullptr) {
+    path = webroot_sys;
   } else {
-    wpi::sys::fs::current_path(tmp);
-    wpi::sys::path::append(tmp, "sim");
+    path = fs::current_path() / "sim";
   }
-  wpi::sys::fs::make_absolute(tmp);
-  m_webroot_sys = wpi::Twine(tmp).str();
+  m_webroot_sys = fs::absolute(path).string();
 
-  tmp.clear();
   const char* webroot_user = std::getenv("HALSIMWS_USERROOT");
-  if (webroot_user != NULL) {
-    wpi::StringRef tstr(webroot_user);
-    tmp.append(tstr);
+  if (webroot_user != nullptr) {
+    path = webroot_sys;
   } else {
-    wpi::sys::fs::current_path(tmp);
-    wpi::sys::path::append(tmp, "sim", "user");
+    path = fs::current_path() / "sim" / "user";
   }
-  wpi::sys::fs::make_absolute(tmp);
-  m_webroot_user = wpi::Twine(tmp).str();
+  m_webroot_user = fs::absolute(path).string();
 
   const char* uri = std::getenv("HALSIMWS_URI");
-  if (uri != NULL) {
+  if (uri != nullptr) {
     m_uri = uri;
   } else {
     m_uri = "/wpilibws";
   }
 
   const char* port = std::getenv("HALSIMWS_PORT");
-  if (port != NULL) {
+  if (port != nullptr) {
     try {
       m_port = std::stoi(port);
     } catch (const std::invalid_argument& err) {
-      wpi::errs() << "Error decoding HALSIMWS_PORT (" << err.what() << ")\n";
+      fmt::print(stderr, "Error decoding HALSIMWS_PORT ({})\n", err.what());
       return false;
     }
   } else {
-    m_port = 8080;
+    m_port = 3300;
   }
 
   return true;
@@ -99,7 +86,9 @@
   // when we get a connection, accept it and start reading
   m_server->connection.connect([this, srv = m_server.get()] {
     auto tcp = srv->Accept();
-    if (!tcp) return;
+    if (!tcp) {
+      return;
+    }
 
     tcp->SetNoDelay(true);
 
@@ -109,8 +98,8 @@
 
   // start listening for incoming connections
   m_server->Listen();
-  wpi::outs() << "Listening at http://localhost:" << m_port << "\n";
-  wpi::outs() << "WebSocket URI: " << m_uri << "\n";
+  fmt::print("Listening at http://localhost:{}\n", m_port);
+  fmt::print("WebSocket URI: {}\n", m_uri);
 }
 
 bool HALSimWeb::RegisterWebsocket(
@@ -165,6 +154,6 @@
       provider->OnNetValueChanged(msg.at("data"));
     }
   } catch (wpi::json::exception& e) {
-    wpi::errs() << "Error with incoming message: " << e.what() << "\n";
+    fmt::print(stderr, "Error with incoming message: {}\n", e.what());
   }
 }
diff --git a/simulation/halsim_ws_server/src/main/native/cpp/main.cpp b/simulation/halsim_ws_server/src/main/native/cpp/main.cpp
index 025a902..1ee0ae8 100644
--- a/simulation/halsim_ws_server/src/main/native/cpp/main.cpp
+++ b/simulation/halsim_ws_server/src/main/native/cpp/main.cpp
@@ -1,14 +1,11 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <cstdio>
 #include <memory>
 
 #include <hal/Extensions.h>
-#include <wpi/raw_ostream.h>
 
 #include "HALSimWSServer.h"
 
@@ -22,7 +19,7 @@
 __declspec(dllexport)
 #endif
     int HALSIM_InitExtension(void) {
-  wpi::outs() << "Websocket WS Server Initializing.\n";
+  std::puts("Websocket WS Server Initializing.");
 
   HAL_OnShutdown(nullptr, [](void*) { gServer.reset(); });
 
@@ -31,7 +28,7 @@
     return -1;
   }
 
-  wpi::outs() << "Websocket WS Server Initialized!\n";
+  std::puts("Websocket WS Server Initialized!");
   return 0;
 }
 }  // extern "C"
diff --git a/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h b/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
index 89e6c83..217e2ba 100644
--- a/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
+++ b/simulation/halsim_ws_server/src/main/native/include/HALSimHttpConnection.h
@@ -1,14 +1,12 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <cinttypes>
 #include <memory>
+#include <string_view>
 #include <utility>
 
 #include <HALSimBaseWebSocketConnection.h>
@@ -41,14 +39,13 @@
 
  protected:
   void ProcessRequest() override;
-  bool IsValidWsUpgrade(wpi::StringRef protocol) override;
+  bool IsValidWsUpgrade(std::string_view protocol) override;
   void ProcessWsUpgrade() override;
-  void SendFileResponse(int code, const wpi::Twine& codeText,
-                        const wpi::Twine& contentType,
-                        const wpi::Twine& filename,
-                        const wpi::Twine& extraHeader = wpi::Twine{});
+  void SendFileResponse(int code, std::string_view codeText,
+                        std::string_view contentType, std::string_view filename,
+                        std::string_view extraHeader = {});
 
-  void MySendError(int code, const wpi::Twine& message);
+  void MySendError(int code, std::string_view message);
   void Log(int code);
 
  private:
diff --git a/simulation/halsim_ws_server/src/main/native/include/HALSimWSServer.h b/simulation/halsim_ws_server/src/main/native/include/HALSimWSServer.h
index d75341b..484bfff 100644
--- a/simulation/halsim_ws_server/src/main/native/include/HALSimWSServer.h
+++ b/simulation/halsim_ws_server/src/main/native/include/HALSimWSServer.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
diff --git a/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h b/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
index f71cbdf..83a680e 100644
--- a/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
+++ b/simulation/halsim_ws_server/src/main/native/include/HALSimWeb.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2020 FIRST. All Rights Reserved.                        */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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
 
@@ -14,7 +11,6 @@
 #include <WSBaseProvider.h>
 #include <WSProviderContainer.h>
 #include <WSProvider_SimDevice.h>
-#include <wpi/StringRef.h>
 #include <wpi/uv/Async.h>
 #include <wpi/uv/Loop.h>
 #include <wpi/uv/Tcp.h>
@@ -45,9 +41,9 @@
   // network -> sim
   void OnNetValueChanged(const wpi::json& msg);
 
-  wpi::StringRef GetWebrootSys() const { return m_webroot_sys; }
-  wpi::StringRef GetWebrootUser() const { return m_webroot_user; }
-  wpi::StringRef GetServerUri() const { return m_uri; }
+  const std::string& GetWebrootSys() const { return m_webroot_sys; }
+  const std::string& GetWebrootUser() const { return m_webroot_user; }
+  const std::string& GetServerUri() const { return m_uri; }
   int GetServerPort() const { return m_port; }
   wpi::uv::Loop& GetLoop() { return m_loop; }
 
diff --git a/simulation/halsim_ws_server/src/test/native/cpp/WebServerClientTest.cpp b/simulation/halsim_ws_server/src/test/native/cpp/WebServerClientTest.cpp
index 6cdac5a..f13a0c6 100644
--- a/simulation/halsim_ws_server/src/test/native/cpp/WebServerClientTest.cpp
+++ b/simulation/halsim_ws_server/src/test/native/cpp/WebServerClientTest.cpp
@@ -1,16 +1,13 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 "WebServerClientTest.h"
 
-#include <sstream>
+#include <cstdio>
 
+#include <fmt/format.h>
 #include <wpi/SmallString.h>
-#include <wpi/raw_ostream.h>
 #include <wpi/raw_uv_ostream.h>
 #include <wpi/uv/util.h>
 
@@ -23,14 +20,12 @@
 // Create Web Socket and specify event callbacks
 void WebServerClientTest::InitializeWebSocket(const std::string& host, int port,
                                               const std::string& uri) {
-  std::stringstream ss;
-  ss << host << ":" << port;
-  wpi::outs() << "Will attempt to connect to: " << ss.str() << uri << "\n";
-  m_websocket =
-      wpi::WebSocket::CreateClient(*m_tcp_client.get(), uri, ss.str());
+  fmt::print("Will attempt to connect to: {}:{}{}\n", host, port, uri);
+  m_websocket = wpi::WebSocket::CreateClient(*m_tcp_client.get(), uri,
+                                             fmt::format("{}:{}", host, port));
 
   // Hook up events
-  m_websocket->open.connect_extended([this](auto conn, wpi::StringRef) {
+  m_websocket->open.connect_extended([this](auto conn, auto) {
     conn.disconnect();
     m_buffers = std::make_unique<BufferPool>();
 
@@ -41,17 +36,17 @@
                                 });
 
     m_ws_connected = true;
-    wpi::errs() << "WebServerClientTest: WebSocket Connected\n";
+    std::fputs("WebServerClientTest: WebSocket Connected\n", stderr);
   });
 
-  m_websocket->text.connect([this](wpi::StringRef msg, bool) {
+  m_websocket->text.connect([this](auto msg, bool) {
     wpi::json j;
     try {
       j = wpi::json::parse(msg);
     } catch (const wpi::json::parse_error& e) {
       std::string err("JSON parse failed: ");
       err += e.what();
-      wpi::errs() << err << "\n";
+      fmt::print(stderr, "{}\n", err);
       m_websocket->Fail(1003, err);
       return;
     }
@@ -59,9 +54,9 @@
     m_json = j;
   });
 
-  m_websocket->closed.connect([this](uint16_t, wpi::StringRef) {
+  m_websocket->closed.connect([this](uint16_t, auto) {
     if (m_ws_connected) {
-      wpi::errs() << "WebServerClientTest: Websocket Disconnected\n";
+      std::fputs("WebServerClientTest: Websocket Disconnected\n", stderr);
       m_ws_connected = false;
     }
   });
@@ -70,11 +65,11 @@
 // Create tcp client, specify callbacks, and create timers for loop
 bool WebServerClientTest::Initialize() {
   m_loop.error.connect(
-      [](uv::Error err) { wpi::errs() << "uv Error: " << err.str() << "\n"; });
+      [](uv::Error err) { fmt::print(stderr, "uv Error: {}\n", err.str()); });
 
   m_tcp_client = uv::Tcp::Create(m_loop);
   if (!m_tcp_client) {
-    wpi::errs() << "ERROR: Could not create TCP Client\n";
+    std::fputs("ERROR: Could not create TCP Client\n", stderr);
     return false;
   }
 
@@ -93,7 +88,7 @@
       });
 
   m_tcp_client->closed.connect(
-      []() { wpi::errs() << "TCP connection closed\n"; });
+      []() { std::fputs("TCP connection closed\n", stderr); });
 
   // Set up the connection timer
   m_connect_timer = uv::Timer::Create(m_loop);
@@ -104,33 +99,32 @@
   m_connect_timer->timeout.connect([this] { AttemptConnect(); });
   m_connect_timer->Start(uv::Timer::Time(0));
 
-  wpi::outs() << "WebServerClientTest Initialized\n";
+  std::puts("WebServerClientTest Initialized");
 
   return true;
 }
 
 void WebServerClientTest::AttemptConnect() {
   m_connect_attempts++;
-  wpi::outs() << "Test Client Connection Attempt " << m_connect_attempts
-              << "\n";
+  fmt::print("Test Client Connection Attempt {}\n", m_connect_attempts);
 
   if (m_connect_attempts >= 5) {
-    wpi::errs() << "Test Client Timeout. Unable to connect\n";
+    std::fputs("Test Client Timeout. Unable to connect\n", stderr);
     m_loop.Stop();
     return;
   }
 
   struct sockaddr_in dest;
-  uv::NameToAddr("localhost", 8080, &dest);
+  uv::NameToAddr("localhost", 3300, &dest);
   m_tcp_client->Connect(dest, [this]() {
     m_tcp_connected = true;
-    InitializeWebSocket("localhost", 8080, "/wpilibws");
+    InitializeWebSocket("localhost", 3300, "/wpilibws");
   });
 }
 
 void WebServerClientTest::SendMessage(const wpi::json& msg) {
   if (msg.empty()) {
-    wpi::errs() << "Message to send is empty\n";
+    std::fputs("Message to send is empty\n", stderr);
     return;
   }
 
@@ -150,13 +144,15 @@
         m_buffers->Release(bufs);
       }
       if (err) {
-        wpi::errs() << err.str() << "\n";
-        wpi::errs().flush();
+        fmt::print(stderr, "{}\n", err.str());
+        std::fflush(stderr);
       }
     });
   });
 }
 
-const wpi::json& WebServerClientTest::GetLastMessage() { return m_json; }
+const wpi::json& WebServerClientTest::GetLastMessage() {
+  return m_json;
+}
 
 }  // namespace wpilibws
diff --git a/simulation/halsim_ws_server/src/test/native/cpp/main.cpp b/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
index 4172f19..ef31ba1 100644
--- a/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
+++ b/simulation/halsim_ws_server/src/test/native/cpp/main.cpp
@@ -1,17 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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 <thread>
 
+#include <fmt/format.h>
 #include <hal/DriverStation.h>
 #include <hal/HALBase.h>
 #include <hal/Main.h>
 #include <hal/simulation/DIOData.h>
-#include <wpi/raw_ostream.h>
 #include <wpi/uv/Loop.h>
 
 #include "HALSimWSServer.h"
@@ -44,7 +41,7 @@
   HALSimWSServer m_server;
 };
 
-TEST_F(WebServerIntegrationTest, DigitalOutput) {
+TEST_F(WebServerIntegrationTest, DISABLED_DigitalOutput) {
   // Create expected results
   const bool EXPECTED_VALUE = false;
   const int PIN = 0;
@@ -58,8 +55,8 @@
         return;
       }
       if (IsConnectedClientWS()) {
-        wpi::outs() << "***** Setting DIO value for pin " << PIN << " to "
-                    << (EXPECTED_VALUE ? "true" : "false") << "\n";
+        fmt::print("***** Setting DIO value for pin {} to {}\n", PIN,
+                   (EXPECTED_VALUE ? "true" : "false"));
         HALSIM_SetDIOValue(PIN, EXPECTED_VALUE);
         done = true;
       }
@@ -86,7 +83,7 @@
       test_value = it.value();
     }
   } catch (wpi::json::exception& e) {
-    wpi::errs() << "Error with incoming message: " << e.what() << "\n";
+    fmt::print(stderr, "Error with incoming message: {}\n", e.what());
   }
 
   // Compare results
@@ -95,7 +92,7 @@
   EXPECT_EQ(EXPECTED_VALUE, test_value);
 }
 
-TEST_F(WebServerIntegrationTest, DigitalInput) {
+TEST_F(WebServerIntegrationTest, DISABLED_DigitalInput) {
   // Create expected results
   const bool EXPECTED_VALUE = false;
   const int PIN = 0;
@@ -112,7 +109,7 @@
         wpi::json msg = {{"type", "DIO"},
                          {"device", std::to_string(PIN)},
                          {"data", {{"<>value", EXPECTED_VALUE}}}};
-        wpi::outs() << "***** Input JSON: " << msg.dump() << "\n";
+        fmt::print("***** Input JSON: {}\n", msg.dump());
         m_webserverClient->SendMessage(msg);
         done = true;
       }
@@ -147,7 +144,7 @@
             {"type", "DriverStation"},
             {"device", ""},
             {"data", {{">enabled", EXPECTED_VALUE}, {">new_data", true}}}};
-        wpi::outs() << "***** Input JSON: " << msg.dump() << "\n";
+        fmt::print("***** Input JSON: {}\n", msg.dump());
         m_webserverClient->SendMessage(msg);
         done = true;
       }
diff --git a/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h b/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
index 909c4ba..08db565 100644
--- a/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
+++ b/simulation/halsim_ws_server/src/test/native/include/WebServerClientTest.h
@@ -1,9 +1,6 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2020 FIRST. All Rights Reserved.                             */
-/* Open Source Software - may be modified and shared by FRC teams. The code   */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project.                                                               */
-/*----------------------------------------------------------------------------*/
+// 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