Skip to content

Commit ecff3ea

Browse files
committed
Clean up the consumer syntax
1 parent e2d8fad commit ecff3ea

File tree

3 files changed

+111
-62
lines changed

3 files changed

+111
-62
lines changed

src/main/java/frc/robot/subsystems/drive/Drive.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import edu.wpi.first.hal.FRCNetComm.tInstances;
2323
import edu.wpi.first.hal.FRCNetComm.tResourceType;
2424
import edu.wpi.first.hal.HAL;
25-
import edu.wpi.first.math.Matrix;
2625
import edu.wpi.first.math.controller.PIDController;
2726
import edu.wpi.first.math.controller.ProfiledPIDController;
2827
import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator;
@@ -52,6 +51,7 @@
5251
import frc.robot.util.LocalADStarAK;
5352
import frc.robot.util.RBSIEnum.Mode;
5453
import frc.robot.util.RBSIParsing;
54+
import frc.robot.util.RBSIPose;
5555
import frc.robot.util.RBSISubsystem;
5656
import java.util.Optional;
5757
import java.util.OptionalDouble;
@@ -743,10 +743,11 @@ public void zeroHeading() {
743743
}
744744

745745
/** Adds a vision measurement safely into the PoseEstimator. */
746-
public void addVisionMeasurement(Pose2d pose, double timestampSeconds, Matrix<N3, N1> stdDevs) {
746+
public void addVisionMeasurement(RBSIPose rbsiPose) {
747747
odometryLock.lock();
748748
try {
749-
m_PoseEstimator.addVisionMeasurement(pose, timestampSeconds, stdDevs);
749+
m_PoseEstimator.addVisionMeasurement(
750+
rbsiPose.pose(), rbsiPose.timestampSeconds(), rbsiPose.stdDevs());
750751
} finally {
751752
odometryLock.unlock();
752753
}

src/main/java/frc/robot/subsystems/vision/Vision.java

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
import edu.wpi.first.math.geometry.Pose3d;
2626
import edu.wpi.first.math.geometry.Rotation2d;
2727
import edu.wpi.first.math.geometry.Transform2d;
28+
import edu.wpi.first.math.geometry.Translation2d;
2829
import edu.wpi.first.math.numbers.N1;
2930
import edu.wpi.first.math.numbers.N3;
3031
import frc.robot.Constants;
3132
import frc.robot.FieldConstants;
3233
import frc.robot.subsystems.drive.Drive;
3334
import frc.robot.subsystems.vision.VisionIO.PoseObservationType;
35+
import frc.robot.util.RBSIPose;
3436
import frc.robot.util.VirtualSubsystem;
3537
import java.util.ArrayDeque;
3638
import 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();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2024-2026 Az-FIRST
2+
// http://github.com/AZ-First
3+
//
4+
// This program is free software; you can redistribute it and/or
5+
// modify it under the terms of the GNU General Public License
6+
// version 3 as published by the Free Software Foundation or
7+
// available in the root directory of this project.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
14+
package frc.robot.util;
15+
16+
import edu.wpi.first.math.Matrix;
17+
import edu.wpi.first.math.geometry.Pose2d;
18+
import edu.wpi.first.math.numbers.N1;
19+
import edu.wpi.first.math.numbers.N3;
20+
21+
public record RBSIPose(Pose2d pose, double timestampSeconds, Matrix<N3, N1> stdDevs) {}

0 commit comments

Comments
 (0)