blob: 420ca61376302343599c6c5d3bf80c161fa2b025 [file] [log] [blame]
Adam Snaiderc4b3c192015-02-01 01:30:39 +00001#include <unistd.h>
2
3#include <memory>
4
5#include <random>
6
7#include "gtest/gtest.h"
Adam Snaiderc4b3c192015-02-01 01:30:39 +00008#include "frc971/zeroing/zeroing.h"
Adam Snaiderb4119252015-02-15 01:30:57 +00009#include "frc971/control_loops/control_loops.q.h"
Brian Silvermanf5f8d8e2015-12-06 18:39:12 -050010#include "aos/testing/test_shm.h"
Adam Snaiderc4b3c192015-02-01 01:30:39 +000011#include "aos/common/util/thread.h"
12#include "aos/common/die.h"
Adam Snaiderb4119252015-02-15 01:30:57 +000013#include "frc971/control_loops/position_sensor_sim.h"
Adam Snaiderc4b3c192015-02-01 01:30:39 +000014
15namespace frc971 {
16namespace zeroing {
17
Adam Snaiderb4119252015-02-15 01:30:57 +000018using control_loops::PositionSensorSimulator;
Tyler Chatowf8f03112017-02-05 14:31:34 -080019using constants::PotAndIndexPulseZeroingConstants;
Austin Schuh5f01f152017-02-11 21:34:08 -080020using constants::PotAndAbsoluteEncoderZeroingConstants;
Adam Snaiderc4b3c192015-02-01 01:30:39 +000021
Adam Snaiderb4119252015-02-15 01:30:57 +000022static const size_t kSampleSize = 30;
23static const double kAcceptableUnzeroedError = 0.2;
Adam Snaider3cd11c52015-02-16 02:16:09 +000024static const double kIndexErrorFraction = 0.3;
Diana Vandenberg8fea6ea2017-02-18 17:24:45 -080025static const size_t kMovingBufferSize = 3;
Adam Snaiderc4b3c192015-02-01 01:30:39 +000026
Adam Snaiderb4119252015-02-15 01:30:57 +000027class ZeroingTest : public ::testing::Test {
Adam Snaiderc4b3c192015-02-01 01:30:39 +000028 protected:
29 void SetUp() override { aos::SetDieTestMode(true); }
30
Tyler Chatowf8f03112017-02-05 14:31:34 -080031 void MoveTo(PositionSensorSimulator *simulator,
32 PotAndIndexPulseZeroingEstimator *estimator,
Adam Snaiderb4119252015-02-15 01:30:57 +000033 double new_position) {
Brian Silvermandc4eb102017-02-05 17:34:41 -080034 PotAndIndexPosition sensor_values;
Adam Snaiderb4119252015-02-15 01:30:57 +000035 simulator->MoveTo(new_position);
Brian Silvermandc4eb102017-02-05 17:34:41 -080036 simulator->GetSensorValues(&sensor_values);
37 estimator->UpdateEstimate(sensor_values);
Adam Snaiderb4119252015-02-15 01:30:57 +000038 }
Adam Snaiderc4b3c192015-02-01 01:30:39 +000039
Austin Schuh5f01f152017-02-11 21:34:08 -080040 void MoveTo(PositionSensorSimulator *simulator,
41 PotAndAbsEncoderZeroingEstimator *estimator, double new_position) {
42 PotAndAbsolutePosition sensor_values_;
43 simulator->MoveTo(new_position);
44 simulator->GetSensorValues(&sensor_values_);
45 estimator->UpdateEstimate(sensor_values_);
46 }
47 // TODO(phil): Add new MoveTo overloads for different zeroing estimators.
48
Brian Silvermanf5f8d8e2015-12-06 18:39:12 -050049 ::aos::testing::TestSharedMemory my_shm_;
Adam Snaiderc4b3c192015-02-01 01:30:39 +000050};
51
Adam Snaiderb4119252015-02-15 01:30:57 +000052TEST_F(ZeroingTest, TestMovingAverageFilter) {
53 const double index_diff = 1.0;
54 PositionSensorSimulator sim(index_diff);
55 sim.Initialize(3.6 * index_diff, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -080056 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +000057 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderc4b3c192015-02-01 01:30:39 +000058
59 // The zeroing code is supposed to perform some filtering on the difference
60 // between the potentiometer value and the encoder value. We assume that 300
61 // samples are sufficient to have updated the filter.
62 for (int i = 0; i < 300; i++) {
Adam Snaiderb4119252015-02-15 01:30:57 +000063 MoveTo(&sim, &estimator, 3.3 * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +000064 }
Adam Snaiderb4119252015-02-15 01:30:57 +000065 ASSERT_NEAR(3.3 * index_diff, estimator.position(),
66 kAcceptableUnzeroedError * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +000067
68 for (int i = 0; i < 300; i++) {
Adam Snaiderb4119252015-02-15 01:30:57 +000069 MoveTo(&sim, &estimator, 3.9 * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +000070 }
Adam Snaiderb4119252015-02-15 01:30:57 +000071 ASSERT_NEAR(3.9 * index_diff, estimator.position(),
72 kAcceptableUnzeroedError * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +000073}
74
Adam Snaiderb4119252015-02-15 01:30:57 +000075TEST_F(ZeroingTest, NotZeroedBeforeEnoughSamplesCollected) {
76 double index_diff = 0.5;
77 double position = 3.6 * index_diff;
78 PositionSensorSimulator sim(index_diff);
79 sim.Initialize(position, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -080080 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +000081 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderb4119252015-02-15 01:30:57 +000082
83 // Make sure that the zeroing code does not consider itself zeroed until we
84 // collect a good amount of samples. In this case we're waiting until the
85 // moving average filter is full.
86 for (unsigned int i = 0; i < kSampleSize - 1; i++) {
87 MoveTo(&sim, &estimator, position += index_diff);
88 ASSERT_FALSE(estimator.zeroed());
89 }
90
91 MoveTo(&sim, &estimator, position);
92 ASSERT_TRUE(estimator.zeroed());
93}
94
95TEST_F(ZeroingTest, TestLotsOfMovement) {
96 double index_diff = 1.0;
97 PositionSensorSimulator sim(index_diff);
98 sim.Initialize(3.6, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -080099 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000100 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000101
102 // The zeroing code is supposed to perform some filtering on the difference
103 // between the potentiometer value and the encoder value. We assume that 300
104 // samples are sufficient to have updated the filter.
105 for (int i = 0; i < 300; i++) {
Adam Snaiderb4119252015-02-15 01:30:57 +0000106 MoveTo(&sim, &estimator, 3.6);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000107 }
Adam Snaiderb4119252015-02-15 01:30:57 +0000108 ASSERT_NEAR(3.6, estimator.position(), kAcceptableUnzeroedError * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000109
110 // With a single index pulse the zeroing estimator should be able to lock
111 // onto the true value of the position.
Adam Snaiderb4119252015-02-15 01:30:57 +0000112 MoveTo(&sim, &estimator, 4.01);
113 ASSERT_NEAR(4.01, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000114
Adam Snaiderb4119252015-02-15 01:30:57 +0000115 MoveTo(&sim, &estimator, 4.99);
116 ASSERT_NEAR(4.99, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000117
Adam Snaiderb4119252015-02-15 01:30:57 +0000118 MoveTo(&sim, &estimator, 3.99);
119 ASSERT_NEAR(3.99, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000120
Adam Snaiderb4119252015-02-15 01:30:57 +0000121 MoveTo(&sim, &estimator, 3.01);
122 ASSERT_NEAR(3.01, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000123
Adam Snaiderb4119252015-02-15 01:30:57 +0000124 MoveTo(&sim, &estimator, 13.55);
125 ASSERT_NEAR(13.55, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000126}
127
Adam Snaiderb4119252015-02-15 01:30:57 +0000128TEST_F(ZeroingTest, TestDifferentIndexDiffs) {
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000129 double index_diff = 0.89;
Adam Snaiderb4119252015-02-15 01:30:57 +0000130 PositionSensorSimulator sim(index_diff);
131 sim.Initialize(3.5 * index_diff, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800132 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000133 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000134
135 // The zeroing code is supposed to perform some filtering on the difference
136 // between the potentiometer value and the encoder value. We assume that 300
137 // samples are sufficient to have updated the filter.
138 for (int i = 0; i < 300; i++) {
Adam Snaiderb4119252015-02-15 01:30:57 +0000139 MoveTo(&sim, &estimator, 3.5 * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000140 }
Adam Snaiderb4119252015-02-15 01:30:57 +0000141 ASSERT_NEAR(3.5 * index_diff, estimator.position(),
142 kAcceptableUnzeroedError * index_diff);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000143
144 // With a single index pulse the zeroing estimator should be able to lock
145 // onto the true value of the position.
Adam Snaiderb4119252015-02-15 01:30:57 +0000146 MoveTo(&sim, &estimator, 4.01);
147 ASSERT_NEAR(4.01, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000148
Adam Snaiderb4119252015-02-15 01:30:57 +0000149 MoveTo(&sim, &estimator, 4.99);
150 ASSERT_NEAR(4.99, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000151
Adam Snaiderb4119252015-02-15 01:30:57 +0000152 MoveTo(&sim, &estimator, 3.99);
153 ASSERT_NEAR(3.99, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000154
Adam Snaiderb4119252015-02-15 01:30:57 +0000155 MoveTo(&sim, &estimator, 3.01);
156 ASSERT_NEAR(3.01, estimator.position(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000157
Adam Snaiderb4119252015-02-15 01:30:57 +0000158 MoveTo(&sim, &estimator, 13.55);
159 ASSERT_NEAR(13.55, estimator.position(), 0.001);
160}
161
162TEST_F(ZeroingTest, TestPercentage) {
163 double index_diff = 0.89;
164 PositionSensorSimulator sim(index_diff);
165 sim.Initialize(3.5 * index_diff, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800166 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000167 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderb4119252015-02-15 01:30:57 +0000168
169 for (unsigned int i = 0; i < kSampleSize / 2; i++) {
170 MoveTo(&sim, &estimator, 3.5 * index_diff);
171 }
172 ASSERT_NEAR(0.5, estimator.offset_ratio_ready(), 0.001);
Austin Schuh7485dbb2016-02-08 00:21:58 -0800173 ASSERT_FALSE(estimator.offset_ready());
174
175 for (unsigned int i = 0; i < kSampleSize / 2; i++) {
176 MoveTo(&sim, &estimator, 3.5 * index_diff);
177 }
178 ASSERT_NEAR(1.0, estimator.offset_ratio_ready(), 0.001);
179 ASSERT_TRUE(estimator.offset_ready());
Adam Snaiderb4119252015-02-15 01:30:57 +0000180}
181
182TEST_F(ZeroingTest, TestOffset) {
183 double index_diff = 0.89;
184 PositionSensorSimulator sim(index_diff);
185 sim.Initialize(3.1 * index_diff, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800186 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000187 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Adam Snaiderb4119252015-02-15 01:30:57 +0000188
Philipp Schrader41d82912015-02-15 03:44:23 +0000189 MoveTo(&sim, &estimator, 3.1 * index_diff);
190
Adam Snaiderb4119252015-02-15 01:30:57 +0000191 for (unsigned int i = 0; i < kSampleSize; i++) {
192 MoveTo(&sim, &estimator, 5.0 * index_diff);
193 }
Philipp Schrader41d82912015-02-15 03:44:23 +0000194
Adam Snaiderb4119252015-02-15 01:30:57 +0000195 ASSERT_NEAR(3.1 * index_diff, estimator.offset(), 0.001);
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000196}
197
Philipp Schrader41d82912015-02-15 03:44:23 +0000198TEST_F(ZeroingTest, WaitForIndexPulseAfterReset) {
199 double index_diff = 0.6;
200 PositionSensorSimulator sim(index_diff);
201 sim.Initialize(3.1 * index_diff, index_diff / 3.0);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800202 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000203 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Philipp Schrader41d82912015-02-15 03:44:23 +0000204
205 // Make sure to fill up the averaging filter with samples.
206 for (unsigned int i = 0; i < kSampleSize; i++) {
207 MoveTo(&sim, &estimator, 3.1 * index_diff);
208 }
209
210 // Make sure we're not zeroed until we hit an index pulse.
211 ASSERT_FALSE(estimator.zeroed());
212
213 // Trigger an index pulse; we should now be zeroed.
214 MoveTo(&sim, &estimator, 4.5 * index_diff);
215 ASSERT_TRUE(estimator.zeroed());
216
217 // Reset the zeroing logic and supply a bunch of samples within the current
218 // index segment.
219 estimator.Reset();
220 for (unsigned int i = 0; i < kSampleSize; i++) {
221 MoveTo(&sim, &estimator, 4.2 * index_diff);
222 }
223
224 // Make sure we're not zeroed until we hit an index pulse.
225 ASSERT_FALSE(estimator.zeroed());
226
227 // Trigger another index pulse; we should be zeroed again.
228 MoveTo(&sim, &estimator, 3.1 * index_diff);
229 ASSERT_TRUE(estimator.zeroed());
230}
231
Philipp Schrader030ad182015-02-15 05:40:58 +0000232TEST_F(ZeroingTest, TestNonZeroIndexPulseOffsets) {
233 const double index_diff = 0.9;
234 const double known_index_pos = 3.5 * index_diff;
235 PositionSensorSimulator sim(index_diff);
236 sim.Initialize(3.3 * index_diff, index_diff / 3.0, known_index_pos);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800237 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000238 kSampleSize, index_diff, known_index_pos, kIndexErrorFraction});
Philipp Schrader030ad182015-02-15 05:40:58 +0000239
240 // Make sure to fill up the averaging filter with samples.
241 for (unsigned int i = 0; i < kSampleSize; i++) {
242 MoveTo(&sim, &estimator, 3.3 * index_diff);
243 }
244
245 // Make sure we're not zeroed until we hit an index pulse.
246 ASSERT_FALSE(estimator.zeroed());
247
248 // Trigger an index pulse; we should now be zeroed.
249 MoveTo(&sim, &estimator, 3.7 * index_diff);
250 ASSERT_TRUE(estimator.zeroed());
251 ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
252 ASSERT_DOUBLE_EQ(3.7 * index_diff, estimator.position());
253
254 // Trigger one more index pulse and check the offset.
255 MoveTo(&sim, &estimator, 4.7 * index_diff);
256 ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
257 ASSERT_DOUBLE_EQ(4.7 * index_diff, estimator.position());
258}
259
Philipp Schrader53f4b6d2015-02-15 22:32:08 +0000260TEST_F(ZeroingTest, BasicErrorAPITest) {
261 const double index_diff = 1.0;
Tyler Chatowf8f03112017-02-05 14:31:34 -0800262 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000263 kSampleSize, index_diff, 0.0, kIndexErrorFraction});
Philipp Schrader53f4b6d2015-02-15 22:32:08 +0000264 PositionSensorSimulator sim(index_diff);
265 sim.Initialize(1.5 * index_diff, index_diff / 3.0, 0.0);
266
267 // Perform a simple move and make sure that no error occured.
268 MoveTo(&sim, &estimator, 3.5 * index_diff);
269 ASSERT_FALSE(estimator.error());
270
271 // Trigger an error and make sure it's reported.
272 estimator.TriggerError();
273 ASSERT_TRUE(estimator.error());
274
275 // Make sure that it can recover after a reset.
276 estimator.Reset();
277 ASSERT_FALSE(estimator.error());
278 MoveTo(&sim, &estimator, 4.5 * index_diff);
279 MoveTo(&sim, &estimator, 5.5 * index_diff);
280 ASSERT_FALSE(estimator.error());
281}
282
Adam Snaider3cd11c52015-02-16 02:16:09 +0000283// I want to test that the the zeroing class can
284// detect an error when the starting position
285// changes too much. I do so by creating the
286// simulator at an 'X' positon, making sure
287// that the estimator is zeroed, and then
288// initializing the simulator at another
289// position. After making sure it's zeroed,
290// if the error() function returns true,
291// then, it works.
292TEST_F(ZeroingTest, TestOffsetError) {
293 const double index_diff = 0.8;
294 const double known_index_pos = 2 * index_diff;
Austin Schuh5f01f152017-02-11 21:34:08 -0800295 const size_t sample_size = 30;
Adam Snaider3cd11c52015-02-16 02:16:09 +0000296 PositionSensorSimulator sim(index_diff);
297 sim.Initialize(10 * index_diff, index_diff / 3.0, known_index_pos);
Tyler Chatowf8f03112017-02-05 14:31:34 -0800298 PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
Adam Snaider3cd11c52015-02-16 02:16:09 +0000299 sample_size, index_diff, known_index_pos, kIndexErrorFraction});
300
Austin Schuh5f01f152017-02-11 21:34:08 -0800301 for (size_t i = 0; i < sample_size; i++) {
Adam Snaider3cd11c52015-02-16 02:16:09 +0000302 MoveTo(&sim, &estimator, 13 * index_diff);
303 }
304 MoveTo(&sim, &estimator, 8 * index_diff);
305
306 ASSERT_TRUE(estimator.zeroed());
307 ASSERT_FALSE(estimator.error());
308 sim.Initialize(9.0 * index_diff + 0.31 * index_diff, index_diff / 3.0,
309 known_index_pos);
310 MoveTo(&sim, &estimator, 9 * index_diff);
311 ASSERT_TRUE(estimator.zeroed());
312 ASSERT_TRUE(estimator.error());
313}
314
Austin Schuh5f01f152017-02-11 21:34:08 -0800315// Makes sure that using an absolute encoder lets us zero without moving.
316TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingWithoutMovement) {
317 const double index_diff = 1.0;
318 PositionSensorSimulator sim(index_diff);
319
320 const double start_pos = 2.1;
321 double measured_absolute_position = 0.3 * index_diff;
322
Diana Vandenberg8fea6ea2017-02-18 17:24:45 -0800323 PotAndAbsoluteEncoderZeroingConstants constants{kSampleSize, index_diff,
324 measured_absolute_position,
325 0.1, kMovingBufferSize};
Austin Schuh5f01f152017-02-11 21:34:08 -0800326
327 sim.Initialize(start_pos, index_diff / 3.0, 0.0,
328 constants.measured_absolute_position);
329
330 PotAndAbsEncoderZeroingEstimator estimator(constants);
331
Diana Vandenberg8fea6ea2017-02-18 17:24:45 -0800332 for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
Austin Schuh5f01f152017-02-11 21:34:08 -0800333 MoveTo(&sim, &estimator, start_pos);
334 ASSERT_FALSE(estimator.zeroed());
335 }
336
337 MoveTo(&sim, &estimator, start_pos);
338 ASSERT_TRUE(estimator.zeroed());
339 EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
340}
341
342// Makes sure that using an absolute encoder doesn't let us zero while moving.
343TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingWithMovement) {
344 const double index_diff = 1.0;
345 PositionSensorSimulator sim(index_diff);
346
347 const double start_pos = 10 * index_diff;
348 double measured_absolute_position = 0.3 * index_diff;
349
Diana Vandenberg8fea6ea2017-02-18 17:24:45 -0800350 PotAndAbsoluteEncoderZeroingConstants constants{kSampleSize, index_diff,
351 measured_absolute_position,
352 0.1, kMovingBufferSize};
Austin Schuh5f01f152017-02-11 21:34:08 -0800353
354 sim.Initialize(start_pos, index_diff / 3.0, 0.0,
355 constants.measured_absolute_position);
356
357 PotAndAbsEncoderZeroingEstimator estimator(constants);
358
Diana Vandenberg8fea6ea2017-02-18 17:24:45 -0800359 for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
Austin Schuh5f01f152017-02-11 21:34:08 -0800360 MoveTo(&sim, &estimator, start_pos + i * index_diff);
361 ASSERT_FALSE(estimator.zeroed());
362 }
363 MoveTo(&sim, &estimator, start_pos + 10 * index_diff);
364
365 MoveTo(&sim, &estimator, start_pos);
366 ASSERT_FALSE(estimator.zeroed());
367}
Adam Snaiderc4b3c192015-02-01 01:30:39 +0000368} // namespace zeroing
369} // namespace frc971