AuxPreprocessor: canonical M100-M103 for ATC pneumatics

Map four user-defined M-codes to the existing ATC hooks:

  M100  DROPTOOL  -> (MSG,HOOK:droptool:)
  M101  GRABTOOL  -> (MSG,HOOK:grabtool:)
  M102  RELEASE   -> (MSG,HOOK:release:)
  M103  CLAMP     -> (MSG,HOOK🗜️)

M100-M103 are in LinuxCNC/Buildbotics user-defined range so the
planner won't error on the raw codes if the preprocessor is bypassed.
Stripped from the residual line and replaced with the hook line.
Order is left-to-right; multiple ATC codes per line and ATC+W on
the same line both work (M100 W10 -> drop then move to W=10).

The file scanner (file_uses_aux, formerly file_uses_w) now wakes
up for either W tokens or ATC M-codes; backwards-compat alias kept.
MDI rewrite (Mach._rewrite_w_mdi) updated likewise.

Tested locally with mixed ATC/W gcode in tmp/20260501_atc_mcodes.
This commit is contained in:
2026-05-01 18:49:44 +02:00
parent 06f0e6517e
commit 3614a2bcd4
2 changed files with 71 additions and 8 deletions

View File

@@ -42,6 +42,30 @@ _PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
# Modal G-code groups we care about.
_MODAL_RE = re.compile(r'(?<![A-Za-z_0-9])[Gg]\s*0*(\d+(?:\.\d+)?)')
# ATC pneumatics. We map a small range of user-defined M-codes onto
# our existing HOOK: events. M100-M103 are in LinuxCNC/Buildbotics'
# user-defined range so the planner won't error on them - but it also
# won't *do* anything with them, so we strip them out and emit the
# matching hook line in their place. M6 is intentionally NOT mapped:
# users keep their own probe-and-prompt M6 override.
#
# M100 DROPTOOL (eject current tool, automatic sequence)
# M101 GRABTOOL (auto-clamp on inserted holder)
# M102 RELEASE (manually open collet, no clamp)
# M103 CLAMP (manually close collet with bleed)
_ATC_M_CODES = {
100: 'droptool',
101: 'grabtool',
102: 'release',
103: 'clamp',
}
# A token like 'M100' or 'm103', not preceded/followed by alnum.
_ATC_M_RE = re.compile(
r'(?<![A-Za-z_0-9])[Mm]\s*0*(' +
'|'.join(str(n) for n in _ATC_M_CODES) +
r')(?![\w.])'
)
class AuxPreprocessorError(Exception):
pass
@@ -65,9 +89,10 @@ class AuxPreprocessor(object):
# ------------------------------------------------------------------ scan
@staticmethod
def file_uses_w(path):
"""Quick check: does this file contain any W-axis word? Used to skip
preprocessing entirely for files that don't care about W."""
def file_uses_aux(path):
"""Quick check: does this file contain anything the preprocessor
would rewrite (W axis tokens or ATC M-codes)? Used to skip
preprocessing entirely for files that don't need it."""
try:
with open(path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
@@ -75,10 +100,16 @@ class AuxPreprocessor(object):
code = code.split(';', 1)[0]
if _W_TOKEN_RE.search(code):
return True
if _ATC_M_RE.search(code):
return True
except Exception:
pass
return False
# Backwards-compat alias: external callers used file_uses_w before
# the ATC M-codes were added.
file_uses_w = file_uses_aux
# ------------------------------------------------------------------ core
def _strip_w(self, line):
@@ -139,8 +170,38 @@ class AuxPreprocessor(object):
# vs incremental matches what the planner sees for XYZ).
self._detect_modals(code, modal)
# ATC M-codes (M100-M103). Each ATC M-code on the line
# is replaced with its (MSG,HOOK:<event>:) line and
# stripped from the residual. Multiple ATC codes per
# line are honoured but order is left-to-right. We do
# this BEFORE the W-axis path so a line like
# "M100 W10" cleanly emits drop+move.
atc_matches = list(_ATC_M_RE.finditer(line))
if atc_matches:
rewrote_any = True
for m in atc_matches:
try: num = int(m.group(1))
except ValueError: continue
event = _ATC_M_CODES.get(num)
if event:
fout.write('(MSG,HOOK:%s:)\n' % event)
line = _ATC_M_RE.sub('', line)
code = _PAREN_COMMENT_RE.sub('', line)
code = code.split(';', 1)[0]
# If nothing else is left on the line, we're done.
if not code.strip():
# Preserve any trailing comment, but skip if the
# whole line is empty after the M-code strip.
rest = line.rstrip()
if rest:
fout.write(rest + '\n')
continue
if not _W_TOKEN_RE.search(code):
fout.write(raw)
# No W work to do; emit whatever's left after ATC
# M-code stripping (or the original line if there
# were no ATC codes).
fout.write(line + '\n' if atc_matches else raw)
continue
rewrote_any = True
@@ -214,9 +275,10 @@ class AuxPreprocessor(object):
def preprocess_file(src_path, log=None, w_first=True):
"""Convenience: rewrite src_path in place if it uses W.
"""Convenience: rewrite src_path in place if it uses anything the
preprocessor handles (W axis tokens or ATC M-codes).
Returns True if the file was rewritten."""
if not AuxPreprocessor.file_uses_w(src_path):
if not AuxPreprocessor.file_uses_aux(src_path):
return False
pre = AuxPreprocessor(log=log, w_first=w_first)
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',

View File

@@ -270,8 +270,9 @@ class Mach(Comm):
"""Apply the W-axis preprocessor to a single MDI line. Returns
possibly-multi-line G-code with HOOK: comments inserted."""
try:
from bbctrl.AuxPreprocessor import AuxPreprocessor, _W_TOKEN_RE
if not _W_TOKEN_RE.search(cmd):
from bbctrl.AuxPreprocessor import (
AuxPreprocessor, _W_TOKEN_RE, _ATC_M_RE)
if not _W_TOKEN_RE.search(cmd) and not _ATC_M_RE.search(cmd):
return cmd
import io, tempfile, os
# AuxPreprocessor.process is file-based; route through