From f8be0a6b6fe053fdd78a3e0bb443732d3b195dc3 Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 18:39:33 +0200 Subject: [PATCH] AuxPreprocessor: precede each HOOK with M0 so atoms block The ATC M-codes are supposed to behave like proper blocking gcode - M100 should not return until the ejector pulse has actually finished, and the next block should not run until M100 has returned. Without that, the drop macro M102 (release) M100 (eject pulse 1) M100 (eject pulse 2) M100 (eject pulse 3) M100 (eject pulse 4) G53 G0 Z0 (lift Z) M103 (clamp) races: gplan emits all the (MSG,HOOK:...) lines and the Z move in quick succession, the AVR queues them, and Z lifts while V2 is still wiggling. The (MSG,...) transport itself is fire-and-forget by gplan's design. The Hooks framework already implements proper blocking via the block_unpause / auto_resume mechanism - but it only takes effect when the program is actually paused. So precede each hook with M0 (program pause) in the rewritten temp file: M0 (MSG,HOOK:release:) M0 (MSG,HOOK:eject:) ... Sequence becomes: M0 -> machine pauses on the AVR side (MSG..) -> hook fires synchronously in a thread hook does ESP RPC, blocks until [eject] done hook completes; auto_resume unpauses next block streams This also fixes the consecutive-comment-line collapse problem naturally: each M0 is its own block, so back-to-back HOOK lines no longer collide. The M0 lives only in the tempfile gplan loads; the operator's macro source still reads as plain M100/M102/M103. --- src/py/bbctrl/AuxPreprocessor.py | 50 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/py/bbctrl/AuxPreprocessor.py b/src/py/bbctrl/AuxPreprocessor.py index f31abba..e0a8f02 100644 --- a/src/py/bbctrl/AuxPreprocessor.py +++ b/src/py/bbctrl/AuxPreprocessor.py @@ -394,22 +394,42 @@ class AuxPreprocessor(object): except ValueError: continue event = _ATC_M_CODES.get(num) if event: - # gplan only delivers `(MSG,...)` to the - # message stream when it's attached to an - # executable block (so the host can release - # it on the matching cmd-id ack). A bare - # comment-only line gets collapsed and the - # message is silently dropped, which means - # back-to-back hook lines (like 4 ejects in - # a row) only deliver the last one. + # We need two things here that aren't + # naturally provided by the (MSG,...) + # transport: # - # Pair each HOOK line with an essentially- - # zero dwell so it gets a planner block id - # of its own. G4 P0.001 = 1us dwell which - # is below any timer resolution and has no - # observable effect on the machine. - fout.write('G4 P0.001 (MSG,HOOK:%s:)\n' - % event) + # (1) Synchronization. (MSG,HOOK:...) is + # fire-and-forget from gplan's view - + # gplan emits the message and keeps + # streaming subsequent blocks (Z + # moves, the next eject, etc.) to the + # AVR. Meanwhile the hook handler + # runs the actual ESP RPC in a + # thread, and Z lifts while V2 is + # still wiggling. To make M-codes + # behave like proper blocking gcode, + # we precede each HOOK with M0 + # (program pause). The Hooks layer + # registers the atom as block_unpause + # + auto_resume, so: + # M0 -> machine pauses + # (MSG,HOOK:event:) fires hook + # hook thread runs ESP RPC + # hook completes, auto-unpauses + # next block streams + # End result: M100/M102/M103 block + # until the ESP says done, just like + # a G-code dwell. + # + # (2) Block separation. gplan collapses + # consecutive comment-only lines + # into a single block, so back-to- + # back HOOK lines used to drop all + # but the last. M0 is its own block + # so this falls out automatically - + # the (MSG,...) attaches cleanly to + # each M0. + fout.write('M0 (MSG,HOOK:%s:)\n' % event) code_stripped = _ATC_M_RE.sub('', code).strip() if code_stripped: # Mixed line: keep the residual executable