blob: 910cd310c1536674734e4147341ad39a5565b6f3 [file] [log] [blame]
James Kuszmaul5398fae2020-02-17 16:44:03 -08001#include <queue>
2
3#include "gtest/gtest.h"
4
5#include "aos/controls/control_loop_test.h"
6#include "aos/events/logging/logger.h"
7#include "aos/network/team_number.h"
8#include "frc971/control_loops/drivetrain/drivetrain.h"
9#include "frc971/control_loops/drivetrain/drivetrain_test_lib.h"
10#include "frc971/control_loops/team_number_test_environment.h"
11#include "y2020/control_loops/drivetrain/drivetrain_base.h"
12#include "y2020/control_loops/drivetrain/localizer.h"
13
14DEFINE_string(output_file, "",
15 "If set, logs all channels to the provided logfile.");
16
17// This file tests that the full 2020 localizer behaves sanely.
18
19namespace y2020 {
20namespace control_loops {
21namespace drivetrain {
22namespace testing {
23
24using frc971::control_loops::drivetrain::DrivetrainConfig;
25using frc971::control_loops::drivetrain::Goal;
26using frc971::control_loops::drivetrain::LocalizerControl;
27using frc971::vision::sift::ImageMatchResult;
28using frc971::vision::sift::ImageMatchResultT;
29using frc971::vision::sift::CameraPoseT;
30using frc971::vision::sift::CameraCalibrationT;
31using frc971::vision::sift::TransformationMatrixT;
32
33namespace {
34DrivetrainConfig<double> GetTest2020DrivetrainConfig() {
35 DrivetrainConfig<double> config = GetDrivetrainConfig();
36 return config;
37}
38
39// Copies an Eigen matrix into a row-major vector of the data.
40std::vector<float> MatrixToVector(const Eigen::Matrix<double, 4, 4> &H) {
41 std::vector<float> data;
42 for (int row = 0; row < 4; ++row) {
43 for (int col = 0; col < 4; ++col) {
44 data.push_back(H(row, col));
45 }
46 }
47 return data;
48}
49
50// Provides the location of the turret to use for simulation. Mostly we care
51// about providing a location that is not perfectly aligned with the robot's
52// origin.
53Eigen::Matrix<double, 4, 4> TurretRobotTransformation() {
54 Eigen::Matrix<double, 4, 4> H;
55 H.setIdentity();
56 H.block<3, 1>(0, 3) << 1, 1, 1;
57 return H;
58}
59
60// Provides the location of the camera on the turret.
61// TODO(james): Also simulate a fixed camera that is *not* on the turret.
62Eigen::Matrix<double, 4, 4> CameraTurretTransformation() {
63 Eigen::Matrix<double, 4, 4> H;
64 H.setIdentity();
65 H.block<3, 1>(0, 3) << 0.1, 0, 0;
66 // Introduce a bit of pitch to make sure that we're exercising all the code.
67 H.block<3, 3>(0, 0) =
68 Eigen::AngleAxis<double>(M_PI_2, Eigen::Vector3d::UnitY()) *
69 H.block<3, 3>(0, 0);
70 return H;
71}
72
73// The absolute target location to use. Not meant to correspond with a
74// particular field target.
75// TODO(james): Make more targets.
76Eigen::Matrix<double, 4, 4> TargetLocation() {
77 Eigen::Matrix<double, 4, 4> H;
78 H.setIdentity();
79 H.block<3, 1>(0, 3) << 10.0, 0, 0;
80 return H;
81}
82} // namespace
83
84namespace chrono = std::chrono;
85using frc971::control_loops::drivetrain::testing::DrivetrainSimulation;
86using frc971::control_loops::drivetrain::DrivetrainLoop;
87using frc971::control_loops::drivetrain::testing::GetTestDrivetrainConfig;
88using aos::monotonic_clock;
89
90class LocalizedDrivetrainTest : public ::aos::testing::ControlLoopTest {
91 protected:
92 // We must use the 2020 drivetrain config so that we don't have to deal
93 // with shifting:
94 LocalizedDrivetrainTest()
95 : aos::testing::ControlLoopTest(
96 aos::configuration::ReadConfig(
97 "y2020/control_loops/drivetrain/simulation_config.json"),
98 GetTest2020DrivetrainConfig().dt),
99 test_event_loop_(MakeEventLoop("test")),
100 drivetrain_goal_sender_(
101 test_event_loop_->MakeSender<Goal>("/drivetrain")),
102 drivetrain_goal_fetcher_(
103 test_event_loop_->MakeFetcher<Goal>("/drivetrain")),
104 localizer_control_sender_(
105 test_event_loop_->MakeSender<LocalizerControl>("/drivetrain")),
106 drivetrain_event_loop_(MakeEventLoop("drivetrain")),
107 dt_config_(GetTest2020DrivetrainConfig()),
108 camera_sender_(
109 test_event_loop_->MakeSender<ImageMatchResult>("/camera")),
110 localizer_(drivetrain_event_loop_.get(), dt_config_),
111 drivetrain_(dt_config_, drivetrain_event_loop_.get(), &localizer_),
112 drivetrain_plant_event_loop_(MakeEventLoop("plant")),
113 drivetrain_plant_(drivetrain_plant_event_loop_.get(), dt_config_),
114 last_frame_(monotonic_now()) {
115 set_team_id(frc971::control_loops::testing::kTeamNumber);
116 SetStartingPosition({3.0, 2.0, 0.0});
117 set_battery_voltage(12.0);
118
119 if (!FLAGS_output_file.empty()) {
120 log_buffer_writer_ = std::make_unique<aos::logger::DetachedBufferWriter>(
121 FLAGS_output_file);
122 logger_event_loop_ = MakeEventLoop("logger");
123 logger_ = std::make_unique<aos::logger::Logger>(log_buffer_writer_.get(),
124 logger_event_loop_.get());
125 }
126
127 test_event_loop_->MakeWatcher(
128 "/drivetrain",
129 [this](const frc971::control_loops::drivetrain::Status &) {
130 // Needs to do camera updates right after we run the control loop.
131 if (enable_cameras_) {
132 SendDelayedFrames();
133 if (last_frame_ + std::chrono::milliseconds(100) <
134 monotonic_now()) {
135 CaptureFrames();
136 last_frame_ = monotonic_now();
137 }
138 }
139 });
140
141 // Run for enough time to allow the gyro/imu zeroing code to run.
142 RunFor(std::chrono::seconds(10));
143 }
144
145 virtual ~LocalizedDrivetrainTest() override {}
146
147 void SetStartingPosition(const Eigen::Matrix<double, 3, 1> &xytheta) {
148 *drivetrain_plant_.mutable_state() << xytheta.x(), xytheta.y(),
149 xytheta(2, 0), 0.0, 0.0;
150 Eigen::Matrix<double, Localizer::HybridEkf::kNStates, 1> localizer_state;
151 localizer_state.setZero();
152 localizer_state.block<3, 1>(0, 0) = xytheta;
153 localizer_.Reset(monotonic_now(), localizer_state);
154 }
155
156 void VerifyNearGoal(double eps = 1e-3) {
157 drivetrain_goal_fetcher_.Fetch();
158 EXPECT_NEAR(drivetrain_goal_fetcher_->left_goal(),
159 drivetrain_plant_.GetLeftPosition(), eps);
160 EXPECT_NEAR(drivetrain_goal_fetcher_->right_goal(),
161 drivetrain_plant_.GetRightPosition(), eps);
162 }
163
164 void VerifyEstimatorAccurate(double eps) {
165 const Eigen::Matrix<double, 5, 1> true_state = drivetrain_plant_.state();
166 EXPECT_NEAR(localizer_.x(), true_state(0, 0), eps);
167 EXPECT_NEAR(localizer_.y(), true_state(1, 0), eps);
168 EXPECT_NEAR(localizer_.theta(), true_state(2, 0), eps);
169 EXPECT_NEAR(localizer_.left_velocity(), true_state(3, 0), eps);
170 EXPECT_NEAR(localizer_.right_velocity(), true_state(4, 0), eps);
171 }
172
173 // Goes through and captures frames on the camera(s), queueing them up to be
174 // sent by SendDelayedFrames().
175 void CaptureFrames() {
176 const frc971::control_loops::Pose robot_pose(
177 {drivetrain_plant_.GetPosition().x(),
178 drivetrain_plant_.GetPosition().y(), 0.0},
179 drivetrain_plant_.state()(2, 0));
180 std::unique_ptr<ImageMatchResultT> frame(new ImageMatchResultT());
181
182 // TODO(james): Test with more than one (and no) target(s).
183 {
184 std::unique_ptr<CameraPoseT> camera_target(new CameraPoseT());
185
186 camera_target->field_to_target.reset(new TransformationMatrixT());
187 camera_target->field_to_target->data = MatrixToVector(TargetLocation());
188
189 // TODO(james): Use non-zero turret angles.
190 camera_target->camera_to_target.reset(new TransformationMatrixT());
191 camera_target->camera_to_target->data = MatrixToVector(
192 (robot_pose.AsTransformationMatrix() * TurretRobotTransformation() *
193 CameraTurretTransformation())
194 .inverse() *
195 TargetLocation());
196
197 frame->camera_poses.emplace_back(std::move(camera_target));
198 }
199
200 frame->image_monotonic_timestamp_ns =
201 chrono::duration_cast<chrono::nanoseconds>(
202 monotonic_now().time_since_epoch())
203 .count();
204 frame->camera_calibration.reset(new CameraCalibrationT());
205 {
206 frame->camera_calibration->fixed_extrinsics.reset(
207 new TransformationMatrixT());
208 TransformationMatrixT *H_turret_robot =
209 frame->camera_calibration->fixed_extrinsics.get();
210 H_turret_robot->data = MatrixToVector(TurretRobotTransformation());
211 }
212 {
213 frame->camera_calibration->turret_extrinsics.reset(
214 new TransformationMatrixT());
215 TransformationMatrixT *H_camera_turret =
216 frame->camera_calibration->turret_extrinsics.get();
217 H_camera_turret->data = MatrixToVector(CameraTurretTransformation());
218 }
219
220 camera_delay_queue_.emplace(monotonic_now(), std::move(frame));
221 }
222
223 // Actually sends out all the camera frames.
224 void SendDelayedFrames() {
225 const std::chrono::milliseconds camera_latency(150);
226 while (!camera_delay_queue_.empty() &&
227 std::get<0>(camera_delay_queue_.front()) <
228 monotonic_now() - camera_latency) {
229 auto builder = camera_sender_.MakeBuilder();
230 ASSERT_TRUE(builder.Send(ImageMatchResult::Pack(
231 *builder.fbb(), std::get<1>(camera_delay_queue_.front()).get())));
232 camera_delay_queue_.pop();
233 }
234 }
235
236 std::unique_ptr<aos::EventLoop> test_event_loop_;
237 aos::Sender<Goal> drivetrain_goal_sender_;
238 aos::Fetcher<Goal> drivetrain_goal_fetcher_;
239 aos::Sender<LocalizerControl> localizer_control_sender_;
240
241 std::unique_ptr<aos::EventLoop> drivetrain_event_loop_;
242 const frc971::control_loops::drivetrain::DrivetrainConfig<double>
243 dt_config_;
244
245 aos::Sender<ImageMatchResult> camera_sender_;
246
247 Localizer localizer_;
248 DrivetrainLoop drivetrain_;
249
250 std::unique_ptr<aos::EventLoop> drivetrain_plant_event_loop_;
251 DrivetrainSimulation drivetrain_plant_;
252 monotonic_clock::time_point last_frame_;
253
254 // A queue of camera frames so that we can add a time delay to the data
255 // coming from the cameras.
256 std::queue<std::tuple<aos::monotonic_clock::time_point,
257 std::unique_ptr<ImageMatchResultT>>>
258 camera_delay_queue_;
259
260 void set_enable_cameras(bool enable) { enable_cameras_ = enable; }
261
262 private:
263 bool enable_cameras_ = false;
264
265 std::unique_ptr<aos::EventLoop> logger_event_loop_;
266 std::unique_ptr<aos::logger::DetachedBufferWriter> log_buffer_writer_;
267 std::unique_ptr<aos::logger::Logger> logger_;
268};
269
270// Tests that no camera updates, combined with a perfect model, results in no
271// error.
272TEST_F(LocalizedDrivetrainTest, NoCameraUpdate) {
273 SetEnabled(true);
274 set_enable_cameras(false);
275 VerifyEstimatorAccurate(1e-7);
276 {
277 auto builder = drivetrain_goal_sender_.MakeBuilder();
278
279 Goal::Builder drivetrain_builder = builder.MakeBuilder<Goal>();
280 drivetrain_builder.add_controller_type(
281 frc971::control_loops::drivetrain::ControllerType::MOTION_PROFILE);
282 drivetrain_builder.add_left_goal(-1.0);
283 drivetrain_builder.add_right_goal(1.0);
284
285 EXPECT_TRUE(builder.Send(drivetrain_builder.Finish()));
286 }
287 RunFor(chrono::seconds(3));
288 VerifyNearGoal();
289 VerifyEstimatorAccurate(5e-4);
290}
291
292// Tests that camera udpates with a perfect models results in no errors.
293TEST_F(LocalizedDrivetrainTest, PerfectCameraUpdate) {
294 SetEnabled(true);
295 set_enable_cameras(true);
296 auto builder = drivetrain_goal_sender_.MakeBuilder();
297
298 Goal::Builder drivetrain_builder = builder.MakeBuilder<Goal>();
299 drivetrain_builder.add_controller_type(
300 frc971::control_loops::drivetrain::ControllerType::MOTION_PROFILE);
301 drivetrain_builder.add_left_goal(-1.0);
302 drivetrain_builder.add_right_goal(1.0);
303
304 EXPECT_TRUE(builder.Send(drivetrain_builder.Finish()));
305 RunFor(chrono::seconds(3));
306 VerifyNearGoal();
307 VerifyEstimatorAccurate(5e-4);
308}
309
310// Tests that camera udpates with a constant initial error in the position
311// results in convergence.
312TEST_F(LocalizedDrivetrainTest, InitialPositionError) {
313 SetEnabled(true);
314 set_enable_cameras(true);
315 drivetrain_plant_.mutable_state()->topRows(3) +=
316 Eigen::Vector3d(0.1, 0.1, 0.01);
317 auto builder = drivetrain_goal_sender_.MakeBuilder();
318
319 Goal::Builder drivetrain_builder = builder.MakeBuilder<Goal>();
320 drivetrain_builder.add_controller_type(
321 frc971::control_loops::drivetrain::ControllerType::MOTION_PROFILE);
322 drivetrain_builder.add_left_goal(-1.0);
323 drivetrain_builder.add_right_goal(1.0);
324
325 EXPECT_TRUE(builder.Send(drivetrain_builder.Finish()));
326 // Give the filters enough time to converge.
327 RunFor(chrono::seconds(10));
328 VerifyNearGoal(5e-3);
329 VerifyEstimatorAccurate(1e-2);
330}
331
332} // namespace testing
333} // namespace drivetrain
334} // namespace control_loops
335} // namespace y2020