diff --git a/src/py/bbctrl/AuxAxis.py b/src/py/bbctrl/AuxAxis.py index c57420d..5629265 100644 --- a/src/py/bbctrl/AuxAxis.py +++ b/src/py/bbctrl/AuxAxis.py @@ -314,9 +314,18 @@ class AuxAxis(object): # pos=

` arrives later; our reader picks it up and resyncs # _pos_steps via the same path as STEPS. def jog_start(self, direction, max_rate_sps=None, - accel_sps2=None, ignore_limits=False): + accel_sps2=None, ignore_limits=False, + target_steps=None): """Begin a continuous-rate jog. `direction` is +1 or -1. - Returns once the ESP has accepted the JOG command.""" + Returns once the ESP has accepted the JOG command. + + target_steps (optional): a signed step-counter value. The + ESP picks the deceleration start point so the motor ramps + smoothly from the current cruise rate to step_start_rate + and stops AT this counter value. Used to enforce host-side + soft limits without overshoot. The target must be on the + side of the current g_pos that matches `direction`; the + ESP rejects a wrong-side target with reason=softlimit.""" self._require_present() if direction not in (-1, +1): raise AuxAxisError('jog_start direction must be +/-1') @@ -327,13 +336,10 @@ class AuxAxis(object): else int(self._cfg['step_accel_sps2'])) if rate < 1: rate = 1 if accel < 1: accel = 1 - # Track the in-flight JOG so the reader can deliver the - # terminal [jog] done line back to us. We use a dedicated - # background thread so jog_start can return as soon as the - # `[jog] started` ack lands -- the terminal line may arrive - # seconds later (after JOGSTOP). cmd = 'JOG dir=%s maxrate=%d accel=%d safe=%d' % ( sign, rate, accel, 0 if ignore_limits else 1) + if target_steps is not None: + cmd += ' target=%d' % int(target_steps) # Capture both the immediate ack AND the eventual terminal # line in a single _rpc call would block; instead fire the # ack-only RPC here and let _on_line handle the terminal diff --git a/src/py/bbctrl/Jog.py b/src/py/bbctrl/Jog.py index 8095255..ce045ed 100644 --- a/src/py/bbctrl/Jog.py +++ b/src/py/bbctrl/Jog.py @@ -116,12 +116,52 @@ class Jog(inevent.JogHandler): except Exception as e: self.log.warning('A-axis jog_stop failed: %s', e) + def _a_soft_limit_target_steps(self, aux, direction): + """Return a step-counter target that the ESP should ramp to + a smooth stop at when jogging in `direction`. Returns None + when no soft limits should be enforced (axis unhomed or + limits not configured). + + The ESP's `g_pos` is the raw signed step counter; the host + side mirror (`aux._pos_steps`) tracks it. Soft limits are + configured in machine-mm; we project them into step space + with the same `_mm_to_steps` used for ordinary moves. + + Direction sign: when `dir_sign=+1` (typical), positive jog + direction increases g_pos. We pick the limit whose step + value is on the `direction` side of the current g_pos.""" + try: + if not bool(aux._homed): + return None + cfg = aux._cfg + lo_mm = float(cfg.get('min_mm', 0.0)) + hi_mm = float(cfg.get('max_mm', 0.0)) + if hi_mm <= lo_mm: + return None + lo_steps = aux._mm_to_steps(lo_mm) + hi_steps = aux._mm_to_steps(hi_mm) + # _mm_to_steps applies dir_sign, so lo_steps may be + # numerically larger than hi_steps when dir_sign<0. + # Sort so we know which is "more positive in g_pos". + top_steps = max(lo_steps, hi_steps) + bottom_steps = min(lo_steps, hi_steps) + return top_steps if direction > 0 else bottom_steps + except Exception: + return None + def _a_start(self, direction): ext = getattr(self.ctrl, 'ext_axis', None) ext_state = ('present' if (ext is not None and ext.enabled) else 'unavailable') + scale = self._a_speed_scale() + target_steps = None + cur_steps = None + if ext is not None and ext.enabled: + target_steps = self._a_soft_limit_target_steps( + ext.aux, direction) + try: cur_steps = int(ext.aux._pos_steps) + except Exception: cur_steps = None if A_DRY_RUN: - scale = self._a_speed_scale() try: step_max = (int(ext.aux._cfg['step_max_sps']) if ext is not None and ext.enabled else -1) @@ -131,26 +171,42 @@ class Jog(inevent.JogHandler): step_max, accel = -1, -1 self.log.info( 'AJOG DRYRUN _a_start dir=%+d ext=%s speed=%d scale=%.4f ' - 'step_max=%d accel=%d (would send JOG)', - direction, ext_state, self.speed, scale, step_max, accel) + 'step_max=%d accel=%d cur_steps=%s target_steps=%s ' + '(would send JOG)', + direction, ext_state, self.speed, scale, step_max, accel, + cur_steps, target_steps) return if ext is None or not ext.enabled or direction == 0: return - scale = self._a_speed_scale() try: aux = ext.aux max_rate = max(1, int(int(aux._cfg['step_max_sps']) * scale)) accel = int(aux._cfg['step_accel_sps2']) - # ignore_limits=True (safe=0): pendant jog is allowed - # before homing, matching the rest of the manual-jog API. - # When the axis IS homed, the ESP still aborts on a - # limit-toward hit because it tracks home_dir separately - # from `safe` in our updated firmware (see jogTask). + # If the axis is already at-or-past the soft-limit + # boundary in the requested direction, refuse the jog + # rather than sending a wrong-side target the ESP would + # reject. The host knows position immediately whereas + # the ESP only learns g_pos via WPOS?. + if target_steps is not None and cur_steps is not None: + at_limit = ((direction > 0 and cur_steps >= target_steps) + or (direction < 0 and cur_steps <= target_steps)) + if at_limit: + self.log.info( + 'A-axis jog refused: at soft limit ' + '(cur=%d target=%d dir=%+d)', + cur_steps, target_steps, direction) + return + # ignore_limits=True (safe=0) when the axis is unhomed: + # pendant jog is allowed before homing for setup. When + # homed, soft limits are enforced via target_steps and + # the ESP's hardware-limit abort still applies + # unconditionally (movingTowardLimit in jogTask). ignore = not bool(aux._homed) aux.jog_start(direction, max_rate_sps=max_rate, accel_sps2=accel, - ignore_limits=ignore) + ignore_limits=ignore, + target_steps=target_steps) except Exception as e: self.log.warning('A-axis jog_start failed: %s', e)