blob: 978eb15e1f3d2a5391cd5fb261b6523a4c8e2c3b [file] [log] [blame]
James Kuszmaul51fa1ae2022-02-26 00:49:57 -08001#ifndef Y2022_LOCALIZER_LOCALIZER_H_
2#define Y2022_LOCALIZER_LOCALIZER_H_
James Kuszmaul29c59522022-02-12 16:44:26 -08003
4#include "Eigen/Dense"
5#include "Eigen/Geometry"
James Kuszmaul29c59522022-02-12 16:44:26 -08006#include "aos/containers/ring_buffer.h"
James Kuszmaul8c4f6592022-02-26 15:49:30 -08007#include "aos/events/event_loop.h"
8#include "aos/network/message_bridge_server_generated.h"
James Kuszmaul29c59522022-02-12 16:44:26 -08009#include "aos/time/time.h"
James Kuszmaul29c59522022-02-12 16:44:26 -080010#include "frc971/control_loops/drivetrain/drivetrain_output_generated.h"
James Kuszmaul8c4f6592022-02-26 15:49:30 -080011#include "frc971/control_loops/drivetrain/improved_down_estimator.h"
James Kuszmaul29c59522022-02-12 16:44:26 -080012#include "frc971/control_loops/drivetrain/localizer_generated.h"
13#include "frc971/zeroing/imu_zeroer.h"
James Kuszmaul5ed29dd2022-02-13 18:32:06 -080014#include "frc971/zeroing/wrap.h"
James Kuszmaul8c4f6592022-02-26 15:49:30 -080015#include "y2022/control_loops/superstructure/superstructure_status_generated.h"
16#include "y2022/localizer/localizer_output_generated.h"
17#include "y2022/localizer/localizer_status_generated.h"
18#include "y2022/vision/target_estimate_generated.h"
James Kuszmaul29c59522022-02-12 16:44:26 -080019
20namespace frc971::controls {
21
22namespace testing {
23class LocalizerTest;
24}
25
26// Localizer implementation that makes use of a 6-axis IMU, encoder readings,
27// drivetrain voltages, and camera returns to localize the robot. Meant to
28// be run on a raspberry pi.
29//
30// This operates on the principle that the drivetrain can be in one of two
31// modes:
32// 1) A "normal" mode where it is obeying the regular drivetrain model, with
33// minimal lateral motion and no major external disturbances. This is
34// referred to as the "model" mode in the code/variable names.
35// 2) An non-holonomic mode where the robot is just flying around on a 2-D
36// plane with no meaningful constraints (referred to as an "accel" model
37// in the code, because we rely primarily on the accelerometer readings).
38//
39// In order to determine which mode to be in, we attempt to track whether the
40// two models are diverging substantially. In order to do this, we maintain a
41// 1-second long queue of "branches". A branch is generated every X iterations
42// and contains a model state and an accel state. When the branch starts, the
43// two will have identical states. For the remaining 1 second, the model state
44// will evolve purely according to the drivetrian model, and the accel state
45// will evolve purely using IMU readings.
46//
47// When the branch reaches 1 second in age, we calculate a residual associated
48// with how much the accel and model based states diverged. If they have
49// diverged substantially, that implies that the model is a poor match for
50// whatever has been happening to the robot in the past second, so if we were
51// previously relying on the model, we will override the current "actual"
52// state with the branched accel state, and then continue to update the accel
53// state based on IMU readings.
54// If we are currently in the accel state, we will continue generating branches
55// until the branches stop diverging--this will indicate that the model
56// matches the accelerometer readings again, and so we will swap back to
57// the model-based state.
58//
59// TODO:
60// * Implement paying attention to camera readings.
61// * Tune for ADIS16505/real robot.
James Kuszmaul29c59522022-02-12 16:44:26 -080062class ModelBasedLocalizer {
63 public:
64 static constexpr size_t kX = 0;
65 static constexpr size_t kY = 1;
66 static constexpr size_t kTheta = 2;
67
68 static constexpr size_t kVelocityX = 3;
69 static constexpr size_t kVelocityY = 4;
70 static constexpr size_t kNAccelStates = 5;
71
72 static constexpr size_t kLeftEncoder = 3;
73 static constexpr size_t kLeftVelocity = 4;
74 static constexpr size_t kLeftVoltageError = 5;
75 static constexpr size_t kRightEncoder = 6;
76 static constexpr size_t kRightVelocity = 7;
77 static constexpr size_t kRightVoltageError = 8;
78 static constexpr size_t kNModelStates = 9;
79
80 static constexpr size_t kNModelOutputs = 3;
81
82 static constexpr size_t kNAccelOuputs = 1;
83
84 static constexpr size_t kAccelX = 0;
85 static constexpr size_t kAccelY = 1;
86 static constexpr size_t kThetaRate = 2;
87 static constexpr size_t kNAccelInputs = 3;
88
89 static constexpr size_t kLeftVoltage = 0;
90 static constexpr size_t kRightVoltage = 1;
91 static constexpr size_t kNModelInputs = 2;
92
James Kuszmaul93825a02022-02-13 16:50:33 -080093 // Branching period, in cycles.
James Kuszmaul5ed29dd2022-02-13 18:32:06 -080094 // Needs 10 to even stay alive, and still at ~96% CPU.
95 // ~20 gives ~55-60% CPU.
James Kuszmaul896b4ec2022-02-26 22:56:29 -080096 static constexpr int kBranchPeriod = 100;
James Kuszmaul93825a02022-02-13 16:50:33 -080097
James Kuszmaul29c59522022-02-12 16:44:26 -080098 typedef Eigen::Matrix<double, kNModelStates, 1> ModelState;
99 typedef Eigen::Matrix<double, kNAccelStates, 1> AccelState;
100 typedef Eigen::Matrix<double, kNModelInputs, 1> ModelInput;
101 typedef Eigen::Matrix<double, kNAccelInputs, 1> AccelInput;
102
103 ModelBasedLocalizer(
104 const control_loops::drivetrain::DrivetrainConfig<double> &dt_config);
105 void HandleImu(aos::monotonic_clock::time_point t,
106 const Eigen::Vector3d &gyro, const Eigen::Vector3d &accel,
107 const Eigen::Vector2d encoders, const Eigen::Vector2d voltage);
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800108 void HandleTurret(aos::monotonic_clock::time_point sample_time,
109 double turret_position, double turret_velocity);
110 void HandleImageMatch(aos::monotonic_clock::time_point sample_time,
111 const y2022::vision::TargetEstimate *target);
James Kuszmaul29c59522022-02-12 16:44:26 -0800112 void HandleReset(aos::monotonic_clock::time_point,
113 const Eigen::Vector3d &xytheta);
114
115 flatbuffers::Offset<ModelBasedStatus> PopulateStatus(
116 flatbuffers::FlatBufferBuilder *fbb);
117
118 Eigen::Vector3d xytheta() const {
119 if (using_model_) {
120 return current_state_.model_state.block<3, 1>(0, 0);
121 } else {
122 return current_state_.accel_state.block<3, 1>(0, 0);
123 }
124 }
125
James Kuszmaul10d3fd42022-02-25 21:57:36 -0800126 Eigen::Quaterniond orientation() const { return last_orientation_; }
127
James Kuszmaul29c59522022-02-12 16:44:26 -0800128 AccelState accel_state() const { return current_state_.accel_state; };
129
James Kuszmaul5ed29dd2022-02-13 18:32:06 -0800130 void set_longitudinal_offset(double offset) { long_offset_ = offset; }
131
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800132 void TallyRejection(const RejectionReason reason) {
133 statistics_.total_candidates++;
134 statistics_.rejection_counts[static_cast<size_t>(reason)]++;
135 }
136
James Kuszmaul29c59522022-02-12 16:44:26 -0800137 private:
138 struct CombinedState {
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800139 AccelState accel_state = AccelState::Zero();
140 ModelState model_state = ModelState::Zero();
141 aos::monotonic_clock::time_point branch_time =
142 aos::monotonic_clock::min_time;
143 double accumulated_divergence = 0.0;
144 };
145
146 // Struct to store state data needed to perform a camera correction, since
147 // camera corrections require looking back in time.
148 struct OldPosition {
149 aos::monotonic_clock::time_point sample_time =
150 aos::monotonic_clock::min_time;
151 Eigen::Vector3d xytheta = Eigen::Vector3d::Zero();
152 double turret_position = 0.0;
153 double turret_velocity = 0.0;
James Kuszmaul29c59522022-02-12 16:44:26 -0800154 };
155
156 static flatbuffers::Offset<AccelBasedState> BuildAccelState(
157 flatbuffers::FlatBufferBuilder *fbb, const AccelState &state);
158
159 static flatbuffers::Offset<ModelBasedState> BuildModelState(
160 flatbuffers::FlatBufferBuilder *fbb, const ModelState &state);
161
162 Eigen::Matrix<double, kNModelStates, kNModelStates> AModel(
163 const ModelState &state) const;
164 Eigen::Matrix<double, kNAccelStates, kNAccelStates> AAccel() const;
165 ModelState DiffModel(const ModelState &state, const ModelInput &U) const;
166 AccelState DiffAccel(const AccelState &state, const AccelInput &U) const;
167
168 ModelState UpdateModel(const ModelState &model, const ModelInput &input,
169 aos::monotonic_clock::duration dt) const;
170 AccelState UpdateAccel(const AccelState &accel, const AccelInput &input,
171 aos::monotonic_clock::duration dt) const;
172
173 AccelState AccelStateForModelState(const ModelState &state) const;
174 ModelState ModelStateForAccelState(const AccelState &state,
175 const Eigen::Vector2d &encoders,
176 const double yaw_rate) const;
177 double ModelDivergence(const CombinedState &state,
178 const AccelInput &accel_inputs,
179 const Eigen::Vector2d &filtered_accel,
180 const ModelInput &model_inputs);
James Kuszmaul5ed29dd2022-02-13 18:32:06 -0800181 void UpdateState(
182 CombinedState *state,
183 const Eigen::Matrix<double, kNModelStates, kNModelOutputs> &K,
184 const Eigen::Matrix<double, kNModelOutputs, 1> &Z,
185 const Eigen::Matrix<double, kNModelOutputs, kNModelStates> &H,
186 const AccelInput &accel_input, const ModelInput &model_input,
187 aos::monotonic_clock::duration dt);
James Kuszmaul29c59522022-02-12 16:44:26 -0800188
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800189 const OldPosition GetStateForTime(aos::monotonic_clock::time_point time);
190
191 // Returns the transformation to get from the camera frame to the robot frame
192 // for the specified state.
193 const Eigen::Matrix<double, 4, 4> CameraTransform(
194 const OldPosition &state,
195 const frc971::vision::calibration::CameraCalibration *calibration,
196 std::optional<RejectionReason> *rejection_reason) const;
197
198 // Returns the robot x/y position implied by the specified camera data and
199 // estimated state from that time.
200 const std::optional<Eigen::Vector2d> CameraMeasuredRobotPosition(
201 const OldPosition &state, const y2022::vision::TargetEstimate *target,
202 std::optional<RejectionReason> *rejection_reason) const;
203
James Kuszmaul29c59522022-02-12 16:44:26 -0800204 const control_loops::drivetrain::DrivetrainConfig<double> dt_config_;
205 const StateFeedbackHybridPlantCoefficients<2, 2, 2, double>
206 velocity_drivetrain_coefficients_;
207 Eigen::Matrix<double, kNModelStates, kNModelStates> A_continuous_model_;
208 Eigen::Matrix<double, kNAccelStates, kNAccelStates> A_continuous_accel_;
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800209 Eigen::Matrix<double, kNAccelStates, kNAccelStates> A_discrete_accel_;
James Kuszmaul29c59522022-02-12 16:44:26 -0800210 Eigen::Matrix<double, kNModelStates, kNModelInputs> B_continuous_model_;
211 Eigen::Matrix<double, kNAccelStates, kNAccelInputs> B_continuous_accel_;
212
213 Eigen::Matrix<double, kNModelStates, kNModelStates> Q_continuous_model_;
214
215 Eigen::Matrix<double, kNModelStates, kNModelStates> P_model_;
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800216
217 Eigen::Matrix<double, kNAccelStates, kNAccelStates> Q_continuous_accel_;
218 Eigen::Matrix<double, kNAccelStates, kNAccelStates> Q_discrete_accel_;
219
220 Eigen::Matrix<double, kNAccelStates, kNAccelStates> P_accel_;
221
222 control_loops::drivetrain::DrivetrainUkf down_estimator_;
223
James Kuszmaul29c59522022-02-12 16:44:26 -0800224 // When we are following the model, we will, on each iteration:
225 // 1) Perform a model-based update of a single state.
226 // 2) Add a hypothetical non-model-based entry based on the current state.
227 // 3) Evict too-old non-model-based entries.
James Kuszmaul29c59522022-02-12 16:44:26 -0800228
229 // Buffer of old branches these are all created by initializing a new
230 // model-based state based on the current state, and then initializing a new
231 // accel-based state on top of that new model-based state (to eliminate the
232 // impact of any lateral motion).
233 // We then integrate up all of these states and observe how much the model and
234 // accel based states of each branch compare to one another.
James Kuszmaul93825a02022-02-13 16:50:33 -0800235 aos::RingBuffer<CombinedState, 2000 / kBranchPeriod> branches_;
236 int branch_counter_ = 0;
James Kuszmaul29c59522022-02-12 16:44:26 -0800237
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800238 // Buffer of old x/y/theta positions. This is used so that we can have a
239 // reference for exactly where we thought a camera was when it took an image.
240 aos::RingBuffer<OldPosition, 500 / kBranchPeriod> old_positions_;
241
James Kuszmaul29c59522022-02-12 16:44:26 -0800242 CombinedState current_state_;
243 aos::monotonic_clock::time_point t_ = aos::monotonic_clock::min_time;
244 bool using_model_;
245
James Kuszmaul5ed29dd2022-02-13 18:32:06 -0800246 // X position of the IMU, in meters. 0 = center of robot, positive = ahead of
247 // center, negative = behind center.
248 double long_offset_ = -0.15;
249
James Kuszmaul29c59522022-02-12 16:44:26 -0800250 double last_residual_ = 0.0;
251 double filtered_residual_ = 0.0;
252 Eigen::Vector2d filtered_residual_accel_ = Eigen::Vector2d::Zero();
253 Eigen::Vector3d abs_accel_ = Eigen::Vector3d::Zero();
254 double velocity_residual_ = 0.0;
255 double accel_residual_ = 0.0;
256 double theta_rate_residual_ = 0.0;
257 int hysteresis_count_ = 0;
James Kuszmaul10d3fd42022-02-25 21:57:36 -0800258 Eigen::Quaterniond last_orientation_ = Eigen::Quaterniond::Identity();
James Kuszmaul29c59522022-02-12 16:44:26 -0800259
260 int clock_resets_ = 0;
261
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800262 std::optional<aos::monotonic_clock::time_point> last_turret_update_;
263 double latest_turret_position_ = 0.0;
264 double latest_turret_velocity_ = 0.0;
265
266 // Stuff to track faults.
267 static constexpr size_t kNumRejectionReasons =
268 static_cast<int>(RejectionReason::MAX) -
269 static_cast<int>(RejectionReason::MIN) + 1;
270
271 struct Statistics {
272 int total_accepted = 0;
273 int total_candidates = 0;
274 static_assert(0 == static_cast<int>(RejectionReason::MIN));
275 static_assert(
276 kNumRejectionReasons ==
277 sizeof(
278 std::invoke_result<decltype(EnumValuesRejectionReason)>::type) /
279 sizeof(RejectionReason),
280 "RejectionReason has non-contiguous error values.");
281 std::array<int, kNumRejectionReasons> rejection_counts;
282 };
283 Statistics statistics_;
284
James Kuszmaul29c59522022-02-12 16:44:26 -0800285 friend class testing::LocalizerTest;
286};
287
288class EventLoopLocalizer {
289 public:
290 EventLoopLocalizer(
291 aos::EventLoop *event_loop,
292 const control_loops::drivetrain::DrivetrainConfig<double> &dt_config);
293
James Kuszmaul5ed29dd2022-02-13 18:32:06 -0800294 ModelBasedLocalizer *localizer() { return &model_based_; }
295
James Kuszmaul29c59522022-02-12 16:44:26 -0800296 private:
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800297 std::optional<aos::monotonic_clock::duration> ClockOffset(
298 std::string_view pi);
James Kuszmaul29c59522022-02-12 16:44:26 -0800299 aos::EventLoop *event_loop_;
300 ModelBasedLocalizer model_based_;
301 aos::Sender<LocalizerStatus> status_sender_;
302 aos::Sender<LocalizerOutput> output_sender_;
James Kuszmaul29c59522022-02-12 16:44:26 -0800303 aos::Fetcher<frc971::control_loops::drivetrain::Output> output_fetcher_;
James Kuszmaul8c4f6592022-02-26 15:49:30 -0800304 aos::Fetcher<aos::message_bridge::ServerStatistics> clock_offset_fetcher_;
James Kuszmaul29c59522022-02-12 16:44:26 -0800305 zeroing::ImuZeroer zeroer_;
306 aos::monotonic_clock::time_point last_output_send_ =
307 aos::monotonic_clock::min_time;
James Kuszmaul5ed29dd2022-02-13 18:32:06 -0800308 std::optional<aos::monotonic_clock::time_point> last_pico_timestamp_;
309 aos::monotonic_clock::duration pico_offset_error_;
310 // t = pico_offset_ + pico_timestamp.
311 // Note that this can drift over sufficiently long time periods!
312 std::optional<std::chrono::nanoseconds> pico_offset_;
313
314 zeroing::UnwrapSensor left_encoder_;
315 zeroing::UnwrapSensor right_encoder_;
James Kuszmaul29c59522022-02-12 16:44:26 -0800316};
317} // namespace frc971::controls
James Kuszmaul51fa1ae2022-02-26 00:49:57 -0800318#endif // Y2022_LOCALIZER_LOCALIZER_H_