1+ // org/team7525/CI/BallDetectionTest.java
2+
3+ package frc .robot .Lib ;
4+
5+ import edu .wpi .first .hal .HAL ;
6+ import edu .wpi .first .math .geometry .*;
7+ import edu .wpi .first .wpilibj .IterativeRobotBase ;
8+ import frc .robot .Subsystems .GamePieceFinder .GamePieceFinder ;
9+ import org .photonvision .targeting .PhotonTrackedTarget ;
10+
11+ public class BallDetectionTest extends IterativeRobotBase {
12+
13+ private GamePieceFinder finder ;
14+ private int testsPassed = 0 ;
15+ private int testsFailed = 0 ;
16+ private long startTime ;
17+
18+ // TODO: No magic numbers
19+
20+ public BallDetectionTest () {
21+ super (0.02 );
22+ HAL .initialize (500 , 0 );
23+ }
24+
25+ @ Override
26+ public void startCompetition () {
27+ System .out .println ("********** Parallax Triangulation Tests **********\n " );
28+
29+ finder = GamePieceFinder .getInstance ();
30+ startTime = System .nanoTime ();
31+
32+ // Test the actual ray intersection math
33+ testParallaxGeometry ();
34+
35+ printResults ();
36+ System .exit (testsFailed > 0 ? 1 : 0 );
37+ }
38+
39+ private void testParallaxGeometry () {
40+ System .out .println ("=== Testing Parallax Ray Intersection Math ===\n " );
41+
42+ // Test Case 1: Ball straight ahead with robot moving sideways
43+ testCase1_StraightAhead ();
44+
45+ // Test Case 2: Ball at an angle
46+ testCase2_AtAngle ();
47+
48+ // Test Case 3: Large baseline (robot moved far between samples)
49+ testCase3_LargeBaseline ();
50+
51+ // Test Case 4: Small angle difference (barely above threshold)
52+ testCase4_SmallAngle ();
53+ }
54+
55+ private void testCase1_StraightAhead () {
56+ System .out .println ("Test 1: Ball straight ahead, robot moves sideways" );
57+ finder .clearPoseEstimates ();
58+
59+ // Ball at (5, 0)
60+ Translation2d ballPos = new Translation2d (5.0 , 0.0 );
61+
62+ // Robot at (0, -1) looking at ball
63+ Pose2d pose1 = new Pose2d (0.0 , -1.0 , new Rotation2d ());
64+ double yaw1 = calculateYaw (pose1 , ballPos );
65+
66+ // Robot at (0, 1) looking at ball
67+ Pose2d pose2 = new Pose2d (0.0 , 1.0 , new Rotation2d ());
68+ double yaw2 = calculateYaw (pose2 , ballPos );
69+
70+ System .out .printf (" Sample 1: Robot at (%.1f, %.1f), Yaw = %.1f°\n " ,
71+ pose1 .getX (), pose1 .getY (), yaw1 );
72+ System .out .printf (" Sample 2: Robot at (%.1f, %.1f), Yaw = %.1f°\n " ,
73+ pose2 .getX (), pose2 .getY (), yaw2 );
74+
75+ // Area random bc uh i like dont have a formula for area that works and wouldnt sim that so yeah
76+ finder .setTestRobotPose (pose1 );
77+ finder .addVisionSample (createTarget (yaw1 , 5.0 ));
78+
79+ try { Thread .sleep (50 ); } catch (InterruptedException e ) {}
80+
81+ finder .setTestRobotPose (pose2 );
82+ finder .addVisionSample (createTarget (yaw2 , 5.0 ));
83+
84+ finder .periodic ();
85+
86+ evaluateResult (ballPos , "Test 1" );
87+ }
88+
89+ private void testCase2_AtAngle () {
90+ System .out .println ("\n Test 2: Ball at an angle" );
91+ finder .clearPoseEstimates ();
92+
93+ // Ball at (4, 2)
94+ Translation2d ballPos = new Translation2d (4.0 , 2.0 );
95+
96+ // Robot at (0, 0) rotated 30°
97+ Pose2d pose1 = new Pose2d (0.0 , 0.0 , Rotation2d .fromDegrees (30 ));
98+ double yaw1 = calculateYaw (pose1 , ballPos );
99+
100+ // Robot at (1, -1) rotated 60°
101+ Pose2d pose2 = new Pose2d (1.0 , -1.0 , Rotation2d .fromDegrees (60 ));
102+ double yaw2 = calculateYaw (pose2 , ballPos );
103+
104+ System .out .printf (" Sample 1: Robot at (%.1f, %.1f) @ %.0f°, Yaw = %.1f°\n " ,
105+ pose1 .getX (), pose1 .getY (), pose1 .getRotation ().getDegrees (), yaw1 );
106+ System .out .printf (" Sample 2: Robot at (%.1f, %.1f) @ %.0f°, Yaw = %.1f°\n " ,
107+ pose2 .getX (), pose2 .getY (), pose2 .getRotation ().getDegrees (), yaw2 );
108+
109+ finder .setTestRobotPose (pose1 );
110+ finder .addVisionSample (createTarget (yaw1 , 5.0 ));
111+
112+ try { Thread .sleep (50 ); } catch (InterruptedException e ) {}
113+
114+ finder .setTestRobotPose (pose2 );
115+ finder .addVisionSample (createTarget (yaw2 , 5.0 ));
116+
117+ finder .periodic ();
118+
119+ evaluateResult (ballPos , "Test 2" );
120+ }
121+
122+ private void testCase3_LargeBaseline () {
123+ System .out .println ("\n Test 3: Large baseline (good triangulation)" );
124+ finder .clearPoseEstimates ();
125+
126+ // Ball at (3, 3)
127+ Translation2d ballPos = new Translation2d (3.0 , 3.0 );
128+
129+ // Robot far left
130+ Pose2d pose1 = new Pose2d (-2.0 , 0.0 , Rotation2d .fromDegrees (45 ));
131+ double yaw1 = calculateYaw (pose1 , ballPos );
132+
133+ // Robot far right
134+ Pose2d pose2 = new Pose2d (2.0 , 0.0 , Rotation2d .fromDegrees (135 ));
135+ double yaw2 = calculateYaw (pose2 , ballPos );
136+
137+ System .out .printf (" Sample 1: Robot at (%.1f, %.1f) @ %.0f°, Yaw = %.1f°\n " ,
138+ pose1 .getX (), pose1 .getY (), pose1 .getRotation ().getDegrees (), yaw1 );
139+ System .out .printf (" Sample 2: Robot at (%.1f, %.1f) @ %.0f°, Yaw = %.1f°\n " ,
140+ pose2 .getX (), pose2 .getY (), pose2 .getRotation ().getDegrees (), yaw2 );
141+
142+ double baseline = pose1 .getTranslation ().getDistance (pose2 .getTranslation ());
143+ System .out .printf (" Baseline distance: %.2fm\n " , baseline );
144+
145+ finder .setTestRobotPose (pose1 );
146+ finder .addVisionSample (createTarget (yaw1 , 5.0 ));
147+
148+ try { Thread .sleep (50 ); } catch (InterruptedException e ) {}
149+
150+ finder .setTestRobotPose (pose2 );
151+ finder .addVisionSample (createTarget (yaw2 , 5.0 ));
152+
153+ finder .periodic ();
154+
155+ evaluateResult (ballPos , "Test 3" );
156+ }
157+
158+ private void testCase4_SmallAngle () {
159+ System .out .println ("\n Test 4: Small angle difference (edge case)" );
160+ finder .clearPoseEstimates ();
161+
162+ // Ball at (6, 1)
163+ Translation2d ballPos = new Translation2d (6.0 , 1.0 );
164+
165+ // Robot moves slightly - should be right at the MIN_YAW_DIFFERENCE threshold
166+ Pose2d pose1 = new Pose2d (0.0 , 0.0 , Rotation2d .fromDegrees (0 ));
167+ double yaw1 = calculateYaw (pose1 , ballPos );
168+
169+ Pose2d pose2 = new Pose2d (0.0 , 1.0 , Rotation2d .fromDegrees (0 ));
170+ double yaw2 = calculateYaw (pose2 , ballPos );
171+
172+ System .out .printf (" Sample 1: Yaw = %.1f°\n " , yaw1 );
173+ System .out .printf (" Sample 2: Yaw = %.1f°\n " , yaw2 );
174+ System .out .printf (" Yaw difference: %.1f° (threshold is 10°)\n " , Math .abs (yaw2 - yaw1 ));
175+
176+ finder .setTestRobotPose (pose1 );
177+ finder .addVisionSample (createTarget (yaw1 , 5.0 ));
178+
179+ try { Thread .sleep (50 ); } catch (InterruptedException e ) {}
180+
181+ finder .setTestRobotPose (pose2 );
182+ finder .addVisionSample (createTarget (yaw2 , 5.0 ));
183+
184+ finder .periodic ();
185+
186+ evaluateResult (ballPos , "Test 4" );
187+ }
188+
189+ // Calculate yaw angle from robot to ball (relative to robot frame)
190+ private double calculateYaw (Pose2d robotPose , Translation2d ballPos ) {
191+ // Vector from robot to ball in global frame
192+ Translation2d robotToBall = ballPos .minus (robotPose .getTranslation ());
193+
194+ // Global angle to ball
195+ double globalAngle = Math .atan2 (robotToBall .getY (), robotToBall .getX ());
196+
197+ // Convert to robot-relative angle
198+ double robotAngle = robotPose .getRotation ().getRadians ();
199+ double yawRad = globalAngle - robotAngle ;
200+
201+ // Convert to degrees and normalize to [-180, 180]
202+ double yawDeg = Math .toDegrees (yawRad );
203+ while (yawDeg > 180 ) yawDeg -= 360 ;
204+ while (yawDeg < -180 ) yawDeg += 360 ;
205+
206+ return yawDeg ;
207+ }
208+
209+ private PhotonTrackedTarget createTarget (double yaw , double area ) {
210+ return new PhotonTrackedTarget (
211+ // just a ton of random stuff, yaw and area only useful
212+ yaw , // yaw
213+ 0 , // pitch (not used)
214+ area , // area (dummy value)
215+ 0 , // skew
216+ -1 , // fiducialId
217+ -1 , // i dont even know what this is
218+ 1 ,
219+ null , // bestCameraToTarget
220+ null , // altCameraToTarget
221+ 0 , // poseAmbiguity
222+ null , // minAreaRectCorners
223+ null // detectedCorners
224+ );
225+ }
226+
227+ private void evaluateResult (Translation2d expectedBall , String testName ) {
228+ Pose2d estimate = finder .getLatestGamepieceEstimate ();
229+
230+ if (estimate == null ) {
231+ System .out .printf (" %s FAILED: No estimate generated\n " , testName );
232+ System .out .println ("(Samples may not have passed MIN_YAW_DIFFERENCE threshold)\n " );
233+ testsFailed ++;
234+ return ;
235+ }
236+
237+ Translation2d estimatedPos = estimate .getTranslation ();
238+ double error = estimatedPos .getDistance (expectedBall );
239+
240+ System .out .printf (" Expected ball at: (%.2f, %.2f)\n " ,
241+ expectedBall .getX (), expectedBall .getY ());
242+ System .out .printf (" Estimated ball at: (%.2f, %.2f)\n " ,
243+ estimatedPos .getX (), estimatedPos .getY ());
244+ System .out .printf (" Error: %.3fm\n " , error );
245+
246+ // Very tight tolerance since we're testing pure geometry
247+ boolean pass = error < 0.1 ; // 10cm tolerance for pure math
248+
249+ if (pass ) {
250+ System .out .printf ("%s PASSED\n \n " , testName );
251+ testsPassed ++;
252+ } else {
253+ System .out .printf ("%s FAILED: Error too large (>0.1m)\n \n " , testName );
254+ testsFailed ++;
255+ }
256+ }
257+
258+ private void printResults () {
259+ double elapsed = (System .nanoTime () - startTime ) / 1e9 ;
260+ System .out .println ("========================================" );
261+ System .out .println ("Parallax Triangulation Test Results" );
262+ System .out .println ("========================================" );
263+ System .out .printf ("Tests Passed: %d\n " , testsPassed );
264+ System .out .printf ("Tests Failed: %d\n " , testsFailed );
265+ System .out .printf ("Time Elapsed: %.2fs\n " , elapsed );
266+ System .out .println ("========================================" );
267+
268+ if (testsFailed == 0 ) {
269+ System .out .println ("ALL TESTS PASSED - Parallax math is correct!" );
270+ } else {
271+ System .out .println ("SOME TESTS FAILED - Check ray intersection logic" );
272+ }
273+ }
274+
275+ @ Override
276+ public void endCompetition () {}
277+ }
0 commit comments