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.
This commit is contained in:
2026-05-03 18:39:33 +02:00
parent 692be42f84
commit f8be0a6b6f

View File

@@ -394,22 +394,42 @@ class AuxPreprocessor(object):
except ValueError: continue except ValueError: continue
event = _ATC_M_CODES.get(num) event = _ATC_M_CODES.get(num)
if event: if event:
# gplan only delivers `(MSG,...)` to the # We need two things here that aren't
# message stream when it's attached to an # naturally provided by the (MSG,...)
# executable block (so the host can release # transport:
# 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.
# #
# Pair each HOOK line with an essentially- # (1) Synchronization. (MSG,HOOK:...) is
# zero dwell so it gets a planner block id # fire-and-forget from gplan's view -
# of its own. G4 P0.001 = 1us dwell which # gplan emits the message and keeps
# is below any timer resolution and has no # streaming subsequent blocks (Z
# observable effect on the machine. # moves, the next eject, etc.) to the
fout.write('G4 P0.001 (MSG,HOOK:%s:)\n' # AVR. Meanwhile the hook handler
% event) # 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() code_stripped = _ATC_M_RE.sub('', code).strip()
if code_stripped: if code_stripped:
# Mixed line: keep the residual executable # Mixed line: keep the residual executable