Hooks: dispatch HOOK messages directly, bypassing state.messages

The previous fix routed (MSG,HOOK:...) lines through state.messages
and then immediately ack'd them to suppress the user-visible popup.
But state changes are debounced 0.25s before listeners fire, so the
HOOK message was already ack'd (removed from the list) by the time
Hooks._on_state_change saw the update - and the hook never ran.

Add Hooks.dispatch_hook_message() as a direct entry point and call
it from Planner._add_message. HOOK lines are dispatched synchronously
from the planner thread; the user message list is left untouched, so
no popup leaks and no debounce race.
This commit is contained in:
2026-05-01 16:32:02 +02:00
parent b0712a5bf0
commit 1a6f926181
2 changed files with 26 additions and 15 deletions

View File

@@ -249,6 +249,23 @@ class Hooks:
# -- Hook execution -- # -- Hook execution --
def dispatch_hook_message(self, text):
"""Direct entry point for HOOK:<event>:<data> messages emitted
by the planner via (MSG,HOOK:...) comments. Bypasses the
state.messages list (which the UI also reads), so callers can
suppress popup display without losing the hook dispatch.
Returns True if the text matched a HOOK: line and was
dispatched, False otherwise."""
if not isinstance(text, str) or not text.startswith('HOOK:'):
return False
parts = text[5:].split(':', 1)
event = parts[0]
data = parts[1] if len(parts) > 1 else ''
self._fire('custom', {'event': event, 'data': data},
custom_name=event)
return True
def register_internal(self, name, fn, block_unpause=True, def register_internal(self, name, fn, block_unpause=True,
auto_resume=True, timeout=120): auto_resume=True, timeout=120):
"""Register an in-process handler for HOOK:<name> events. """Register an in-process handler for HOOK:<name> events.

View File

@@ -196,25 +196,19 @@ class Planner():
def _add_message(self, text): def _add_message(self, text):
# HOOK:<event>:<data> messages are an internal IPC channel
# between the gcode preprocessor and Hooks; they should not
# surface as user-visible message popups. Hooks._on_state_change
# will still see them via the messages list before we filter.
line = self.ctrl.state.get('line', 0) line = self.ctrl.state.get('line', 0)
if 0 <= line: where = '%s:%d' % (self.where, line) if 0 <= line: where = '%s:%d' % (self.where, line)
else: where = self.where else: where = self.where
if isinstance(text, str) and text.startswith('HOOK:'): # HOOK:<event>:<data> messages are an internal IPC channel
# Push it through state.messages so Hooks._on_state_change # between the gcode preprocessor and Hooks; bypass the user
# can see and dispatch it, then immediately ack it so the UI # message list so they don't surface as popups, and dispatch
# doesn't render a popup. # the hook directly. Routing through state.messages would
self.ctrl.state.add_message(text) # only deliver it after the 0.25s state-change debounce, by
try: # which point we'd have to keep it visible to ensure Hooks
msgs = self.ctrl.state.get('messages', []) or [] # could see it.
if msgs: hooks = getattr(self.ctrl, 'hooks', None)
self.ctrl.state.ack_message(msgs[-1].get('id', -1)) if hooks is not None and hooks.dispatch_hook_message(text):
except Exception:
pass
self.log.info('HOOK msg: %s' % text, where = where) self.log.info('HOOK msg: %s' % text, where = where)
return return