From 1c69c0a157256c4da3baacb5dab08495cb3a87b5 Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Fri, 1 May 2026 16:27:14 +0200 Subject: [PATCH] Macros: fix wrong-file race, suppress HOOK message popups, preprocess at load Three fixes for macro/W-axis interaction: 1. run_macro raced the file selection. The frontend mutated state.selected client-side and immediately fired api.put('start'). Selection on the server is a side effect of GET /api/file/ (FileHandler.get calls state.select_file). The GET request was often still in flight when start ran, so mach.start() executed whichever file was selected last - pressing W Down would re-run W Up. Now run_macro awaits the file fetch before starting. 2. (MSG,HOOK:aux:N) lines, used as the IPC channel between the W-axis preprocessor and the Hooks system, were leaking to the user as message popups (because the planner forwards every (MSG,...) comment to state.messages). Filter HOOK: messages in Planner._add_message: still pushed through state.messages so Hooks._on_state_change can dispatch them, but immediately acked so the UI doesn't render them. 3. AuxPreprocessor only ran at upload time (FileHandler.put_ok and Mach.mdi). Files written via scp, restored from a config backup, or hand-edited still contained raw W tokens that the planner couldn't parse. Run preprocess_file in Planner.load() too. It's idempotent (no-op when no W tokens remain) so re-loading a already-rewritten file is free. --- src/js/program-mixin.js | 35 ++++++++++++++++++++++------------- src/py/bbctrl/Planner.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/js/program-mixin.js b/src/js/program-mixin.js index a4d0b32..a9f1a0d 100644 --- a/src/js/program-mixin.js +++ b/src/js/program-mixin.js @@ -564,23 +564,32 @@ module.exports = { override_feed: function () { api.put(`override/feed/${this.feed_override}`); }, override_speed: function () { api.put(`override/speed/${this.speed_override}`); }, - run_macro: function (id) { + run_macro: async function (id) { if (this.state.macros[id].file_name == "default") { this.showNoGcodeMessage = true; - } else { - if (this.state.macros[id].file_name != this.state.selected) { - this.state.selected = this.state.macros[id].file_name; + return; + } + const file_name = this.state.macros[id].file_name; + try { + // Selecting a file on the server is a side effect of + // GET /api/file/. The macro button used to mutate + // state.selected client-side and immediately call start, which + // raced the file fetch: if the server hadn't seen the new + // selection yet, mach.start() ran whichever file was selected + // last. Do it explicitly and await so start always sees the + // right file. + if (file_name != this.state.selected) { + this.state.selected = file_name; + await api.get(`file/${encodeURIComponent(file_name)}`); } - try { - this.load(); - if (this.state.macros[id].alert == true) { - this.macrosLoading = true; - } else { - setImmediate(() => this.start_pause()); - } - } catch (error) { - console.warn("Error running program: ", error); + this.load(); + if (this.state.macros[id].alert == true) { + this.macrosLoading = true; + } else { + await this.start_pause(); } + } catch (error) { + console.warn("Error running macro: ", error); } }, }, diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index c967611..1df62f4 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -196,12 +196,29 @@ class Planner(): def _add_message(self, text): - self.ctrl.state.add_message(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 + self.log.info('HOOK msg: %s' % text, where = where) + return + + self.ctrl.state.add_message(text) self.log.message(text, where = where) @@ -369,6 +386,17 @@ class Planner(): self.where = path path = self.ctrl.get_path('upload', path) self.log.info('GCode:' + path) + # Make sure W-axis tokens are rewritten before the planner sees + # the file. preprocess_file is a no-op for files without W and + # for files already rewritten (no W tokens remain after the + # first pass), so this is safe to run on every load. + try: + from bbctrl.AuxPreprocessor import preprocess_file + if preprocess_file(path, log = self.log): + self.log.info('Rewrote W-axis tokens in %s' % path) + except Exception: + self.log.exception('W-axis preprocess at load failed; ' + 'attempting to load file unchanged') self._sync_position() self.planner.load(path, self.get_config(False, True)) self.reset_times()