001/************************* PROJECT RON *************************/
002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */
003/* Use of this source code is governed by an MIT-style license */
004/* that can be found in the repository LICENSE file.           */
005/***************************************************************/
006package com.stuypulse.robot.util.vision;
007
008// LimelightHelpers v1.14 (REQUIRES LLOS 2026.0 OR LATER)
009import com.fasterxml.jackson.annotation.JsonFormat;
010import com.fasterxml.jackson.annotation.JsonFormat.Shape;
011import com.fasterxml.jackson.annotation.JsonProperty;
012import com.fasterxml.jackson.core.JsonProcessingException;
013import com.fasterxml.jackson.databind.DeserializationFeature;
014import com.fasterxml.jackson.databind.ObjectMapper;
015import edu.wpi.first.math.geometry.Pose2d;
016import edu.wpi.first.math.geometry.Pose3d;
017import edu.wpi.first.math.geometry.Rotation2d;
018import edu.wpi.first.math.geometry.Rotation3d;
019import edu.wpi.first.math.geometry.Translation2d;
020import edu.wpi.first.math.geometry.Translation3d;
021import edu.wpi.first.math.util.Units;
022import edu.wpi.first.net.PortForwarder;
023import edu.wpi.first.networktables.DoubleArrayEntry;
024import edu.wpi.first.networktables.NetworkTable;
025import edu.wpi.first.networktables.NetworkTableEntry;
026import edu.wpi.first.networktables.NetworkTableInstance;
027import edu.wpi.first.networktables.TimestampedDoubleArray;
028import java.util.Arrays;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031
032/**
033 * LimelightHelpers provides static methods and classes for interfacing with
034 * Limelight vision
035 * cameras in FRC. This library supports all Limelight features including
036 * AprilTag tracking, Neural
037 * Networks, and standard color/retroreflective tracking.
038 */
039public class LimelightHelpers {
040
041    private static final Map<String, DoubleArrayEntry> doubleArrayEntries = new ConcurrentHashMap<>();
042
043    /**
044     * Represents a Color/Retroreflective Target Result extracted from JSON Output
045     */
046    public static class LimelightTarget_Retro {
047
048        @JsonProperty("t6c_ts")
049        private double[] cameraPose_TargetSpace;
050
051        @JsonProperty("t6r_fs")
052        private double[] robotPose_FieldSpace;
053
054        @JsonProperty("t6r_ts")
055        private double[] robotPose_TargetSpace;
056
057        @JsonProperty("t6t_cs")
058        private double[] targetPose_CameraSpace;
059
060        @JsonProperty("t6t_rs")
061        private double[] targetPose_RobotSpace;
062
063        public Pose3d getCameraPose_TargetSpace() {
064            return toPose3D(cameraPose_TargetSpace);
065        }
066
067        public Pose3d getRobotPose_FieldSpace() {
068            return toPose3D(robotPose_FieldSpace);
069        }
070
071        public Pose3d getRobotPose_TargetSpace() {
072            return toPose3D(robotPose_TargetSpace);
073        }
074
075        public Pose3d getTargetPose_CameraSpace() {
076            return toPose3D(targetPose_CameraSpace);
077        }
078
079        public Pose3d getTargetPose_RobotSpace() {
080            return toPose3D(targetPose_RobotSpace);
081        }
082
083        public Pose2d getCameraPose_TargetSpace2D() {
084            return toPose2D(cameraPose_TargetSpace);
085        }
086
087        public Pose2d getRobotPose_FieldSpace2D() {
088            return toPose2D(robotPose_FieldSpace);
089        }
090
091        public Pose2d getRobotPose_TargetSpace2D() {
092            return toPose2D(robotPose_TargetSpace);
093        }
094
095        public Pose2d getTargetPose_CameraSpace2D() {
096            return toPose2D(targetPose_CameraSpace);
097        }
098
099        public Pose2d getTargetPose_RobotSpace2D() {
100            return toPose2D(targetPose_RobotSpace);
101        }
102
103        @JsonProperty("ta")
104        public double ta;
105
106        @JsonProperty("tx")
107        public double tx;
108
109        @JsonProperty("ty")
110        public double ty;
111
112        @JsonProperty("txp")
113        public double tx_pixels;
114
115        @JsonProperty("typ")
116        public double ty_pixels;
117
118        @JsonProperty("tx_nocross")
119        public double tx_nocrosshair;
120
121        @JsonProperty("ty_nocross")
122        public double ty_nocrosshair;
123
124        @JsonProperty("ts")
125        public double ts;
126
127        public LimelightTarget_Retro() {
128            cameraPose_TargetSpace = new double[6];
129            robotPose_FieldSpace = new double[6];
130            robotPose_TargetSpace = new double[6];
131            targetPose_CameraSpace = new double[6];
132            targetPose_RobotSpace = new double[6];
133        }
134    }
135
136    /** Represents an AprilTag/Fiducial Target Result extracted from JSON Output */
137    public static class LimelightTarget_Fiducial {
138
139        @JsonProperty("fID")
140        public double fiducialID;
141
142        @JsonProperty("fam")
143        public String fiducialFamily;
144
145        @JsonProperty("t6c_ts")
146        private double[] cameraPose_TargetSpace;
147
148        @JsonProperty("t6r_fs")
149        private double[] robotPose_FieldSpace;
150
151        @JsonProperty("t6r_ts")
152        private double[] robotPose_TargetSpace;
153
154        @JsonProperty("t6t_cs")
155        private double[] targetPose_CameraSpace;
156
157        @JsonProperty("t6t_rs")
158        private double[] targetPose_RobotSpace;
159
160        public Pose3d getCameraPose_TargetSpace() {
161            return toPose3D(cameraPose_TargetSpace);
162        }
163
164        public Pose3d getRobotPose_FieldSpace() {
165            return toPose3D(robotPose_FieldSpace);
166        }
167
168        public Pose3d getRobotPose_TargetSpace() {
169            return toPose3D(robotPose_TargetSpace);
170        }
171
172        public Pose3d getTargetPose_CameraSpace() {
173            return toPose3D(targetPose_CameraSpace);
174        }
175
176        public Pose3d getTargetPose_RobotSpace() {
177            return toPose3D(targetPose_RobotSpace);
178        }
179
180        public Pose2d getCameraPose_TargetSpace2D() {
181            return toPose2D(cameraPose_TargetSpace);
182        }
183
184        public Pose2d getRobotPose_FieldSpace2D() {
185            return toPose2D(robotPose_FieldSpace);
186        }
187
188        public Pose2d getRobotPose_TargetSpace2D() {
189            return toPose2D(robotPose_TargetSpace);
190        }
191
192        public Pose2d getTargetPose_CameraSpace2D() {
193            return toPose2D(targetPose_CameraSpace);
194        }
195
196        public Pose2d getTargetPose_RobotSpace2D() {
197            return toPose2D(targetPose_RobotSpace);
198        }
199
200        @JsonProperty("ta")
201        public double ta;
202
203        @JsonProperty("tx")
204        public double tx;
205
206        @JsonProperty("ty")
207        public double ty;
208
209        @JsonProperty("txp")
210        public double tx_pixels;
211
212        @JsonProperty("typ")
213        public double ty_pixels;
214
215        @JsonProperty("tx_nocross")
216        public double tx_nocrosshair;
217
218        @JsonProperty("ty_nocross")
219        public double ty_nocrosshair;
220
221        @JsonProperty("ts")
222        public double ts;
223
224        public LimelightTarget_Fiducial() {
225            cameraPose_TargetSpace = new double[6];
226            robotPose_FieldSpace = new double[6];
227            robotPose_TargetSpace = new double[6];
228            targetPose_CameraSpace = new double[6];
229            targetPose_RobotSpace = new double[6];
230        }
231    }
232
233    /** Represents a Barcode Target Result extracted from JSON Output */
234    public static class LimelightTarget_Barcode {
235
236        /** Barcode family type (e.g. "QR", "DataMatrix", etc.) */
237        @JsonProperty("fam")
238        public String family;
239
240        /** Gets the decoded data content of the barcode */
241        @JsonProperty("data")
242        public String data;
243
244        @JsonProperty("txp")
245        public double tx_pixels;
246
247        @JsonProperty("typ")
248        public double ty_pixels;
249
250        @JsonProperty("tx")
251        public double tx;
252
253        @JsonProperty("ty")
254        public double ty;
255
256        @JsonProperty("tx_nocross")
257        public double tx_nocrosshair;
258
259        @JsonProperty("ty_nocross")
260        public double ty_nocrosshair;
261
262        @JsonProperty("ta")
263        public double ta;
264
265        @JsonProperty("pts")
266        public double[][] corners;
267
268        public LimelightTarget_Barcode() {
269        }
270
271        public String getFamily() {
272            return family;
273        }
274    }
275
276    /** Represents a Neural Classifier Pipeline Result extracted from JSON Output */
277    public static class LimelightTarget_Classifier {
278
279        @JsonProperty("class")
280        public String className;
281
282        @JsonProperty("classID")
283        public double classID;
284
285        @JsonProperty("conf")
286        public double confidence;
287
288        @JsonProperty("zone")
289        public double zone;
290
291        @JsonProperty("tx")
292        public double tx;
293
294        @JsonProperty("txp")
295        public double tx_pixels;
296
297        @JsonProperty("ty")
298        public double ty;
299
300        @JsonProperty("typ")
301        public double ty_pixels;
302
303        public LimelightTarget_Classifier() {
304        }
305    }
306
307    /** Represents a Neural Detector Pipeline Result extracted from JSON Output */
308    public static class LimelightTarget_Detector {
309
310        @JsonProperty("class")
311        public String className;
312
313        @JsonProperty("classID")
314        public double classID;
315
316        @JsonProperty("conf")
317        public double confidence;
318
319        @JsonProperty("ta")
320        public double ta;
321
322        @JsonProperty("tx")
323        public double tx;
324
325        @JsonProperty("ty")
326        public double ty;
327
328        @JsonProperty("txp")
329        public double tx_pixels;
330
331        @JsonProperty("typ")
332        public double ty_pixels;
333
334        @JsonProperty("tx_nocross")
335        public double tx_nocrosshair;
336
337        @JsonProperty("ty_nocross")
338        public double ty_nocrosshair;
339
340        public LimelightTarget_Detector() {
341        }
342    }
343
344    /** Represents hardware statistics from the Limelight. */
345    public static class HardwareReport {
346
347        @JsonProperty("cid")
348        public String cameraId;
349
350        @JsonProperty("cpu")
351        public double cpuUsage;
352
353        @JsonProperty("dfree")
354        public double diskFree;
355
356        @JsonProperty("dtot")
357        public double diskTotal;
358
359        @JsonProperty("ram")
360        public double ramUsage;
361
362        @JsonProperty("temp")
363        public double temperature;
364
365        public HardwareReport() {
366        }
367    }
368
369    /** Represents IMU data from the JSON results. */
370    public static class IMUResults {
371
372        @JsonProperty("data")
373        public double[] data;
374
375        @JsonProperty("quat")
376        public double[] quaternion;
377
378        @JsonProperty("yaw")
379        public double yaw;
380
381        // Parsed from data array
382        public double robotYaw;
383
384        public double roll;
385
386        public double pitch;
387
388        public double rawYaw;
389
390        public double gyroZ;
391
392        public double gyroX;
393
394        public double gyroY;
395
396        public double accelZ;
397
398        public double accelX;
399
400        public double accelY;
401
402        public IMUResults() {
403            data = new double[0];
404            quaternion = new double[4];
405        }
406
407        public void parseDataArray() {
408            if (data != null && data.length >= 10) {
409                robotYaw = data[0];
410                roll = data[1];
411                pitch = data[2];
412                rawYaw = data[3];
413                gyroZ = data[4];
414                gyroX = data[5];
415                gyroY = data[6];
416                accelZ = data[7];
417                accelX = data[8];
418                accelY = data[9];
419            }
420        }
421    }
422
423    /** Represents capture rewind buffer statistics. */
424    public static class RewindStats {
425
426        @JsonProperty("bufferUsage")
427        public double bufferUsage;
428
429        @JsonProperty("enabled")
430        public int enabled;
431
432        @JsonProperty("flushing")
433        public int flushing;
434
435        @JsonProperty("frameCount")
436        public int frameCount;
437
438        @JsonProperty("latpen")
439        public int latencyPenalty;
440
441        @JsonProperty("storedSeconds")
442        public double storedSeconds;
443
444        public RewindStats() {
445        }
446    }
447
448    /** Limelight Results object, parsed from a Limelight's JSON results output. */
449    public static class LimelightResults {
450
451        public String error;
452
453        @JsonProperty("pID")
454        public double pipelineID;
455
456        @JsonProperty("tl")
457        public double latency_pipeline;
458
459        @JsonProperty("cl")
460        public double latency_capture;
461
462        public double latency_jsonParse;
463
464        @JsonProperty("ts")
465        public double timestamp_LIMELIGHT_publish;
466
467        @JsonProperty("ts_rio")
468        public double timestamp_RIOFPGA_capture;
469
470        @JsonProperty("ts_nt")
471        public long timestamp_nt;
472
473        @JsonProperty("ts_sys")
474        public long timestamp_sys;
475
476        @JsonProperty("ts_us")
477        public long timestamp_us;
478
479        @JsonProperty("v")
480        @JsonFormat(shape = Shape.NUMBER)
481        public boolean valid;
482
483        @JsonProperty("pTYPE")
484        public String pipelineType;
485
486        @JsonProperty("tx")
487        public double tx;
488
489        @JsonProperty("ty")
490        public double ty;
491
492        @JsonProperty("txnc")
493        public double tx_nocrosshair;
494
495        @JsonProperty("tync")
496        public double ty_nocrosshair;
497
498        @JsonProperty("ta")
499        public double ta;
500
501        @JsonProperty("botpose")
502        public double[] botpose;
503
504        @JsonProperty("botpose_wpired")
505        public double[] botpose_wpired;
506
507        @JsonProperty("botpose_wpiblue")
508        public double[] botpose_wpiblue;
509
510        @JsonProperty("botpose_tagcount")
511        public double botpose_tagcount;
512
513        @JsonProperty("botpose_span")
514        public double botpose_span;
515
516        @JsonProperty("botpose_avgdist")
517        public double botpose_avgdist;
518
519        @JsonProperty("botpose_avgarea")
520        public double botpose_avgarea;
521
522        @JsonProperty("botpose_orb")
523        public double[] botpose_orb;
524
525        @JsonProperty("botpose_orb_wpiblue")
526        public double[] botpose_orb_wpiblue;
527
528        @JsonProperty("botpose_orb_wpired")
529        public double[] botpose_orb_wpired;
530
531        @JsonProperty("t6c_rs")
532        public double[] camerapose_robotspace;
533
534        @JsonProperty("hw")
535        public HardwareReport hardware;
536
537        @JsonProperty("imu")
538        public IMUResults imuResults;
539
540        @JsonProperty("rewind")
541        public RewindStats rewindStats;
542
543        @JsonProperty("PythonOut")
544        public double[] pythonOutput;
545
546        public Pose3d getBotPose3d() {
547            return toPose3D(botpose);
548        }
549
550        public Pose3d getBotPose3d_wpiRed() {
551            return toPose3D(botpose_wpired);
552        }
553
554        public Pose3d getBotPose3d_wpiBlue() {
555            return toPose3D(botpose_wpiblue);
556        }
557
558        public Pose2d getBotPose2d() {
559            return toPose2D(botpose);
560        }
561
562        public Pose2d getBotPose2d_wpiRed() {
563            return toPose2D(botpose_wpired);
564        }
565
566        public Pose2d getBotPose2d_wpiBlue() {
567            return toPose2D(botpose_wpiblue);
568        }
569
570        @JsonProperty("Retro")
571        public LimelightTarget_Retro[] targets_Retro;
572
573        @JsonProperty("Fiducial")
574        public LimelightTarget_Fiducial[] targets_Fiducials;
575
576        @JsonProperty("Classifier")
577        public LimelightTarget_Classifier[] targets_Classifier;
578
579        @JsonProperty("Detector")
580        public LimelightTarget_Detector[] targets_Detector;
581
582        @JsonProperty("Barcode")
583        public LimelightTarget_Barcode[] targets_Barcode;
584
585        public LimelightResults() {
586            botpose = new double[6];
587            botpose_wpired = new double[6];
588            botpose_wpiblue = new double[6];
589            botpose_orb = new double[6];
590            botpose_orb_wpiblue = new double[6];
591            botpose_orb_wpired = new double[6];
592            camerapose_robotspace = new double[6];
593            targets_Retro = new LimelightTarget_Retro[0];
594            targets_Fiducials = new LimelightTarget_Fiducial[0];
595            targets_Classifier = new LimelightTarget_Classifier[0];
596            targets_Detector = new LimelightTarget_Detector[0];
597            targets_Barcode = new LimelightTarget_Barcode[0];
598            pythonOutput = new double[0];
599            pipelineType = "";
600        }
601    }
602
603    /**
604     * Represents a Limelight Raw Fiducial result from Limelight's NetworkTables
605     * output.
606     */
607    public static class RawFiducial {
608
609        public int id = 0;
610
611        public double txnc = 0;
612
613        public double tync = 0;
614
615        public double ta = 0;
616
617        public double distToCamera = 0;
618
619        public double distToRobot = 0;
620
621        public double ambiguity = 0;
622
623        public RawFiducial(
624                int id,
625                double txnc,
626                double tync,
627                double ta,
628                double distToCamera,
629                double distToRobot,
630                double ambiguity) {
631            this.id = id;
632            this.txnc = txnc;
633            this.tync = tync;
634            this.ta = ta;
635            this.distToCamera = distToCamera;
636            this.distToRobot = distToRobot;
637            this.ambiguity = ambiguity;
638        }
639
640        @Override
641        public boolean equals(Object obj) {
642            if (this == obj)
643                return true;
644            if (obj == null || getClass() != obj.getClass())
645                return false;
646            RawFiducial other = (RawFiducial) obj;
647            return id == other.id
648                    && Double.compare(txnc, other.txnc) == 0
649                    && Double.compare(tync, other.tync) == 0
650                    && Double.compare(ta, other.ta) == 0
651                    && Double.compare(distToCamera, other.distToCamera) == 0
652                    && Double.compare(distToRobot, other.distToRobot) == 0
653                    && Double.compare(ambiguity, other.ambiguity) == 0;
654        }
655    }
656
657    /**
658     * Represents a Limelight Raw Target/Contour result from Limelight's
659     * NetworkTables output.
660     */
661    public static class RawTarget {
662
663        public double txnc = 0;
664
665        public double tync = 0;
666
667        public double ta = 0;
668
669        public RawTarget(double txnc, double tync, double ta) {
670            this.txnc = txnc;
671            this.tync = tync;
672            this.ta = ta;
673        }
674
675        @Override
676        public boolean equals(Object obj) {
677            if (this == obj)
678                return true;
679            if (obj == null || getClass() != obj.getClass())
680                return false;
681            RawTarget other = (RawTarget) obj;
682            return Double.compare(txnc, other.txnc) == 0
683                    && Double.compare(tync, other.tync) == 0
684                    && Double.compare(ta, other.ta) == 0;
685        }
686    }
687
688    /**
689     * Represents a Limelight Raw Neural Detector result from Limelight's
690     * NetworkTables output.
691     */
692    public static class RawDetection {
693
694        public int classId = 0;
695
696        public double txnc = 0;
697
698        public double tync = 0;
699
700        public double ta = 0;
701
702        public double corner0_X = 0;
703
704        public double corner0_Y = 0;
705
706        public double corner1_X = 0;
707
708        public double corner1_Y = 0;
709
710        public double corner2_X = 0;
711
712        public double corner2_Y = 0;
713
714        public double corner3_X = 0;
715
716        public double corner3_Y = 0;
717
718        public RawDetection(
719                int classId,
720                double txnc,
721                double tync,
722                double ta,
723                double corner0_X,
724                double corner0_Y,
725                double corner1_X,
726                double corner1_Y,
727                double corner2_X,
728                double corner2_Y,
729                double corner3_X,
730                double corner3_Y) {
731            this.classId = classId;
732            this.txnc = txnc;
733            this.tync = tync;
734            this.ta = ta;
735            this.corner0_X = corner0_X;
736            this.corner0_Y = corner0_Y;
737            this.corner1_X = corner1_X;
738            this.corner1_Y = corner1_Y;
739            this.corner2_X = corner2_X;
740            this.corner2_Y = corner2_Y;
741            this.corner3_X = corner3_X;
742            this.corner3_Y = corner3_Y;
743        }
744    }
745
746    /** Represents a 3D Pose Estimate. */
747    public static class PoseEstimate {
748
749        public Pose2d pose;
750
751        public double timestampSeconds;
752
753        public double latency;
754
755        public int tagCount;
756
757        public double tagSpan;
758
759        public double avgTagDist;
760
761        public double avgTagArea;
762
763        public RawFiducial[] rawFiducials;
764
765        public boolean isMegaTag2;
766
767        /** Instantiates a PoseEstimate object with default values */
768        public PoseEstimate() {
769            this.pose = new Pose2d();
770            this.timestampSeconds = 0;
771            this.latency = 0;
772            this.tagCount = 0;
773            this.tagSpan = 0;
774            this.avgTagDist = 0;
775            this.avgTagArea = 0;
776            this.rawFiducials = new RawFiducial[] {};
777            this.isMegaTag2 = false;
778        }
779
780        public PoseEstimate(
781                Pose2d pose,
782                double timestampSeconds,
783                double latency,
784                int tagCount,
785                double tagSpan,
786                double avgTagDist,
787                double avgTagArea,
788                RawFiducial[] rawFiducials,
789                boolean isMegaTag2) {
790            this.pose = pose;
791            this.timestampSeconds = timestampSeconds;
792            this.latency = latency;
793            this.tagCount = tagCount;
794            this.tagSpan = tagSpan;
795            this.avgTagDist = avgTagDist;
796            this.avgTagArea = avgTagArea;
797            this.rawFiducials = rawFiducials;
798            this.isMegaTag2 = isMegaTag2;
799        }
800
801        @Override
802        public boolean equals(Object obj) {
803            if (this == obj)
804                return true;
805            if (obj == null || getClass() != obj.getClass())
806                return false;
807            PoseEstimate that = (PoseEstimate) obj;
808            // We don't compare the timestampSeconds as it isn't relevant for equality and
809            // makes
810            // unit testing harder
811            return Double.compare(that.latency, latency) == 0
812                    && tagCount == that.tagCount
813                    && Double.compare(that.tagSpan, tagSpan) == 0
814                    && Double.compare(that.avgTagDist, avgTagDist) == 0
815                    && Double.compare(that.avgTagArea, avgTagArea) == 0
816                    && pose.equals(that.pose)
817                    && Arrays.equals(rawFiducials, that.rawFiducials);
818        }
819    }
820
821    /** Encapsulates the state of an internal Limelight IMU. */
822    public static class IMUData {
823
824        public double robotYaw = 0.0;
825
826        public double Roll = 0.0;
827
828        public double Pitch = 0.0;
829
830        public double Yaw = 0.0;
831
832        public double gyroX = 0.0;
833
834        public double gyroY = 0.0;
835
836        public double gyroZ = 0.0;
837
838        public double accelX = 0.0;
839
840        public double accelY = 0.0;
841
842        public double accelZ = 0.0;
843
844        public IMUData() {
845        }
846
847        public IMUData(double[] imuData) {
848            if (imuData != null && imuData.length >= 10) {
849                this.robotYaw = imuData[0];
850                this.Roll = imuData[1];
851                this.Pitch = imuData[2];
852                this.Yaw = imuData[3];
853                this.gyroX = imuData[4];
854                this.gyroY = imuData[5];
855                this.gyroZ = imuData[6];
856                this.accelX = imuData[7];
857                this.accelY = imuData[8];
858                this.accelZ = imuData[9];
859            }
860        }
861    }
862
863    private static ObjectMapper mapper;
864
865    /** Print JSON Parse time to the console in milliseconds */
866    static boolean profileJSON = false;
867
868    static final String sanitizeName(String name) {
869        if ("".equals(name) || name == null) {
870            return "limelight";
871        }
872        return name;
873    }
874
875    /**
876     * Takes a 6-length array of pose data and converts it to a Pose3d object. Array
877     * format: [x, y, z,
878     * roll, pitch, yaw] where angles are in degrees.
879     *
880     * @param inData Array containing pose data [x, y, z, roll, pitch, yaw]
881     * @return Pose3d object representing the pose, or empty Pose3d if invalid data
882     */
883    public static Pose3d toPose3D(double[] inData) {
884        if (inData.length < 6) {
885            // System.err.println("Bad LL 3D Pose Data!");
886            return new Pose3d();
887        }
888        return new Pose3d(
889                new Translation3d(inData[0], inData[1], inData[2]),
890                new Rotation3d(
891                        Units.degreesToRadians(inData[3]),
892                        Units.degreesToRadians(inData[4]),
893                        Units.degreesToRadians(inData[5])));
894    }
895
896    /**
897     * Takes a 6-length array of pose data and converts it to a Pose2d object. Uses
898     * only x, y, and yaw
899     * components, ignoring z, roll, and pitch. Array format: [x, y, z, roll, pitch,
900     * yaw] where angles
901     * are in degrees.
902     *
903     * @param inData Array containing pose data [x, y, z, roll, pitch, yaw]
904     * @return Pose2d object representing the pose, or empty Pose2d if invalid data
905     */
906    public static Pose2d toPose2D(double[] inData) {
907        if (inData.length < 6) {
908            // System.err.println("Bad LL 2D Pose Data!");
909            return new Pose2d();
910        }
911        Translation2d tran2d = new Translation2d(inData[0], inData[1]);
912        Rotation2d r2d = new Rotation2d(Units.degreesToRadians(inData[5]));
913        return new Pose2d(tran2d, r2d);
914    }
915
916    /**
917     * Converts a Pose3d object to an array of doubles in the format [x, y, z, roll,
918     * pitch, yaw].
919     * Translation components are in meters, rotation components are in degrees.
920     *
921     * @param pose The Pose3d object to convert
922     * @return A 6-element array containing [x, y, z, roll, pitch, yaw]
923     */
924    public static double[] pose3dToArray(Pose3d pose) {
925        double[] result = new double[6];
926        result[0] = pose.getTranslation().getX();
927        result[1] = pose.getTranslation().getY();
928        result[2] = pose.getTranslation().getZ();
929        result[3] = Units.radiansToDegrees(pose.getRotation().getX());
930        result[4] = Units.radiansToDegrees(pose.getRotation().getY());
931        result[5] = Units.radiansToDegrees(pose.getRotation().getZ());
932        return result;
933    }
934
935    /**
936     * Converts a Pose2d object to an array of doubles in the format [x, y, z, roll,
937     * pitch, yaw].
938     * Translation components are in meters, rotation components are in degrees.
939     * Note: z, roll, and
940     * pitch will be 0 since Pose2d only contains x, y, and yaw.
941     *
942     * @param pose The Pose2d object to convert
943     * @return A 6-element array containing [x, y, 0, 0, 0, yaw]
944     */
945    public static double[] pose2dToArray(Pose2d pose) {
946        double[] result = new double[6];
947        result[0] = pose.getTranslation().getX();
948        result[1] = pose.getTranslation().getY();
949        result[2] = 0;
950        result[3] = Units.radiansToDegrees(0);
951        result[4] = Units.radiansToDegrees(0);
952        result[5] = Units.radiansToDegrees(pose.getRotation().getRadians());
953        return result;
954    }
955
956    private static double extractArrayEntry(double[] inData, int position) {
957        if (inData.length < position + 1) {
958            return 0;
959        }
960        return inData[position];
961    }
962
963    private static PoseEstimate getBotPoseEstimate(
964            String limelightName, String entryName, boolean isMegaTag2) {
965        DoubleArrayEntry poseEntry = LimelightHelpers.getLimelightDoubleArrayEntry(limelightName, entryName);
966        TimestampedDoubleArray tsValue = poseEntry.getAtomic();
967        double[] poseArray = tsValue.value;
968        long timestamp = tsValue.timestamp;
969        if (poseArray.length == 0) {
970            // Handle the case where no data is available
971            return new PoseEstimate();
972        }
973        var pose = toPose2D(poseArray);
974        double latency = extractArrayEntry(poseArray, 6);
975        int tagCount = (int) extractArrayEntry(poseArray, 7);
976        double tagSpan = extractArrayEntry(poseArray, 8);
977        double tagDist = extractArrayEntry(poseArray, 9);
978        double tagArea = extractArrayEntry(poseArray, 10);
979        // Convert server timestamp from microseconds to seconds and adjust for latency
980        double adjustedTimestamp = (timestamp / 1000000.0) - (latency / 1000.0);
981        int valsPerFiducial = 7;
982        int expectedTotalVals = 11 + valsPerFiducial * tagCount;
983        RawFiducial[] rawFiducials;
984        if (poseArray.length != expectedTotalVals) {
985            // Array size mismatch - return empty array instead of null-filled array
986            rawFiducials = new RawFiducial[0];
987        } else {
988            rawFiducials = new RawFiducial[tagCount];
989            for (int i = 0; i < tagCount; i++) {
990                int baseIndex = 11 + (i * valsPerFiducial);
991                int id = (int) poseArray[baseIndex];
992                double txnc = poseArray[baseIndex + 1];
993                double tync = poseArray[baseIndex + 2];
994                double ta = poseArray[baseIndex + 3];
995                double distToCamera = poseArray[baseIndex + 4];
996                double distToRobot = poseArray[baseIndex + 5];
997                double ambiguity = poseArray[baseIndex + 6];
998                rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
999            }
1000        }
1001        return new PoseEstimate(
1002                pose,
1003                adjustedTimestamp,
1004                latency,
1005                tagCount,
1006                tagSpan,
1007                tagDist,
1008                tagArea,
1009                rawFiducials,
1010                isMegaTag2);
1011    }
1012
1013    /**
1014     * Gets the latest raw fiducial/AprilTag detection results from NetworkTables.
1015     *
1016     * @param limelightName Name/identifier of the Limelight
1017     * @return Array of RawFiducial objects containing detection details
1018     */
1019    public static RawFiducial[] getRawFiducials(String limelightName) {
1020        var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawfiducials");
1021        var rawFiducialArray = entry.getDoubleArray(new double[0]);
1022        int valsPerEntry = 7;
1023        if (rawFiducialArray.length % valsPerEntry != 0) {
1024            return new RawFiducial[0];
1025        }
1026        int numFiducials = rawFiducialArray.length / valsPerEntry;
1027        RawFiducial[] rawFiducials = new RawFiducial[numFiducials];
1028        for (int i = 0; i < numFiducials; i++) {
1029            int baseIndex = i * valsPerEntry;
1030            int id = (int) extractArrayEntry(rawFiducialArray, baseIndex);
1031            double txnc = extractArrayEntry(rawFiducialArray, baseIndex + 1);
1032            double tync = extractArrayEntry(rawFiducialArray, baseIndex + 2);
1033            double ta = extractArrayEntry(rawFiducialArray, baseIndex + 3);
1034            double distToCamera = extractArrayEntry(rawFiducialArray, baseIndex + 4);
1035            double distToRobot = extractArrayEntry(rawFiducialArray, baseIndex + 5);
1036            double ambiguity = extractArrayEntry(rawFiducialArray, baseIndex + 6);
1037            rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
1038        }
1039        return rawFiducials;
1040    }
1041
1042    /**
1043     * Gets the latest raw neural detector results from NetworkTables
1044     *
1045     * @param limelightName Name/identifier of the Limelight
1046     * @return Array of RawDetection objects containing detection details
1047     */
1048    public static RawDetection[] getRawDetections(String limelightName) {
1049        var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawdetections");
1050        var rawDetectionArray = entry.getDoubleArray(new double[0]);
1051        int valsPerEntry = 12;
1052        if (rawDetectionArray.length % valsPerEntry != 0) {
1053            return new RawDetection[0];
1054        }
1055        int numDetections = rawDetectionArray.length / valsPerEntry;
1056        RawDetection[] rawDetections = new RawDetection[numDetections];
1057        for (int i = 0; i < numDetections; i++) {
1058            // Starting index for this detection's data
1059            int baseIndex = i * valsPerEntry;
1060            int classId = (int) extractArrayEntry(rawDetectionArray, baseIndex);
1061            double txnc = extractArrayEntry(rawDetectionArray, baseIndex + 1);
1062            double tync = extractArrayEntry(rawDetectionArray, baseIndex + 2);
1063            double ta = extractArrayEntry(rawDetectionArray, baseIndex + 3);
1064            double corner0_X = extractArrayEntry(rawDetectionArray, baseIndex + 4);
1065            double corner0_Y = extractArrayEntry(rawDetectionArray, baseIndex + 5);
1066            double corner1_X = extractArrayEntry(rawDetectionArray, baseIndex + 6);
1067            double corner1_Y = extractArrayEntry(rawDetectionArray, baseIndex + 7);
1068            double corner2_X = extractArrayEntry(rawDetectionArray, baseIndex + 8);
1069            double corner2_Y = extractArrayEntry(rawDetectionArray, baseIndex + 9);
1070            double corner3_X = extractArrayEntry(rawDetectionArray, baseIndex + 10);
1071            double corner3_Y = extractArrayEntry(rawDetectionArray, baseIndex + 11);
1072            rawDetections[i] = new RawDetection(
1073                    classId, txnc, tync, ta, corner0_X, corner0_Y, corner1_X, corner1_Y, corner2_X,
1074                    corner2_Y, corner3_X, corner3_Y);
1075        }
1076        return rawDetections;
1077    }
1078
1079    /**
1080     * Gets the raw target contours from NetworkTables. Returns ungrouped contours
1081     * in normalized
1082     * screen space (-1 to 1).
1083     *
1084     * @param limelightName Name/identifier of the Limelight
1085     * @return Array of RawTarget objects containing up to 3 contours
1086     */
1087    public static RawTarget[] getRawTargets(String limelightName) {
1088        var entry = LimelightHelpers.getLimelightNTTableEntry(limelightName, "rawtargets");
1089        var rawTargetArray = entry.getDoubleArray(new double[0]);
1090        int valsPerEntry = 3;
1091        if (rawTargetArray.length % valsPerEntry != 0) {
1092            return new RawTarget[0];
1093        }
1094        int numTargets = rawTargetArray.length / valsPerEntry;
1095        RawTarget[] rawTargets = new RawTarget[numTargets];
1096        for (int i = 0; i < numTargets; i++) {
1097            int baseIndex = i * valsPerEntry;
1098            double txnc = extractArrayEntry(rawTargetArray, baseIndex);
1099            double tync = extractArrayEntry(rawTargetArray, baseIndex + 1);
1100            double ta = extractArrayEntry(rawTargetArray, baseIndex + 2);
1101            rawTargets[i] = new RawTarget(txnc, tync, ta);
1102        }
1103        return rawTargets;
1104    }
1105
1106    /**
1107     * Gets the corner coordinates of detected targets from NetworkTables. Requires
1108     * "send contours" to
1109     * be enabled in the Limelight Output tab.
1110     *
1111     * @param limelightName Name/identifier of the Limelight
1112     * @return Array of doubles containing corner coordinates [x0, y0, x1, y1, ...]
1113     */
1114    public static double[] getCornerCoordinates(String limelightName) {
1115        return getLimelightNTDoubleArray(limelightName, "tcornxy");
1116    }
1117
1118    /**
1119     * Prints detailed information about a PoseEstimate to standard output. Includes
1120     * timestamp,
1121     * latency, tag count, tag span, average tag distance, average tag area, and
1122     * detailed information
1123     * about each detected fiducial.
1124     *
1125     * @param pose The PoseEstimate object to print. If null, prints "No
1126     *             PoseEstimate available."
1127     */
1128    public static void printPoseEstimate(PoseEstimate pose) {
1129        if (pose == null) {
1130            System.out.println("No PoseEstimate available.");
1131            return;
1132        }
1133        System.out.printf("Pose Estimate Information:%n");
1134        System.out.printf("Timestamp (Seconds): %.3f%n", pose.timestampSeconds);
1135        System.out.printf("Latency: %.3f ms%n", pose.latency);
1136        System.out.printf("Tag Count: %d%n", pose.tagCount);
1137        System.out.printf("Tag Span: %.2f meters%n", pose.tagSpan);
1138        System.out.printf("Average Tag Distance: %.2f meters%n", pose.avgTagDist);
1139        System.out.printf("Average Tag Area: %.2f%% of image%n", pose.avgTagArea);
1140        System.out.printf("Is MegaTag2: %b%n", pose.isMegaTag2);
1141        System.out.println();
1142        if (pose.rawFiducials == null || pose.rawFiducials.length == 0) {
1143            System.out.println("No RawFiducials data available.");
1144            return;
1145        }
1146        System.out.println("Raw Fiducials Details:");
1147        for (int i = 0; i < pose.rawFiducials.length; i++) {
1148            RawFiducial fiducial = pose.rawFiducials[i];
1149            System.out.printf(" Fiducial #%d:%n", i + 1);
1150            System.out.printf("  ID: %d%n", fiducial.id);
1151            System.out.printf("  TXNC: %.2f%n", fiducial.txnc);
1152            System.out.printf("  TYNC: %.2f%n", fiducial.tync);
1153            System.out.printf("  TA: %.2f%n", fiducial.ta);
1154            System.out.printf("  Distance to Camera: %.2f meters%n", fiducial.distToCamera);
1155            System.out.printf("  Distance to Robot: %.2f meters%n", fiducial.distToRobot);
1156            System.out.printf("  Ambiguity: %.2f%n", fiducial.ambiguity);
1157            System.out.println();
1158        }
1159    }
1160
1161    public static Boolean validPoseEstimate(PoseEstimate pose) {
1162        return pose != null && pose.rawFiducials != null && pose.rawFiducials.length != 0;
1163    }
1164
1165    public static NetworkTable getLimelightNTTable(String tableName) {
1166        return NetworkTableInstance.getDefault().getTable(sanitizeName(tableName));
1167    }
1168
1169    public static void Flush() {
1170        NetworkTableInstance.getDefault().flush();
1171    }
1172
1173    public static NetworkTableEntry getLimelightNTTableEntry(String tableName, String entryName) {
1174        return getLimelightNTTable(tableName).getEntry(entryName);
1175    }
1176
1177    public static DoubleArrayEntry getLimelightDoubleArrayEntry(String tableName, String entryName) {
1178        String key = tableName + "/" + entryName;
1179        return doubleArrayEntries.computeIfAbsent(
1180                key,
1181                k -> {
1182                    NetworkTable table = getLimelightNTTable(tableName);
1183                    return table.getDoubleArrayTopic(entryName).getEntry(new double[0]);
1184                });
1185    }
1186
1187    public static double getLimelightNTDouble(String tableName, String entryName) {
1188        return getLimelightNTTableEntry(tableName, entryName).getDouble(0.0);
1189    }
1190
1191    public static void setLimelightNTDouble(String tableName, String entryName, double val) {
1192        getLimelightNTTableEntry(tableName, entryName).setDouble(val);
1193    }
1194
1195    public static void setLimelightNTDoubleArray(String tableName, String entryName, double[] val) {
1196        getLimelightNTTableEntry(tableName, entryName).setDoubleArray(val);
1197    }
1198
1199    public static double[] getLimelightNTDoubleArray(String tableName, String entryName) {
1200        return getLimelightNTTableEntry(tableName, entryName).getDoubleArray(new double[0]);
1201    }
1202
1203    public static String getLimelightNTString(String tableName, String entryName) {
1204        return getLimelightNTTableEntry(tableName, entryName).getString("");
1205    }
1206
1207    public static String[] getLimelightNTStringArray(String tableName, String entryName) {
1208        return getLimelightNTTableEntry(tableName, entryName).getStringArray(new String[0]);
1209    }
1210
1211    // ///
1212    /**
1213     * Does the Limelight have a valid target?
1214     *
1215     * @param limelightName Name of the Limelight camera ("" for default)
1216     * @return True if a valid target is present, false otherwise
1217     */
1218    public static boolean getTV(String limelightName) {
1219        return 1.0 == getLimelightNTDouble(limelightName, "tv");
1220    }
1221
1222    /**
1223     * Gets the horizontal offset from the crosshair to the target in degrees.
1224     *
1225     * @param limelightName Name of the Limelight camera ("" for default)
1226     * @return Horizontal offset angle in degrees
1227     */
1228    public static double getTX(String limelightName) {
1229        return getLimelightNTDouble(limelightName, "tx");
1230    }
1231
1232    /**
1233     * Gets the vertical offset from the crosshair to the target in degrees.
1234     *
1235     * @param limelightName Name of the Limelight camera ("" for default)
1236     * @return Vertical offset angle in degrees
1237     */
1238    public static double getTY(String limelightName) {
1239        return getLimelightNTDouble(limelightName, "ty");
1240    }
1241
1242    /**
1243     * Gets the horizontal offset from the principal pixel/point to the target in
1244     * degrees. This is the
1245     * most accurate 2d metric if you are using a calibrated camera and you don't
1246     * need adjustable
1247     * crosshair functionality.
1248     *
1249     * @param limelightName Name of the Limelight camera ("" for default)
1250     * @return Horizontal offset angle in degrees
1251     */
1252    public static double getTXNC(String limelightName) {
1253        return getLimelightNTDouble(limelightName, "txnc");
1254    }
1255
1256    /**
1257     * Gets the vertical offset from the principal pixel/point to the target in
1258     * degrees. This is the
1259     * most accurate 2d metric if you are using a calibrated camera and you don't
1260     * need adjustable
1261     * crosshair functionality.
1262     *
1263     * @param limelightName Name of the Limelight camera ("" for default)
1264     * @return Vertical offset angle in degrees
1265     */
1266    public static double getTYNC(String limelightName) {
1267        return getLimelightNTDouble(limelightName, "tync");
1268    }
1269
1270    /**
1271     * Gets the target area as a percentage of the image (0-100%).
1272     *
1273     * @param limelightName Name of the Limelight camera ("" for default)
1274     * @return Target area percentage (0-100)
1275     */
1276    public static double getTA(String limelightName) {
1277        return getLimelightNTDouble(limelightName, "ta");
1278    }
1279
1280    /**
1281     * T2D is an array that contains several targeting metrcis
1282     *
1283     * @param limelightName Name of the Limelight camera
1284     * @return Array containing [targetValid, targetCount, targetLatency,
1285     *         captureLatency, tx, ty,
1286     *         txnc, tync, ta, tid, targetClassIndexDetector,
1287     *         targetClassIndexClassifier,
1288     *         targetLongSidePixels, targetShortSidePixels,
1289     *         targetHorizontalExtentPixels,
1290     *         targetVerticalExtentPixels, targetSkewDegrees]
1291     */
1292    public static double[] getT2DArray(String limelightName) {
1293        return getLimelightNTDoubleArray(limelightName, "t2d");
1294    }
1295
1296    /**
1297     * Gets the number of targets currently detected.
1298     *
1299     * @param limelightName Name of the Limelight camera
1300     * @return Number of detected targets
1301     */
1302    public static int getTargetCount(String limelightName) {
1303        double[] t2d = getT2DArray(limelightName);
1304        if (t2d.length == 17) {
1305            return (int) t2d[1];
1306        }
1307        return 0;
1308    }
1309
1310    /**
1311     * Gets the classifier class index from the currently running neural classifier
1312     * pipeline
1313     *
1314     * @param limelightName Name of the Limelight camera
1315     * @return Class index from classifier pipeline
1316     */
1317    public static int getClassifierClassIndex(String limelightName) {
1318        double[] t2d = getT2DArray(limelightName);
1319        if (t2d.length == 17) {
1320            return (int) t2d[11];
1321        }
1322        return 0;
1323    }
1324
1325    /**
1326     * Gets the detector class index from the primary result of the currently
1327     * running neural detector
1328     * pipeline.
1329     *
1330     * @param limelightName Name of the Limelight camera
1331     * @return Class index from detector pipeline
1332     */
1333    public static int getDetectorClassIndex(String limelightName) {
1334        double[] t2d = getT2DArray(limelightName);
1335        if (t2d.length == 17) {
1336            return (int) t2d[10];
1337        }
1338        return 0;
1339    }
1340
1341    /**
1342     * Gets the current neural classifier result class name.
1343     *
1344     * @param limelightName Name of the Limelight camera
1345     * @return Class name string from classifier pipeline
1346     */
1347    public static String getClassifierClass(String limelightName) {
1348        return getLimelightNTString(limelightName, "tcclass");
1349    }
1350
1351    /**
1352     * Gets the primary neural detector result class name.
1353     *
1354     * @param limelightName Name of the Limelight camera
1355     * @return Class name string from detector pipeline
1356     */
1357    public static String getDetectorClass(String limelightName) {
1358        return getLimelightNTString(limelightName, "tdclass");
1359    }
1360
1361    /**
1362     * Gets the pipeline's processing latency contribution.
1363     *
1364     * @param limelightName Name of the Limelight camera
1365     * @return Pipeline latency in milliseconds
1366     */
1367    public static double getLatency_Pipeline(String limelightName) {
1368        return getLimelightNTDouble(limelightName, "tl");
1369    }
1370
1371    /**
1372     * Gets the capture latency.
1373     *
1374     * @param limelightName Name of the Limelight camera
1375     * @return Capture latency in milliseconds
1376     */
1377    public static double getLatency_Capture(String limelightName) {
1378        return getLimelightNTDouble(limelightName, "cl");
1379    }
1380
1381    /**
1382     * Gets the active pipeline index.
1383     *
1384     * @param limelightName Name of the Limelight camera
1385     * @return Current pipeline index (0-9)
1386     */
1387    public static double getCurrentPipelineIndex(String limelightName) {
1388        return getLimelightNTDouble(limelightName, "getpipe");
1389    }
1390
1391    /**
1392     * Gets the current pipeline type.
1393     *
1394     * @param limelightName Name of the Limelight camera
1395     * @return Pipeline type string (e.g. "retro", "apriltag", etc)
1396     */
1397    public static String getCurrentPipelineType(String limelightName) {
1398        return getLimelightNTString(limelightName, "getpipetype");
1399    }
1400
1401    /**
1402     * Gets the full JSON results dump.
1403     *
1404     * @param limelightName Name of the Limelight camera
1405     * @return JSON string containing all current results
1406     */
1407    public static String getJSONDump(String limelightName) {
1408        return getLimelightNTString(limelightName, "json");
1409    }
1410
1411    /**
1412     * Switch to getBotPose
1413     *
1414     * @param limelightName
1415     * @return
1416     */
1417    @Deprecated
1418    public static double[] getBotpose(String limelightName) {
1419        return getLimelightNTDoubleArray(limelightName, "botpose");
1420    }
1421
1422    /**
1423     * Switch to getBotPose_wpiRed
1424     *
1425     * @param limelightName
1426     * @return
1427     */
1428    @Deprecated
1429    public static double[] getBotpose_wpiRed(String limelightName) {
1430        return getLimelightNTDoubleArray(limelightName, "botpose_wpired");
1431    }
1432
1433    /**
1434     * Switch to getBotPose_wpiBlue
1435     *
1436     * @param limelightName
1437     * @return
1438     */
1439    @Deprecated
1440    public static double[] getBotpose_wpiBlue(String limelightName) {
1441        return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue");
1442    }
1443
1444    public static double[] getBotPose(String limelightName) {
1445        return getLimelightNTDoubleArray(limelightName, "botpose");
1446    }
1447
1448    public static double[] getBotPose_wpiRed(String limelightName) {
1449        return getLimelightNTDoubleArray(limelightName, "botpose_wpired");
1450    }
1451
1452    public static double[] getBotPose_wpiBlue(String limelightName) {
1453        return getLimelightNTDoubleArray(limelightName, "botpose_wpiblue");
1454    }
1455
1456    public static double[] getBotPose_TargetSpace(String limelightName) {
1457        return getLimelightNTDoubleArray(limelightName, "botpose_targetspace");
1458    }
1459
1460    public static double[] getCameraPose_TargetSpace(String limelightName) {
1461        return getLimelightNTDoubleArray(limelightName, "camerapose_targetspace");
1462    }
1463
1464    public static double[] getTargetPose_CameraSpace(String limelightName) {
1465        return getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace");
1466    }
1467
1468    public static double[] getTargetPose_RobotSpace(String limelightName) {
1469        return getLimelightNTDoubleArray(limelightName, "targetpose_robotspace");
1470    }
1471
1472    /**
1473     * Gets the average color under the crosshair region as a 3-element array.
1474     *
1475     * @param limelightName Name of the Limelight camera
1476     * @return Array containing [Blue, Green, Red] color values (BGR order)
1477     */
1478    public static double[] getTargetColor(String limelightName) {
1479        return getLimelightNTDoubleArray(limelightName, "tc");
1480    }
1481
1482    public static double getFiducialID(String limelightName) {
1483        return getLimelightNTDouble(limelightName, "tid");
1484    }
1485
1486    /**
1487     * Gets the Limelight heartbeat value. Increments once per frame, allowing you
1488     * to detect if the
1489     * Limelight is connected and alive.
1490     *
1491     * @param limelightName Name of the Limelight camera
1492     * @return Heartbeat value that increments each frame
1493     */
1494    public static double getHeartbeat(String limelightName) {
1495        return getLimelightNTDouble(limelightName, "hb");
1496    }
1497
1498    public static String getNeuralClassID(String limelightName) {
1499        return getLimelightNTString(limelightName, "tclass");
1500    }
1501
1502    public static String[] getRawBarcodeData(String limelightName) {
1503        return getLimelightNTStringArray(limelightName, "rawbarcodes");
1504    }
1505
1506    // ///
1507    // ///
1508    public static Pose3d getBotPose3d(String limelightName) {
1509        double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose");
1510        return toPose3D(poseArray);
1511    }
1512
1513    /**
1514     * (Not Recommended) Gets the robot's 3D pose in the WPILib Red Alliance
1515     * Coordinate System.
1516     *
1517     * @param limelightName Name/identifier of the Limelight
1518     * @return Pose3d object representing the robot's position and orientation in
1519     *         Red Alliance field
1520     *         space
1521     */
1522    public static Pose3d getBotPose3d_wpiRed(String limelightName) {
1523        double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpired");
1524        return toPose3D(poseArray);
1525    }
1526
1527    /**
1528     * (Recommended) Gets the robot's 3D pose in the WPILib Blue Alliance Coordinate
1529     * System.
1530     *
1531     * @param limelightName Name/identifier of the Limelight
1532     * @return Pose3d object representing the robot's position and orientation in
1533     *         Blue Alliance field
1534     *         space
1535     */
1536    public static Pose3d getBotPose3d_wpiBlue(String limelightName) {
1537        double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_wpiblue");
1538        return toPose3D(poseArray);
1539    }
1540
1541    /**
1542     * Gets the robot's 3D pose with respect to the currently tracked target's
1543     * coordinate system.
1544     *
1545     * @param limelightName Name/identifier of the Limelight
1546     * @return Pose3d object representing the robot's position and orientation
1547     *         relative to the target
1548     */
1549    public static Pose3d getBotPose3d_TargetSpace(String limelightName) {
1550        double[] poseArray = getLimelightNTDoubleArray(limelightName, "botpose_targetspace");
1551        return toPose3D(poseArray);
1552    }
1553
1554    /**
1555     * Gets the camera's 3D pose with respect to the currently tracked target's
1556     * coordinate system.
1557     *
1558     * @param limelightName Name/identifier of the Limelight
1559     * @return Pose3d object representing the camera's position and orientation
1560     *         relative to the target
1561     */
1562    public static Pose3d getCameraPose3d_TargetSpace(String limelightName) {
1563        double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_targetspace");
1564        return toPose3D(poseArray);
1565    }
1566
1567    /**
1568     * Gets the target's 3D pose with respect to the camera's coordinate system.
1569     *
1570     * @param limelightName Name/identifier of the Limelight
1571     * @return Pose3d object representing the target's position and orientation
1572     *         relative to the camera
1573     */
1574    public static Pose3d getTargetPose3d_CameraSpace(String limelightName) {
1575        double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_cameraspace");
1576        return toPose3D(poseArray);
1577    }
1578
1579    /**
1580     * Gets the target's 3D pose with respect to the robot's coordinate system.
1581     *
1582     * @param limelightName Name/identifier of the Limelight
1583     * @return Pose3d object representing the target's position and orientation
1584     *         relative to the robot
1585     */
1586    public static Pose3d getTargetPose3d_RobotSpace(String limelightName) {
1587        double[] poseArray = getLimelightNTDoubleArray(limelightName, "targetpose_robotspace");
1588        return toPose3D(poseArray);
1589    }
1590
1591    /**
1592     * Gets the camera's 3D pose with respect to the robot's coordinate system.
1593     *
1594     * @param limelightName Name/identifier of the Limelight
1595     * @return Pose3d object representing the camera's position and orientation
1596     *         relative to the robot
1597     */
1598    public static Pose3d getCameraPose3d_RobotSpace(String limelightName) {
1599        double[] poseArray = getLimelightNTDoubleArray(limelightName, "camerapose_robotspace");
1600        return toPose3D(poseArray);
1601    }
1602
1603    /**
1604     * Gets the Pose2d for easy use with Odometry vision pose estimator
1605     * (addVisionMeasurement)
1606     *
1607     * @param limelightName
1608     * @return
1609     */
1610    public static Pose2d getBotPose2d_wpiBlue(String limelightName) {
1611        double[] result = getBotPose_wpiBlue(limelightName);
1612        return toPose2D(result);
1613    }
1614
1615    /**
1616     * Gets the MegaTag1 Pose2d and timestamp for use with WPILib pose estimator
1617     * (addVisionMeasurement) in the WPILib Blue alliance coordinate system.
1618     *
1619     * @param limelightName
1620     * @return
1621     */
1622    public static PoseEstimate getBotPoseEstimate_wpiBlue(String limelightName) {
1623        return getBotPoseEstimate(limelightName, "botpose_wpiblue", false);
1624    }
1625
1626    /**
1627     * Gets the MegaTag2 Pose2d and timestamp for use with WPILib pose estimator
1628     * (addVisionMeasurement) in the WPILib Blue alliance coordinate system. Make
1629     * sure you are calling
1630     * setRobotOrientation() before calling this method.
1631     *
1632     * @param limelightName
1633     * @return
1634     */
1635    public static PoseEstimate getBotPoseEstimate_wpiBlue_MegaTag2(String limelightName) {
1636        return getBotPoseEstimate(limelightName, "botpose_orb_wpiblue", true);
1637    }
1638
1639    /**
1640     * Gets the Pose2d for easy use with Odometry vision pose estimator
1641     * (addVisionMeasurement)
1642     *
1643     * @param limelightName
1644     * @return
1645     */
1646    public static Pose2d getBotPose2d_wpiRed(String limelightName) {
1647        double[] result = getBotPose_wpiRed(limelightName);
1648        return toPose2D(result);
1649    }
1650
1651    /**
1652     * Gets the Pose2d and timestamp for use with WPILib pose estimator
1653     * (addVisionMeasurement) when
1654     * you are on the RED alliance
1655     *
1656     * @param limelightName
1657     * @return
1658     */
1659    public static PoseEstimate getBotPoseEstimate_wpiRed(String limelightName) {
1660        return getBotPoseEstimate(limelightName, "botpose_wpired", false);
1661    }
1662
1663    /**
1664     * Gets the Pose2d and timestamp for use with WPILib pose estimator
1665     * (addVisionMeasurement) when
1666     * you are on the RED alliance
1667     *
1668     * @param limelightName
1669     * @return
1670     */
1671    public static PoseEstimate getBotPoseEstimate_wpiRed_MegaTag2(String limelightName) {
1672        return getBotPoseEstimate(limelightName, "botpose_orb_wpired", true);
1673    }
1674
1675    /**
1676     * Gets the Pose2d for easy use with Odometry vision pose estimator
1677     * (addVisionMeasurement)
1678     *
1679     * @param limelightName
1680     * @return
1681     */
1682    public static Pose2d getBotPose2d(String limelightName) {
1683        double[] result = getBotPose(limelightName);
1684        return toPose2D(result);
1685    }
1686
1687    /**
1688     * Gets the current IMU data from NetworkTables. IMU data is formatted as
1689     * [robotYaw, Roll, Pitch,
1690     * Yaw, gyroX, gyroY, gyroZ, accelX, accelY, accelZ]. Returns all zeros if data
1691     * is invalid or
1692     * unavailable.
1693     *
1694     * @param limelightName Name/identifier of the Limelight
1695     * @return IMUData object containing all current IMU data
1696     */
1697    public static IMUData getIMUData(String limelightName) {
1698        double[] imuData = getLimelightNTDoubleArray(limelightName, "imu");
1699        if (imuData == null || imuData.length < 10) {
1700            // Returns object with all zeros
1701            return new IMUData();
1702        }
1703        return new IMUData(imuData);
1704    }
1705
1706    // ///
1707    // ///
1708    public static void setPipelineIndex(String limelightName, int pipelineIndex) {
1709        setLimelightNTDouble(limelightName, "pipeline", pipelineIndex);
1710    }
1711
1712    public static void setPriorityTagID(String limelightName, int ID) {
1713        setLimelightNTDouble(limelightName, "priorityid", ID);
1714    }
1715
1716    /**
1717     * Sets LED mode to be controlled by the current pipeline.
1718     *
1719     * @param limelightName Name of the Limelight camera
1720     */
1721    public static void setLEDMode_PipelineControl(String limelightName) {
1722        setLimelightNTDouble(limelightName, "ledMode", 0);
1723    }
1724
1725    public static void setLEDMode_ForceOff(String limelightName) {
1726        setLimelightNTDouble(limelightName, "ledMode", 1);
1727    }
1728
1729    public static void setLEDMode_ForceBlink(String limelightName) {
1730        setLimelightNTDouble(limelightName, "ledMode", 2);
1731    }
1732
1733    public static void setLEDMode_ForceOn(String limelightName) {
1734        setLimelightNTDouble(limelightName, "ledMode", 3);
1735    }
1736
1737    /**
1738     * Enables standard side-by-side stream mode.
1739     *
1740     * @param limelightName Name of the Limelight camera
1741     */
1742    public static void setStreamMode_Standard(String limelightName) {
1743        setLimelightNTDouble(limelightName, "stream", 0);
1744    }
1745
1746    /**
1747     * Enables Picture-in-Picture mode with secondary stream in the corner.
1748     *
1749     * @param limelightName Name of the Limelight camera
1750     */
1751    public static void setStreamMode_PiPMain(String limelightName) {
1752        setLimelightNTDouble(limelightName, "stream", 1);
1753    }
1754
1755    /**
1756     * Enables Picture-in-Picture mode with primary stream in the corner.
1757     *
1758     * @param limelightName Name of the Limelight camera
1759     */
1760    public static void setStreamMode_PiPSecondary(String limelightName) {
1761        setLimelightNTDouble(limelightName, "stream", 2);
1762    }
1763
1764    /**
1765     * Sets the crop window for the camera. The crop window in the UI must be
1766     * completely open.
1767     *
1768     * @param limelightName Name of the Limelight camera
1769     * @param cropXMin      Minimum X value (-1 to 1)
1770     * @param cropXMax      Maximum X value (-1 to 1)
1771     * @param cropYMin      Minimum Y value (-1 to 1)
1772     * @param cropYMax      Maximum Y value (-1 to 1)
1773     */
1774    public static void setCropWindow(
1775            String limelightName, double cropXMin, double cropXMax, double cropYMin, double cropYMax) {
1776        double[] entries = new double[4];
1777        entries[0] = cropXMin;
1778        entries[1] = cropXMax;
1779        entries[2] = cropYMin;
1780        entries[3] = cropYMax;
1781        setLimelightNTDoubleArray(limelightName, "crop", entries);
1782    }
1783
1784    /**
1785     * Sets the keystone modification for the crop window.
1786     *
1787     * @param limelightName Name of the Limelight camera
1788     * @param horizontal    Horizontal keystone value (-0.95 to 0.95)
1789     * @param vertical      Vertical keystone value (-0.95 to 0.95)
1790     */
1791    public static void setKeystone(String limelightName, double horizontal, double vertical) {
1792        double[] entries = new double[2];
1793        entries[0] = horizontal;
1794        entries[1] = vertical;
1795        setLimelightNTDoubleArray(limelightName, "keystone_set", entries);
1796    }
1797
1798    /** Sets 3D offset point for easy 3D targeting. */
1799    public static void setFiducial3DOffset(
1800            String limelightName, double offsetX, double offsetY, double offsetZ) {
1801        double[] entries = new double[3];
1802        entries[0] = offsetX;
1803        entries[1] = offsetY;
1804        entries[2] = offsetZ;
1805        setLimelightNTDoubleArray(limelightName, "fiducial_offset_set", entries);
1806    }
1807
1808    /**
1809     * Sets robot orientation values used by MegaTag2 localization algorithm.
1810     *
1811     * @param limelightName Name/identifier of the Limelight
1812     * @param yaw           Robot yaw in degrees. 0 = robot facing red alliance wall
1813     *                      in FRC
1814     * @param yawRate       (Unnecessary) Angular velocity of robot yaw in degrees
1815     *                      per second
1816     * @param pitch         (Unnecessary) Robot pitch in degrees
1817     * @param pitchRate     (Unnecessary) Angular velocity of robot pitch in degrees
1818     *                      per second
1819     * @param roll          (Unnecessary) Robot roll in degrees
1820     * @param rollRate      (Unnecessary) Angular velocity of robot roll in degrees
1821     *                      per second
1822     */
1823    public static void SetRobotOrientation(
1824            String limelightName,
1825            double yaw,
1826            double yawRate,
1827            double pitch,
1828            double pitchRate,
1829            double roll,
1830            double rollRate) {
1831        SetRobotOrientation_INTERNAL(
1832                limelightName, yaw, yawRate, pitch, pitchRate, roll, rollRate, true);
1833    }
1834
1835    public static void SetRobotOrientation_NoFlush(
1836            String limelightName,
1837            double yaw,
1838            double yawRate,
1839            double pitch,
1840            double pitchRate,
1841            double roll,
1842            double rollRate) {
1843        SetRobotOrientation_INTERNAL(
1844                limelightName, yaw, yawRate, pitch, pitchRate, roll, rollRate, false);
1845    }
1846
1847    private static void SetRobotOrientation_INTERNAL(
1848            String limelightName,
1849            double yaw,
1850            double yawRate,
1851            double pitch,
1852            double pitchRate,
1853            double roll,
1854            double rollRate,
1855            boolean flush) {
1856        double[] entries = new double[6];
1857        entries[0] = yaw;
1858        entries[1] = yawRate;
1859        entries[2] = pitch;
1860        entries[3] = pitchRate;
1861        entries[4] = roll;
1862        entries[5] = rollRate;
1863        setLimelightNTDoubleArray(limelightName, "robot_orientation_set", entries);
1864        if (flush) {
1865            Flush();
1866        }
1867    }
1868
1869    /**
1870     * Configures the IMU mode for MegaTag2 Localization
1871     *
1872     * @param limelightName Name/identifier of the Limelight
1873     * @param mode          IMU mode.
1874     */
1875    public static void SetIMUMode(String limelightName, int mode) {
1876        setLimelightNTDouble(limelightName, "imumode_set", mode);
1877    }
1878
1879    /**
1880     * Configures the complementary filter alpha value for IMU Assist Modes (Modes 3
1881     * and 4)
1882     *
1883     * @param limelightName Name/identifier of the Limelight
1884     * @param alpha         Defaults to .001. Higher values will cause the internal
1885     *                      IMU to converge onto the
1886     *                      assist source more rapidly.
1887     */
1888    public static void SetIMUAssistAlpha(String limelightName, double alpha) {
1889        setLimelightNTDouble(limelightName, "imuassistalpha_set", alpha);
1890    }
1891
1892    /**
1893     * Configures the throttle value. Set to 100-200 while disabled to reduce
1894     * thermal
1895     * output/temperature.
1896     *
1897     * @param limelightName Name/identifier of the Limelight
1898     * @param throttle      Defaults to 0. Your Limelgiht will process one frame
1899     *                      after skipping throttle
1900     *                      frames.
1901     */
1902    public static void SetThrottle(String limelightName, int throttle) {
1903        setLimelightNTDouble(limelightName, "throttle_set", throttle);
1904    }
1905
1906    /**
1907     * Overrides the valid AprilTag IDs that will be used for localization. Tags not
1908     * in this list will
1909     * be ignored for robot pose estimation.
1910     *
1911     * @param limelightName Name/identifier of the Limelight
1912     * @param validIDs      Array of valid AprilTag IDs to track
1913     */
1914    public static void SetFiducialIDFiltersOverride(String limelightName, int[] validIDs) {
1915        double[] validIDsDouble = new double[validIDs.length];
1916        for (int i = 0; i < validIDs.length; i++) {
1917            validIDsDouble[i] = validIDs[i];
1918        }
1919        setLimelightNTDoubleArray(limelightName, "fiducial_id_filters_set", validIDsDouble);
1920    }
1921
1922    /**
1923     * Sets the downscaling factor for AprilTag detection. Increasing downscale can
1924     * improve
1925     * performance at the cost of potentially reduced detection range.
1926     *
1927     * @param limelightName Name/identifier of the Limelight
1928     * @param downscale     Downscale factor. Valid values: 1.0 (no downscale), 1.5,
1929     *                      2.0, 3.0, 4.0. Set to
1930     *                      0 for pipeline control.
1931     */
1932    public static void SetFiducialDownscalingOverride(String limelightName, float downscale) {
1933        // pipeline
1934        int d = 0;
1935        if (downscale == 1.0) {
1936            d = 1;
1937        }
1938        if (downscale == 1.5) {
1939            d = 2;
1940        }
1941        if (downscale == 2) {
1942            d = 3;
1943        }
1944        if (downscale == 3) {
1945            d = 4;
1946        }
1947        if (downscale == 4) {
1948            d = 5;
1949        }
1950        setLimelightNTDouble(limelightName, "fiducial_downscale_set", d);
1951    }
1952
1953    /**
1954     * Sets the camera pose relative to the robot.
1955     *
1956     * @param limelightName Name of the Limelight camera
1957     * @param forward       Forward offset in meters
1958     * @param side          Side offset in meters
1959     * @param up            Up offset in meters
1960     * @param roll          Roll angle in degrees
1961     * @param pitch         Pitch angle in degrees
1962     * @param yaw           Yaw angle in degrees
1963     */
1964    public static void setCameraPose_RobotSpace(
1965            String limelightName,
1966            double forward,
1967            double side,
1968            double up,
1969            double roll,
1970            double pitch,
1971            double yaw) {
1972        double[] entries = new double[6];
1973        entries[0] = forward;
1974        entries[1] = side;
1975        entries[2] = up;
1976        entries[3] = roll;
1977        entries[4] = pitch;
1978        entries[5] = yaw;
1979        setLimelightNTDoubleArray(limelightName, "camerapose_robotspace_set", entries);
1980    }
1981
1982    // ///
1983    // ///
1984    public static void setPythonScriptData(String limelightName, double[] outgoingPythonData) {
1985        setLimelightNTDoubleArray(limelightName, "llrobot", outgoingPythonData);
1986    }
1987
1988    public static double[] getPythonScriptData(String limelightName) {
1989        return getLimelightNTDoubleArray(limelightName, "llpython");
1990    }
1991
1992    // ///
1993    // ///
1994    /**
1995     * Triggers a snapshot capture via NetworkTables by incrementing the snapshot
1996     * counter.
1997     * Rate-limited to once per 10 frames on the Limelight.
1998     *
1999     * @param limelightName Name of the Limelight camera
2000     */
2001    public static void triggerSnapshot(String limelightName) {
2002        double current = getLimelightNTDouble(limelightName, "snapshot");
2003        setLimelightNTDouble(limelightName, "snapshot", current + 1);
2004    }
2005
2006    /**
2007     * Enables or pauses the rewind buffer recording.
2008     *
2009     * @param limelightName Name of the Limelight camera
2010     * @param enabled       True to enable recording, false to pause
2011     */
2012    public static void setRewindEnabled(String limelightName, boolean enabled) {
2013        setLimelightNTDouble(limelightName, "rewind_enable_set", enabled ? 1 : 0);
2014    }
2015
2016    /**
2017     * Triggers a rewind capture with the specified duration. Maximum duration is
2018     * 165 seconds.
2019     * Rate-limited on the Limelight.
2020     *
2021     * @param limelightName   Name of the Limelight camera
2022     * @param durationSeconds Duration of rewind capture in seconds (max 165)
2023     */
2024    public static void triggerRewindCapture(String limelightName, double durationSeconds) {
2025        double[] currentArray = getLimelightNTDoubleArray(limelightName, "capture_rewind");
2026        double counter = (currentArray.length > 0) ? currentArray[0] : 0;
2027        double[] entries = new double[2];
2028        entries[0] = counter + 1;
2029        entries[1] = Math.min(durationSeconds, 165);
2030        setLimelightNTDoubleArray(limelightName, "capture_rewind", entries);
2031    }
2032
2033    /**
2034     * Gets the latest JSON results output and returns a LimelightResults object.
2035     *
2036     * @param limelightName Name of the Limelight camera
2037     * @return LimelightResults object containing all current target data
2038     */
2039    public static LimelightResults getLatestResults(String limelightName) {
2040        long start = System.nanoTime();
2041        LimelightHelpers.LimelightResults results = new LimelightHelpers.LimelightResults();
2042        if (mapper == null) {
2043            mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
2044        }
2045        try {
2046            String jsonString = getJSONDump(limelightName);
2047            if (jsonString == null || jsonString.isEmpty() || jsonString.isBlank()) {
2048                results.error = "lljson error: empty json";
2049            } else {
2050                results = mapper.readValue(jsonString, LimelightResults.class);
2051                if (results.imuResults != null) {
2052                    results.imuResults.parseDataArray();
2053                }
2054            }
2055        } catch (JsonProcessingException e) {
2056            results.error = "lljson error: " + e.getMessage();
2057        }
2058        long end = System.nanoTime();
2059        double millis = (end - start) * .000001;
2060        results.latency_jsonParse = millis;
2061        if (profileJSON) {
2062            System.out.printf("lljson: %.2f\r\n", millis);
2063        }
2064        return results;
2065    }
2066
2067    /**
2068     * Sets up port forwarding for a Limelight 3A/3G connected via USB. This allows
2069     * access to the
2070     * Limelight web interface and video stream when connected to the robot over
2071     * USB.
2072     *
2073     * <p>
2074     * For usbIndex 0: ports 5800-5809 forward to 172.29.0.1 For usbIndex 1: ports
2075     * 5810-5819
2076     * forward to 172.29.1.1 etc.
2077     *
2078     * <p>
2079     * Call this method once during robot initialization. To access the interface of
2080     * the camera
2081     * with usbIndex0, you would go to roboRIO-(teamnum)-FRC.local:5801. Port 5811
2082     * for usb index 1
2083     *
2084     * @param usbIndex The USB index of the Limelight (0, 1, 2, etc.)
2085     */
2086    public static void setupPortForwardingUSB(int usbIndex) {
2087        String ip = "172.29." + usbIndex + ".1";
2088        int basePort = 5800 + (usbIndex * 10);
2089        for (int i = 0; i < 10; i++) {
2090            PortForwarder.add(basePort + i, ip, 5800 + i);
2091        }
2092    }
2093}