Compare commits

...

4 Commits

Author SHA1 Message Date
1c69c0a157 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.
2026-05-01 16:27:14 +02:00
muehe
748f092795 control: properly wait for gantry homing before W
api.put('home') returns immediately when queued, not when homing
finishes, so the previous polling loop saw cycle=='idle' (homing
hadn't started yet) and fired W right away. Now we first wait up
to 5s for the cycle to *leave* idle, then up to 2min for it to
return, before kicking off the auxcnc W home.
2026-05-01 16:18:23 +02:00
muehe
cfc14643d2 control: home W last in 'Home All'
Previously XYZ and W homing dispatched in parallel. Wait for the
main AVR homing cycle to return to idle before kicking off the W
auxcnc home so they never run simultaneously.
2026-05-01 16:16:51 +02:00
muehe
b0f38619ba control: keep jog grid visible during jog/home/probe/mdi
is_program_executing was checking only state.xx (RUNNING/HOLDING/
STOPPING) which is also true during jogging, homing, probing and
MDI cycles. The Now Running panel therefore took over the Control
view whenever the user jogged. Add a state.cycle check so only the
'running' cycle (a loaded program executing) triggers the swap.
2026-05-01 16:08:39 +02:00
3 changed files with 120 additions and 36 deletions

View File

@@ -255,19 +255,55 @@ module.exports = {
});
},
// Home every enabled axis at once (legacy Onefinity behavior).
// The XYZ home is fired first via the standard /api/home endpoint,
// then the W axis is homed if it is enabled. The two cycles run
// in parallel — W is on the auxcnc ESP and doesn't share motors
// with the AVR — so the user sees one click homing everything.
home_all: function () {
// Home every enabled axis (legacy Onefinity "Home All"). Sequence:
// 1. Z, X, Y (and A/B/C if enabled) via /api/home on the AVR
// 2. W axis via /api/aux/home on the ESP
// /api/home returns as soon as the request is queued, not when
// homing completes, so we have to watch state.cycle:
// - first wait for it to *leave* 'idle' (cycle began),
// - then wait for it to come *back* to 'idle' (cycle ended).
// Only then do we fire the W home, so the gantry and the auxcnc
// ESP never move at the same time.
home_all: async function () {
this.ask_home = false;
api.put("home");
if (this.w && this.w.enabled) {
try {
await api.put("home");
} catch (e) {
console.error("Home all (XYZ) failed:", e);
return;
}
if (!this.w || !this.w.enabled) return;
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const cycleNow = () => (this.state && this.state.cycle) || "idle";
// Phase 1: wait up to 5s for the homing cycle to actually start.
// If the request was rejected upstream (e.g. estopped) cycle
// never leaves idle and we bail rather than home W in isolation.
const startedAt = Date.now();
while (Date.now() - startedAt < 5000) {
if (cycleNow() != "idle") break;
await wait(100);
}
if (cycleNow() == "idle") {
console.warn("home_all: main homing cycle never started; skipping W");
return;
}
// Phase 2: wait up to 2 minutes for the gantry to finish.
const settledAt = Date.now();
while (Date.now() - settledAt < 120000) {
if (cycleNow() == "idle") break;
await wait(200);
}
if (cycleNow() != "idle") {
console.warn("home_all: gantry homing did not complete in time");
return;
}
api.put("aux/home").catch(function (err) {
console.error("W home failed:", err);
});
}
},
aux_jog: function (delta_mm) {

View File

@@ -79,17 +79,28 @@ module.exports = {
// True only while a loaded G-code program is actually being
// executed (running, paused/holding, or stopping). Excludes
// jogging, homing, MDI commands and other one-off motion that
// also leave state.cycle != 'idle' but should not bring up the
// "Now Running" panel on the Control tab.
// jogging, homing, probing, MDI commands and other one-off
// motion that also leave state.xx == "RUNNING" but must not
// swap the jog grid out for the "Now Running" panel.
//
// Distinguishing signal is state.cycle:
// - "idle" : nothing happening
// - "jogging" : user-initiated jog
// - "homing" : home cycle
// - "probing" : probe cycle
// - "mdi" : single MDI command
// - "running" : an actual loaded program is being run
// Only "running" (combined with a selected file) is what we want.
is_program_executing: function () {
const xx = this.state && this.state.xx;
if (xx == "RUNNING" || xx == "HOLDING" || xx == "STOPPING") {
// Only count it as a program run if a file is selected.
// Otherwise an MDI submission also reads xx=RUNNING.
return !!(this.state && this.state.selected);
}
return false;
if (!this.state) return false;
const xx = this.state.xx;
const cycle = this.state.cycle;
const isExecState = xx == "RUNNING" || xx == "HOLDING" || xx == "STOPPING";
if (!isExecState) return false;
// The cycle string narrows it to a real program run; anything
// else (jogging / homing / probing / mdi) is a one-off.
if (cycle && cycle != "running" && cycle != "idle") return false;
return !!this.state.selected;
},
is_paused: function () {
@@ -553,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/<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)}`);
}
this.load();
if (this.state.macros[id].alert == true) {
this.macrosLoading = true;
} else {
setImmediate(() => this.start_pause());
await this.start_pause();
}
} catch (error) {
console.warn("Error running program: ", error);
}
console.warn("Error running macro: ", error);
}
},
},

View File

@@ -196,12 +196,29 @@ class Planner():
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)
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()