Z-A coupling: drop active jog/MDI auto-coordination, keep refuse-only check
The active rewriter for jogs/MDI didn't help anyway because the
continuous-jog buttons send rate-based /api/jog commands to the AVR
and bypass the planner+MDI path entirely. Rather than build out
continuous-jog coupling on the ESP firmware or fake it with browser
ticks, simplify back to:
* Runtime check (Planner.__encode + ExternalAxis motion entry
points) refuses any move that would worsen the Z-A gap. Already
improvement-aware so X/Y jogs and Z-up/A-down recoveries pass.
* File preprocessor (AuxPreprocessor) injects pre-position A
moves into uploaded gcode so well-formed programs run without
operator intervention.
Operator workflow: jog freely down to the safe band; if you need to
go deeper, lower A first (aux jog mm) or use a step-jog MDI like
'G91 G0 Z-10 A-10' that includes the A delta. Programs do the right
thing on their own.
This commit is contained in:
@@ -38,7 +38,6 @@
|
|||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
import re
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -259,212 +258,6 @@ class ExternalAxis(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ---- MDI / jog auto-coordination -----------------------------------
|
|
||||||
#
|
|
||||||
# The file preprocessor injects pre-position A moves at upload time,
|
|
||||||
# but jogs and ad-hoc MDI lines bypass that path. Without help, a
|
|
||||||
# jog past the safe band would be refused and the operator has to
|
|
||||||
# manually drop A before retrying. That's no fun and no longer
|
|
||||||
# matches the documented behaviour, so we apply the same rule here:
|
|
||||||
# if a Z-down move would worsen the gap, attach an A delta on the
|
|
||||||
# *same* line so the planner runs Z and A together.
|
|
||||||
#
|
|
||||||
# Jogs always come in as a multi-line MDI sequence:
|
|
||||||
# M70 ; push modal state
|
|
||||||
# G91 ; relative
|
|
||||||
# G0 X.. Y.. Z.. A..
|
|
||||||
# M72 ; pop modal state
|
|
||||||
# so we have to be careful: G90/G91 modal must be tracked across
|
|
||||||
# lines (the G91 set up by the jog applies to the G0 below it),
|
|
||||||
# and we must not re-set absolute mode for free-form MDI.
|
|
||||||
#
|
|
||||||
# Coordinate frame: gcode is in *work coords*. To compare against
|
|
||||||
# the machine-coord constraint we use offset_z and offset_a. We
|
|
||||||
# work in machine coords throughout; emitted A token is in work
|
|
||||||
# coords (= A_machine - offset_a) so the resulting gcode is
|
|
||||||
# consistent with whatever modal frame the operator is in.
|
|
||||||
|
|
||||||
_MDI_AXIS_RES = {
|
|
||||||
'x': re.compile(r'(?<![A-Za-z_0-9])[Xx]\s*([-+]?\d*\.?\d+)'),
|
|
||||||
'y': re.compile(r'(?<![A-Za-z_0-9])[Yy]\s*([-+]?\d*\.?\d+)'),
|
|
||||||
'z': re.compile(r'(?<![A-Za-z_0-9])[Zz]\s*([-+]?\d*\.?\d+)'),
|
|
||||||
'a': re.compile(r'(?<![A-Za-z_0-9])[Aa]\s*([-+]?\d*\.?\d+)'),
|
|
||||||
}
|
|
||||||
_MDI_G_CODE_RE = re.compile(
|
|
||||||
r'(?<![A-Za-z_0-9])[Gg]\s*0*(\d+(?:\.\d+)?)')
|
|
||||||
_MDI_PAREN_RE = re.compile(r'\([^)]*\)')
|
|
||||||
|
|
||||||
def _state_machine_z(self):
|
|
||||||
try:
|
|
||||||
zp = self.ctrl.state.get('zp', None)
|
|
||||||
if zp is None: return None
|
|
||||||
return float(zp)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _state_offset(self, axis):
|
|
||||||
try:
|
|
||||||
return float(self.ctrl.state.get('offset_' + axis, 0.0) or 0.0)
|
|
||||||
except Exception:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def _soft_min_machine(self):
|
|
||||||
"""A's soft minimum in machine coords (the deepest A may go).
|
|
||||||
Falls back to None when soft limits are disabled."""
|
|
||||||
lo, _hi = self._soft_limits()
|
|
||||||
return lo
|
|
||||||
|
|
||||||
def coordinate_mdi(self, cmd):
|
|
||||||
"""Rewrite an MDI/jog command sequence to keep A coupled with
|
|
||||||
Z. Returns the (possibly modified) command string.
|
|
||||||
|
|
||||||
Behaviour:
|
|
||||||
* If coupling is disabled or A isn't homed, returns cmd
|
|
||||||
unchanged.
|
|
||||||
* Tracks G90/G91 modal across lines; the modal state at
|
|
||||||
entry is taken from gplan-side global modal (we assume
|
|
||||||
G90 if not seen on a previous line - jogs always set
|
|
||||||
G91 as their first line, MDI typed by the operator is
|
|
||||||
usually G90 unless they say otherwise).
|
|
||||||
* For each motion line that includes Z, computes the
|
|
||||||
target Z in machine coords and the resulting (A-Z)
|
|
||||||
gap. If the gap would exceed K, augments / overrides
|
|
||||||
the line's A token so the resulting A drops Z + K.
|
|
||||||
* Refuses (raises ExternalAxisError) only when the required
|
|
||||||
A would violate A's soft minimum.
|
|
||||||
"""
|
|
||||||
if not self.couple_z_enabled:
|
|
||||||
return cmd
|
|
||||||
try:
|
|
||||||
homed = bool(self.aux._homed)
|
|
||||||
except Exception:
|
|
||||||
return cmd
|
|
||||||
if not homed:
|
|
||||||
return cmd
|
|
||||||
K = self.couple_K
|
|
||||||
if K is None:
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
# Live state. Use machine-coord values throughout.
|
|
||||||
a_mach = self._a_machine_now()
|
|
||||||
z_mach = self._state_machine_z()
|
|
||||||
if a_mach is None or z_mach is None:
|
|
||||||
return cmd
|
|
||||||
a_off = self._state_offset(self.axis_letter)
|
|
||||||
z_off = self._state_offset('z')
|
|
||||||
a_soft_min = self._soft_min_machine()
|
|
||||||
|
|
||||||
# Modal across the lines we rewrite. We default to absolute
|
|
||||||
# because jogs explicitly set G91 first, and standard MDI is
|
|
||||||
# G90. If the user's prior session ended in G91 we'll be
|
|
||||||
# wrong on the very first line of free-form MDI; that's a
|
|
||||||
# known shortcoming - file uploads use the file preprocessor
|
|
||||||
# which has the same default.
|
|
||||||
absolute = True
|
|
||||||
|
|
||||||
out_lines = []
|
|
||||||
for raw in cmd.splitlines():
|
|
||||||
line = raw.rstrip('\n')
|
|
||||||
code = self._MDI_PAREN_RE.sub('', line)
|
|
||||||
code = code.split(';', 1)[0]
|
|
||||||
if not code.strip():
|
|
||||||
out_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Track modal G90/G91.
|
|
||||||
for m in self._MDI_G_CODE_RE.finditer(code):
|
|
||||||
try: g = int(float(m.group(1)))
|
|
||||||
except Exception: continue
|
|
||||||
if g == 90: absolute = True
|
|
||||||
elif g == 91: absolute = False
|
|
||||||
|
|
||||||
# Find Z and A tokens (last one wins).
|
|
||||||
z_tok = None
|
|
||||||
a_tok = None
|
|
||||||
for m in self._MDI_AXIS_RES['z'].finditer(code):
|
|
||||||
try: z_tok = float(m.group(1))
|
|
||||||
except Exception: pass
|
|
||||||
for m in self._MDI_AXIS_RES['a'].finditer(code):
|
|
||||||
try: a_tok = float(m.group(1))
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
if z_tok is None and a_tok is None:
|
|
||||||
out_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Compute machine-coord targets for this line.
|
|
||||||
if z_tok is not None:
|
|
||||||
z_target_mach = (z_off + z_tok) if absolute \
|
|
||||||
else (z_mach + z_tok)
|
|
||||||
else:
|
|
||||||
z_target_mach = z_mach
|
|
||||||
if a_tok is not None:
|
|
||||||
a_target_mach = (a_off + a_tok) if absolute \
|
|
||||||
else (a_mach + a_tok)
|
|
||||||
else:
|
|
||||||
a_target_mach = a_mach
|
|
||||||
|
|
||||||
eps = 1e-4
|
|
||||||
gap_after = a_target_mach - z_target_mach
|
|
||||||
gap_before = a_mach - z_mach
|
|
||||||
|
|
||||||
if gap_after > K + eps and gap_after > gap_before - eps:
|
|
||||||
# Need to drop A so the gap is exactly K (the deepest
|
|
||||||
# we may take A is whatever Z's target requires).
|
|
||||||
a_required_mach = z_target_mach + K
|
|
||||||
# Refuse when A would have to go below its soft
|
|
||||||
# minimum to satisfy the rule. The operator should
|
|
||||||
# raise Z first.
|
|
||||||
if a_soft_min is not None and a_required_mach < a_soft_min - eps:
|
|
||||||
raise ExternalAxisError(
|
|
||||||
'Z-A coupling: jog would require A=%.3f mm '
|
|
||||||
'(machine), below soft minimum %.3f mm. '
|
|
||||||
'Raise Z first.' % (
|
|
||||||
a_required_mach, a_soft_min))
|
|
||||||
# Refuse only when the operator explicitly asked A
|
|
||||||
# to *move upward* (delta > 0) and the unsafe target
|
|
||||||
# is above the bound. A0 from the jog UI (G91 delta
|
|
||||||
# of zero) is treated as 'no A intent' and is freely
|
|
||||||
# overridden; an A token that already drops A below
|
|
||||||
# what we need is also overridden to the required
|
|
||||||
# depth (deeper-than-needed is fine in the other
|
|
||||||
# direction handled by the no-rewrite branch).
|
|
||||||
if a_tok is not None:
|
|
||||||
delta_a = a_target_mach - a_mach
|
|
||||||
if delta_a > eps and a_target_mach > a_required_mach + eps:
|
|
||||||
raise ExternalAxisError(
|
|
||||||
'Z-A coupling: line requests A=%.3f mm '
|
|
||||||
'while Z=%.3f mm needs A<=%.3f mm.' % (
|
|
||||||
a_target_mach, z_target_mach,
|
|
||||||
a_required_mach))
|
|
||||||
# Convert required machine A back to a token in the
|
|
||||||
# operator's frame.
|
|
||||||
if absolute:
|
|
||||||
a_token_value = a_required_mach - a_off
|
|
||||||
else:
|
|
||||||
a_token_value = a_required_mach - a_mach
|
|
||||||
new_token = 'A%.4f' % a_token_value
|
|
||||||
if a_tok is None:
|
|
||||||
# Append A to the line.
|
|
||||||
line = line.rstrip() + ' ' + new_token
|
|
||||||
else:
|
|
||||||
# Replace existing A token.
|
|
||||||
line = self._MDI_AXIS_RES['a'].sub(
|
|
||||||
new_token, line, count=1)
|
|
||||||
# Update the running mirror so subsequent lines in
|
|
||||||
# this same MDI burst compute correctly.
|
|
||||||
a_mach = a_required_mach
|
|
||||||
if z_tok is not None:
|
|
||||||
z_mach = z_target_mach
|
|
||||||
else:
|
|
||||||
# Move was already safe; just update mirrors for
|
|
||||||
# subsequent lines.
|
|
||||||
if z_tok is not None: z_mach = z_target_mach
|
|
||||||
if a_tok is not None: a_mach = a_target_mach
|
|
||||||
|
|
||||||
out_lines.append(line)
|
|
||||||
return '\n'.join(out_lines)
|
|
||||||
|
|
||||||
def coupling_for_preprocessor(self):
|
def coupling_for_preprocessor(self):
|
||||||
"""Return the dict the AuxPreprocessor wants for in-file
|
"""Return the dict the AuxPreprocessor wants for in-file
|
||||||
injection, or None when coupling is off. We assume the
|
injection, or None when coupling is off. We assume the
|
||||||
|
|||||||
@@ -266,14 +266,6 @@ class Mach(Comm):
|
|||||||
# now that the auxcnc stepper is exposed as a virtual
|
# now that the auxcnc stepper is exposed as a virtual
|
||||||
# A axis (see ExternalAxis).
|
# A axis (see ExternalAxis).
|
||||||
cmd = self._rewrite_aux_mdi(cmd)
|
cmd = self._rewrite_aux_mdi(cmd)
|
||||||
# Z-A coupling: if a line in this MDI/jog burst would
|
|
||||||
# exceed the safe band, attach an A delta on the same
|
|
||||||
# line so the planner runs Z and A together. Refuses
|
|
||||||
# only when the operator explicitly asks for an unsafe
|
|
||||||
# A lift or A would have to violate its soft min.
|
|
||||||
cmd = self._coordinate_mdi_for_coupling(cmd)
|
|
||||||
if not cmd or not cmd.strip():
|
|
||||||
return
|
|
||||||
self._begin_cycle('mdi')
|
self._begin_cycle('mdi')
|
||||||
self.planner.mdi(cmd, with_limits)
|
self.planner.mdi(cmd, with_limits)
|
||||||
super().resume()
|
super().resume()
|
||||||
@@ -281,26 +273,6 @@ class Mach(Comm):
|
|||||||
self.mlog.info("Exception during MDI: %s" % err)
|
self.mlog.info("Exception during MDI: %s" % err)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _coordinate_mdi_for_coupling(self, cmd):
|
|
||||||
"""Apply ExternalAxis.coordinate_mdi to the MDI string. On
|
|
||||||
ExternalAxisError surface a user message and re-raise so the
|
|
||||||
outer try/except in mdi() halts cleanly."""
|
|
||||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
|
||||||
if ext is None:
|
|
||||||
return cmd
|
|
||||||
try:
|
|
||||||
return ext.coordinate_mdi(cmd)
|
|
||||||
except Exception as e:
|
|
||||||
try:
|
|
||||||
self.ctrl.state.add_message(
|
|
||||||
'Z-A coupling refused move: ' + str(e))
|
|
||||||
except Exception: pass
|
|
||||||
self.mlog.warning('Z-A coupling rewrite refused: %s' % e)
|
|
||||||
# Returning the original cmd would let the unsafe move
|
|
||||||
# through to the planner check, which would also refuse.
|
|
||||||
# Better to bail here and skip the MDI burst entirely.
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def _rewrite_aux_mdi(self, cmd):
|
def _rewrite_aux_mdi(self, cmd):
|
||||||
"""Apply the ATC M-code preprocessor to a single MDI line.
|
"""Apply the ATC M-code preprocessor to a single MDI line.
|
||||||
Returns possibly-multi-line G-code with HOOK: comments inserted."""
|
Returns possibly-multi-line G-code with HOOK: comments inserted."""
|
||||||
|
|||||||
Reference in New Issue
Block a user