From 06f0e6517ebd8723796b6d96bc812130091bda33 Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Fri, 1 May 2026 16:35:45 +0200 Subject: [PATCH] AuxAxis: wire DROPTOOL/GRABTOOL/RELEASE/CLAMP as gcode hooks Adds atc_droptool/atc_grabtool/atc_release/atc_clamp wrappers in AuxAxis (each just an RPC waiting on the matching terminal reply line from the firmware), and registers them as internal hook handlers in Ctrl. Macros and gcode programs can now invoke the tool changer with: (MSG,HOOK:droptool:) (MSG,HOOK:grabtool:) (MSG,HOOK:release:) (MSG,HOOK:clamp:) block_unpause + auto_resume mirrors the W-axis hooks: the program pauses while the ESP runs the pneumatic sequence and resumes when done. Soft timeouts match the worst-case ESP sequence durations. --- src/py/bbctrl/AuxAxis.py | 49 ++++++++++++++++++++++++++++++++++++++++ src/py/bbctrl/Ctrl.py | 20 ++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/py/bbctrl/AuxAxis.py b/src/py/bbctrl/AuxAxis.py index 7ff1a4b..bbcc7bf 100644 --- a/src/py/bbctrl/AuxAxis.py +++ b/src/py/bbctrl/AuxAxis.py @@ -230,6 +230,55 @@ class AuxAxis(object): except Exception as e: self.log.warning('ABORT send failed: %s' % e) + # ---------------------------------------------------------- ATC commands + # + # The auxcnc firmware drives an AMB 1050 FME-W DI tool changer via + # three pneumatic valves on relays 1-3. The ESP runs the timed + # sequences itself; the host just kicks them off and waits for the + # terminal reply. + + def atc_droptool(self, timeout=30.0): + """Eject the current tool. Opens the collet (V1), oscillates the + ejector (V2), then re-clamps with a bleed cycle. Blocks until + the ESP reports done. Raises on failure.""" + self._require_present() + line = self._rpc('DROPTOOL', topic='droptool', timeout=timeout) + if line.startswith('done'): + return + reason = line.split('reason=', 1)[1] if 'reason=' in line else line + raise AuxAxisError('DROPTOOL failed: %s' % reason) + + def atc_grabtool(self, timeout=30.0): + """Pick up a tool that's already been seated by the operator. + Opens V1 (releases the collet), waits for the operator to insert + the holder, then re-clamps with a bleed cycle. Blocks.""" + self._require_present() + line = self._rpc('GRABTOOL', topic='grabtool', timeout=timeout) + if line.startswith('done'): + return + reason = line.split('reason=', 1)[1] if 'reason=' in line else line + raise AuxAxisError('GRABTOOL failed: %s' % reason) + + def atc_release(self, timeout=5.0): + """Manually open the collet (release-only, no clamp). Use + atc_clamp() afterwards once the new holder is in place.""" + self._require_present() + line = self._rpc('RELEASE', topic='release', timeout=timeout) + if line.startswith('done'): + return + reason = line.split('reason=', 1)[1] if 'reason=' in line else line + raise AuxAxisError('RELEASE failed: %s' % reason) + + def atc_clamp(self, timeout=10.0): + """Manually clamp the collet (run a full bleed cycle). Pairs + with atc_release() for two-step manual tool changes.""" + self._require_present() + line = self._rpc('CLAMP', topic='clamp', timeout=timeout) + if line.startswith('done'): + return + reason = line.split('reason=', 1)[1] if 'reason=' in line else line + raise AuxAxisError('CLAMP failed: %s' % reason) + def close(self): self._stop.set() try: diff --git a/src/py/bbctrl/Ctrl.py b/src/py/bbctrl/Ctrl.py index ede7c52..b2d8862 100644 --- a/src/py/bbctrl/Ctrl.py +++ b/src/py/bbctrl/Ctrl.py @@ -156,6 +156,11 @@ class Ctrl(object): mm = float(data) if data else 0.0 self.aux.set_position_mm(mm) + def _hook_droptool(ctx): self.aux.atc_droptool() + def _hook_grabtool(ctx): self.aux.atc_grabtool() + def _hook_release(ctx): self.aux.atc_release() + def _hook_clamp(ctx): self.aux.atc_clamp() + self.hooks.register_internal('aux', _hook_move, block_unpause=True, auto_resume=True) self.hooks.register_internal('aux_rel', _hook_move_rel, @@ -165,6 +170,21 @@ class Ctrl(object): timeout=180) self.hooks.register_internal('aux_setzero', _hook_setzero, block_unpause=True, auto_resume=True) + # ATC pneumatics. block_unpause + auto_resume so a program + # using M6 - implemented as (MSG,HOOK:droptool:) etc - pauses + # at the right point and resumes once the sequence is done. + self.hooks.register_internal('droptool', _hook_droptool, + block_unpause=True, auto_resume=True, + timeout=60) + self.hooks.register_internal('grabtool', _hook_grabtool, + block_unpause=True, auto_resume=True, + timeout=60) + self.hooks.register_internal('release', _hook_release, + block_unpause=True, auto_resume=True, + timeout=10) + self.hooks.register_internal('clamp', _hook_clamp, + block_unpause=True, auto_resume=True, + timeout=15) log.info('Aux hooks registered')