2525import edu .wpi .first .math .geometry .Pose3d ;
2626import edu .wpi .first .math .geometry .Rotation2d ;
2727import edu .wpi .first .math .geometry .Transform2d ;
28+ import edu .wpi .first .math .geometry .Translation2d ;
2829import edu .wpi .first .math .numbers .N1 ;
2930import edu .wpi .first .math .numbers .N3 ;
3031import frc .robot .Constants ;
3132import frc .robot .FieldConstants ;
3233import frc .robot .subsystems .drive .Drive ;
3334import frc .robot .subsystems .vision .VisionIO .PoseObservationType ;
35+ import frc .robot .util .RBSIPose ;
3436import frc .robot .util .VirtualSubsystem ;
3537import java .util .ArrayDeque ;
3638import java .util .ArrayList ;
@@ -45,13 +47,11 @@ public class Vision extends VirtualSubsystem {
4547
4648 /** Vision Consumer definition */
4749 @ FunctionalInterface
48- public interface VisionConsumer {
49- void accept (Pose2d visionRobotPoseMeters , double timestampSeconds , Matrix < N3 , N1 > stdDevs );
50+ public interface PoseMeasurementConsumer {
51+ void accept (RBSIPose measurement );
5052 }
5153
52- private record TimedEstimate (Pose2d pose , double ts , Matrix <N3 , N1 > stdDevs ) {}
53-
54- private final VisionConsumer consumer ;
54+ private final PoseMeasurementConsumer consumer ;
5555 private final Drive drive ;
5656 private long lastSeenPoseResetEpoch = -1 ;
5757
@@ -65,7 +65,7 @@ private record TimedEstimate(Pose2d pose, double ts, Matrix<N3, N1> stdDevs) {}
6565 private volatile double lastPoseResetTimestamp = Double .NEGATIVE_INFINITY ;
6666
6767 // Smoothing buffer (recent fused estimates)
68- private final ArrayDeque <TimedEstimate > fusedBuffer = new ArrayDeque <>();
68+ private final ArrayDeque <RBSIPose > fusedBuffer = new ArrayDeque <>();
6969 private final double smoothWindowSec = 0.25 ;
7070 private final int smoothMaxSize = 12 ;
7171
@@ -82,8 +82,10 @@ private record TimedEstimate(Pose2d pose, double ts, Matrix<N3, N1> stdDevs) {}
8282 private volatile double yawGateLookbackSec = 0.30 ;
8383 private volatile double yawGateLimitRadPerSec = 5.0 ;
8484
85+ private static final double kMinVariance = 1e-12 ; // prevents divide-by-zero explosions
86+
8587 /** Constructor */
86- public Vision (Drive drive , VisionConsumer consumer , VisionIO ... io ) {
88+ public Vision (Drive drive , PoseMeasurementConsumer consumer , VisionIO ... io ) {
8789 this .drive = drive ;
8890 this .consumer = consumer ;
8991 this .io = io ;
@@ -123,14 +125,14 @@ public void rbsiPeriodic() {
123125 }
124126
125127 // Pick one best accepted estimate per camera for this loop
126- final ArrayList <TimedEstimate > perCamAccepted = new ArrayList <>(io .length );
128+ final ArrayList <RBSIPose > perCamAccepted = new ArrayList <>(io .length );
127129
128130 for (int cam = 0 ; cam < io .length ; cam ++) {
129131 int seen = 0 ;
130132 int accepted = 0 ;
131133 int rejected = 0 ;
132134
133- TimedEstimate best = null ;
135+ RBSIPose best = null ;
134136 double bestTrustScale = Double .NaN ;
135137 int bestTrustedCount = 0 ;
136138 int bestTagCount = 0 ;
@@ -150,7 +152,7 @@ public void rbsiPeriodic() {
150152 continue ;
151153 }
152154
153- TimedEstimate est = built .estimate ;
155+ RBSIPose est = built .estimate ;
154156 if (best == null || isBetter (est , best )) {
155157 best = est ;
156158 bestTrustScale = built .trustScale ;
@@ -161,12 +163,12 @@ public void rbsiPeriodic() {
161163
162164 if (best != null ) {
163165 accepted ++;
164- lastAcceptedTsPerCam [cam ] = best .ts ;
166+ lastAcceptedTsPerCam [cam ] = best .timestampSeconds () ;
165167 perCamAccepted .add (best );
166168
167- Logger .recordOutput ("Vision/Camera" + cam + "/InjectedPose2d" , best .pose );
168- Logger .recordOutput ("Vision/Camera" + cam + "/InjectedTimestamp" , best .ts );
169- Logger .recordOutput ("Vision/Camera" + cam + "/InjectedStdDevs" , best .stdDevs );
169+ Logger .recordOutput ("Vision/Camera" + cam + "/InjectedPose2d" , best .pose () );
170+ Logger .recordOutput ("Vision/Camera" + cam + "/InjectedTimestamp" , best .timestampSeconds () );
171+ Logger .recordOutput ("Vision/Camera" + cam + "/InjectedStdDevs" , best .stdDevs () );
170172 Logger .recordOutput ("Vision/Camera" + cam + "/LastAcceptedTrustScale" , bestTrustScale );
171173 Logger .recordOutput ("Vision/Camera" + cam + "/LastAcceptedTrustedCount" , bestTrustedCount );
172174 Logger .recordOutput ("Vision/Camera" + cam + "/LastAcceptedTagCount" , bestTagCount );
@@ -180,23 +182,24 @@ public void rbsiPeriodic() {
180182 if (perCamAccepted .isEmpty ()) return ;
181183
182184 // Fusion time is the newest timestamp among accepted per-camera samples
183- double tF = perCamAccepted .stream ().mapToDouble (e -> e .ts ).max ().orElse (Double .NaN );
185+ double tF =
186+ perCamAccepted .stream ().mapToDouble (e -> e .timestampSeconds ()).max ().orElse (Double .NaN );
184187 if (!Double .isFinite (tF )) return ;
185188
186189 // Time-align camera estimates to tF using odometry buffer, then inverse-variance fuse
187- TimedEstimate fused = fuseAtTime (perCamAccepted , tF );
190+ RBSIPose fused = fuseAtTime (perCamAccepted , tF );
188191 if (fused == null ) return ;
189192
190193 // Smooth by fusing recent fused estimates (also aligned to tF)
191194 pushFused (fused );
192- TimedEstimate smoothed = smoothAtTime (tF );
195+ RBSIPose smoothed = smoothAtTime (tF );
193196 if (smoothed == null ) return ;
194197
195- // Inject the pose
196- consumer .accept (smoothed . pose , smoothed . ts , smoothed . stdDevs );
198+ // Inject the pose -- commented out for now...
199+ consumer .accept (smoothed );
197200
198- Logger .recordOutput ("Vision/FusedPose" , fused .pose );
199- Logger .recordOutput ("Vision/SmoothedPose" , smoothed .pose );
201+ Logger .recordOutput ("Vision/FusedPose" , fused .pose () );
202+ Logger .recordOutput ("Vision/SmoothedPose" , smoothed .pose () );
200203 Logger .recordOutput ("Vision/FusedTimestamp" , tF );
201204 }
202205
@@ -301,11 +304,11 @@ private GateResult passesHardGatesAndYawGate(int cam, VisionIO.PoseObservation o
301304 }
302305
303306 private static final class BuiltEstimate {
304- final TimedEstimate estimate ;
307+ final RBSIPose estimate ;
305308 final double trustScale ;
306309 final int trustedCount ;
307310
308- BuiltEstimate (TimedEstimate estimate , double trustScale , int trustedCount ) {
311+ BuiltEstimate (RBSIPose estimate , double trustScale , int trustedCount ) {
309312 this .estimate = estimate ;
310313 this .trustScale = trustScale ;
311314 this .trustedCount = trustedCount ;
@@ -353,28 +356,28 @@ private BuiltEstimate buildEstimate(int cam, VisionIO.PoseObservation obs) {
353356 Logger .recordOutput ("Vision/Camera" + cam + "/InjectedFracTrusted" , fracTrusted );
354357
355358 return new BuiltEstimate (
356- new TimedEstimate (
359+ new RBSIPose (
357360 obs .pose ().toPose2d (),
358361 obs .timestamp (),
359362 VecBuilder .fill (linearStdDev , linearStdDev , angularStdDev )),
360363 trustScale ,
361364 trustedCount );
362365 }
363366
364- private boolean isBetter (TimedEstimate a , TimedEstimate b ) {
367+ private boolean isBetter (RBSIPose a , RBSIPose b ) {
365368 // Lower trace of stddev vector (x+y+theta) is better
366- double ta = a .stdDevs .get (0 , 0 ) + a .stdDevs .get (1 , 0 ) + a .stdDevs .get (2 , 0 );
367- double tb = b .stdDevs .get (0 , 0 ) + b .stdDevs .get (1 , 0 ) + b .stdDevs .get (2 , 0 );
369+ double ta = a .stdDevs () .get (0 , 0 ) + a .stdDevs () .get (1 , 0 ) + a .stdDevs () .get (2 , 0 );
370+ double tb = b .stdDevs () .get (0 , 0 ) + b .stdDevs () .get (1 , 0 ) + b .stdDevs () .get (2 , 0 );
368371 return ta < tb ;
369372 }
370373
371374 /** Time alignment & fusion ********************************************** */
372- private TimedEstimate fuseAtTime (ArrayList <TimedEstimate > estimates , double tF ) {
373- final ArrayList <TimedEstimate > aligned = new ArrayList <>(estimates .size ());
375+ private RBSIPose fuseAtTime (ArrayList <RBSIPose > estimates , double tF ) {
376+ final ArrayList <RBSIPose > aligned = new ArrayList <>(estimates .size ());
374377 for (var e : estimates ) {
375- Pose2d alignedPose = timeAlignPose (e .pose , e .ts , tF );
378+ Pose2d alignedPose = timeAlignPose (e .pose () , e .timestampSeconds () , tF );
376379 if (alignedPose == null ) return null ;
377- aligned .add (new TimedEstimate (alignedPose , tF , e .stdDevs ));
380+ aligned .add (new RBSIPose (alignedPose , tF , e .stdDevs () ));
378381 }
379382 return inverseVarianceFuse (aligned , tF );
380383 }
@@ -394,69 +397,93 @@ private Pose2d timeAlignPose(Pose2d visionPoseAtTs, double ts, double tF) {
394397 return visionPoseAtTs .transformBy (ts_T_tf );
395398 }
396399
397- private TimedEstimate inverseVarianceFuse (ArrayList <TimedEstimate > alignedAtTF , double tF ) {
398- if (alignedAtTF .isEmpty ()) return null ;
400+ private RBSIPose inverseVarianceFuse (ArrayList <RBSIPose > alignedAtTF , double tF ) {
401+ if (alignedAtTF == null || alignedAtTF .isEmpty ()) return null ;
399402 if (alignedAtTF .size () == 1 ) return alignedAtTF .get (0 );
400403
401- double sumWx = 0 , sumWy = 0 , sumWth = 0 ;
402- double sumX = 0 , sumY = 0 ;
403- double sumCos = 0 , sumSin = 0 ;
404+ double sumWx = 0.0 , sumWy = 0.0 , sumWth = 0.0 ;
405+ double sumX = 0.0 , sumY = 0.0 ;
406+ double sumCos = 0.0 , sumSin = 0.0 ;
407+
408+ for (int i = 0 ; i < alignedAtTF .size (); i ++) {
409+ final RBSIPose e = alignedAtTF .get (i );
410+ final Pose2d p = e .pose ();
411+ final Matrix <N3 , N1 > s = e .stdDevs ();
412+
413+ final double sx = s .get (0 , 0 );
414+ final double sy = s .get (1 , 0 );
415+ final double sth = s .get (2 , 0 );
404416
405- for ( var e : alignedAtTF ) {
406- double sx = e . stdDevs . get ( 0 , 0 );
407- double sy = e . stdDevs . get ( 1 , 0 );
408- double sth = e . stdDevs . get ( 2 , 0 );
417+ // variance = std^2, clamp away from 0
418+ final double vx = Math . max ( sx * sx , kMinVariance );
419+ final double vy = Math . max ( sy * sy , kMinVariance );
420+ final double vth = Math . max ( sth * sth , kMinVariance );
409421
410- double vx = sx * sx ;
411- double vy = sy * sy ;
412- double vth = sth * sth ;
422+ final double wx = 1.0 / vx ;
423+ final double wy = 1.0 / vy ;
424+ final double wth = 1.0 / vth ;
413425
414- double wx = 1.0 / vx ;
415- double wy = 1.0 / vy ;
416- double wth = 1.0 / vth ;
426+ // If any weight goes NaN/Inf, skip this measurement rather than poisoning the fuse.
427+ if (!Double .isFinite (wx ) || !Double .isFinite (wy ) || !Double .isFinite (wth )) {
428+ continue ;
429+ }
417430
418431 sumWx += wx ;
419432 sumWy += wy ;
420433 sumWth += wth ;
421434
422- sumX += e . pose .getX () * wx ;
423- sumY += e . pose .getY () * wy ;
435+ sumX += p .getX () * wx ;
436+ sumY += p .getY () * wy ;
424437
425- sumCos += e .pose .getRotation ().getCos () * wth ;
426- sumSin += e .pose .getRotation ().getSin () * wth ;
438+ final Rotation2d rot = p .getRotation ();
439+ sumCos += rot .getCos () * wth ;
440+ sumSin += rot .getSin () * wth ;
427441 }
428442
429- Pose2d fused = new Pose2d (sumX / sumWx , sumY / sumWy , new Rotation2d (sumCos , sumSin ));
443+ // If everything got skipped, fail closed.
444+ if (sumWx <= 0.0 || sumWy <= 0.0 || sumWth <= 0.0 ) return null ;
445+
446+ final Translation2d fusedTranslation = new Translation2d (sumX / sumWx , sumY / sumWy );
447+
448+ // Rotation2d(cos, sin) will normalize internally; if both are ~0, fall back to zero.
449+ final Rotation2d fusedRotation =
450+ (Math .abs (sumCos ) < 1e-12 && Math .abs (sumSin ) < 1e-12 )
451+ ? Rotation2d .kZero
452+ : new Rotation2d (sumCos , sumSin );
453+
454+ final Pose2d fusedPose = new Pose2d (fusedTranslation , fusedRotation );
430455
431- Matrix <N3 , N1 > fusedStd =
456+ final Matrix <N3 , N1 > fusedStdDevs =
432457 VecBuilder .fill (Math .sqrt (1.0 / sumWx ), Math .sqrt (1.0 / sumWy ), Math .sqrt (1.0 / sumWth ));
433458
434- return new TimedEstimate ( fused , tF , fusedStd );
459+ return new RBSIPose ( fusedPose , tF , fusedStdDevs );
435460 }
436461
437462 /** Smoothing buffer ***************************************************** */
438- private void pushFused (TimedEstimate fused ) {
463+ private void pushFused (RBSIPose fused ) {
439464 fusedBuffer .addLast (fused );
440465
441466 while (fusedBuffer .size () > smoothMaxSize ) {
442467 fusedBuffer .removeFirst ();
443468 }
444469
445470 // Trim by time window relative to newest
446- while (!fusedBuffer .isEmpty () && fused .ts - fusedBuffer .peekFirst ().ts > smoothWindowSec ) {
471+ while (!fusedBuffer .isEmpty ()
472+ && fused .timestampSeconds () - fusedBuffer .peekFirst ().timestampSeconds ()
473+ > smoothWindowSec ) {
447474 fusedBuffer .removeFirst ();
448475 }
449476 }
450477
451- private TimedEstimate smoothAtTime (double tF ) {
478+ private RBSIPose smoothAtTime (double tF ) {
452479 if (fusedBuffer .isEmpty ()) return null ;
453480 if (fusedBuffer .size () == 1 ) return fusedBuffer .peekLast ();
454481
455- final ArrayList <TimedEstimate > aligned = new ArrayList <>(fusedBuffer .size ());
482+ final ArrayList <RBSIPose > aligned = new ArrayList <>(fusedBuffer .size ());
456483 for (var e : fusedBuffer ) {
457- Pose2d alignedPose = timeAlignPose (e .pose , e .ts , tF );
484+ Pose2d alignedPose = timeAlignPose (e .pose () , e .timestampSeconds () , tF );
458485 if (alignedPose == null ) continue ;
459- aligned .add (new TimedEstimate (alignedPose , tF , e .stdDevs ));
486+ aligned .add (new RBSIPose (alignedPose , tF , e .stdDevs () ));
460487 }
461488
462489 if (aligned .isEmpty ()) return fusedBuffer .peekLast ();
0 commit comments