Lightweight FRC libraries made by Finn, for the 2026 season (Java only).
| Library | What it does | Vendordep URL |
|---|---|---|
| LightLogger | Logging and live tuning over NetworkTables | https://raw.githubusercontent.com/JamesEHart/LightLib/main/LightLogger.json |
| LightSim | Physics simulation for motors, mechanisms, and drivetrains, viewed in AdvantageScope | https://raw.githubusercontent.com/JamesEHart/LightLib/main/LightSim.json |
| LightUtil | Everyday robot-code helpers: aiming, alliance flipping, driver input, auto-align, vision filtering, LEDs, state machines | https://raw.githubusercontent.com/JamesEHart/LightLib/main/LightUtil.json |
Install any of them, in any combination, from VS Code: open the command palette (Ctrl+Shift+P), run WPILib: Manage Vendor Libraries → Install new library (online), and paste the URL.
LightLogger is a lightweight logging library designed for FRC teams. It provides a simple way to record robot states, sensor data, subsystem values, and poses during both real and simulation operations.
- High-speed logging via NetworkTables
- Log numbers, booleans, strings, events, warnings, errors, and geometry types
- Simple setup with one line in
Robot.java - Optional file logging to
.wpilogfiles - Live tuning: change numbers, booleans, and strings from a dashboard while the robot runs
- Frame timing utilities for loop performance tracking
- Compatible with NetworkTables dashboards (Glass, AdvantageScope, Elastic, etc.)
- In VS Code, open the command palette (
Ctrl+Shift+P) and run WPILib: Manage Vendor Libraries. - Choose Install new library (online).
- Paste this URL:
https://raw.githubusercontent.com/JamesEHart/LightLib/main/LightLogger.json - Build your project so Gradle downloads it.
import io.github.jamesehart.lightlogger.LightLogger;LightLogger.start(true); // true = also save logs to a file, false = NetworkTables onlyLightLogger.startFrame();
// ... your periodic code ...
LightLogger.endFrame();| Method | Description |
|---|---|
LightLogger.start(boolean recordToFile) |
Starts the logger. Pass true to also record everything to a .wpilog file, false for NetworkTables only. |
LightLogger.stop() |
Flushes and stops the logger. |
LightLogger.startFrame() |
Records the start time of the current loop frame. |
LightLogger.endFrame() |
Flushes all pending NetworkTables updates. |
LightLogger.getFrameTime() |
Returns elapsed time in seconds since startFrame() was called. |
Log files are written using WPILib's DataLogManager, to a USB stick if one is plugged into the roboRIO, otherwise /home/lvuser/logs. In simulation they go to logs/ in your project folder. Open them with AdvantageScope.
All values are published to NetworkTables under the /LightLogger/<key> topic.
| Method | Description |
|---|---|
LightLogger.logNumber(String key, double value) |
Log a numeric value (doubles, ints, etc.). |
LightLogger.logBoolean(String key, boolean value) |
Log a boolean value. |
LightLogger.logString(String key, String value) |
Log a string value. |
Example:
LightLogger.logNumber("Drive/LeftSpeed", leftMotor.get());
LightLogger.logBoolean("Intake/HasNote", intakeSensor.get());
LightLogger.logString("Robot/State", currentState.name());These log timestamped messages (relative to the current frame) to fixed keys.
| Method | Published to | Description |
|---|---|---|
LightLogger.logEvent(String message) |
/LightLogger/Events |
Log a general event. |
LightLogger.warn(String message) |
/LightLogger/Warnings |
Log a warning. |
LightLogger.error(String message) |
/LightLogger/Errors |
Log an error. |
Example:
LightLogger.logEvent("AutoStarted");
LightLogger.warn("Vision target lost");
LightLogger.error("Motor controller disconnected");Geometry types are published as WPILib structs, making them compatible with AdvantageScope's 2D/3D field views.
| Method | Type |
|---|---|
LightLogger.logPose2d(String key, Pose2d pose) |
Pose2d |
LightLogger.logPose3d(String key, Pose3d pose) |
Pose3d |
LightLogger.logTranslation2d(String key, Translation2d t) |
Translation2d |
LightLogger.logTranslation3d(String key, Translation3d t) |
Translation3d |
LightLogger.logRotation2d(String key, Rotation2d r) |
Rotation2d |
LightLogger.logRotation3d(String key, Rotation3d r) |
Rotation3d |
Example:
LightLogger.logPose2d("Drive/EstimatedPose", poseEstimator.getEstimatedPosition());
LightLogger.logRotation2d("Drive/Heading", gyro.getRotation2d());Tunable values show up on your dashboard under /LightLogger/<key> like any other value, but you can also edit them while the robot is running, and the robot code uses the new value right away. Great for tuning PID gains, setpoints, and speeds without redeploying.
There are two ways to use them.
1. Read the value every loop. Call it in a periodic method and it returns the current value:
| Method | Description |
|---|---|
LightLogger.tunableNumber(String key, double defaultValue) |
Returns the current number. |
LightLogger.tunableBoolean(String key, boolean defaultValue) |
Returns the current boolean. |
LightLogger.tunableString(String key, String defaultValue) |
Returns the current string. |
double speed = LightLogger.tunableNumber("Intake/Speed", 0.8);
intakeMotor.set(speed);2. Register once with a callback. Call it once (e.g. in a constructor). The callback runs immediately with the starting value, then again every time the value is edited:
| Method | Description |
|---|---|
LightLogger.tunableNumber(String key, double defaultValue, Consumer<Double> onChange) |
Calls onChange when the number changes. |
LightLogger.tunableBoolean(String key, boolean defaultValue, Consumer<Boolean> onChange) |
Calls onChange when the boolean changes. |
LightLogger.tunableString(String key, String defaultValue, Consumer<String> onChange) |
Calls onChange when the string changes. |
LightLogger.tunableNumber("Drive/kP", 0.1, pid::setP);
LightLogger.tunableNumber("Drive/kD", 0.0, pid::setD);Callbacks are checked in
LightLogger.startFrame(), so you must call it at the top ofrobotPeriodic()for them to fire.
Editing values from a dashboard:
- Glass: open the NetworkTables view, find the value under
LightLogger, and click it to edit. - AdvantageScope: while connected live, turn on Tuning Mode (button at the top of the sidebar), then click a value in the sidebar to edit it.
- Elastic: add the value as a widget (e.g. Text Display or Toggle Switch) and edit it there.
Things to know:
- Edits are not saved. When robot code restarts, everything goes back to the defaults in your code, so copy tuned values back into your code when you're done.
- Don't use
logNumber/logBoolean/logStringwith the same key as a tunable value, or the logged value will overwrite your edits. - Edits are recorded in the
.wpilogfile along with everything else, so you can see what was changed and when.
LightSim simulates your robot with real physics, with as little setup as possible:
- Swap your motor controllers for LightSim's versions (
TalonFX→SimTalonFX). - Add one line to your
Robotconstructor. - Motors spin with real motor physics, mechanisms feel gravity and hit their limits, and your drivetrain pushes a rigid body around the field, colliding with the walls (using the dyn4j physics engine).
- Everything shows up in AdvantageScope on its own 2026 field and robot models.
On a real robot the Sim* classes are exactly the normal vendor classes and LightSim does nothing, so you never have to remove it.
- TalonFX, TalonFXS, TalonSRX, SparkMax, and SparkFlex, including their onboard closed-loop control (PID, Motion Magic, MAXMotion) and your inversion and gear-ratio configs
- Flywheels/rollers, elevators, and arms with gravity and hard stops
- Swerve and tank drivetrains with wheel traction limits (wheels slip if you push too hard or drive into a wall)
- Simulated Pigeon2 that follows the robot's heading
- Battery voltage sag when motors draw current
- Field perimeter walls and custom obstacles
- Install LightSim with the URL above.
- Also install the vendor libraries for the motor controllers you use (Phoenix 6, Phoenix 5 and/or REVLib), as you normally would. LightSim doesn't install them for you, so REV-only teams don't need Phoenix and vice versa.
At the very top of your Robot constructor:
import io.github.jamesehart.lightsim.LightSim;
LightSim.enable(this);| Instead of | Use | Default motor |
|---|---|---|
new TalonFX(id) |
new SimTalonFX(id) |
Kraken X60 |
new TalonFXS(id) |
new SimTalonFXS(id, DCMotor.getMinion(1)) |
none, you pass it |
new WPI_TalonSRX(id) |
new SimTalonSRX(id, DCMotor.getCIM(1)) |
none, you pass it |
new SparkMax(id, type) |
new SimSparkMax(id, type) |
NEO (brushless) / CIM (brushed) |
new SparkFlex(id, type) |
new SimSparkFlex(id, type) |
NEO Vortex (brushless) / CIM (brushed) |
new Pigeon2(id) |
new SimPigeon2(id) |
They're in io.github.jamesehart.lightsim.motors. Every constructor also takes a DCMotor to use a different motor, e.g. new SimTalonFX(1, DCMotor.getFalcon500(1)), and the CTRE ones take a CANBus like normal. Since they extend the vendor classes, all your existing code and configs keep working.
That's already enough: every motor now spins a light load, so encoders and closed-loop control respond. To make the physics match your robot, tell LightSim what each motor is connected to.
Call these once, after creating the motors (e.g. in a subsystem constructor):
// Flywheel/roller/intake: gearing, moment of inertia (kg·m²)
LightSim.flywheel(shooterMotor, 1.0, 0.004);
// Elevator: gearing, carriage mass (kg), drum radius (m), min and max height (m)
LightSim.elevator(elevatorMotor, 10, 8, 0.03, 0, 1.2);
// Arm: gearing, moment of inertia (kg·m²), length (m), min and max angle (rad; 0 = horizontal)
LightSim.arm(armMotor, 60, 0.5, 0.6, Math.toRadians(-30), Math.toRadians(110));For several motors on one mechanism, pass an array: LightSim.flywheel(new SimMotor[] {left, right}, 1.0, 0.004). Each returns the mechanism, so you can read getPosition() / getVelocity().
import io.github.jamesehart.lightsim.physics.SwerveSimConfig;
LightSim.swerve(new SwerveSimConfig()
.module(flDrive, flSteer, new Translation2d( 0.28, 0.28)) // x forward, y left, meters
.module(frDrive, frSteer, new Translation2d( 0.28, -0.28))
.module(blDrive, blSteer, new Translation2d(-0.28, 0.28))
.module(brDrive, brSteer, new Translation2d(-0.28, -0.28))
.driveGearing(6.75)
.steerGearing(150.0 / 7)
.wheelRadius(0.0508)
.robotMass(55) // kg, with bumpers and battery
.bumperSize(0.9, 0.9) // meters
.gyro(pigeon)); // optional SimPigeon2
LightSim.setRobotPose(new Pose2d(2, 4, Rotation2d.kZero)); // starting positionimport io.github.jamesehart.lightsim.physics.TankSimConfig;
LightSim.tank(new TankSimConfig()
.left(leftLeader, leftFollower)
.right(rightLeader, rightFollower)
.trackWidth(0.6)
.gearing(8.45)
.wheelRadius(0.0762)
.robotMass(50)
.gyro(pigeon));Configure your motors (inversion etc.) exactly like on your real robot. LightSim assumes a positive command drives the robot forward and turns swerve modules counter-clockwise.
The field walls match the official 2026 field. Add other things the robot should bump into as boxes:
LightSim.addObstacle(new Pose2d(4.6, 4.0, Rotation2d.kZero), 1.2, 1.2); // center, length, widthLightSim.getRobotPose() returns the robot's true simulated pose, which is handy for checking your odometry.
LightSim publishes everything under /LightSim/ in the formats AdvantageScope understands:
| Topic | Type | Use it for |
|---|---|---|
/LightSim/RobotPose |
Pose2d |
The robot on the 2D or 3D field |
/LightSim/RobotPose3d |
Pose3d |
Same, as a 3D pose |
/LightSim/SwerveStates |
SwerveModuleState[] |
The Swerve tab |
/LightSim/Components |
Pose3d[] |
Moving parts of your robot model (elevators, arms) |
/LightSim/BatteryVoltage |
double |
Battery sag |
- Run Simulate Robot Code, then in AdvantageScope choose File → Connect to Simulator.
- Open a 3D Field tab and pick the 2026 field.
- Drag
/LightSim/RobotPoseonto the tab's pose list and pick a robot model: one of AdvantageScope's built-in robots, or your own. - For swerve, open a Swerve tab and drag in
/LightSim/SwerveStates.
Moving parts: if your robot model has components (the components list in its config.json; see AdvantageScope's docs on custom robot models), tell LightSim which mechanism is which component:
LightSim.elevator(elevatorMotor, 10, 8, 0.03, 0, 1.2)
.component(0, new Translation3d(0, 0, 0.1)); // component 0, moves up along z
LightSim.arm(armMotor, 60, 0.5, 0.6, -0.5, 1.9)
.component(1, new Translation3d(0.2, 0, 0.5)); // component 1, pivots hereThen drag /LightSim/Components onto the robot in the 3D Field tab as its component poses. By default, elevators slide along +z and arms pivot about -y. Pass a third argument (an axis) to change that, e.g. new Translation3d(0, 0, 1) for a turret.
- No game pieces, field elements (other than walls and your obstacles), other robots, or vision yet.
- Brake mode is assumed: a motor with no output resists being turned.
- Swerve steering, drive wheels, and the robot body are simplified models. They're good for testing code and tuning, not for predicting exact cycle times.
LightUtil is a toolbox of the helpers most robots end up writing from scratch. It only uses WPILib types and lambdas, so it works with any motor controller or camera.
| Package | What for |
|---|---|
math, field |
Angle and pose helpers, lookup tables, alliance flipping, field zones |
targeting |
Distance and angle to a goal, turret aiming, shot tables, shooting on the move |
drive, auto |
Joystick shaping with aim assist, auto-align to a pose, skid detection, auto start-pose check |
input, commands |
Button patterns, rumble, state machines, trigger and command helpers |
mechanisms, leds |
Stall detection, homing, SysId setup, prioritized LED patterns |
vision |
AprilTag estimate filtering, game piece detection and tracking |
diagnostics, robot |
Device health alerts, match timer, comp vs. practice robot constants |
season2026 |
REBUILT hub shift timing |
DriveInput input = new DriveInput(
() -> -controller.getLeftY(), () -> -controller.getLeftX(), () -> -controller.getRightX())
.maxSpeed(4.5)
.slowMode(controller::getLeftBumperButton, 0.4);
drive.driveRobotRelative(input.fieldRelative(drive.getPose().getRotation()));| Path | What it is |
|---|---|
lightlogger/ |
The LightLogger library. |
lightsim/ |
The LightSim library. physics/ is pure Java with unit tests; motors/ wraps the vendor classes. |
lightutil/ |
The LightUtil library and its docs. The math, field, targeting, vision and 2026 hub code has unit tests. |
example/ |
A robot project using all three libraries (swerve, flywheel, elevator). It builds the libraries from this repo, so it's the place to test changes in simulation before releasing. |
LightLogger.json, LightSim.json, LightUtil.json |
The vendordep files teams install. |
repos/ |
The published Maven repository. GitHub serves it to GradleRIO. Generated by Gradle; don't edit by hand. |
./gradlew :lightsim:testruns the physics tests, and./gradlew :lightutil:testthe LightUtil tests.- Open the
example/folder in WPILib VS Code and run Simulate Robot Code, then connect AdvantageScope../gradlew simulateJava -Pheadlessinexample/runs it without the sim GUI.
- Bump
versionin the library'sbuild.gradle(lightlogger/,lightsim/orlightutil/). - Bump the matching versions in
LightLogger.json/LightSim.json/LightUtil.json, and copy the file toexample/vendordeps/. - Run
./gradlew publishfrom the repo root. The new version appears inrepos/. - Commit and push to
main. Teams get the update with WPILib: Manage Vendor Libraries → Check for updates (online).
Use the WPILib JDK when running Gradle from a terminal (VS Code's WPILib terminal already does this):
JAVA_HOME=C:\Users\Public\wpilib\2026\jdk
- Update
wpilibVersionand thewpilib/2026path in the rootbuild.gradle, and the Phoenix/REVLib versions inlightsim/build.gradle. - Update
frcYearin all three vendordep JSONs, and checkAllianceFlip's default symmetry against the new field. - Update the example project with the WPILib importer.
- Then release as above. GradleRIO refuses vendordeps whose
frcYeardoesn't match the project's year.