Skip to main content
The vision simulation pipeline uses PhotonVision’s 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:
The 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:
All cameras share a single 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:
Standard deviation heuristics for the pose estimator:
When only one tag is visible and the robot is farther than kAvgDistTolerance, std devs are set to Double.MAX_VALUE (the estimate is effectively ignored).

VisionSim

Each VisionSim instance wraps one PhotonCamera and one PhotonPoseEstimator. Its periodic() method (called from SimWrapper.robotPeriodic()) drains all unread PhotonPipelineResult objects and for each one:
  1. Attempts a multi-tag coprocessor pose estimate (estimateCoprocMultiTagPose).
  2. Falls back to lowest-ambiguity single-tag estimate (estimateLowestAmbiguityPose).
  3. Updates dynamic std devs via the distance/tag-count heuristic.
  4. Converts the result to LimelightData via PhotonToLimelightConverter.convertPipelineResult().
  5. Fills in the botpose arrays via PhotonToLimelightConverter.convertBotpose().
  6. Publishes everything through LimelightTablePublisher.
The 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, tid
  • tl (pipeline latency), cl (capture latency)
  • hb (heartbeat counter — increments each frame, wraps at 2 billion)
  • targetpose_cameraspace, camerapose_targetspace
  • rawfiducials, t2d
  • botpose_wpiblue, botpose_wpired
  • botpose_orb_wpiblue, botpose_orb_wpired (MegaTag2 entries — same data as MegaTag1 in simulation)
  • botpose_wpiblue_pose3d (convenience Pose3d struct, 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:
Load the VisionSim SimGUI layout (Simgui_layouts/VisionSim/simgui.json) to see all three cameras’ estimated poses, ground truth, and odometry estimate on the same Field2d widget simultaneously. See SimGUI Layouts for copy instructions.