Jog: enforce A-axis soft limits during hold-to-jog
Pendant hold-to-jog could drive A past min_mm / max_mm because the
JOG path bypassed the planner-driven soft-limit checks. Wire the
host to compute a step-counter target for whichever soft-limit
boundary lies in the requested direction and pass it through the
new JOG target= parameter.
AuxAxis.jog_start now accepts target_steps; when given it emits
'JOG ... target=<n>'. The ESP picks the decel start point so the
motor ramps to a smooth stop AT the boundary, with no overshoot.
Jog._a_soft_limit_target_steps:
- Returns None when the axis is not homed -- pre-home setup jogs
are still allowed (matches the rest of the manual-jog API).
- Otherwise projects min_mm/max_mm into step space (honoring
dir_sign) and returns the boundary on the requested side of
the current position.
Jog._a_start additionally refuses to send the JOG when the
position is already at-or-past the boundary in the requested
direction, so we don't depend on the ESP's wrong-side reject path
for the common 'press button while sitting on the limit' case.
Verified end-to-end on hardware (bare ESP, no gantry):
JOG dir=+ maxrate=400 target=300 stops at pos=299
JOG dir=+ target=-50 (wrong side) rejected immediately.
This commit is contained in:
@@ -314,9 +314,18 @@ class AuxAxis(object):
|
||||
# pos=<p>` 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user