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 --
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,
auto_resume=True, timeout=120):
"""Register an in-process handler for HOOK:<name> events.

View File

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