-
Notifications
You must be signed in to change notification settings - Fork 12
/
RobotState.java
301 lines (242 loc) · 11.7 KB
/
RobotState.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
package com.team254.frc2022;
import com.team254.frc2022.subsystems.Limelight;
import com.team254.lib.geometry.Pose2d;
import com.team254.lib.geometry.Rotation2d;
import com.team254.lib.geometry.Translation2d;
import com.team254.lib.geometry.Twist2d;
import com.team254.lib.util.InterpolatingDouble;
import com.team254.lib.util.InterpolatingTreeMap;
import com.team254.lib.util.MovingAverageTwist2d;
import com.team254.lib.util.Util;
import com.team254.lib.vision.AimingParameters;
import com.team254.lib.vision.GoalTracker;
import com.team254.lib.vision.GoalTracker.TrackReportComparator;
import com.team254.lib.vision.TargetInfo;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class RobotState {
private static RobotState mInstance;
public static RobotState getInstance() {
if (mInstance == null) {
mInstance = new RobotState();
}
return mInstance;
}
private static final int kObservationBufferSize = 50;
/*
* RobotState keeps track of the poses of various coordinate frames throughout
* the match. A coordinate frame is simply a point and direction in space that
* defines an (x,y) coordinate system. Transforms (or poses) keep track of the
* spatial relationship between different frames.
*
* Robot frames of interest (from parent to child):
*
* 1. Field frame: origin is where the robot is turned on.
*
* 2. Vehicle frame: origin is the center of the robot wheelbase, facing
* forwards
*
* 3. Turret frame: origin is the center of the turret.
*
* 4. Camera frame: origin is the center of the Limelight relative to the
* turret.
*
* 5. Target frame: origin is the center of the vision target, facing outwards
* along the normal.
*
* As a kinematic chain with 5 frames, there are 4 transforms of interest:
*
* 1. Field-to-vehicle: This is tracked over time by integrating encoder and
* gyro measurements. It will inevitably drift, but is usually accurate over
* short time periods.
*
* 2. Vehicle-to-turret: Rotation measured by the turret encoder; translation is constant.
*
* 3. Turret-to-camera: This is a constant (per camera).
*
* 4. Camera-to-target: Measured by the vision system.
*/
// FPGATimestamp -> Pose2d or Rotation2d
private GoalTracker goal_tracker_;
private Translation2d camera_to_goal_ = Translation2d.identity();
private InterpolatingTreeMap<InterpolatingDouble, Pose2d> field_to_vehicle_;
private InterpolatingTreeMap<InterpolatingDouble, Pose2d> vehicle_to_turret_;
private Twist2d vehicle_velocity_predicted_;
private Twist2d vehicle_velocity_measured_;
private MovingAverageTwist2d vehicle_velocity_measured_filtered_;
// In deg/s
private double turret_velocity_measured_;
public Rotation2d prev_heading_;
private RobotState() {
reset(0.0, Pose2d.identity(), Pose2d.identity());
goal_tracker_ = new GoalTracker(Constants.kGoalTrackerConstants);
}
/**
* Resets the field to robot transform (robot's position on the field)
*/
public synchronized void reset(double start_time, Pose2d initial_field_to_vehicle,
Pose2d initial_vehicle_to_turret) {
reset(start_time, initial_field_to_vehicle);
vehicle_to_turret_ = new InterpolatingTreeMap<>(kObservationBufferSize);
vehicle_to_turret_.put(new InterpolatingDouble(start_time), initial_vehicle_to_turret);
}
public synchronized void reset(double start_time, Pose2d initial_field_to_vehicle) {
field_to_vehicle_ = new InterpolatingTreeMap<>(kObservationBufferSize);
field_to_vehicle_.put(new InterpolatingDouble(start_time), initial_field_to_vehicle);
vehicle_velocity_predicted_ = Twist2d.identity();
vehicle_velocity_measured_ = Twist2d.identity();
vehicle_velocity_measured_filtered_ = new MovingAverageTwist2d(25);
}
public synchronized void reset() {
reset(Timer.getFPGATimestamp(), Pose2d.identity(), Pose2d.identity());
}
/**
* Returns the robot's position on the field at a certain time. Linearly interpolates between stored robot positions
* to fill in the gaps.
*/
public synchronized Pose2d getFieldToVehicle(double timestamp) {
return field_to_vehicle_.getInterpolated(new InterpolatingDouble(timestamp));
}
public synchronized Pose2d getVehicleToTurret(double timestamp) {
return vehicle_to_turret_.getInterpolated(new InterpolatingDouble(timestamp));
}
public synchronized Pose2d getFieldToTurret(double timestamp) {
return getFieldToVehicle(timestamp).transformBy(getVehicleToTurret(timestamp));
}
public synchronized Optional<Pose2d> getVehicleToGoal(double timestamp) {
Pose2d fieldToGoal = getFieldToGoal();
if (fieldToGoal == null) {
return Optional.empty();
}
return Optional.of(getFieldToVehicle(timestamp).inverse().transformBy(fieldToGoal));
}
public synchronized Map.Entry<InterpolatingDouble, Pose2d> getLatestFieldToVehicle() {
return field_to_vehicle_.lastEntry();
}
public synchronized Map.Entry<InterpolatingDouble, Pose2d> getLatestVehicleToTurret() {
return vehicle_to_turret_.lastEntry();
}
public synchronized Pose2d getPredictedFieldToVehicle(double lookahead_time) {
return getLatestFieldToVehicle().getValue()
.transformBy(Pose2d.exp(vehicle_velocity_predicted_.scaled(lookahead_time)));
}
public synchronized void addVehicleToTurretObservation(double timestamp, Pose2d observation,
double vel_deg_s) {
vehicle_to_turret_.put(new InterpolatingDouble(timestamp), observation);
turret_velocity_measured_ = vel_deg_s;
}
public synchronized void addFieldToVehicleObservation(double timestamp, Pose2d observation) {
field_to_vehicle_.put(new InterpolatingDouble(timestamp), observation);
}
public synchronized void addObservations(double timestamp, Pose2d field_to_robot, Twist2d measured_velocity, Twist2d predicted_velocity) {
addFieldToVehicleObservation(timestamp, field_to_robot);
vehicle_velocity_measured_ = measured_velocity;
vehicle_velocity_measured_filtered_.add(vehicle_velocity_measured_);
vehicle_velocity_predicted_ = predicted_velocity;
}
public synchronized Twist2d getPredictedVelocity() {
return vehicle_velocity_predicted_;
}
public synchronized Twist2d getMeasuredVelocity() {
return vehicle_velocity_measured_;
}
public synchronized double getMeasuredTurretVelocity() {
return turret_velocity_measured_;
}
public synchronized Twist2d getSmoothedVelocity() {
return vehicle_velocity_measured_filtered_.getAverage();
}
public synchronized void resetVision() {
goal_tracker_.reset();
camera_to_goal_ = Translation2d.identity();
}
public synchronized void addVisionUpdate(double timestamp, List<TargetInfo> observations, Limelight source) {
if (observations == null || observations.isEmpty()) {
goal_tracker_.maybePruneTracks();
return;
}
updateGoalTracker(timestamp, getCameraToGoalTranslation(observations.get(0), source), goal_tracker_, source);
}
public Rotation2d getCameraToTargetRotation() {
return new Rotation2d(camera_to_goal_, true);
}
private Translation2d getCameraToGoalTranslation(TargetInfo target, Limelight source) {
camera_to_goal_ = getCameraToGoalTranslation(target, source.getLensHeight(), source.getHorizontalPlaneToLens());
return camera_to_goal_;
}
public static Translation2d getCameraToGoalTranslation(TargetInfo target, double cameraHeight, Rotation2d cameraPitch) {
// Compensate for camera pitch
Translation2d xz_plane_translation = new Translation2d(target.getX(), target.getZ()).rotateBy(cameraPitch);
double x = xz_plane_translation.x();
double y = target.getY();
double z = xz_plane_translation.y();
// find intersection with the goal
double differential_height = Constants.kVisionTargetHeight - cameraHeight;
if ((z > 0.0) == (differential_height > 0.0)) {
double scaling = differential_height / z;
double distance = Math.hypot(x, y) * scaling + Constants.kVisionTargetToGoalCenter;
Rotation2d angle = new Rotation2d(x, y, true);
return new Translation2d(distance * angle.cos(), distance * angle.sin());
}
return null;
}
private void updateGoalTracker(double timestamp, Translation2d cameraToGoalTranslation, GoalTracker tracker, Limelight source) {
if (cameraToGoalTranslation == null) {
return;
}
Pose2d cameraToGoal = Pose2d.fromTranslation(cameraToGoalTranslation);
Pose2d fieldToGoal = getFieldToTurret(timestamp).transformBy(source.getTurretToLens()).transformBy(cameraToGoal);
// Goal normal is always oriented at 180 deg.
tracker.update(timestamp, List.of(new Pose2d(fieldToGoal.getTranslation(), Rotation2d.fromDegrees(180.0))));
}
public synchronized Pose2d getFieldToGoal() {
GoalTracker tracker = goal_tracker_;
if (!tracker.hasTracks()) {
return null;
}
return tracker.getTracks().get(0).field_to_target;
}
public synchronized Optional<AimingParameters> getAimingParameters(int prev_track_id, double max_track_age) {
GoalTracker tracker = goal_tracker_;
List<GoalTracker.TrackReport> reports = tracker.getTracks();
double latest_timestamp = 0.0;
if (reports.isEmpty()) {
return Optional.empty();
}
double timestamp = Timer.getFPGATimestamp();
// Find the best track.
TrackReportComparator comparator = tracker.getComparator(prev_track_id, timestamp);
reports.sort(comparator);
Optional<GoalTracker.TrackReport> report = Optional.empty();
for (GoalTracker.TrackReport track : reports) {
latest_timestamp = Math.max(latest_timestamp, track.latest_timestamp);
if (track.latest_timestamp > timestamp - max_track_age &&
report.isEmpty()) {
report = Optional.of(track);
}
}
if (report.isEmpty()) {
return Optional.empty();
}
AimingParameters params = new AimingParameters(getFieldToVehicle(timestamp),
report.get().field_to_target, report.get().id,
Util.epsilonEquals(report.get().latest_timestamp, latest_timestamp));
return Optional.of(params);
}
public Pose2d getRobot() {
return new Pose2d();
}
public void outputToSmartDashboard() {
SmartDashboard.putString("Robot Velocity", getMeasuredVelocity().toString());
SmartDashboard.putString("Field To Robot", getLatestFieldToVehicle().getValue().toString());
getVehicleToGoal(Timer.getFPGATimestamp()).ifPresent(p -> SmartDashboard.putString("Robot to Goal", p.toString()));
Optional<AimingParameters> params = getAimingParameters(-1, 0.5);
params.ifPresent(aimingParameters -> SmartDashboard.putNumber("Target Range", aimingParameters.getRange()));
params.ifPresent(aimingParameters -> SmartDashboard.putNumber("Target Range (ft)", Units.metersToFeet(aimingParameters.getRange())));
params.ifPresent(aimingParameters -> SmartDashboard.putNumber("Robot To Goal Rotation", aimingParameters.getRobotToGoalRotation().getDegrees()));
}
}