blob: c8c7b6b1476e8cb64c8740048232926f8439876b [file] [log] [blame]
James Kuszmaul04a343c2023-02-20 16:38:22 -08001#include "y2023/localizer/localizer.h"
2
3#include "aos/events/logging/log_writer.h"
4#include "aos/events/simulated_event_loop.h"
5#include "frc971/control_loops/drivetrain/drivetrain_test_lib.h"
6#include "frc971/control_loops/drivetrain/localizer_generated.h"
7#include "frc971/control_loops/pose.h"
8#include "frc971/vision/target_map_generated.h"
9#include "gtest/gtest.h"
10#include "y2023/constants/simulated_constants_sender.h"
11#include "y2023/control_loops/drivetrain/drivetrain_base.h"
12#include "y2023/localizer/status_generated.h"
James Kuszmaul18008f82023-02-23 20:52:50 -080013#include "y2023/localizer/utils.h"
James Kuszmaul04a343c2023-02-20 16:38:22 -080014
15DEFINE_string(output_folder, "",
16 "If set, logs all channels to the provided logfile.");
17DECLARE_bool(die_on_malloc);
18
19namespace y2023::localizer::testing {
20
21using frc971::control_loops::drivetrain::Output;
22
23class LocalizerTest : public ::testing::Test {
24 protected:
25 static constexpr uint64_t kTargetId = 1;
26 LocalizerTest()
27 : configuration_(aos::configuration::ReadConfig("y2023/aos_config.json")),
28 event_loop_factory_(&configuration_.message()),
29 roborio_node_([this]() {
30 // Get the constants sent before anything else happens.
31 // It has nothing to do with the roborio node.
32 SendSimulationConstants(&event_loop_factory_, 7971,
33 "y2023/constants/test_constants.json");
34 return aos::configuration::GetNode(&configuration_.message(),
35 "roborio");
36 }()),
37 imu_node_(
38 aos::configuration::GetNode(&configuration_.message(), "imu")),
39 camera_node_(
40 aos::configuration::GetNode(&configuration_.message(), "pi1")),
41 dt_config_(frc971::control_loops::drivetrain::testing::
42 GetTestDrivetrainConfig()),
43 localizer_event_loop_(
44 event_loop_factory_.MakeEventLoop("localizer", imu_node_)),
45 localizer_(localizer_event_loop_.get(), dt_config_),
46 drivetrain_plant_event_loop_(event_loop_factory_.MakeEventLoop(
47 "drivetrain_plant", roborio_node_)),
48 drivetrain_plant_imu_event_loop_(
49 event_loop_factory_.MakeEventLoop("drivetrain_plant", imu_node_)),
50 drivetrain_plant_(drivetrain_plant_event_loop_.get(),
51 drivetrain_plant_imu_event_loop_.get(), dt_config_,
52 std::chrono::microseconds(500)),
53 roborio_test_event_loop_(
54 event_loop_factory_.MakeEventLoop("test", roborio_node_)),
55 imu_test_event_loop_(
56 event_loop_factory_.MakeEventLoop("test", imu_node_)),
57 camera_test_event_loop_(
58 event_loop_factory_.MakeEventLoop("test", camera_node_)),
59 logger_test_event_loop_(
60 event_loop_factory_.GetNodeEventLoopFactory("logger")
61 ->MakeEventLoop("test")),
62 constants_fetcher_(imu_test_event_loop_.get()),
63 output_sender_(
64 roborio_test_event_loop_->MakeSender<Output>("/drivetrain")),
65 target_sender_(
66 camera_test_event_loop_->MakeSender<frc971::vision::TargetMap>(
67 "/camera")),
68 control_sender_(roborio_test_event_loop_->MakeSender<
69 frc971::control_loops::drivetrain::LocalizerControl>(
70 "/drivetrain")),
71 output_fetcher_(
72 roborio_test_event_loop_
73 ->MakeFetcher<frc971::controls::LocalizerOutput>("/localizer")),
74 status_fetcher_(
75 imu_test_event_loop_->MakeFetcher<Status>("/localizer")) {
76 FLAGS_die_on_malloc = true;
77 {
78 aos::TimerHandler *timer = roborio_test_event_loop_->AddTimer([this]() {
79 {
80 auto builder = output_sender_.MakeBuilder();
81 auto output_builder = builder.MakeBuilder<Output>();
82 output_builder.add_left_voltage(output_voltages_(0));
83 output_builder.add_right_voltage(output_voltages_(1));
84 builder.CheckOk(builder.Send(output_builder.Finish()));
85 }
86 });
87 roborio_test_event_loop_->OnRun([timer, this]() {
88 timer->Setup(roborio_test_event_loop_->monotonic_now(),
89 std::chrono::milliseconds(5));
90 });
91 }
92 {
93 // Sanity check that the test calibration files look like what we
94 // expect.
95 CHECK_EQ("pi1", constants_fetcher_.constants()
96 .cameras()
97 ->Get(0)
98 ->calibration()
99 ->node_name()
100 ->string_view());
101 const Eigen::Matrix<double, 4, 4> H_robot_camera =
102 frc971::control_loops::drivetrain::FlatbufferToTransformationMatrix(
103 *constants_fetcher_.constants()
104 .cameras()
105 ->Get(0)
106 ->calibration()
107 ->fixed_extrinsics());
108
109 CHECK_EQ(kTargetId, constants_fetcher_.constants()
110 .target_map()
111 ->target_poses()
112 ->Get(0)
113 ->id());
114 const Eigen::Matrix<double, 4, 4> H_field_target = PoseToTransform(
115 constants_fetcher_.constants().target_map()->target_poses()->Get(0));
116 // For reference, the camera should pointed straight forwards on the
117 // robot, offset by 1 meter.
118 aos::TimerHandler *timer = camera_test_event_loop_->AddTimer(
119 [this, H_robot_camera, H_field_target]() {
120 if (!send_targets_) {
121 return;
122 }
123 const frc971::control_loops::Pose robot_pose(
124 {drivetrain_plant_.GetPosition().x(),
125 drivetrain_plant_.GetPosition().y(), 0.0},
milind-ub45c6d92023-03-11 18:56:15 -0800126 drivetrain_plant_.state()(2, 0) + implied_yaw_error_);
James Kuszmaul04a343c2023-02-20 16:38:22 -0800127
128 const Eigen::Matrix<double, 4, 4> H_field_camera =
129 robot_pose.AsTransformationMatrix() * H_robot_camera;
130 const Eigen::Matrix<double, 4, 4> H_camera_target =
131 H_field_camera.inverse() * H_field_target;
132
133 const Eigen::Quaterniond quat(H_camera_target.block<3, 3>(0, 0));
134 const Eigen::Vector3d translation(
135 H_camera_target.block<3, 1>(0, 3));
136
137 auto builder = target_sender_.MakeBuilder();
138 frc971::vision::Quaternion::Builder quat_builder(*builder.fbb());
139 quat_builder.add_w(quat.w());
140 quat_builder.add_x(quat.x());
141 quat_builder.add_y(quat.y());
142 quat_builder.add_z(quat.z());
143 auto quat_offset = quat_builder.Finish();
144 frc971::vision::Position::Builder position_builder(*builder.fbb());
145 position_builder.add_x(translation.x());
146 position_builder.add_y(translation.y());
147 position_builder.add_z(translation.z());
148 auto position_offset = position_builder.Finish();
149
150 frc971::vision::TargetPoseFbs::Builder target_builder(
151 *builder.fbb());
152 target_builder.add_id(send_target_id_);
153 target_builder.add_position(position_offset);
154 target_builder.add_orientation(quat_offset);
milind-u7a7f6662023-02-26 16:41:29 -0800155 target_builder.add_pose_error(pose_error_);
James Kuszmaul04a343c2023-02-20 16:38:22 -0800156 auto target_offset = target_builder.Finish();
157
158 auto targets_offset = builder.fbb()->CreateVector({target_offset});
159 frc971::vision::TargetMap::Builder map_builder(*builder.fbb());
160 map_builder.add_target_poses(targets_offset);
161 map_builder.add_monotonic_timestamp_ns(
162 std::chrono::duration_cast<std::chrono::nanoseconds>(
163 camera_test_event_loop_->monotonic_now().time_since_epoch())
164 .count());
165
166 builder.CheckOk(builder.Send(map_builder.Finish()));
167 });
168 camera_test_event_loop_->OnRun([timer, this]() {
169 timer->Setup(camera_test_event_loop_->monotonic_now(),
170 std::chrono::milliseconds(50));
171 });
172 }
173
174 localizer_control_send_timer_ =
175 roborio_test_event_loop_->AddTimer([this]() {
176 auto builder = control_sender_.MakeBuilder();
177 auto control_builder = builder.MakeBuilder<
178 frc971::control_loops::drivetrain::LocalizerControl>();
179 control_builder.add_x(localizer_control_x_);
180 control_builder.add_y(localizer_control_y_);
181 control_builder.add_theta(localizer_control_theta_);
182 control_builder.add_theta_uncertainty(0.01);
183 control_builder.add_keep_current_theta(false);
184 builder.CheckOk(builder.Send(control_builder.Finish()));
185 });
186
187 // Get things zeroed.
188 event_loop_factory_.RunFor(std::chrono::seconds(10));
189 CHECK(status_fetcher_.Fetch());
190 CHECK(status_fetcher_->imu()->zeroed());
191
192 if (!FLAGS_output_folder.empty()) {
193 logger_event_loop_ =
194 event_loop_factory_.MakeEventLoop("logger", imu_node_);
195 logger_ = std::make_unique<aos::logger::Logger>(logger_event_loop_.get());
196 logger_->StartLoggingOnRun(FLAGS_output_folder);
197 }
198 }
199
200 void SendLocalizerControl(double x, double y, double theta) {
201 localizer_control_x_ = x;
202 localizer_control_y_ = y;
203 localizer_control_theta_ = theta;
204 localizer_control_send_timer_->Setup(
205 roborio_test_event_loop_->monotonic_now());
206 }
207 ::testing::AssertionResult IsNear(double expected, double actual,
208 double epsilon) {
209 if (std::abs(expected - actual) < epsilon) {
210 return ::testing::AssertionSuccess();
211 } else {
212 return ::testing::AssertionFailure()
213 << "Expected " << expected << " but got " << actual
214 << " with a max difference of " << epsilon
215 << " and an actual difference of " << std::abs(expected - actual);
216 }
217 }
218 ::testing::AssertionResult VerifyEstimatorAccurate(double eps) {
219 const Eigen::Matrix<double, 5, 1> true_state = drivetrain_plant_.state();
220 ::testing::AssertionResult result(true);
221 status_fetcher_.Fetch();
222 if (!(result = IsNear(status_fetcher_->state()->x(), true_state(0), eps))) {
223 return result;
224 }
225 if (!(result = IsNear(status_fetcher_->state()->y(), true_state(1), eps))) {
226 return result;
227 }
228 if (!(result =
229 IsNear(status_fetcher_->state()->theta(), true_state(2), eps))) {
230 return result;
231 }
232 return result;
233 }
234
235 aos::FlatbufferDetachedBuffer<aos::Configuration> configuration_;
236 aos::SimulatedEventLoopFactory event_loop_factory_;
237 const aos::Node *const roborio_node_;
238 const aos::Node *const imu_node_;
239 const aos::Node *const camera_node_;
240 const frc971::control_loops::drivetrain::DrivetrainConfig<double> dt_config_;
241 std::unique_ptr<aos::EventLoop> localizer_event_loop_;
242 Localizer localizer_;
243
244 std::unique_ptr<aos::EventLoop> drivetrain_plant_event_loop_;
245 std::unique_ptr<aos::EventLoop> drivetrain_plant_imu_event_loop_;
246 frc971::control_loops::drivetrain::testing::DrivetrainSimulation
247 drivetrain_plant_;
248
249 std::unique_ptr<aos::EventLoop> roborio_test_event_loop_;
250 std::unique_ptr<aos::EventLoop> imu_test_event_loop_;
251 std::unique_ptr<aos::EventLoop> camera_test_event_loop_;
252 std::unique_ptr<aos::EventLoop> logger_test_event_loop_;
253
254 frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
255
256 aos::Sender<Output> output_sender_;
257 aos::Sender<frc971::vision::TargetMap> target_sender_;
258 aos::Sender<frc971::control_loops::drivetrain::LocalizerControl>
259 control_sender_;
260 aos::Fetcher<frc971::controls::LocalizerOutput> output_fetcher_;
261 aos::Fetcher<Status> status_fetcher_;
262
263 Eigen::Vector2d output_voltages_ = Eigen::Vector2d::Zero();
264
265 aos::TimerHandler *localizer_control_send_timer_;
266
267 bool send_targets_ = false;
268
269 double localizer_control_x_ = 0.0;
270 double localizer_control_y_ = 0.0;
271 double localizer_control_theta_ = 0.0;
272
273 std::unique_ptr<aos::EventLoop> logger_event_loop_;
274 std::unique_ptr<aos::logger::Logger> logger_;
275
276 uint64_t send_target_id_ = kTargetId;
milind-u7a7f6662023-02-26 16:41:29 -0800277 double pose_error_ = 1e-7;
milind-ub45c6d92023-03-11 18:56:15 -0800278 double implied_yaw_error_ = 0.0;
James Kuszmaul04a343c2023-02-20 16:38:22 -0800279
280 gflags::FlagSaver flag_saver_;
281};
282
283// Test a simple scenario with no errors where the robot should just drive
284// straight forwards.
285TEST_F(LocalizerTest, Nominal) {
286 output_voltages_ << 1.0, 1.0;
287 event_loop_factory_.RunFor(std::chrono::seconds(2));
288 CHECK(output_fetcher_.Fetch());
289 CHECK(status_fetcher_.Fetch());
290 // The two can be different because they may've been sent at different
291 // times.
292 EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
293 EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
294 EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
295 1e-6);
296 // Confirm that we did indeed drive forwards (and straight), as expected.
297 EXPECT_LT(0.1, output_fetcher_->x());
298 EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
299 EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
300 EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
301 EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
302
303 // And check that we actually think that we are near where the simulator
304 // says we are.
305 EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
306}
307
308// Confirm that when the robot drives backwards that we localize correctly.
309TEST_F(LocalizerTest, NominalReverse) {
310 output_voltages_ << -1.0, -1.0;
311 event_loop_factory_.RunFor(std::chrono::seconds(2));
312 CHECK(output_fetcher_.Fetch());
313 CHECK(status_fetcher_.Fetch());
314 // The two can be different because they may've been sent at different
315 // times.
316 EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
317 EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
318 EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
319 1e-6);
320 // Confirm that we did indeed drive backwards (and straight), as expected.
321 EXPECT_GT(-0.1, output_fetcher_->x());
322 EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
323 EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
324 EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
325 EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
326
327 // And check that we actually think that we are near where the simulator
328 // says we are.
329 EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
330}
331
332// Confirm that when the robot turns counter-clockwise that we localize
333// correctly.
334TEST_F(LocalizerTest, NominalSpinInPlace) {
335 output_voltages_ << -1.0, 1.0;
James Kuszmaul456774b2023-03-08 21:29:15 -0800336 // Go 1 ms over 2 sec to make sure we actually see relatively recent messages
337 // on each channel.
338 event_loop_factory_.RunFor(std::chrono::milliseconds(2001));
James Kuszmaul04a343c2023-02-20 16:38:22 -0800339 CHECK(output_fetcher_.Fetch());
340 CHECK(status_fetcher_.Fetch());
341 // The two can be different because they may've been sent at different
342 // times.
343 EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-6);
344 EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
345 EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
346 1e-2);
347 // Confirm that we did indeed turn counter-clockwise.
348 EXPECT_NEAR(0.0, output_fetcher_->x(), 1e-10);
349 EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
350 EXPECT_LT(0.1, output_fetcher_->theta());
351 EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
352 EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
353
354 // And check that we actually think that we are near where the simulator
355 // says we are.
356 EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
357}
358
359// Confirm that when the robot drives in a curve that we localize
360// successfully.
361TEST_F(LocalizerTest, NominalCurve) {
362 output_voltages_ << 2.0, 3.0;
363 event_loop_factory_.RunFor(std::chrono::seconds(4));
364 CHECK(output_fetcher_.Fetch());
365 CHECK(status_fetcher_.Fetch());
366 // The two can be different because they may've been sent at different
367 // times.
368 EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
369 EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-2);
370 EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
371 1e-2);
372 // Confirm that we did indeed drive in a rough, counter-clockwise, curve.
373 EXPECT_LT(0.1, output_fetcher_->x());
374 EXPECT_LT(0.1, output_fetcher_->y());
375 EXPECT_LT(0.1, output_fetcher_->theta());
376
377 // And check that we actually think that we are near where the simulator
378 // says we are.
379 EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
380}
381
382// Tests that, in the presence of a non-zero voltage error, that we correct
383// for it.
384TEST_F(LocalizerTest, VoltageErrorDisabled) {
385 output_voltages_ << 0.0, 0.0;
386 drivetrain_plant_.set_left_voltage_offset(2.0);
387 drivetrain_plant_.set_right_voltage_offset(2.0);
388
389 event_loop_factory_.RunFor(std::chrono::seconds(2));
390 CHECK(output_fetcher_.Fetch());
391 CHECK(status_fetcher_.Fetch());
392 // We should've just ended up driving straight forwards.
393 EXPECT_LT(0.1, output_fetcher_->x());
394 EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
395 EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
396 EXPECT_NEAR(2.0, status_fetcher_->state()->left_voltage_error(), 1.0);
397 EXPECT_NEAR(2.0, status_fetcher_->state()->right_voltage_error(), 1.0);
398
399 // And check that we actually think that we are near where the simulator
400 // says we are.
401 EXPECT_TRUE(VerifyEstimatorAccurate(0.05));
402}
403
404// Tests that image corrections in the nominal case (no errors) causes no
405// issues.
406TEST_F(LocalizerTest, NominalImageCorrections) {
407 output_voltages_ << 3.0, 2.0;
408 send_targets_ = true;
409
410 event_loop_factory_.RunFor(std::chrono::seconds(4));
411 CHECK(status_fetcher_.Fetch());
412 EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
413 ASSERT_TRUE(status_fetcher_->has_statistics());
414 ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
415 ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
416 status_fetcher_->statistics()->Get(0)->total_accepted());
417 ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
418}
419
420// Tests that image corrections when there is an error at the start results
421// in us actually getting corrected over time.
422TEST_F(LocalizerTest, ImageCorrections) {
423 output_voltages_ << 0.0, 0.0;
424 drivetrain_plant_.mutable_state()->x() = 2.0;
425 drivetrain_plant_.mutable_state()->y() = 2.0;
426 SendLocalizerControl(5.0, 3.0, 0.0);
427 event_loop_factory_.RunFor(std::chrono::seconds(4));
428 CHECK(output_fetcher_.Fetch());
429 ASSERT_NEAR(5.0, output_fetcher_->x(), 1e-5);
430 ASSERT_NEAR(3.0, output_fetcher_->y(), 1e-5);
431 ASSERT_NEAR(0.0, output_fetcher_->theta(), 1e-5);
432
433 send_targets_ = true;
434
435 event_loop_factory_.RunFor(std::chrono::seconds(4));
436 CHECK(status_fetcher_.Fetch());
437 EXPECT_TRUE(VerifyEstimatorAccurate(0.1));
438 ASSERT_TRUE(status_fetcher_->has_statistics());
439 ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
440 ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
441 status_fetcher_->statistics()->Get(0)->total_accepted());
442 ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
443}
444
445// Tests that we correctly reject an invalid target.
446TEST_F(LocalizerTest, InvalidTargetId) {
447 output_voltages_ << 0.0, 0.0;
448 send_targets_ = true;
449 send_target_id_ = 100;
450
451 event_loop_factory_.RunFor(std::chrono::seconds(4));
452 CHECK(status_fetcher_.Fetch());
453 ASSERT_TRUE(status_fetcher_->has_statistics());
454 ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
455 ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
456 ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
457 ASSERT_EQ(status_fetcher_->statistics()
458 ->Get(0)
459 ->rejection_reasons()
460 ->Get(static_cast<size_t>(RejectionReason::NO_SUCH_TARGET))
461 ->count(),
462 status_fetcher_->statistics()->Get(0)->total_candidates());
463}
464
milind-u7a7f6662023-02-26 16:41:29 -0800465// Tests that we correctly reject a detection with a high pose error.
466TEST_F(LocalizerTest, HighPoseError) {
467 output_voltages_ << 0.0, 0.0;
468 send_targets_ = true;
469 // Send the minimum pose error to be rejected
470 constexpr double kEps = 1e-9;
471 pose_error_ = 1e-6 + kEps;
472
473 event_loop_factory_.RunFor(std::chrono::seconds(4));
474 CHECK(status_fetcher_.Fetch());
475 ASSERT_TRUE(status_fetcher_->has_statistics());
476 ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
477 ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
478 ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
479 ASSERT_EQ(status_fetcher_->statistics()
480 ->Get(0)
481 ->rejection_reasons()
482 ->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR))
483 ->count(),
484 status_fetcher_->statistics()->Get(0)->total_candidates());
485}
486
milind-ub45c6d92023-03-11 18:56:15 -0800487// Tests that we correctly reject a detection with a high implied yaw error.
488TEST_F(LocalizerTest, HighImpliedYawError) {
489 output_voltages_ << 0.0, 0.0;
490 send_targets_ = true;
491 implied_yaw_error_ = 31.0 * M_PI / 180.0;
492
493 event_loop_factory_.RunFor(std::chrono::seconds(4));
494 CHECK(status_fetcher_.Fetch());
495 ASSERT_TRUE(status_fetcher_->has_statistics());
496 ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
497 ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
498 ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
499 ASSERT_EQ(
500 status_fetcher_->statistics()
501 ->Get(0)
502 ->rejection_reasons()
503 ->Get(static_cast<size_t>(RejectionReason::HIGH_IMPLIED_YAW_ERROR))
504 ->count(),
505 status_fetcher_->statistics()->Get(0)->total_candidates());
506}
507
James Kuszmaul04a343c2023-02-20 16:38:22 -0800508} // namespace y2023::localizer::testing