From 1a6f9261812356109dfce426c66dc2ee0516688a Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Fri, 1 May 2026 16:32:02 +0200 Subject: [PATCH] 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. --- src/py/bbctrl/Hooks.py | 17 +++++++++++++++++ src/py/bbctrl/Planner.py | 24 +++++++++--------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/py/bbctrl/Hooks.py b/src/py/bbctrl/Hooks.py index 4708a47..18e6ea9 100644 --- a/src/py/bbctrl/Hooks.py +++ b/src/py/bbctrl/Hooks.py @@ -249,6 +249,23 @@ class Hooks: # -- Hook execution -- + def dispatch_hook_message(self, text): + """Direct entry point for HOOK:: 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: events. diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index 1df62f4..f2369b1 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -196,25 +196,19 @@ class Planner(): def _add_message(self, text): - # HOOK:: 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:: 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