ATC: split tool-change M-codes into composable atoms
Match auxcnc firmware v3, which dropped the monolithic DROPTOOL /
GRABTOOL ESP tasks in favour of three atoms: RELEASE, CLAMP, EJECT.
This lets host macros interleave Z moves between ejector pulses
(the old DROPTOOL ran open->oscillate->clamp in a single ESP task,
so you couldn't lift Z mid-eject).
AuxAxis: replace atc_droptool() / atc_grabtool() with atc_eject(
pulse_ms=, dwell_ms=). atc_release() / atc_clamp() are unchanged.
Ctrl: register internal hooks for release / clamp / eject only.
The eject hook parses 'pulse=' and 'dwell=' kwargs out of the
HOOK:eject:<data> payload so macros can emit
(MSG,HOOK:eject:pulse=400 dwell=300) for tuned wiggles.
AuxPreprocessor: M100 now maps to eject (was droptool); M101 is
unmapped (was grabtool, now a pure host-side macro); M102/M103
are unchanged. Header comment updated.
docs/AUX_A_AXIS.md: mention the new atom set.
The drop.nc and grab.nc gcode macros on the controller are
correspondingly rewritten on-device as compositions:
drop = M102 + 4xM100 + G53 G0 Z0 + M103
grab = M102 + G4 P2 + M103
This commit is contained in:
@@ -8,8 +8,9 @@
|
|||||||
> blended with XYZ in the same S-curve plan and the gcode surface
|
> blended with XYZ in the same S-curve plan and the gcode surface
|
||||||
> below applies as plain `A` words.
|
> below applies as plain `A` words.
|
||||||
>
|
>
|
||||||
> The HOOK pipeline still exists for ATC pneumatics (M100..M103),
|
> The HOOK pipeline still exists for ATC pneumatic atoms (M100 EJECT,
|
||||||
> see `bbctrl/AuxPreprocessor.py`.
|
> M102 RELEASE, M103 CLAMP) - see `bbctrl/AuxPreprocessor.py`. Macros
|
||||||
|
> compose drop/grab tool sequences from those atoms.
|
||||||
|
|
||||||
This adds a virtual `A` axis to the bbctrl controller, driven by the
|
This adds a virtual `A` axis to the bbctrl controller, driven by the
|
||||||
auxcnc ESP32 over USB serial (`/dev/ttyUSB0`). The ESP owns step-pulse
|
auxcnc ESP32 over USB serial (`/dev/ttyUSB0`). The ESP owns step-pulse
|
||||||
|
|||||||
@@ -382,38 +382,23 @@ class AuxAxis(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning('ABORT send failed: %s' % e)
|
self.log.warning('ABORT send failed: %s' % e)
|
||||||
|
|
||||||
# ---------------------------------------------------------- ATC commands
|
# ---------------------------------------------------------- ATC atoms
|
||||||
#
|
#
|
||||||
# The auxcnc firmware drives an AMB 1050 FME-W DI tool changer via
|
# 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
|
# two pneumatic valves on relays 1-2:
|
||||||
# sequences itself; the host just kicks them off and waits for the
|
# V1 (clamp, 3/2 valve) - relay 2: ON = collet open, OFF = vent + spring closes
|
||||||
# terminal reply.
|
# V2 (ejector) - relay 1: ON = ejector cylinder extends
|
||||||
|
#
|
||||||
def atc_droptool(self, timeout=30.0):
|
# The host exposes three composable atoms - RELEASE, CLAMP, EJECT -
|
||||||
"""Eject the current tool. Opens the collet (V1), oscillates the
|
# and composes drop/grab sequences from G-code macros that call
|
||||||
ejector (V2), then re-clamps with a bleed cycle. Blocks until
|
# them in order. (Older firmware exposed monolithic DROPTOOL /
|
||||||
the ESP reports done. Raises on failure."""
|
# GRABTOOL verbs; protocol v3 dropped them in favour of these
|
||||||
self._require_present()
|
# atoms so callers can interleave Z moves between ejector pulses.)
|
||||||
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):
|
def atc_release(self, timeout=5.0):
|
||||||
"""Manually open the collet (release-only, no clamp). Use
|
"""Open the collet (V1 on). Instant. Idempotent. Pairs with
|
||||||
atc_clamp() afterwards once the new holder is in place."""
|
atc_clamp() to bracket a sequence of host-side moves and/or
|
||||||
|
ejector pulses with the collet held open."""
|
||||||
self._require_present()
|
self._require_present()
|
||||||
line = self._rpc('RELEASE', topic='release', timeout=timeout)
|
line = self._rpc('RELEASE', topic='release', timeout=timeout)
|
||||||
if line.startswith('done'):
|
if line.startswith('done'):
|
||||||
@@ -422,8 +407,8 @@ class AuxAxis(object):
|
|||||||
raise AuxAxisError('RELEASE failed: %s' % reason)
|
raise AuxAxisError('RELEASE failed: %s' % reason)
|
||||||
|
|
||||||
def atc_clamp(self, timeout=10.0):
|
def atc_clamp(self, timeout=10.0):
|
||||||
"""Manually clamp the collet (run a full bleed cycle). Pairs
|
"""Close the collet: V1 off, then dwell for the line to vent
|
||||||
with atc_release() for two-step manual tool changes."""
|
and the spring to re-engage. Idempotent."""
|
||||||
self._require_present()
|
self._require_present()
|
||||||
line = self._rpc('CLAMP', topic='clamp', timeout=timeout)
|
line = self._rpc('CLAMP', topic='clamp', timeout=timeout)
|
||||||
if line.startswith('done'):
|
if line.startswith('done'):
|
||||||
@@ -431,6 +416,29 @@ class AuxAxis(object):
|
|||||||
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||||
raise AuxAxisError('CLAMP failed: %s' % reason)
|
raise AuxAxisError('CLAMP failed: %s' % reason)
|
||||||
|
|
||||||
|
def atc_eject(self, pulse_ms=None, dwell_ms=None, timeout=10.0):
|
||||||
|
"""One ejector wiggle: V2 on for pulse_ms, then off for
|
||||||
|
dwell_ms. The collet (V1) is left in whatever state the caller
|
||||||
|
set it to via atc_release/atc_clamp - typically RELEASE first
|
||||||
|
so the holder can actually drop.
|
||||||
|
|
||||||
|
Repeatedly calling atc_eject gives the wiggle that the old
|
||||||
|
monolithic DROPTOOL did internally, but as discrete blocking
|
||||||
|
calls so a macro can interleave Z moves between pulses.
|
||||||
|
|
||||||
|
pulse_ms / dwell_ms default to the ESP-side defaults
|
||||||
|
(currently 500 / 500). Pass explicit values to override."""
|
||||||
|
self._require_present()
|
||||||
|
parts = ['EJECT']
|
||||||
|
if pulse_ms is not None: parts.append('pulse=%d' % int(pulse_ms))
|
||||||
|
if dwell_ms is not None: parts.append('dwell=%d' % int(dwell_ms))
|
||||||
|
cmd = ' '.join(parts)
|
||||||
|
line = self._rpc(cmd, topic='eject', timeout=timeout)
|
||||||
|
if line.startswith('done'):
|
||||||
|
return
|
||||||
|
reason = line.split('reason=', 1)[1] if 'reason=' in line else line
|
||||||
|
raise AuxAxisError('EJECT failed: %s' % reason)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,20 +11,28 @@
|
|||||||
# so gplan handles W motion natively. The preprocessor no longer
|
# so gplan handles W motion natively. The preprocessor no longer
|
||||||
# touches W tokens. ATC pneumatics still go through the hook
|
# touches W tokens. ATC pneumatics still go through the hook
|
||||||
# channel because they're events, not motion.
|
# channel because they're events, not motion.
|
||||||
|
# v3: ATC primitives split into atoms. The composite DROPTOOL /
|
||||||
|
# GRABTOOL hooks are gone; macros now compose tool changes from
|
||||||
|
# RELEASE / CLAMP / EJECT.
|
||||||
#
|
#
|
||||||
# What this still does
|
# What this still does
|
||||||
# --------------------
|
# --------------------
|
||||||
# Maps four user-defined M-codes onto pneumatic-tool-changer events:
|
# Maps three user-defined M-codes onto pneumatic-tool-changer atoms:
|
||||||
#
|
#
|
||||||
# M100 DROPTOOL -> (MSG,HOOK:droptool:)
|
# M100 EJECT -> (MSG,HOOK:eject:) one V2 ejector pulse
|
||||||
# M101 GRABTOOL -> (MSG,HOOK:grabtool:)
|
# M102 RELEASE -> (MSG,HOOK:release:) open collet (V1 on)
|
||||||
# M102 RELEASE -> (MSG,HOOK:release:)
|
# M103 CLAMP -> (MSG,HOOK:clamp:) close collet (V1 off + vent)
|
||||||
# M103 CLAMP -> (MSG,HOOK:clamp:)
|
#
|
||||||
|
# M101 (formerly GRABTOOL) is intentionally unmapped - it's now a
|
||||||
|
# pure host-side macro composed from RELEASE / dwell / CLAMP. If a
|
||||||
|
# legacy file still emits M101 the preprocessor leaves it alone and
|
||||||
|
# the planner ignores it (M101 is in the user-defined range, so it
|
||||||
|
# won't error - it just won't do anything).
|
||||||
#
|
#
|
||||||
# M100-M103 are in LinuxCNC/Buildbotics' user-defined range, so the
|
# M100-M103 are in LinuxCNC/Buildbotics' user-defined range, so the
|
||||||
# planner won't error if the codes leak through unrewritten - it just
|
# planner won't error if the codes leak through unrewritten - it just
|
||||||
# won't *do* anything. We strip them out and emit the matching hook
|
# won't *do* anything. We strip the recognized ones out and emit the
|
||||||
# line in their place.
|
# matching hook line in their place.
|
||||||
#
|
#
|
||||||
# The preprocessor is intentionally conservative: anything it doesn't
|
# The preprocessor is intentionally conservative: anything it doesn't
|
||||||
# understand is left alone.
|
# understand is left alone.
|
||||||
@@ -40,10 +48,10 @@ import tempfile
|
|||||||
# Strip line comments so we don't get fooled by "(M100 not really)".
|
# Strip line comments so we don't get fooled by "(M100 not really)".
|
||||||
_PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
|
_PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
|
||||||
|
|
||||||
# ATC pneumatics M-codes mapped onto hook events.
|
# ATC pneumatics M-codes mapped onto hook events. M101 is
|
||||||
|
# deliberately unassigned (see header).
|
||||||
_ATC_M_CODES = {
|
_ATC_M_CODES = {
|
||||||
100: 'droptool',
|
100: 'eject',
|
||||||
101: 'grabtool',
|
|
||||||
102: 'release',
|
102: 'release',
|
||||||
103: 'clamp',
|
103: 'clamp',
|
||||||
}
|
}
|
||||||
@@ -339,7 +347,7 @@ class AuxPreprocessor(object):
|
|||||||
if self._maybe_inject_a_down(code, fout):
|
if self._maybe_inject_a_down(code, fout):
|
||||||
rewrote_any = True
|
rewrote_any = True
|
||||||
|
|
||||||
# ATC M-codes (M100-M103). Each ATC M-code on the line
|
# ATC M-codes (M100/M102/M103). Each ATC M-code on the line
|
||||||
# is replaced with its (MSG,HOOK:<event>:) line and
|
# is replaced with its (MSG,HOOK:<event>:) line and
|
||||||
# stripped from the residual.
|
# stripped from the residual.
|
||||||
atc_matches = list(_ATC_M_RE.finditer(line))
|
atc_matches = list(_ATC_M_RE.finditer(line))
|
||||||
|
|||||||
@@ -166,31 +166,46 @@ class Ctrl(object):
|
|||||||
else:
|
else:
|
||||||
self.aux.home()
|
self.aux.home()
|
||||||
|
|
||||||
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_release(ctx): self.aux.atc_release()
|
||||||
def _hook_clamp(ctx): self.aux.atc_clamp()
|
def _hook_clamp(ctx): self.aux.atc_clamp()
|
||||||
|
|
||||||
|
def _hook_eject(ctx):
|
||||||
|
# ctx['data'] is the payload after HOOK:eject:. Allow
|
||||||
|
# operators to override pulse / dwell from gcode via
|
||||||
|
# (MSG,HOOK:eject:pulse=400 dwell=300). Empty data ->
|
||||||
|
# ESP defaults.
|
||||||
|
data = (ctx.get('data') or '').strip()
|
||||||
|
kw = {}
|
||||||
|
for tok in data.split():
|
||||||
|
if '=' not in tok: continue
|
||||||
|
k, v = tok.split('=', 1)
|
||||||
|
k = k.strip().lower()
|
||||||
|
if k in ('pulse', 'pulse_ms'):
|
||||||
|
try: kw['pulse_ms'] = int(v)
|
||||||
|
except ValueError: pass
|
||||||
|
elif k in ('dwell', 'dwell_ms'):
|
||||||
|
try: kw['dwell_ms'] = int(v)
|
||||||
|
except ValueError: pass
|
||||||
|
self.aux.atc_eject(**kw)
|
||||||
|
|
||||||
# Legacy alias for older gcode that used aux_home.
|
# Legacy alias for older gcode that used aux_home.
|
||||||
self.hooks.register_internal('aux_home', _hook_aux_home,
|
self.hooks.register_internal('aux_home', _hook_aux_home,
|
||||||
block_unpause=True, auto_resume=True,
|
block_unpause=True, auto_resume=True,
|
||||||
timeout=180)
|
timeout=180)
|
||||||
|
|
||||||
# ATC pneumatics. block_unpause + auto_resume so a program
|
# ATC pneumatic atoms. block_unpause + auto_resume so a
|
||||||
# using M100/M101/M102/M103 pauses at the right point and
|
# program using M100/M102/M103 pauses at the right point and
|
||||||
# resumes once the sequence is done.
|
# resumes once each atom finishes. Macros compose drop/grab
|
||||||
self.hooks.register_internal('droptool', _hook_droptool,
|
# sequences from these primitives.
|
||||||
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,
|
self.hooks.register_internal('release', _hook_release,
|
||||||
block_unpause=True, auto_resume=True,
|
block_unpause=True, auto_resume=True,
|
||||||
timeout=10)
|
timeout=10)
|
||||||
self.hooks.register_internal('clamp', _hook_clamp,
|
self.hooks.register_internal('clamp', _hook_clamp,
|
||||||
block_unpause=True, auto_resume=True,
|
block_unpause=True, auto_resume=True,
|
||||||
timeout=15)
|
timeout=15)
|
||||||
|
self.hooks.register_internal('eject', _hook_eject,
|
||||||
|
block_unpause=True, auto_resume=True,
|
||||||
|
timeout=15)
|
||||||
log.info('Aux hooks registered')
|
log.info('Aux hooks registered')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user