From 9d7bc570560391df8a94e2deb0c92675020380cd Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 15:03:01 +0200 Subject: [PATCH] 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. --- src/py/bbctrl/ExternalAxis.py | 207 ---------------------------------- src/py/bbctrl/Mach.py | 28 ----- 2 files changed, 235 deletions(-) diff --git a/src/py/bbctrl/ExternalAxis.py b/src/py/bbctrl/ExternalAxis.py index 1436972..8fd690c 100644 --- a/src/py/bbctrl/ExternalAxis.py +++ b/src/py/bbctrl/ExternalAxis.py @@ -38,7 +38,6 @@ # ################################################################################ -import re import threading try: @@ -259,212 +258,6 @@ class ExternalAxis(object): except Exception: 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'(? 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): """Return the dict the AuxPreprocessor wants for in-file injection, or None when coupling is off. We assume the diff --git a/src/py/bbctrl/Mach.py b/src/py/bbctrl/Mach.py index 47094a3..fd18680 100644 --- a/src/py/bbctrl/Mach.py +++ b/src/py/bbctrl/Mach.py @@ -266,14 +266,6 @@ class Mach(Comm): # now that the auxcnc stepper is exposed as a virtual # A axis (see ExternalAxis). 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.planner.mdi(cmd, with_limits) super().resume() @@ -281,26 +273,6 @@ class Mach(Comm): self.mlog.info("Exception during MDI: %s" % err) 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): """Apply the ATC M-code preprocessor to a single MDI line. Returns possibly-multi-line G-code with HOOK: comments inserted."""