@@ -83,6 +83,14 @@ bool SteerController::on_cal(kart::SteerCalCmd cmd, uint16_t pot_raw) {
8383 return false ;
8484}
8585
86+ bool SteerController::raw_over_travel (uint16_t raw) const {
87+ if (!cal_.valid ) return false ;
88+ int32_t lo = cal_.raw_left < cal_.raw_right ? cal_.raw_left : cal_.raw_right ;
89+ int32_t hi = cal_.raw_left < cal_.raw_right ? cal_.raw_right : cal_.raw_left ;
90+ int32_t m = cfg_.over_travel_margin_raw ;
91+ return (int32_t )raw < lo - m || (int32_t )raw > hi + m;
92+ }
93+
8694int16_t SteerController::clamp_to_soft_limits (int16_t setpoint_cdeg) const {
8795 int32_t lo = cal_.angle_left_cdeg + cfg_.soft_limit_margin_cdeg ;
8896 int32_t hi = cal_.angle_right_cdeg - cfg_.soft_limit_margin_cdeg ;
@@ -104,6 +112,16 @@ float SteerController::tick(uint32_t now_ms, uint16_t pot_raw) {
104112 measured_cdeg_ = pot_to_angle_cdeg (cal_, pot_raw);
105113 }
106114
115+ // Over-travel: the steering went past a calibrated end stop. If the motor was
116+ // actively driving, this is a hard latched fault (it should have held inside
117+ // the soft limits) — cut power and require inspection. If the motor was not
118+ // driving (e.g. hand-moved during setup), don't latch, but block activation
119+ // below until the pot comes back inside range.
120+ bool over_travel_now = raw_over_travel (pot_raw);
121+ if (over_travel_now && state_ == kart::SteerState::kActive ) {
122+ over_travel_fault_ = true ;
123+ }
124+
107125 if (hard_faulted ()) {
108126 state_ = kart::SteerState::kFault ;
109127 last_output_ = 0 .0f ;
@@ -132,7 +150,8 @@ float SteerController::tick(uint32_t now_ms, uint16_t pot_raw) {
132150 enable_ = false ;
133151 }
134152
135- bool want_active = enable_ && fresh && cal_.valid ;
153+ // Never (re)start the motor while the pot is sitting past a stop.
154+ bool want_active = enable_ && fresh && cal_.valid && !over_travel_now;
136155 if (!want_active) {
137156 if (state_ == kart::SteerState::kActive ) {
138157 pid_.reset ();
@@ -149,30 +168,33 @@ float SteerController::tick(uint32_t now_ms, uint16_t pot_raw) {
149168 float error = (float )(target - measured_cdeg_);
150169 float out = pid_.update (error, 10 ); // caller runs a fixed 100 Hz tick
151170
152- // Stall detection: sustained near-saturated output with no movement.
171+ // Convergence watchdog: while pushing hard the |error| must keep shrinking.
172+ // A jammed motor (no movement) holds |error| constant; a wrong-way runaway
173+ // (e.g. inverted feedback sign / swapped motor leads) grows it. Either way,
174+ // if we push for stall_timeout_ms without making stall_min_delta_cdeg of
175+ // progress, fault and cut the motor — this catches a runaway *before* it
176+ // reaches a stop, in addition to the over-travel guard above.
177+ int32_t abs_err = error < 0 .0f ? (int32_t )-error : (int32_t )error;
153178 bool pushing = (out > cfg_.stall_output_frac * cfg_.output_limit ) ||
154179 (out < -cfg_.stall_output_frac * cfg_.output_limit );
155180 if (pushing) {
156181 if (!stall_window_open_) {
157182 stall_window_open_ = true ;
158183 stall_window_start_ms_ = now_ms;
159- stall_window_start_cdeg_ = measured_cdeg_;
160- } else {
161- int16_t delta = (int16_t )(measured_cdeg_ - stall_window_start_cdeg_);
162- if (delta < 0 ) delta = (int16_t )-delta;
163- if (delta >= cfg_.stall_min_delta_cdeg ) {
164- // It is moving; restart the window.
165- stall_window_start_ms_ = now_ms;
166- stall_window_start_cdeg_ = measured_cdeg_;
167- } else if ((uint32_t )(now_ms - stall_window_start_ms_) >=
168- cfg_.stall_timeout_ms ) {
169- stall_fault_ = true ;
170- state_ = kart::SteerState::kFault ;
171- pid_.reset ();
172- stall_window_open_ = false ;
173- last_output_ = 0 .0f ;
174- return 0 .0f ;
175- }
184+ stall_window_start_abserr_ = abs_err;
185+ } else if (stall_window_start_abserr_ - abs_err >=
186+ cfg_.stall_min_delta_cdeg ) {
187+ // Error is shrinking: the loop is converging. Restart the window.
188+ stall_window_start_ms_ = now_ms;
189+ stall_window_start_abserr_ = abs_err;
190+ } else if ((uint32_t )(now_ms - stall_window_start_ms_) >=
191+ cfg_.stall_timeout_ms ) {
192+ stall_fault_ = true ;
193+ state_ = kart::SteerState::kFault ;
194+ pid_.reset ();
195+ stall_window_open_ = false ;
196+ last_output_ = 0 .0f ;
197+ return 0 .0f ;
176198 }
177199 } else {
178200 stall_window_open_ = false ;
@@ -186,6 +208,7 @@ uint8_t SteerController::fault_bits() const {
186208 uint8_t bits = 0 ;
187209 if (pot_range_fault_) bits |= kart::kSteerFaultPotRange ;
188210 if (stall_fault_) bits |= kart::kSteerFaultStall ;
211+ if (over_travel_fault_) bits |= kart::kSteerFaultOverTravel ;
189212 if (setpoint_stale_) bits |= kart::kSteerFaultSetpointStale ;
190213 if (talon_lost_) bits |= kart::kSteerFaultTalonLost ;
191214 if (!cal_.valid ) bits |= kart::kSteerFaultNotCalibrated ;
0 commit comments