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/<name>
   (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.
This commit is contained in:
2026-05-01 16:27:14 +02:00
parent 748f092795
commit 1c69c0a157
2 changed files with 52 additions and 15 deletions

View File

@@ -564,23 +564,32 @@ module.exports = {
override_feed: function () { api.put(`override/feed/${this.feed_override}`); }, override_feed: function () { api.put(`override/feed/${this.feed_override}`); },
override_speed: function () { api.put(`override/speed/${this.speed_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") { if (this.state.macros[id].file_name == "default") {
this.showNoGcodeMessage = true; this.showNoGcodeMessage = true;
} else { return;
if (this.state.macros[id].file_name != this.state.selected) { }
this.state.selected = this.state.macros[id].file_name; const file_name = this.state.macros[id].file_name;
try {
// Selecting a file on the server is a side effect of
// GET /api/file/<name>. 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();
this.load(); if (this.state.macros[id].alert == true) {
if (this.state.macros[id].alert == true) { this.macrosLoading = true;
this.macrosLoading = true; } else {
} else { await this.start_pause();
setImmediate(() => this.start_pause());
}
} catch (error) {
console.warn("Error running program: ", error);
} }
} catch (error) {
console.warn("Error running macro: ", error);
} }
}, },
}, },

View File

@@ -196,12 +196,29 @@ class Planner():
def _add_message(self, text): def _add_message(self, text):
self.ctrl.state.add_message(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:'):
# 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) self.log.message(text, where = where)
@@ -369,6 +386,17 @@ class Planner():
self.where = path self.where = path
path = self.ctrl.get_path('upload', path) path = self.ctrl.get_path('upload', path)
self.log.info('GCode:' + 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._sync_position()
self.planner.load(path, self.get_config(False, True)) self.planner.load(path, self.get_config(False, True))
self.reset_times() self.reset_times()