James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 1 | #include <fcntl.h> |
| 2 | |
Austin Schuh | 99f7c6a | 2024-06-25 22:07:44 -0700 | [diff] [blame] | 3 | #include "absl/flags/flag.h" |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 4 | |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 5 | #include "aos/init.h" |
| 6 | #include "aos/logging/implementations.h" |
| 7 | #include "aos/logging/replay.h" |
| 8 | #include "aos/network/team_number.h" |
| 9 | #include "aos/queue.h" |
| 10 | #include "frc971/control_loops/drivetrain/drivetrain.q.h" |
| 11 | #include "frc971/control_loops/drivetrain/localizer.q.h" |
| 12 | #include "frc971/wpilib/imu.q.h" |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 13 | #include "y2019/constants.h" |
| 14 | #include "y2019/control_loops/drivetrain/drivetrain_base.h" |
| 15 | #include "y2019/control_loops/drivetrain/event_loop_localizer.h" |
| 16 | #if defined(SUPPORT_PLOT) |
| 17 | #include "third_party/matplotlib-cpp/matplotlibcpp.h" |
| 18 | #endif |
| 19 | |
Austin Schuh | 99f7c6a | 2024-06-25 22:07:44 -0700 | [diff] [blame] | 20 | ABSL_FLAG(std::string, logfile, "", "Path to the logfile to replay."); |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 21 | // TODO(james): Figure out how to reliably source team number from logfile. |
Austin Schuh | 99f7c6a | 2024-06-25 22:07:44 -0700 | [diff] [blame] | 22 | ABSL_FLAG(int32_t, team, 971, "Team number to use for logfile replay."); |
| 23 | ABSL_FLAG(int32_t, plot_duration, 30, |
| 24 | "Duration, in seconds, to plot after the start time."); |
| 25 | ABSL_FLAG(int32_t, start_offset, 0, |
| 26 | "Time, in seconds, to start replay plot after the first enable."); |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 27 | |
Stephan Pleines | f63bde8 | 2024-01-13 15:59:33 -0800 | [diff] [blame] | 28 | namespace y2019::control_loops::drivetrain { |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 29 | using ::y2019::constants::Field; |
| 30 | |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 31 | typedef TypedLocalizer<constants::Values::kNumCameras, Field::kNumTargets, |
| 32 | Field::kNumObstacles, |
| 33 | EventLoopLocalizer::kMaxTargetsPerFrame, double> |
| 34 | TestLocalizer; |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 35 | typedef typename TestLocalizer::Camera TestCamera; |
| 36 | typedef typename TestCamera::Pose Pose; |
| 37 | typedef typename TestCamera::LineSegment Obstacle; |
| 38 | |
| 39 | #if defined(SUPPORT_PLOT) |
| 40 | // Plots a line from a vector of Pose's. |
| 41 | void PlotPlotPts(const ::std::vector<Pose> &poses, |
| 42 | const ::std::map<::std::string, ::std::string> &kwargs) { |
| 43 | ::std::vector<double> x; |
| 44 | ::std::vector<double> y; |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 45 | for (const Pose &p : poses) { |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 46 | x.push_back(p.abs_pos().x()); |
| 47 | y.push_back(p.abs_pos().y()); |
| 48 | } |
| 49 | matplotlibcpp::plot(x, y, kwargs); |
| 50 | } |
| 51 | #endif |
| 52 | |
| 53 | class LocalizerReplayer { |
| 54 | // This class is for replaying logfiles from the 2019 robot and seeing how the |
| 55 | // localizer performs withi any new changes versus how it performed during the |
| 56 | // real match. This starts running replay on the first enable and then will |
| 57 | // actually plot a subset of the run based on the --plot_duration and |
| 58 | // --start_offset flags. |
| 59 | public: |
| 60 | typedef ::frc971::control_loops::DrivetrainQueue::Goal DrivetrainGoal; |
| 61 | typedef ::frc971::control_loops::DrivetrainQueue::Position DrivetrainPosition; |
| 62 | typedef ::frc971::control_loops::DrivetrainQueue::Status DrivetrainStatus; |
| 63 | typedef ::frc971::control_loops::DrivetrainQueue::Output DrivetrainOutput; |
| 64 | typedef ::frc971::IMUValues IMUValues; |
| 65 | typedef ::aos::JoystickState JoystickState; |
| 66 | |
Austin Schuh | 9fe68f7 | 2019-08-10 19:32:03 -0700 | [diff] [blame] | 67 | LocalizerReplayer() : localizer_(&event_loop_, GetDrivetrainConfig()) { |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 68 | replayer_.AddDirectQueueSender<CameraFrame>( |
| 69 | "wpilib_interface.stripped.IMU", "camera_frames", |
| 70 | ".y2019.control_loops.drivetrain.camera_frames"); |
| 71 | |
| 72 | const char *drivetrain = "drivetrain.stripped"; |
| 73 | |
| 74 | replayer_.AddHandler<DrivetrainPosition>( |
| 75 | drivetrain, "position", [this](const DrivetrainPosition &msg) { |
| 76 | if (has_been_enabled_) { |
| 77 | localizer_.Update(last_last_U_ * battery_voltage_ / 12.0, |
| 78 | msg.sent_time, msg.left_encoder, |
| 79 | msg.right_encoder, latest_gyro_, 0.0); |
| 80 | if (start_time_ <= msg.sent_time && |
| 81 | start_time_ + plot_duration_ >= msg.sent_time) { |
| 82 | t_pos_.push_back( |
| 83 | ::std::chrono::duration_cast<::std::chrono::duration<double>>( |
| 84 | msg.sent_time.time_since_epoch()) |
| 85 | .count()); |
| 86 | new_x_.push_back(localizer_.x()); |
| 87 | new_y_.push_back(localizer_.y()); |
| 88 | new_theta_.push_back(localizer_.theta()); |
| 89 | new_left_encoder_.push_back(localizer_.left_encoder()); |
| 90 | new_right_encoder_.push_back(localizer_.right_encoder()); |
| 91 | new_left_velocity_.push_back(localizer_.left_velocity()); |
| 92 | new_right_velocity_.push_back(localizer_.right_velocity()); |
| 93 | |
| 94 | left_voltage_.push_back(last_last_U_(0, 0)); |
| 95 | right_voltage_.push_back(last_last_U_(1, 0)); |
| 96 | } |
| 97 | } |
| 98 | }); |
| 99 | |
| 100 | replayer_.AddHandler<DrivetrainGoal>( |
| 101 | drivetrain, "goal", [this](const DrivetrainGoal &msg) { |
| 102 | if (has_been_enabled_ && start_time_ <= msg.sent_time && |
| 103 | (start_time_ + plot_duration_ >= msg.sent_time)) { |
| 104 | t_goal_.push_back( |
| 105 | ::std::chrono::duration_cast<::std::chrono::duration<double>>( |
| 106 | msg.sent_time.time_since_epoch()) |
| 107 | .count()); |
| 108 | drivetrain_mode_.push_back(msg.controller_type); |
| 109 | throttle_.push_back(msg.throttle); |
| 110 | wheel_.push_back(msg.wheel); |
| 111 | } |
| 112 | }); |
| 113 | |
| 114 | replayer_.AddHandler<DrivetrainStatus>( |
| 115 | drivetrain, "status", [this](const DrivetrainStatus &msg) { |
| 116 | if (has_been_enabled_ && start_time_ <= msg.sent_time && |
| 117 | (start_time_ + plot_duration_ >= msg.sent_time)) { |
| 118 | t_status_.push_back( |
| 119 | ::std::chrono::duration_cast<::std::chrono::duration<double>>( |
| 120 | msg.sent_time.time_since_epoch()) |
| 121 | .count()); |
| 122 | original_x_.push_back(msg.x); |
| 123 | original_y_.push_back(msg.y); |
| 124 | original_theta_.push_back(msg.theta); |
| 125 | original_line_theta_.push_back(msg.line_follow_logging.rel_theta); |
| 126 | original_line_goal_theta_.push_back( |
| 127 | msg.line_follow_logging.goal_theta); |
| 128 | original_line_distance_.push_back( |
| 129 | msg.line_follow_logging.distance_to_target); |
| 130 | } |
| 131 | }); |
| 132 | |
| 133 | replayer_.AddHandler<DrivetrainOutput>( |
| 134 | drivetrain, "output", [this](const DrivetrainOutput &msg) { |
| 135 | last_last_U_ = last_U_; |
| 136 | last_U_ << msg.left_voltage, msg.right_voltage; |
| 137 | }); |
| 138 | |
| 139 | replayer_.AddHandler<frc971::control_loops::drivetrain::LocalizerControl>( |
| 140 | drivetrain, "localizer_control", |
| 141 | [this](const frc971::control_loops::drivetrain::LocalizerControl &msg) { |
Austin Schuh | f257f3c | 2019-10-27 21:00:43 -0700 | [diff] [blame] | 142 | AOS_LOG_STRUCT(DEBUG, "localizer_control", msg); |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 143 | localizer_.ResetPosition(msg.sent_time, msg.x, msg.y, msg.theta, |
| 144 | msg.theta_uncertainty, |
| 145 | !msg.keep_current_theta); |
| 146 | }); |
| 147 | |
| 148 | replayer_.AddHandler<JoystickState>( |
| 149 | "wpilib_interface.stripped.DSReader", "joystick_state", |
| 150 | [this](const JoystickState &msg) { |
| 151 | if (msg.enabled && !has_been_enabled_) { |
| 152 | has_been_enabled_ = true; |
| 153 | start_time_ = |
| 154 | msg.sent_time + ::std::chrono::seconds(FLAGS_start_offset); |
| 155 | } |
| 156 | }); |
| 157 | |
| 158 | replayer_.AddHandler<IMUValues>( |
| 159 | "wpilib_interface.stripped.IMU", "sending", |
| 160 | [this](const IMUValues &msg) { |
| 161 | latest_gyro_ = msg.gyro_z; |
| 162 | if (has_been_enabled_ && start_time_ <= msg.sent_time && |
| 163 | (start_time_ + plot_duration_ >= msg.sent_time)) { |
| 164 | t_imu_.push_back( |
| 165 | ::std::chrono::duration_cast<::std::chrono::duration<double>>( |
| 166 | msg.sent_time.time_since_epoch()) |
| 167 | .count()); |
| 168 | lat_accel_.push_back(msg.accelerometer_y); |
| 169 | long_accel_.push_back(msg.accelerometer_x); |
| 170 | z_accel_.push_back(msg.accelerometer_z); |
| 171 | } |
| 172 | }); |
| 173 | |
| 174 | replayer_.AddHandler<::aos::RobotState>( |
| 175 | "wpilib_interface.stripped.SensorReader", "robot_state", |
| 176 | [this](const ::aos::RobotState &msg) { |
| 177 | battery_voltage_ = msg.voltage_battery; |
| 178 | }); |
| 179 | } |
| 180 | |
| 181 | void ProcessFile(const char *filename) { |
| 182 | int fd; |
| 183 | if (strcmp(filename, "-") == 0) { |
| 184 | fd = STDIN_FILENO; |
| 185 | } else { |
| 186 | fd = open(filename, O_RDONLY); |
| 187 | } |
| 188 | if (fd == -1) { |
Austin Schuh | f257f3c | 2019-10-27 21:00:43 -0700 | [diff] [blame] | 189 | AOS_PLOG(FATAL, "couldn't open file '%s' for reading", filename); |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 190 | } |
| 191 | |
| 192 | replayer_.OpenFile(fd); |
| 193 | ::aos::RawQueue *queue = ::aos::logging::GetLoggingQueue(); |
| 194 | while (!replayer_.ProcessMessage()) { |
| 195 | const ::aos::logging::LogMessage *const msg = |
| 196 | static_cast<const ::aos::logging::LogMessage *>( |
| 197 | queue->ReadMessage(::aos::RawQueue::kNonBlock)); |
| 198 | if (msg != NULL) { |
| 199 | ::aos::logging::internal::PrintMessage(stdout, *msg); |
| 200 | |
| 201 | queue->FreeMessage(msg); |
| 202 | } |
| 203 | continue; |
| 204 | } |
| 205 | replayer_.CloseCurrentFile(); |
| 206 | |
Austin Schuh | f257f3c | 2019-10-27 21:00:43 -0700 | [diff] [blame] | 207 | AOS_PCHECK(close(fd)); |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 208 | |
| 209 | Plot(); |
| 210 | } |
| 211 | |
| 212 | void Plot() { |
| 213 | #if defined(SUPPORT_PLOT) |
| 214 | int start_idx = 0; |
| 215 | for (const float t : t_pos_) { |
| 216 | if (t < ::std::chrono::duration_cast<::std::chrono::duration<double>>( |
| 217 | start_time_.time_since_epoch()) |
| 218 | .count() + |
| 219 | 0.0) { |
| 220 | ++start_idx; |
| 221 | } else { |
| 222 | break; |
| 223 | } |
| 224 | } |
| 225 | ::std::vector<float> trunc_orig_x(original_x_.begin() + start_idx, |
| 226 | original_x_.end()); |
| 227 | ::std::vector<float> trunc_orig_y(original_y_.begin() + start_idx, |
| 228 | original_y_.end()); |
| 229 | ::std::vector<float> trunc_new_x(new_x_.begin() + start_idx, new_x_.end()); |
| 230 | ::std::vector<float> trunc_new_y(new_y_.begin() + start_idx, new_y_.end()); |
| 231 | matplotlibcpp::figure(); |
| 232 | matplotlibcpp::plot(trunc_orig_x, trunc_orig_y, |
| 233 | {{"label", "original"}, {"marker", "o"}}); |
| 234 | matplotlibcpp::plot(trunc_new_x, trunc_new_y, |
| 235 | {{"label", "new"}, {"marker", "o"}}); |
| 236 | matplotlibcpp::xlim(-0.1, 19.0); |
| 237 | matplotlibcpp::ylim(-5.0, 5.0); |
| 238 | Field field; |
| 239 | for (const Target &target : field.targets()) { |
| 240 | PlotPlotPts(target.PlotPoints(), {{"c", "g"}}); |
| 241 | } |
| 242 | for (const Obstacle &obstacle : field.obstacles()) { |
| 243 | PlotPlotPts(obstacle.PlotPoints(), {{"c", "k"}}); |
| 244 | } |
| 245 | matplotlibcpp::grid(true); |
| 246 | matplotlibcpp::legend(); |
| 247 | |
| 248 | matplotlibcpp::figure(); |
| 249 | matplotlibcpp::plot(t_status_, original_x_, |
| 250 | {{"label", "original x"}, {"marker", "o"}}); |
| 251 | matplotlibcpp::plot(t_status_, original_y_, |
| 252 | {{"label", "original y"}, {"marker", "o"}}); |
| 253 | matplotlibcpp::plot(t_status_, original_theta_, |
| 254 | {{"label", "original theta"}, {"marker", "o"}}); |
| 255 | matplotlibcpp::plot(t_pos_, new_x_, {{"label", "new x"}, {"marker", "o"}}); |
| 256 | matplotlibcpp::plot(t_pos_, new_y_, {{"label", "new y"}, {"marker", "o"}}); |
| 257 | matplotlibcpp::plot(t_pos_, new_theta_, |
| 258 | {{"label", "new theta"}, {"marker", "o"}}); |
| 259 | matplotlibcpp::grid(true); |
| 260 | matplotlibcpp::legend(); |
| 261 | |
| 262 | matplotlibcpp::figure(); |
| 263 | matplotlibcpp::plot(t_pos_, left_voltage_, |
| 264 | {{"label", "left voltage"}, {"marker", "o"}}); |
| 265 | matplotlibcpp::plot(t_pos_, right_voltage_, |
| 266 | {{"label", "right voltage"}, {"marker", "o"}}); |
| 267 | matplotlibcpp::grid(true); |
| 268 | matplotlibcpp::legend(); |
| 269 | |
| 270 | matplotlibcpp::figure(); |
| 271 | matplotlibcpp::plot(t_pos_, new_left_encoder_, |
| 272 | {{"label", "left encoder"}, {"marker", "o"}}); |
| 273 | matplotlibcpp::plot(t_pos_, new_right_encoder_, |
| 274 | {{"label", "right encoder"}, {"marker", "o"}}); |
| 275 | matplotlibcpp::plot(t_pos_, new_left_velocity_, |
| 276 | {{"label", "left velocity"}, {"marker", "o"}}); |
| 277 | matplotlibcpp::plot(t_pos_, new_right_velocity_, |
| 278 | {{"label", "right velocity"}, {"marker", "o"}}); |
| 279 | matplotlibcpp::grid(true); |
| 280 | matplotlibcpp::legend(); |
| 281 | |
| 282 | matplotlibcpp::figure(); |
| 283 | matplotlibcpp::plot(t_goal_, drivetrain_mode_, |
| 284 | {{"label", "mode"}, {"marker", "o"}}); |
| 285 | matplotlibcpp::plot(t_goal_, throttle_, |
| 286 | {{"label", "throttle"}, {"marker", "o"}}); |
| 287 | matplotlibcpp::plot(t_goal_, wheel_, {{"label", "wheel"}, {"marker", "o"}}); |
| 288 | matplotlibcpp::plot(t_status_, original_line_theta_, |
| 289 | {{"label", "relative theta"}, {"marker", "o"}}); |
| 290 | matplotlibcpp::plot(t_status_, original_line_goal_theta_, |
| 291 | {{"label", "goal theta"}, {"marker", "o"}}); |
| 292 | matplotlibcpp::plot(t_status_, original_line_distance_, |
| 293 | {{"label", "dist to target"}, {"marker", "o"}}); |
| 294 | matplotlibcpp::grid(true); |
| 295 | matplotlibcpp::legend(); |
| 296 | |
| 297 | matplotlibcpp::figure(); |
| 298 | ::std::vector<double> filt_lat_accel_ = FilterValues(lat_accel_, 10); |
| 299 | ::std::vector<double> filt_long_accel_ = FilterValues(long_accel_, 10); |
| 300 | ::std::vector<double> filt_z_accel_ = FilterValues(z_accel_, 10); |
| 301 | matplotlibcpp::plot(t_imu_, filt_lat_accel_, |
| 302 | {{"label", "lateral accel"}, {"marker", "o"}}); |
| 303 | matplotlibcpp::plot(t_imu_, filt_long_accel_, |
| 304 | {{"label", "longitudinal accel"}, {"marker", "o"}}); |
| 305 | matplotlibcpp::plot(t_imu_, filt_z_accel_, |
| 306 | {{"label", "z accel"}, {"marker", "o"}}); |
| 307 | matplotlibcpp::grid(true); |
| 308 | matplotlibcpp::legend(); |
| 309 | |
| 310 | matplotlibcpp::show(); |
| 311 | #endif |
| 312 | } |
| 313 | |
| 314 | private: |
| 315 | // Do a simple moving average with a width of N over the given raw data and |
| 316 | // return the results. For calculating values with N/2 of either edge, we |
| 317 | // assume that the space past the edges is filled with zeroes. |
| 318 | ::std::vector<double> FilterValues(const ::std::vector<double> &raw, int N) { |
| 319 | ::std::vector<double> filt; |
| 320 | for (int ii = 0; ii < static_cast<int>(raw.size()); ++ii) { |
| 321 | double sum = 0; |
| 322 | for (int jj = ::std::max(0, ii - N / 2); |
| 323 | jj < ::std::min(ii + N / 2, static_cast<int>(raw.size()) - 1); |
| 324 | ++jj) { |
| 325 | sum += raw[jj]; |
| 326 | } |
| 327 | filt.push_back(sum / static_cast<double>(N)); |
| 328 | } |
| 329 | return filt; |
| 330 | } |
| 331 | |
| 332 | // TODO(james): Move to some sort of virtual event loop. |
| 333 | ::aos::ShmEventLoop event_loop_; |
| 334 | |
| 335 | EventLoopLocalizer localizer_; |
| 336 | // Whether the robot has been enabled yet. |
| 337 | bool has_been_enabled_ = false; |
| 338 | // Cache of last gyro value to forward to the localizer. |
Philipp Schrader | 790cb54 | 2023-07-05 21:06:52 -0700 | [diff] [blame] | 339 | double latest_gyro_ = 0.0; // rad/sec |
| 340 | double battery_voltage_ = 12.0; // volts |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 341 | ::Eigen::Matrix<double, 2, 1> last_U_{0, 0}; |
| 342 | ::Eigen::Matrix<double, 2, 1> last_last_U_{0, 0}; |
| 343 | |
| 344 | ::aos::logging::linux_code::LogReplayer replayer_; |
| 345 | |
| 346 | // Time at which to start the plot. |
| 347 | ::aos::monotonic_clock::time_point start_time_; |
| 348 | // Length of time to plot. |
| 349 | ::std::chrono::seconds plot_duration_{FLAGS_plot_duration}; |
| 350 | |
| 351 | // All the values to plot, organized in blocks by vectors of data that come in |
| 352 | // at the same time (e.g., status messages come in offset from position |
| 353 | // messages and so while they are all generally the same frequency, we can end |
| 354 | // receiving slightly different numbers of messages on different queues). |
| 355 | |
| 356 | // TODO(james): Improve plotting support. |
| 357 | ::std::vector<double> t_status_; |
| 358 | ::std::vector<double> original_x_; |
| 359 | ::std::vector<double> original_y_; |
| 360 | ::std::vector<double> original_theta_; |
| 361 | ::std::vector<double> original_line_theta_; |
| 362 | ::std::vector<double> original_line_goal_theta_; |
| 363 | ::std::vector<double> original_line_distance_; |
| 364 | |
| 365 | ::std::vector<double> t_pos_; |
| 366 | ::std::vector<double> new_x_; |
| 367 | ::std::vector<double> new_y_; |
| 368 | ::std::vector<double> new_left_encoder_; |
| 369 | ::std::vector<double> new_right_encoder_; |
| 370 | ::std::vector<double> new_left_velocity_; |
| 371 | ::std::vector<double> new_right_velocity_; |
| 372 | ::std::vector<double> new_theta_; |
| 373 | ::std::vector<double> left_voltage_; |
| 374 | ::std::vector<double> right_voltage_; |
| 375 | |
| 376 | ::std::vector<double> t_goal_; |
| 377 | ::std::vector<double> drivetrain_mode_; |
| 378 | ::std::vector<double> throttle_; |
| 379 | ::std::vector<double> wheel_; |
| 380 | |
| 381 | ::std::vector<double> t_imu_; |
| 382 | ::std::vector<double> lat_accel_; |
| 383 | ::std::vector<double> z_accel_; |
| 384 | ::std::vector<double> long_accel_; |
| 385 | }; |
| 386 | |
Stephan Pleines | f63bde8 | 2024-01-13 15:59:33 -0800 | [diff] [blame] | 387 | } // namespace y2019::control_loops::drivetrain |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 388 | |
| 389 | int main(int argc, char *argv[]) { |
| 390 | gflags::ParseCommandLineFlags(&argc, &argv, false); |
| 391 | |
| 392 | if (FLAGS_logfile.empty()) { |
| 393 | fprintf(stderr, "Need a file to replay!\n"); |
| 394 | return EXIT_FAILURE; |
| 395 | } |
| 396 | |
| 397 | ::aos::network::OverrideTeamNumber(FLAGS_team); |
| 398 | |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 399 | ::y2019::control_loops::drivetrain::LocalizerReplayer replay; |
| 400 | replay.ProcessFile(FLAGS_logfile.c_str()); |
| 401 | |
Austin Schuh | ae87e31 | 2020-08-01 16:15:01 -0700 | [diff] [blame] | 402 | return 0; |
James Kuszmaul | f5f50dd | 2019-03-23 12:43:15 -0700 | [diff] [blame] | 403 | } |