ATC pneumatics in g-code (drop tool / grab tool / release clamp / engage clamp) are expressed as M100..M103. AuxPreprocessor rewrites those into (MSG,HOOK:droptool:) etc on file upload + on planner load + on MDI input, so the Hooks layer (B1) can dispatch them via registered ATC handlers in Ctrl. - AuxPreprocessor.py: regex-based file rewriter, idempotent. - FileHandler: invoke preprocessor on every upload. - Planner.init: also re-preprocess on load (catches files written before this version). - Mach.mdi: same rewrite for ad-hoc MDI input so M101 typed at the console produces a HOOK message. - Ctrl: register the four ATC hooks (droptool/grabtool/release/clamp) with block_unpause + auto_resume so programs using them pause at the right point and resume cleanly. aux_home retained as a legacy alias for older preprocessed files.
184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
################################################################################
|
|
#
|
|
# AuxPreprocessor - rewrite ATC M-codes into hook calls
|
|
#
|
|
# History
|
|
# -------
|
|
# v1: rewrote W tokens into (MSG,HOOK:aux:N) lines because the bbctrl
|
|
# planner only understood XYZABC and the W axis was driven via a
|
|
# side-channel.
|
|
# v2: W is now exposed to gplan as a virtual A axis (see ExternalAxis),
|
|
# so gplan handles W motion natively. The preprocessor no longer
|
|
# touches W tokens. ATC pneumatics still go through the hook
|
|
# channel because they're events, not motion.
|
|
#
|
|
# What this still does
|
|
# --------------------
|
|
# Maps four user-defined M-codes onto pneumatic-tool-changer events:
|
|
#
|
|
# M100 DROPTOOL -> (MSG,HOOK:droptool:)
|
|
# M101 GRABTOOL -> (MSG,HOOK:grabtool:)
|
|
# M102 RELEASE -> (MSG,HOOK:release:)
|
|
# M103 CLAMP -> (MSG,HOOK:clamp:)
|
|
#
|
|
# M100-M103 are in LinuxCNC/Buildbotics' user-defined range, so the
|
|
# 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
|
|
# line in their place.
|
|
#
|
|
# The preprocessor is intentionally conservative: anything it doesn't
|
|
# understand is left alone.
|
|
#
|
|
################################################################################
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import tempfile
|
|
|
|
|
|
# Strip line comments so we don't get fooled by "(M100 not really)".
|
|
_PAREN_COMMENT_RE = re.compile(r'\([^)]*\)')
|
|
|
|
# ATC pneumatics M-codes mapped onto hook events.
|
|
_ATC_M_CODES = {
|
|
100: 'droptool',
|
|
101: 'grabtool',
|
|
102: 'release',
|
|
103: 'clamp',
|
|
}
|
|
_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.])'
|
|
)
|
|
|
|
# Detect a W axis token. We no longer rewrite W to A automatically;
|
|
# instead we warn so the user knows their old gcode needs migration.
|
|
# (The W support was removed when the axis was integrated as a real
|
|
# A axis through gplan.)
|
|
_W_TOKEN_RE = re.compile(r'(?<![A-Za-z_0-9])[Ww]\s*[-+]?\d*\.?\d+')
|
|
|
|
|
|
class AuxPreprocessorError(Exception):
|
|
pass
|
|
|
|
|
|
class AuxPreprocessor(object):
|
|
def __init__(self, log=None):
|
|
self.log = log
|
|
self._w_warned = False
|
|
|
|
def _info(self, msg):
|
|
if self.log: self.log.info(msg)
|
|
|
|
def _warn(self, msg):
|
|
if self.log: self.log.warning(msg)
|
|
|
|
# ------------------------------------------------------------------ scan
|
|
|
|
@staticmethod
|
|
def file_uses_aux(path):
|
|
"""Quick check: does this file contain anything the preprocessor
|
|
would rewrite (currently: just ATC M-codes)?"""
|
|
try:
|
|
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
|
for line in f:
|
|
code = _PAREN_COMMENT_RE.sub('', line)
|
|
code = code.split(';', 1)[0]
|
|
if _ATC_M_RE.search(code):
|
|
return True
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
# Backwards-compat alias.
|
|
file_uses_w = file_uses_aux
|
|
|
|
# ------------------------------------------------------------------ run
|
|
|
|
def process(self, src_path, dst_path):
|
|
"""Read src_path, write rewritten G-code to dst_path. Returns
|
|
True if any rewrite happened."""
|
|
rewrote_any = False
|
|
|
|
with open(src_path, 'r', encoding='utf-8', errors='replace') as fin, \
|
|
open(dst_path, 'w', encoding='utf-8') as fout:
|
|
for raw in fin:
|
|
line = raw.rstrip('\n')
|
|
|
|
# Comment-only or blank lines pass through verbatim.
|
|
code = _PAREN_COMMENT_RE.sub('', line)
|
|
code = code.split(';', 1)[0]
|
|
if not code.strip():
|
|
fout.write(raw)
|
|
continue
|
|
|
|
# Warn (once) if the file still uses W tokens. The
|
|
# standard way is now G1 A<value>; old files must be
|
|
# migrated by hand.
|
|
if (not self._w_warned) and _W_TOKEN_RE.search(code):
|
|
self._warn('Found W axis token in gcode; W is no '
|
|
'longer recognized by bbctrl. Use A '
|
|
'instead. (warning suppressed for '
|
|
'subsequent W tokens in this file)')
|
|
self._w_warned = True
|
|
|
|
# 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.
|
|
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 not code.strip():
|
|
# Nothing meaningful left; preserve any trailing
|
|
# comment text but skip empty lines.
|
|
rest = line.rstrip()
|
|
if rest:
|
|
fout.write(rest + '\n')
|
|
continue
|
|
# Other gcode remains on the line - emit it.
|
|
fout.write(line + '\n')
|
|
continue
|
|
|
|
# No rewrite needed.
|
|
fout.write(raw)
|
|
|
|
return rewrote_any
|
|
|
|
|
|
def preprocess_file(src_path, log=None, **_unused):
|
|
"""Convenience: rewrite src_path in place if it contains ATC
|
|
M-codes. Returns True if the file was rewritten.
|
|
|
|
Extra keyword args are accepted for backwards compat (the old
|
|
w_first arg is no longer used)."""
|
|
if not AuxPreprocessor.file_uses_aux(src_path):
|
|
return False
|
|
pre = AuxPreprocessor(log=log)
|
|
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',
|
|
dir=os.path.dirname(src_path) or None)
|
|
os.close(fd)
|
|
try:
|
|
rewrote = pre.process(src_path, tmp)
|
|
if rewrote:
|
|
shutil.move(tmp, src_path)
|
|
return True
|
|
os.unlink(tmp)
|
|
return False
|
|
except Exception:
|
|
try:
|
|
os.unlink(tmp)
|
|
except OSError:
|
|
pass
|
|
raise
|