ExternalAxis: enforce soft limits in execute_to_mm and enqueue_target_mm
Soft limits in machine coords (min_w/max_w from aux.json) were only checked by gplan. UI jog/move endpoints went through ExternalAxis directly without any check, so the W+ button at home would happily push past max_w into a physical crash. Add _check_soft_limit(target_abs_mm) called by both motion paths: the synchronous execute_to_mm (UI) and the non-blocking enqueue_target_mm (planner). Boundaries inclusive within a 1e-4 epsilon for floating-point round-trip stability. Skipped when the axis isn't homed, matching the standard bbctrl convention that soft limits are gated by homing state. Skipped when max <= min (disabled). Tested locally: - pre-home: 200mm allowed (jog-out-of-trouble path) - post-home: 0 and 134 (boundaries) accepted - post-home: 135 and -1 rejected with clear error - 134.00005 accepted (within epsilon), 134.001 rejected - enqueue path also rejects, propagating up through Planner.next() - max==min config skips check
This commit is contained in:
@@ -140,6 +140,51 @@ class ExternalAxis(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
# ------------------------------------------------------- soft limits
|
||||||
|
|
||||||
|
def _soft_limits(self):
|
||||||
|
"""Return (min_mm, max_mm) in machine coords, or (None, None)
|
||||||
|
if soft limits are disabled (max <= min)."""
|
||||||
|
try:
|
||||||
|
lo = float(self.aux._cfg.get('min_w', 0.0))
|
||||||
|
hi = float(self.aux._cfg.get('max_w', 0.0))
|
||||||
|
except Exception:
|
||||||
|
return (None, None)
|
||||||
|
if hi <= lo:
|
||||||
|
return (None, None)
|
||||||
|
return (lo, hi)
|
||||||
|
|
||||||
|
def _check_soft_limit(self, target_abs_mm):
|
||||||
|
"""Raise ExternalAxisError if target_abs_mm is outside the
|
||||||
|
configured soft limits. Skips the check when the axis isn't
|
||||||
|
homed (matching the standard bbctrl convention that soft
|
||||||
|
limits are gated by homing state) - that lets the user jog
|
||||||
|
away from a stuck position before homing without false
|
||||||
|
rejections.
|
||||||
|
|
||||||
|
Called by both planner-driven motion (enqueue_target_mm) and
|
||||||
|
UI motion (execute_to_mm), so this is the single source of
|
||||||
|
truth regardless of which path triggered the move."""
|
||||||
|
# Honour the homing gate.
|
||||||
|
try:
|
||||||
|
homed = bool(self.aux._homed)
|
||||||
|
except Exception:
|
||||||
|
homed = False
|
||||||
|
if not homed:
|
||||||
|
return
|
||||||
|
lo, hi = self._soft_limits()
|
||||||
|
if lo is None:
|
||||||
|
return
|
||||||
|
# Use a tiny epsilon so floating-point round-trip targets
|
||||||
|
# right at the boundary aren't rejected.
|
||||||
|
eps = 1e-4
|
||||||
|
target = float(target_abs_mm)
|
||||||
|
if target < lo - eps or target > hi + eps:
|
||||||
|
raise ExternalAxisError(
|
||||||
|
'%s axis target %.4f mm is outside soft limits '
|
||||||
|
'[%.3f, %.3f] mm' % (
|
||||||
|
self.axis_letter.upper(), target, lo, hi))
|
||||||
|
|
||||||
# ----------------------------------------------------------- conversion
|
# ----------------------------------------------------------- conversion
|
||||||
|
|
||||||
def mm_to_steps_delta(self, delta_mm):
|
def mm_to_steps_delta(self, delta_mm):
|
||||||
@@ -267,6 +312,9 @@ class ExternalAxis(object):
|
|||||||
endpoints which may want to wait. Most planner-driven motion
|
endpoints which may want to wait. Most planner-driven motion
|
||||||
goes through enqueue_target_mm instead, which is non-blocking.
|
goes through enqueue_target_mm instead, which is non-blocking.
|
||||||
|
|
||||||
|
Soft limits are enforced here (not just in gplan) because the
|
||||||
|
UI jog/move endpoints don't go through the planner.
|
||||||
|
|
||||||
Updates state.<axis>p immediately on completion. For the
|
Updates state.<axis>p immediately on completion. For the
|
||||||
planner-driven path that goes through enqueue_target_mm, the
|
planner-driven path that goes through enqueue_target_mm, the
|
||||||
AVR's own ap reports drive state.<axis>p instead."""
|
AVR's own ap reports drive state.<axis>p instead."""
|
||||||
@@ -275,6 +323,7 @@ class ExternalAxis(object):
|
|||||||
'External axis %r not available (aux disabled or '
|
'External axis %r not available (aux disabled or '
|
||||||
'not connected)' % self.axis_letter)
|
'not connected)' % self.axis_letter)
|
||||||
|
|
||||||
|
self._check_soft_limit(ext_mm)
|
||||||
steps, abs_mm = self._compute_move(ext_mm)
|
steps, abs_mm = self._compute_move(ext_mm)
|
||||||
if steps == 0:
|
if steps == 0:
|
||||||
self._pos_mm = abs_mm
|
self._pos_mm = abs_mm
|
||||||
@@ -304,10 +353,15 @@ class ExternalAxis(object):
|
|||||||
the next line block computes the correct delta.
|
the next line block computes the correct delta.
|
||||||
|
|
||||||
The Planner.__encode hook calls this so the AVR comm thread
|
The Planner.__encode hook calls this so the AVR comm thread
|
||||||
is never blocked by serial RPCs to the ESP."""
|
is never blocked by serial RPCs to the ESP.
|
||||||
|
|
||||||
|
Soft limits are enforced here (defense in depth on top of
|
||||||
|
gplan). Raising stops the planner via Planner.next's
|
||||||
|
exception handler."""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
raise ExternalAxisError(
|
raise ExternalAxisError(
|
||||||
'External axis %r not available' % self.axis_letter)
|
'External axis %r not available' % self.axis_letter)
|
||||||
|
self._check_soft_limit(ext_mm)
|
||||||
steps, abs_mm = self._compute_move(ext_mm)
|
steps, abs_mm = self._compute_move(ext_mm)
|
||||||
# Internal mirror only - drives subsequent delta computation.
|
# Internal mirror only - drives subsequent delta computation.
|
||||||
# state.<axis>p is left to the AVR's status reports.
|
# state.<axis>p is left to the AVR's status reports.
|
||||||
|
|||||||
Reference in New Issue
Block a user