blob: 7506a0ca2e94e61ac9a2183cc19394e3d1165c11 [file] [log] [blame]
Brian Silverman17f503e2015-08-02 18:17:18 -07001#include "y2014/control_loops/shooter/shooter.h"
2
3#include <stdio.h>
4
5#include <algorithm>
6#include <limits>
7
8#include "aos/common/controls/control_loops.q.h"
9#include "aos/common/logging/logging.h"
10#include "aos/common/logging/queue_logging.h"
11
12#include "y2014/constants.h"
13#include "y2014/control_loops/shooter/shooter_motor_plant.h"
14
15namespace frc971 {
16namespace control_loops {
17
Austin Schuh9d4aca82015-11-08 14:41:31 -080018using ::y2014::control_loops::shooter::kSpringConstant;
19using ::y2014::control_loops::shooter::kMaxExtension;
20using ::y2014::control_loops::shooter::MakeShooterLoop;
Brian Silverman17f503e2015-08-02 18:17:18 -070021using ::aos::time::Time;
22
23void ZeroedStateFeedbackLoop::CapU() {
24 const double old_voltage = voltage_;
25 voltage_ += U(0, 0);
26
27 uncapped_voltage_ = voltage_;
28
29 // Make sure that reality and the observer can't get too far off. There is a
30 // delay by one cycle between the applied voltage and X_hat(2, 0), so compare
31 // against last cycle's voltage.
32 if (X_hat(2, 0) > last_voltage_ + 4.0) {
33 voltage_ -= X_hat(2, 0) - (last_voltage_ + 4.0);
34 LOG(DEBUG, "Capping due to runaway\n");
35 } else if (X_hat(2, 0) < last_voltage_ - 4.0) {
36 voltage_ += X_hat(2, 0) - (last_voltage_ - 4.0);
37 LOG(DEBUG, "Capping due to runaway\n");
38 }
39
40 voltage_ = std::min(max_voltage_, voltage_);
41 voltage_ = std::max(-max_voltage_, voltage_);
42 mutable_U(0, 0) = voltage_ - old_voltage;
43
44 LOG_STRUCT(DEBUG, "output", ShooterVoltageToLog(X_hat(2, 0), voltage_));
45
46 last_voltage_ = voltage_;
47 capped_goal_ = false;
48}
49
50void ZeroedStateFeedbackLoop::CapGoal() {
51 if (uncapped_voltage() > max_voltage_) {
52 double dx;
53 if (controller_index() == 0) {
54 dx = (uncapped_voltage() - max_voltage_) /
55 (K(0, 0) - A(1, 0) * K(0, 2) / A(1, 2));
56 mutable_R(0, 0) -= dx;
57 mutable_R(2, 0) -= -A(1, 0) / A(1, 2) * dx;
58 } else {
59 dx = (uncapped_voltage() - max_voltage_) / K(0, 0);
60 mutable_R(0, 0) -= dx;
61 }
62 capped_goal_ = true;
63 LOG_STRUCT(DEBUG, "to prevent windup", ShooterMovingGoal(dx));
64 } else if (uncapped_voltage() < -max_voltage_) {
65 double dx;
66 if (controller_index() == 0) {
67 dx = (uncapped_voltage() + max_voltage_) /
68 (K(0, 0) - A(1, 0) * K(0, 2) / A(1, 2));
69 mutable_R(0, 0) -= dx;
70 mutable_R(2, 0) -= -A(1, 0) / A(1, 2) * dx;
71 } else {
72 dx = (uncapped_voltage() + max_voltage_) / K(0, 0);
73 mutable_R(0, 0) -= dx;
74 }
75 capped_goal_ = true;
76 LOG_STRUCT(DEBUG, "to prevent windup", ShooterMovingGoal(dx));
77 } else {
78 capped_goal_ = false;
79 }
80}
81
82void ZeroedStateFeedbackLoop::RecalculatePowerGoal() {
83 if (controller_index() == 0) {
84 mutable_R(2, 0) = (-A(1, 0) / A(1, 2) * R(0, 0) - A(1, 1) / A(1, 2) * R(1, 0));
85 } else {
86 mutable_R(2, 0) = -A(1, 1) / A(1, 2) * R(1, 0);
87 }
88}
89
90void ZeroedStateFeedbackLoop::SetCalibration(double encoder_val,
91 double known_position) {
92 double old_position = absolute_position();
93 double previous_offset = offset_;
94 offset_ = known_position - encoder_val;
95 double doffset = offset_ - previous_offset;
96 mutable_X_hat(0, 0) += doffset;
Brian Silverman17f503e2015-08-02 18:17:18 -070097 // Offset the goal so we don't move.
98 mutable_R(0, 0) += doffset;
99 if (controller_index() == 0) {
100 mutable_R(2, 0) += -A(1, 0) / A(1, 2) * (doffset);
101 }
102 LOG_STRUCT(
103 DEBUG, "sensor edge (fake?)",
104 ShooterChangeCalibration(encoder_val, known_position, old_position,
105 absolute_position(), previous_offset, offset_));
106}
107
Brian Silvermanbe5ded62015-05-14 00:23:49 -0400108ShooterMotor::ShooterMotor(control_loops::ShooterQueue *my_shooter)
109 : aos::controls::ControlLoop<control_loops::ShooterQueue>(my_shooter),
Brian Silverman17f503e2015-08-02 18:17:18 -0700110 shooter_(MakeShooterLoop()),
111 state_(STATE_INITIALIZE),
112 loading_problem_end_time_(0, 0),
113 load_timeout_(0, 0),
114 shooter_brake_set_time_(0, 0),
115 unload_timeout_(0, 0),
116 shot_end_time_(0, 0),
117 cycles_not_moved_(0),
118 shot_count_(0),
119 zeroed_(false),
120 distal_posedge_validation_cycles_left_(0),
121 proximal_posedge_validation_cycles_left_(0),
122 last_distal_current_(true),
123 last_proximal_current_(true) {}
124
125double ShooterMotor::PowerToPosition(double power) {
126 const frc971::constants::Values &values = constants::GetValues();
127 double maxpower = 0.5 * kSpringConstant *
128 (kMaxExtension * kMaxExtension -
129 (kMaxExtension - values.shooter.upper_limit) *
130 (kMaxExtension - values.shooter.upper_limit));
131 if (power < 0) {
132 LOG_STRUCT(WARNING, "negative power", PowerAdjustment(power, 0));
133 power = 0;
134 } else if (power > maxpower) {
135 LOG_STRUCT(WARNING, "power too high", PowerAdjustment(power, maxpower));
136 power = maxpower;
137 }
138
139 double mp = kMaxExtension * kMaxExtension - (power + power) / kSpringConstant;
140 double new_pos = 0.10;
141 if (mp < 0) {
142 LOG(ERROR,
143 "Power calculation has negative number before square root (%f).\n", mp);
144 } else {
145 new_pos = kMaxExtension - ::std::sqrt(mp);
146 }
147
148 new_pos = ::std::min(::std::max(new_pos, values.shooter.lower_limit),
149 values.shooter.upper_limit);
150 return new_pos;
151}
152
153double ShooterMotor::PositionToPower(double position) {
154 double power = kSpringConstant * position * (kMaxExtension - position / 2.0);
155 return power;
156}
157
158void ShooterMotor::CheckCalibrations(
Brian Silvermanbe5ded62015-05-14 00:23:49 -0400159 const control_loops::ShooterQueue::Position *position) {
Brian Silverman17f503e2015-08-02 18:17:18 -0700160 CHECK_NOTNULL(position);
161 const frc971::constants::Values &values = constants::GetValues();
162
163 // TODO(austin): Validate that this is the right edge.
164 // If we see a posedge on any of the hall effects,
165 if (position->pusher_proximal.posedge_count != last_proximal_posedge_count_ &&
166 !last_proximal_current_) {
167 proximal_posedge_validation_cycles_left_ = 2;
168 }
169 if (proximal_posedge_validation_cycles_left_ > 0) {
170 if (position->pusher_proximal.current) {
171 --proximal_posedge_validation_cycles_left_;
172 if (proximal_posedge_validation_cycles_left_ == 0) {
173 shooter_.SetCalibration(
174 position->pusher_proximal.posedge_value,
175 values.shooter.pusher_proximal.upper_angle);
176
177 LOG(DEBUG, "Setting calibration using proximal sensor\n");
178 zeroed_ = true;
179 }
180 } else {
181 proximal_posedge_validation_cycles_left_ = 0;
182 }
183 }
184
185 if (position->pusher_distal.posedge_count != last_distal_posedge_count_ &&
186 !last_distal_current_) {
187 distal_posedge_validation_cycles_left_ = 2;
188 }
189 if (distal_posedge_validation_cycles_left_ > 0) {
190 if (position->pusher_distal.current) {
191 --distal_posedge_validation_cycles_left_;
192 if (distal_posedge_validation_cycles_left_ == 0) {
193 shooter_.SetCalibration(
194 position->pusher_distal.posedge_value,
195 values.shooter.pusher_distal.upper_angle);
196
197 LOG(DEBUG, "Setting calibration using distal sensor\n");
198 zeroed_ = true;
199 }
200 } else {
201 distal_posedge_validation_cycles_left_ = 0;
202 }
203 }
204}
205
206// Positive is out, and positive power is out.
207void ShooterMotor::RunIteration(
Brian Silvermanbe5ded62015-05-14 00:23:49 -0400208 const control_loops::ShooterQueue::Goal *goal,
209 const control_loops::ShooterQueue::Position *position,
210 control_loops::ShooterQueue::Output *output,
211 control_loops::ShooterQueue::Status *status) {
Brian Silverman17f503e2015-08-02 18:17:18 -0700212 constexpr double dt = 0.01;
213
214 if (goal && ::std::isnan(goal->shot_power)) {
215 state_ = STATE_ESTOP;
216 LOG(ERROR, "Estopping because got a shot power of NAN.\n");
217 }
218
219 // we must always have these or we have issues.
220 if (status == NULL) {
221 if (output) output->voltage = 0;
222 LOG(ERROR, "Thought I would just check for null and die.\n");
223 return;
224 }
225 status->ready = false;
226
227 if (WasReset()) {
228 state_ = STATE_INITIALIZE;
229 last_distal_current_ = position->pusher_distal.current;
230 last_proximal_current_ = position->pusher_proximal.current;
231 }
232 if (position) {
233 shooter_.CorrectPosition(position->position);
234 }
235
236 // Disable the motors now so that all early returns will return with the
237 // motors disabled.
238 if (output) output->voltage = 0;
239
240 const frc971::constants::Values &values = constants::GetValues();
241
242 // Don't even let the control loops run.
243 bool shooter_loop_disable = false;
244
245 const bool disabled =
246 !::aos::joystick_state.get() || !::aos::joystick_state->enabled;
247
248 // If true, move the goal if we saturate.
249 bool cap_goal = false;
250
251 // TODO(austin): Move the offset if we see or don't see a hall effect when we
252 // expect to see one.
253 // Probably not needed yet.
254
255 if (position) {
256 int last_controller_index = shooter_.controller_index();
257 if (position->plunger && position->latch) {
258 // Use the controller without the spring if the latch is set and the
259 // plunger is back
260 shooter_.set_controller_index(1);
261 } else {
262 // Otherwise use the controller with the spring.
263 shooter_.set_controller_index(0);
264 }
265 if (shooter_.controller_index() != last_controller_index) {
266 shooter_.RecalculatePowerGoal();
267 }
268 }
269
270 switch (state_) {
271 case STATE_INITIALIZE:
272 if (position) {
273 // Reinitialize the internal filter state.
274 shooter_.InitializeState(position->position);
275
276 // Start off with the assumption that we are at the value
277 // futhest back given our sensors.
278 if (position->pusher_distal.current) {
279 shooter_.SetCalibration(position->position,
280 values.shooter.pusher_distal.lower_angle);
281 } else if (position->pusher_proximal.current) {
282 shooter_.SetCalibration(position->position,
283 values.shooter.pusher_proximal.upper_angle);
284 } else {
285 shooter_.SetCalibration(position->position,
286 values.shooter.upper_limit);
287 }
288
289 // Go to the current position.
290 shooter_.SetGoalPosition(shooter_.absolute_position(), 0.0);
291 // If the plunger is all the way back, we want to be latched.
292 latch_piston_ = position->plunger;
293 brake_piston_ = false;
294 if (position->latch == latch_piston_) {
295 state_ = STATE_REQUEST_LOAD;
296 } else {
297 shooter_loop_disable = true;
298 LOG(DEBUG,
299 "Not moving on until the latch has moved to avoid a crash\n");
300 }
301 } else {
302 // If we can't start yet because we don't know where we are, set the
303 // latch and brake to their defaults.
304 latch_piston_ = true;
305 brake_piston_ = true;
306 }
307 break;
308 case STATE_REQUEST_LOAD:
309 if (position) {
310 zeroed_ = false;
311 if (position->pusher_distal.current ||
312 position->pusher_proximal.current) {
313 // We started on the sensor, back up until we are found.
314 // If the plunger is all the way back and not latched, it won't be
315 // there for long.
316 state_ = STATE_LOAD_BACKTRACK;
317
318 // The plunger is already back and latched. Don't release it.
319 if (position->plunger && position->latch) {
320 latch_piston_ = true;
321 } else {
322 latch_piston_ = false;
323 }
324 } else if (position->plunger && position->latch) {
325 // The plunger is back and we are latched. We most likely got here
326 // from Initialize, in which case we want to 'load' again anyways to
327 // zero.
328 Load();
329 latch_piston_ = true;
330 } else {
331 // Off the sensor, start loading.
332 Load();
333 latch_piston_ = false;
334 }
335 }
336
337 // Hold our current position.
338 shooter_.SetGoalPosition(shooter_.absolute_position(), 0.0);
339 brake_piston_ = false;
340 break;
341 case STATE_LOAD_BACKTRACK:
342 // If we are here, then that means we started past the edge where we want
343 // to zero. Move backwards until we don't see the sensor anymore.
344 // The plunger is contacting the pusher (or will be shortly).
345
346 if (!disabled) {
347 shooter_.SetGoalPosition(
348 shooter_.goal_position() + values.shooter.zeroing_speed * dt,
349 values.shooter.zeroing_speed);
350 }
351 cap_goal = true;
352 shooter_.set_max_voltage(4.0);
353
354 if (position) {
355 if (!position->pusher_distal.current &&
356 !position->pusher_proximal.current) {
357 Load();
358 }
359 latch_piston_ = position->plunger;
360 }
361
362 brake_piston_ = false;
363 break;
364 case STATE_LOAD:
365 // If we are disabled right now, reset the timer.
366 if (disabled) {
367 Load();
368 // Latch defaults to true when disabled. Leave it latched until we have
369 // useful sensor data.
370 latch_piston_ = true;
371 }
372 if (output == nullptr) {
373 load_timeout_ += ::aos::controls::kLoopFrequency;
374 }
375 // Go to 0, which should be the latch position, or trigger a hall effect
376 // on the way. If we don't see edges where we are supposed to, the
377 // offset will be updated by code above.
378 shooter_.SetGoalPosition(0.0, 0.0);
379
380 if (position) {
381 CheckCalibrations(position);
382
383 // Latch if the plunger is far enough back to trigger the hall effect.
384 // This happens when the distal sensor is triggered.
385 latch_piston_ = position->pusher_distal.current || position->plunger;
386
387 // Check if we are latched and back. Make sure the plunger is all the
388 // way back as well.
389 if (position->plunger && position->latch &&
390 position->pusher_distal.current) {
391 if (!zeroed_) {
392 state_ = STATE_REQUEST_LOAD;
393 } else {
394 state_ = STATE_PREPARE_SHOT;
395 }
396 } else if (position->plunger &&
397 ::std::abs(shooter_.absolute_position() -
398 shooter_.goal_position()) < 0.001) {
399 // We are at the goal, but not latched.
400 state_ = STATE_LOADING_PROBLEM;
401 loading_problem_end_time_ = Time::Now() + kLoadProblemEndTimeout;
402 }
403 }
404 if (load_timeout_ < Time::Now()) {
405 if (position) {
406 if (!position->pusher_distal.current ||
407 !position->pusher_proximal.current) {
408 state_ = STATE_ESTOP;
409 LOG(ERROR, "Estopping because took too long to load.\n");
410 }
411 }
412 }
413 brake_piston_ = false;
414 break;
415 case STATE_LOADING_PROBLEM:
416 if (disabled) {
417 state_ = STATE_REQUEST_LOAD;
418 break;
419 }
420 // We got to the goal, but the latch hasn't registered as down. It might
421 // be stuck, or on it's way but not there yet.
422 if (Time::Now() > loading_problem_end_time_) {
423 // Timeout by unloading.
424 Unload();
425 } else if (position && position->plunger && position->latch) {
426 // If both trigger, we are latched.
427 state_ = STATE_PREPARE_SHOT;
428 }
429 // Move a bit further back to help it trigger.
430 // If the latch is slow due to the air flowing through the tubes or
431 // inertia, but is otherwise free, this won't have much time to do
432 // anything and is safe. Otherwise this gives us a bit more room to free
433 // up the latch.
434 shooter_.SetGoalPosition(values.shooter.lower_limit, 0.0);
435 if (position) {
436 LOG(DEBUG, "Waiting on latch: plunger %d, latch: %d\n",
437 position->plunger, position->latch);
438 }
439
440 latch_piston_ = true;
441 brake_piston_ = false;
442 break;
443 case STATE_PREPARE_SHOT:
444 // Move the shooter to the shot power set point and then lock the brake.
445 // TODO(austin): Timeout. Low priority.
446
447 if (goal) {
448 shooter_.SetGoalPosition(PowerToPosition(goal->shot_power), 0.0);
449 }
450
451 LOG(DEBUG, "PDIFF: absolute_position: %.2f, pow: %.2f\n",
452 shooter_.absolute_position(),
453 goal ? PowerToPosition(goal->shot_power)
454 : ::std::numeric_limits<double>::quiet_NaN());
455 if (goal &&
456 ::std::abs(shooter_.absolute_position() -
457 PowerToPosition(goal->shot_power)) < 0.001 &&
458 ::std::abs(shooter_.absolute_velocity()) < 0.005) {
459 // We are there, set the brake and move on.
460 latch_piston_ = true;
461 brake_piston_ = true;
462 shooter_brake_set_time_ = Time::Now() + kShooterBrakeSetTime;
463 state_ = STATE_READY;
464 } else {
465 latch_piston_ = true;
466 brake_piston_ = false;
467 }
468 if (goal && goal->unload_requested) {
469 Unload();
470 }
471 break;
472 case STATE_READY:
473 LOG(DEBUG, "In ready\n");
474 // Wait until the brake is set, and a shot is requested or the shot power
475 // is changed.
476 if (Time::Now() > shooter_brake_set_time_) {
477 status->ready = true;
478 // We have waited long enough for the brake to set, turn the shooter
479 // control loop off.
480 shooter_loop_disable = true;
481 LOG(DEBUG, "Brake is now set\n");
482 if (goal && goal->shot_requested && !disabled) {
483 LOG(DEBUG, "Shooting now\n");
484 shooter_loop_disable = true;
485 shot_end_time_ = Time::Now() + kShotEndTimeout;
486 firing_starting_position_ = shooter_.absolute_position();
487 state_ = STATE_FIRE;
488 }
489 }
490 if (state_ == STATE_READY && goal &&
491 ::std::abs(shooter_.absolute_position() -
492 PowerToPosition(goal->shot_power)) > 0.002) {
493 // TODO(austin): Add a state to release the brake.
494
495 // TODO(austin): Do we want to set the brake here or after shooting?
496 // Depends on air usage.
497 status->ready = false;
498 LOG(DEBUG, "Preparing shot again.\n");
499 state_ = STATE_PREPARE_SHOT;
500 }
501
502 if (goal) {
503 shooter_.SetGoalPosition(PowerToPosition(goal->shot_power), 0.0);
504 }
505
506 latch_piston_ = true;
507 brake_piston_ = true;
508
509 if (goal && goal->unload_requested) {
510 Unload();
511 }
512 break;
513
514 case STATE_FIRE:
515 if (disabled) {
516 if (position) {
517 if (position->plunger) {
518 // If disabled and the plunger is still back there, reset the
519 // timeout.
520 shot_end_time_ = Time::Now() + kShotEndTimeout;
521 }
522 }
523 }
524 shooter_loop_disable = true;
525 // Count the number of contiguous cycles during which we haven't moved.
526 if (::std::abs(last_position_.position - shooter_.absolute_position()) <
527 0.0005) {
528 ++cycles_not_moved_;
529 } else {
530 cycles_not_moved_ = 0;
531 }
532
533 // If we have moved any amount since the start and the shooter has now
534 // been still for a couple cycles, the shot finished.
535 // Also move on if it times out.
Austin Schuh829e6ad2015-11-08 14:10:37 -0800536 if (((::std::abs(firing_starting_position_ -
537 shooter_.absolute_position()) > 0.0005 &&
538 cycles_not_moved_ > 3) ||
539 Time::Now() > shot_end_time_) &&
540 ::aos::robot_state->voltage_battery > 10.5) {
Brian Silverman17f503e2015-08-02 18:17:18 -0700541 state_ = STATE_REQUEST_LOAD;
542 ++shot_count_;
543 }
544 latch_piston_ = false;
545 brake_piston_ = true;
546 break;
547 case STATE_UNLOAD:
548 // Reset the timeouts.
549 if (disabled) Unload();
550
551 // If it is latched and the plunger is back, move the pusher back to catch
552 // the plunger.
553 bool all_back;
554 if (position) {
555 all_back = position->plunger && position->latch;
556 } else {
557 all_back = last_position_.plunger && last_position_.latch;
558 }
559
560 if (all_back) {
561 // Pull back to 0, 0.
562 shooter_.SetGoalPosition(0.0, 0.0);
563 if (shooter_.absolute_position() < 0.005) {
564 // When we are close enough, 'fire'.
565 latch_piston_ = false;
566 } else {
567 latch_piston_ = true;
568
569 if (position) {
570 CheckCalibrations(position);
571 }
572 }
573 } else {
574 // The plunger isn't all the way back, or it is and it is unlatched, so
575 // we can now unload.
576 shooter_.SetGoalPosition(shooter_.absolute_position(), 0.0);
577 latch_piston_ = false;
578 state_ = STATE_UNLOAD_MOVE;
579 unload_timeout_ = Time::Now() + kUnloadTimeout;
580 }
581
582 if (Time::Now() > unload_timeout_) {
583 // We have been stuck trying to unload for way too long, give up and
584 // turn everything off.
585 state_ = STATE_ESTOP;
586 LOG(ERROR, "Estopping because took too long to unload.\n");
587 }
588
589 brake_piston_ = false;
590 break;
591 case STATE_UNLOAD_MOVE: {
592 if (disabled) {
593 unload_timeout_ = Time::Now() + kUnloadTimeout;
594 shooter_.SetGoalPosition(shooter_.absolute_position(), 0.0);
595 }
596 cap_goal = true;
597 shooter_.set_max_voltage(6.0);
598
599 // Slowly move back until we hit the upper limit.
600 // If we were at the limit last cycle, we are done unloading.
601 // This is because if we saturate, we might hit the limit before we are
602 // actually there.
603 if (shooter_.goal_position() >= values.shooter.upper_limit) {
604 shooter_.SetGoalPosition(values.shooter.upper_limit, 0.0);
605 // We don't want the loop fighting the spring when we are unloaded.
606 // Turn it off.
607 shooter_loop_disable = true;
608 state_ = STATE_READY_UNLOAD;
609 } else {
610 shooter_.SetGoalPosition(
611 ::std::min(
612 values.shooter.upper_limit,
613 shooter_.goal_position() + values.shooter.unload_speed * dt),
614 values.shooter.unload_speed);
615 }
616
617 latch_piston_ = false;
618 brake_piston_ = false;
619 } break;
620 case STATE_READY_UNLOAD:
621 if (goal && goal->load_requested) {
622 state_ = STATE_REQUEST_LOAD;
623 }
624 // If we are ready to load again,
625 shooter_loop_disable = true;
626
627 latch_piston_ = false;
628 brake_piston_ = false;
629 break;
630
631 case STATE_ESTOP:
632 LOG(WARNING, "estopped\n");
633 // Totally lost, go to a safe state.
634 shooter_loop_disable = true;
635 latch_piston_ = true;
636 brake_piston_ = true;
637 break;
638 }
639
640 if (!shooter_loop_disable) {
641 LOG_STRUCT(DEBUG, "running the loop",
642 ShooterStatusToLog(shooter_.goal_position(),
643 shooter_.absolute_position()));
644 if (!cap_goal) {
645 shooter_.set_max_voltage(12.0);
646 }
647 shooter_.Update(output == NULL);
648 if (cap_goal) {
649 shooter_.CapGoal();
650 }
651 // We don't really want to output anything if we went through everything
652 // assuming the motors weren't working.
653 if (output) output->voltage = shooter_.voltage();
654 } else {
655 shooter_.Update(true);
656 shooter_.ZeroPower();
657 if (output) output->voltage = 0.0;
658 }
659
660 status->hard_stop_power = PositionToPower(shooter_.absolute_position());
661
662 if (output) {
663 output->latch_piston = latch_piston_;
664 output->brake_piston = brake_piston_;
665 }
666
667 if (position) {
668 LOG_STRUCT(DEBUG, "internal state",
669 ShooterStateToLog(
670 shooter_.absolute_position(), shooter_.absolute_velocity(),
671 state_, position->latch, position->pusher_proximal.current,
672 position->pusher_distal.current, position->plunger,
673 brake_piston_, latch_piston_));
674
675 last_position_ = *position;
676
677 last_distal_posedge_count_ = position->pusher_distal.posedge_count;
678 last_proximal_posedge_count_ = position->pusher_proximal.posedge_count;
679 last_distal_current_ = position->pusher_distal.current;
680 last_proximal_current_ = position->pusher_proximal.current;
681 }
682
683 status->shots = shot_count_;
684}
685
686void ShooterMotor::ZeroOutputs() {
687 queue_group()->output.MakeWithBuilder()
688 .voltage(0)
689 .latch_piston(latch_piston_)
690 .brake_piston(brake_piston_)
691 .Send();
692}
693
694} // namespace control_loops
695} // namespace frc971