Skip to content

Comments

Add Vision#9

Open
JosephTLockwood wants to merge 1 commit into2-Loggingfrom
3-Limelight
Open

Add Vision#9
JosephTLockwood wants to merge 1 commit into2-Loggingfrom
3-Limelight

Conversation

@JosephTLockwood
Copy link
Collaborator

@JosephTLockwood JosephTLockwood commented Oct 14, 2025

Summary by CodeRabbit

  • New Features
    • Added integrated vision subsystem using Limelight that feeds validated pose estimates into drivetrain.
    • Introduced comprehensive vision utilities to read results and IMU data, access target info, and convert between 2D/3D poses.
    • Enabled configuration of pipelines, LEDs, streaming modes, region of interest, fiducial filters/downscaling, pose offsets, and camera pose.
    • Added asynchronous snapshot capture capability.
    • Improved reliability with defensive handling of missing/malformed data and convenient helpers for pose validation and diagnostics.

@coderabbitai
Copy link

coderabbitai bot commented Oct 14, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Add Vision” succinctly captures the primary change of integrating a new vision subsystem using Limelight without extraneous detail, providing clear context for reviewers scanning the project history.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@JosephTLockwood
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
src/main/java/frc/robot/LimelightHelpers.java (5)

639-650: Null-safety for pose conversion and array extraction

Defensive null checks avoid NPEs if external sources return null arrays.

Apply:

   public static Pose3d toPose3D(double[] inData) {
-    if (inData.length < 6) {
+    if (inData == null || inData.length < 6) {
       return new Pose3d();
     }
   public static Pose2d toPose2D(double[] inData) {
-    if (inData.length < 6) {
+    if (inData == null || inData.length < 6) {
       return new Pose2d();
     }
   private static double extractArrayEntry(double[] inData, int position) {
-    if (inData.length < position + 1) {
+    if (inData == null || inData.length <= position) {
       return 0;
     }
     return inData[position];
   }

Also applies to: 660-668, 707-712


937-947: Improve error detail for malformed URLs

Including the attempted URL aids debugging.

Apply:

-    } catch (MalformedURLException e) {
-      System.err.println("bad LL URL");
+    } catch (MalformedURLException e) {
+      System.err.println("Bad Limelight URL: " + urlString);

1590-1597: Deprecate misspelled API alias or rename for consistency

Method name has a typo: SetFidcuial3DOffset. You already expose setFiducial3DOffset. Mark the misspelled one deprecated to steer users.

Apply:

-  public static void SetFidcuial3DOffset(String limelightName, double x, double y, double z) {
+  @Deprecated
+  public static void SetFidcuial3DOffset(String limelightName, double x, double y, double z) {

1622-1640: Avoid exact float equality for downscaling selection

Direct float comparisons can be brittle. Prefer a small epsilon or map discrete allowed values.

Example using epsilon:

-  public static void SetFiducialDownscalingOverride(String limelightName, float downscale) {
-    int d = 0; // pipeline
-    if (downscale == 1.0) {
+  public static void SetFiducialDownscalingOverride(String limelightName, float downscale) {
+    int d = 0; // pipeline
+    final float eps = 1e-3f;
+    if (Math.abs(downscale - 1.0f) < eps) {
       d = 1;
-    }
-    if (downscale == 1.5) {
+    } else if (Math.abs(downscale - 1.5f) < eps) {
       d = 2;
-    }
-    if (downscale == 2) {
+    } else if (Math.abs(downscale - 2.0f) < eps) {
       d = 3;
-    }
-    if (downscale == 3) {
+    } else if (Math.abs(downscale - 3.0f) < eps) {
       d = 4;
-    }
-    if (downscale == 4) {
+    } else if (Math.abs(downscale - 4.0f) < eps) {
       d = 5;
     }
     setLimelightNTDouble(limelightName, "fiducial_downscale_set", d);
   }

872-884: Guard against null RawFiducial entries in printing

If any element is null, printing will NPE.

Apply:

-    for (int i = 0; i < pose.rawFiducials.length; i++) {
-      RawFiducial fiducial = pose.rawFiducials[i];
+    for (int i = 0; i < pose.rawFiducials.length; i++) {
+      RawFiducial fiducial = pose.rawFiducials[i];
+      if (fiducial == null) {
+        System.out.printf(" Fiducial #%d: <no data>%n", i + 1);
+        continue;
+      }
src/main/java/frc/robot/subsystems/vision/Limelight.java (2)

28-31: Fix constructor parameter typo

Rename limelighName to limelightName for clarity and consistency.

Apply:

-  public Limelight(VisionConsumer consumer, String limelighName) {
+  public Limelight(VisionConsumer consumer, String limelightName) {
     m_Consumer = consumer;
-    m_limelightName = limelighName;
+    m_limelightName = limelightName;
   }

49-53: Protect against divide-by-zero; clamp tag count

Even if validity checks change, ensure safe math.

Apply:

-      double xyStandardDev = 0.5 * Math.pow(poseEstimate.avgTagDist, 2.0) / poseEstimate.tagCount;
-      double rotationStandardDev =
-          5.0 * Math.pow(poseEstimate.avgTagDist, 2.0) / poseEstimate.tagCount;
+      double count = Math.max(1.0, poseEstimate.tagCount);
+      double xyStandardDev = 0.5 * Math.pow(poseEstimate.avgTagDist, 2.0) / count;
+      double rotationStandardDev = 5.0 * Math.pow(poseEstimate.avgTagDist, 2.0) / count;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1c58c5 and fadf5b8.

📒 Files selected for processing (3)
  • src/main/java/frc/robot/LimelightHelpers.java (1 hunks)
  • src/main/java/frc/robot/RobotContainer.java (2 hunks)
  • src/main/java/frc/robot/subsystems/vision/Limelight.java (1 hunks)
🔇 Additional comments (1)
src/main/java/frc/robot/RobotContainer.java (1)

43-44: Vision subsystem wiring looks good

Limelight instantiated with drivetrain vision consumer and name. Field order ensures drivetrain exists first.

Comment on lines +738 to +768
RawFiducial[] rawFiducials = new RawFiducial[tagCount];
int valsPerFiducial = 7;
int expectedTotalVals = 11 + valsPerFiducial * tagCount;

if (poseArray.length != expectedTotalVals) {
// Don't populate fiducials
} else {
for (int i = 0; i < tagCount; i++) {
int baseIndex = 11 + (i * valsPerFiducial);
int id = (int) poseArray[baseIndex];
double txnc = poseArray[baseIndex + 1];
double tync = poseArray[baseIndex + 2];
double ta = poseArray[baseIndex + 3];
double distToCamera = poseArray[baseIndex + 4];
double distToRobot = poseArray[baseIndex + 5];
double ambiguity = poseArray[baseIndex + 6];
rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
}
}

return new PoseEstimate(
pose,
adjustedTimestamp,
latency,
tagCount,
tagSpan,
tagDist,
tagArea,
rawFiducials,
isMegaTag2);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid partially-filled RawFiducials; keep counts consistent to prevent NPEs downstream

When poseArray.length != expectedTotalVals, you create an array sized to tagCount but never fill it, leaving null elements. printPoseEstimate will NPE and validPoseEstimate can return true due to non-zero length despite missing data.

Apply:

-    RawFiducial[] rawFiducials = new RawFiducial[tagCount];
-    int valsPerFiducial = 7;
-    int expectedTotalVals = 11 + valsPerFiducial * tagCount;
-
-    if (poseArray.length != expectedTotalVals) {
-      // Don't populate fiducials
-    } else {
-      for (int i = 0; i < tagCount; i++) {
+    int valsPerFiducial = 7;
+    int expectedTotalVals = 11 + valsPerFiducial * tagCount;
+    int fidCount = (poseArray.length == expectedTotalVals) ? tagCount : 0;
+    RawFiducial[] rawFiducials = new RawFiducial[fidCount];
+    if (fidCount > 0) {
+      for (int i = 0; i < fidCount; i++) {
         int baseIndex = 11 + (i * valsPerFiducial);
         int id = (int) poseArray[baseIndex];
         double txnc = poseArray[baseIndex + 1];
         double tync = poseArray[baseIndex + 2];
         double ta = poseArray[baseIndex + 3];
         double distToCamera = poseArray[baseIndex + 4];
         double distToRobot = poseArray[baseIndex + 5];
         double ambiguity = poseArray[baseIndex + 6];
         rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
       }
     }
 
     return new PoseEstimate(
         pose,
         adjustedTimestamp,
         latency,
-        tagCount,
+        fidCount,
         tagSpan,
         tagDist,
         tagArea,
         rawFiducials,
         isMegaTag2);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RawFiducial[] rawFiducials = new RawFiducial[tagCount];
int valsPerFiducial = 7;
int expectedTotalVals = 11 + valsPerFiducial * tagCount;
if (poseArray.length != expectedTotalVals) {
// Don't populate fiducials
} else {
for (int i = 0; i < tagCount; i++) {
int baseIndex = 11 + (i * valsPerFiducial);
int id = (int) poseArray[baseIndex];
double txnc = poseArray[baseIndex + 1];
double tync = poseArray[baseIndex + 2];
double ta = poseArray[baseIndex + 3];
double distToCamera = poseArray[baseIndex + 4];
double distToRobot = poseArray[baseIndex + 5];
double ambiguity = poseArray[baseIndex + 6];
rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
}
}
return new PoseEstimate(
pose,
adjustedTimestamp,
latency,
tagCount,
tagSpan,
tagDist,
tagArea,
rawFiducials,
isMegaTag2);
}
int valsPerFiducial = 7;
int expectedTotalVals = 11 + valsPerFiducial * tagCount;
int fidCount = (poseArray.length == expectedTotalVals) ? tagCount : 0;
RawFiducial[] rawFiducials = new RawFiducial[fidCount];
if (fidCount > 0) {
for (int i = 0; i < fidCount; i++) {
int baseIndex = 11 + (i * valsPerFiducial);
int id = (int) poseArray[baseIndex];
double txnc = poseArray[baseIndex + 1];
double tync = poseArray[baseIndex + 2];
double ta = poseArray[baseIndex + 3];
double distToCamera = poseArray[baseIndex + 4];
double distToRobot = poseArray[baseIndex + 5];
double ambiguity = poseArray[baseIndex + 6];
rawFiducials[i] = new RawFiducial(id, txnc, tync, ta, distToCamera, distToRobot, ambiguity);
}
}
return new PoseEstimate(
pose,
adjustedTimestamp,
latency,
fidCount,
tagSpan,
tagDist,
tagArea,
rawFiducials,
isMegaTag2);
}

Comment on lines +1693 to +1712
private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) {
URL url = getLimelightURLString(tableName, "capturesnapshot");
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if (snapshotName != null && !"".equals(snapshotName)) {
connection.setRequestProperty("snapname", snapshotName);
}

int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return true;
} else {
System.err.println("Bad LL Request");
}
} catch (IOException e) {
System.err.println(e.getMessage());
}
return false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle null URL and add HTTP timeouts to prevent hangs

getLimelightURLString can return null. SYNCH_TAKESNAPSHOT dereferences it, causing NPE. Also, no connect/read timeouts: a stalled device can hang the async worker.

Apply:

-  private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) {
-    URL url = getLimelightURLString(tableName, "capturesnapshot");
-    try {
-      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-      connection.setRequestMethod("GET");
-      if (snapshotName != null && !"".equals(snapshotName)) {
-        connection.setRequestProperty("snapname", snapshotName);
-      }
-
-      int responseCode = connection.getResponseCode();
-      if (responseCode == 200) {
-        return true;
-      } else {
-        System.err.println("Bad LL Request");
-      }
-    } catch (IOException e) {
-      System.err.println(e.getMessage());
-    }
-    return false;
-  }
+  private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) {
+    URL url = getLimelightURLString(tableName, "capturesnapshot");
+    if (url == null) {
+      System.err.println("Invalid Limelight URL for table: " + tableName);
+      return false;
+    }
+    HttpURLConnection connection = null;
+    try {
+      connection = (HttpURLConnection) url.openConnection();
+      connection.setRequestMethod("GET");
+      connection.setConnectTimeout(1000);
+      connection.setReadTimeout(1500);
+      if (snapshotName != null && !"".equals(snapshotName)) {
+        connection.setRequestProperty("snapname", snapshotName);
+      }
+      int responseCode = connection.getResponseCode();
+      if (responseCode == 200) {
+        return true;
+      } else {
+        System.err.println("Bad LL Request, HTTP " + responseCode);
+      }
+    } catch (IOException e) {
+      System.err.println("Snapshot error: " + e.getMessage());
+    } finally {
+      if (connection != null) {
+        connection.disconnect();
+      }
+    }
+    return false;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) {
URL url = getLimelightURLString(tableName, "capturesnapshot");
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if (snapshotName != null && !"".equals(snapshotName)) {
connection.setRequestProperty("snapname", snapshotName);
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return true;
} else {
System.err.println("Bad LL Request");
}
} catch (IOException e) {
System.err.println(e.getMessage());
}
return false;
}
private static boolean SYNCH_TAKESNAPSHOT(String tableName, String snapshotName) {
URL url = getLimelightURLString(tableName, "capturesnapshot");
if (url == null) {
System.err.println("Invalid Limelight URL for table: " + tableName);
return false;
}
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(1000);
connection.setReadTimeout(1500);
if (snapshotName != null && !"".equals(snapshotName)) {
connection.setRequestProperty("snapname", snapshotName);
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return true;
} else {
System.err.println("Bad LL Request, HTTP " + responseCode);
}
} catch (IOException e) {
System.err.println("Snapshot error: " + e.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/frc/robot/LimelightHelpers.java around lines 1693 to 1712,
SYNCH_TAKESNAPSHOT currently dereferences a URL that can be null and opens an
HttpURLConnection without timeouts which can hang; update the method to first
check if the URL returned from getLimelightURLString is null and immediately log
and return false if so, then after opening the connection set sensible connect
and read timeouts (e.g. a few seconds) before setRequestMethod/headers, and
ensure the connection is properly closed/disconnected in a finally block; also
improve exception handling to log the exception (not just getMessage()) and
always return false on errors or non-200 responses.

Comment on lines +90 to +93
/** Logging: number of tags contributing to the last estimate. */
public double getTagCount() {
return getPoseEstimate().tagCount;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Return type for tag count

PoseEstimate.tagCount is an int. Expose it as int for consistency.

Apply:

-  public double getTagCount() {
-    return getPoseEstimate().tagCount;
+  public int getTagCount() {
+    return getPoseEstimate().tagCount;
   }

If other code already consumes double getTagCount(), confirm before changing. Search usages:


🏁 Script executed:

#!/bin/bash
rg -n -C2 --type=java '\bgetTagCount\s*\('

Length of output: 443


Use int return type for getTagCount

Replace the return type from double to int to match PoseEstimate.tagCount:

- public double getTagCount() {
-   return getPoseEstimate().tagCount;
+ public int getTagCount() {
+   return getPoseEstimate().tagCount;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Logging: number of tags contributing to the last estimate. */
public double getTagCount() {
return getPoseEstimate().tagCount;
}
/** Logging: number of tags contributing to the last estimate. */
public int getTagCount() {
return getPoseEstimate().tagCount;
}
🤖 Prompt for AI Agents
In src/main/java/frc/robot/subsystems/vision/Limelight.java around lines 90 to
93, the getTagCount method currently returns a double but should return an int
to match PoseEstimate.tagCount; change the method signature to return int and
return getPoseEstimate().tagCount (or cast only if absolutely necessary),
ensuring the return type and any callers are updated to use int.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant