VisionSystemSim to generate synthetic AprilTag observations, then converts them to Limelight NetworkTables format so VisionSubsystem’s real pipeline processes them completely unchanged in simulation versus on a real robot. No simulation-specific branches exist in VisionSubsystem — it simply reads from the limelight-* NetworkTables, which are populated by real hardware or by the converter depending on the environment.
VisionSim (and all classes in frc.robot.sim.visionproducers) are only instantiated when Robot.isSimulation() returns true. Both VisionSim and VisionSimFactory throw IllegalStateException if constructed outside simulation.Pipeline architecture
Data flows through the following stages each robot cycle:VisionSystemSim scene is updated once per cycle in SimWrapper.simulationPeriodic() using the ground-truth pose so the cameras see AprilTags at the robot’s true physical location — not its drifted odometry estimate.
VisionSimFactory
VisionSimFactory.create() constructs all three camera simulations and returns a VisionSimResult record:
VisionSystemSim instance (named "main") and the same Field2d debug widget. The three cameras are configured as:
Camera transforms (
robotToCam) for cameras A and B come from VisionSubsystem.COMP_BOT_LEFT_CAMERA and VisionSubsystem.COMP_BOT_FRONT_CAMERA so simulation geometry matches the real robot exactly.
VisionSimConstants
VisionSimConstants holds all camera simulation parameters. Values switch between two sets depending on the highFidelityVision JVM system property:
Parameters shared regardless of fidelity mode:
kAvgDistTolerance, std devs are set to Double.MAX_VALUE (the estimate is effectively ignored).
VisionSim
EachVisionSim instance wraps one PhotonCamera and one PhotonPoseEstimator. Its periodic() method (called from SimWrapper.robotPeriodic()) drains all unread PhotonPipelineResult objects and for each one:
- Attempts a multi-tag coprocessor pose estimate (
estimateCoprocMultiTagPose). - Falls back to lowest-ambiguity single-tag estimate (
estimateLowestAmbiguityPose). - Updates dynamic std devs via the distance/tag-count heuristic.
- Converts the result to
LimelightDataviaPhotonToLimelightConverter.convertPipelineResult(). - Fills in the botpose arrays via
PhotonToLimelightConverter.convertBotpose(). - Publishes everything through
LimelightTablePublisher.
PhotonCameraSim is added to the shared VisionSystemSim with wireframe rendering enabled (enableDrawWireframe(true)), so each camera’s view can be inspected in the SimGUI.
PhotonToLimelightConverter
PhotonToLimelightConverter is a stateless utility class (no I/O, fully unit-testable) that translates PhotonVision data structures into the LimelightData format. Key conversions:
The botpose arrays use WPILib field coordinate origins. For the Red alliance array, both X and Y coordinates are mirrored and yaw is rotated 180°.
LimelightData
LimelightData is a plain mutable data class with no WPILib or NetworkTables dependencies, mirroring exactly what a physical Limelight 3G/4 publishes:
LimelightTablePublisher
LimelightTablePublisher is the only class with a NetworkTables dependency. It writes every LimelightData field to the named limelight-* table each cycle, including:
tv,tx,ty,txnc,tync,ta,tidtl(pipeline latency),cl(capture latency)hb(heartbeat counter — increments each frame, wraps at 2 billion)targetpose_cameraspace,camerapose_targetspacerawfiducials,t2dbotpose_wpiblue,botpose_wpiredbotpose_orb_wpiblue,botpose_orb_wpired(MegaTag2 entries — same data as MegaTag1 in simulation)botpose_wpiblue_pose3d(conveniencePose3dstruct, not present on real hardware)
ShowVisionOnField
ShowVisionOnField draws three categories of robot poses onto the shared Field2d:
The
FieldType enum selects between REAL_FIELD and SIMULATION_FIELD. In simulation only SIMULATION_FIELD is used. VisionSubsystem calls showPointInTimeVisionEstimate() on SIMULATION_FIELD via the ShowVisionOnField instance wired up in Robot: