Compare commits

...

6 Commits

Author SHA1 Message Date
549b69c234 motor-view: stop clobbering user edits with controller state
The legacy Vue 1 motor settings page had nine `current_xxx` computed
props mirroring controller state vars (`<idx>vm`, `<idx>am`,
`<idx>jm`, `<idx>sa`, `<idx>tr`, `<idx>mi`, `<idx>tm`,
`<idx>tn`, `<idx>an`) paired with watchers that copied the state
value back into `config.motors[index]`, plus an `attached()` hook
running the same sync on mount.

The controller streams those vars continuously over the websocket.
Whenever a user typed into a field, the next state tick reverted it
to the controller's pre-edit value, so the form felt racy and edits
disappeared before Save. The same path also nuked unsaved edits when
navigating to another settings page and back.

The watcher logic was added in 749d63e to handle the case where
toggling rotary mode (PUT /api/rotary) rewrites motor 1+2 in
config.json on the server. Move that fix to the right place: refetch
config after the rotary PUT in app.js. The form now edits config
directly, Save PUTs it, and incoming controller state never overwrites
the user's in-progress edits.

Also drop the unused `syncStateToConfig` method.
2026-05-01 16:08:31 +02:00
muehe
5376d23f8b control: shrink Home All button in DRO header
Smaller than the per-axis home buttons so it reads as a header-level
shortcut rather than competing visually with each row's action.
2026-05-01 15:57:31 +02:00
muehe
ecf3191fcc control: restore Home All button in DRO header
Legacy Onefinity exposed a master Home All in the DRO header. Our
V09 redesign only kept it inside the no-W fallback row, so machines
with the W axis enabled (which is most users now) had no master
home button. Add it back to the DRO header's Actions column.

home_all() fires /api/home for X/Y/Z/A and /api/aux/home for W in
parallel \u2014 the AVR and the auxcnc ESP run independent homing cycles
so the user sees one click homing everything.
2026-05-01 15:54:52 +02:00
muehe
3baa67360c control: align XY/Z origin tile color with other axis tiles
Drop the .ghost class from the XY Origin and Z Origin tiles so they
use the same flat slate color as the X-/X+/Z+ neighbors. The lighter
ghost tone made them look like a different category of action when
they are just origin shortcuts.
2026-05-01 14:50:22 +02:00
muehe
b7bd7a1c9c AuxAxis: faster sane defaults for homing and stepping
Old defaults (4000 fast / 400 slow / 200 backoff / 16000 accel /
200 start) were never aggressive in practice because the user-
saved config drifted to even lower values (600/80/110). Re-tune
the DEFAULTS dict to values that are sensible at 25 steps/mm:

  home_fast_sps     2500   ~100 mm/s seek
  home_slow_sps      250    ~10 mm/s re-engage
  home_backoff_steps 400     ~16 mm clear hysteresis
  step_max_sps      4000   ~160 mm/s normal-move cap
  step_accel_sps2  12000

These only affect machines without an existing aux.json. The Pi
at 10.1.10.55 was patched manually.
2026-05-01 14:47:11 +02:00
muehe
6fe2e79bff settings: unify W axis save into master Save button
- Drop the in-form 'Save W Axis Settings' button. The Svelte
  WAxisSettings component now listens for a global onefin:save-all
  event and PUTs aux/config/save when fired.
- Vue root's save() dispatches that event after saving config.json,
  so a single click of the master Save button persists both the
  controller config and aux.json atomically.
- Editing any W axis field triggers onefin:dirty, which the Vue
  root catches to set modified=true so the master Save lights up
  with the unsaved-changes indicator.
2026-05-01 14:28:15 +02:00
7 changed files with 105 additions and 166 deletions

View File

@@ -369,6 +369,13 @@ module.exports = new Vue({
ready: function() { ready: function() {
window.onhashchange = () => this.parse_hash(); window.onhashchange = () => this.parse_hash();
// Embedded Svelte subviews (W axis settings, etc.) signal
// unsaved changes via this event. The master Save button
// highlights when modified is true.
window.addEventListener("onefin:dirty", () => {
this.modified = true;
});
// Resolve the initial route before the websocket connects so // Resolve the initial route before the websocket connects so
@@ -481,6 +488,12 @@ module.exports = new Vue({
toggle_rotary: async function(isActive) { toggle_rotary: async function(isActive) {
try { try {
await api.put("rotary", {status: isActive}); await api.put("rotary", {status: isActive});
// The /api/rotary endpoint rewrites motors[1]/[2]
// in config.json on the server. Refetch so the UI
// reflects the new motor config (otherwise the
// motor settings page keeps showing pre-toggle
// values until the next page reload).
await this.update();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
alert("Error occured"); alert("Error occured");
@@ -673,9 +686,16 @@ module.exports = new Vue({
this.config["selected-tool-settings"][selected_tool] = settings; this.config["selected-tool-settings"][selected_tool] = settings;
this.display_units = this.config.settings["units"]; this.display_units = this.config.settings["units"];
try { try {
await api.put("config/save", this.config); await api.put("config/save", this.config);
// Notify any embedded Svelte subviews that own their
// own persistence (W axis -> aux.json, etc.) that
// the user just hit the master Save button. They
// listen for `onefin:save-all` and PUT their state.
try {
window.dispatchEvent(new CustomEvent("onefin:save-all"));
} catch (_e) {}
this.modified = false; this.modified = false;
} catch (error) { } catch (error) {
console.error("Save failed:", error); console.error("Save failed:", error);

View File

@@ -255,6 +255,21 @@ 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 () {
this.ask_home = false;
api.put("home");
if (this.w && this.w.enabled) {
api.put("aux/home").catch(function (err) {
console.error("W home failed:", err);
});
}
},
aux_jog: function (delta_mm) { aux_jog: function (delta_mm) {
api.put("aux/jog", { mm: delta_mm }).catch(function (err) { api.put("aux/jog", { mm: delta_mm }).catch(function (err) {
console.error("W jog failed:", err); console.error("W jog failed:", err);

View File

@@ -87,100 +87,16 @@ module.exports = {
return this.stallRPM * this.stepsPerRev * ustep / 60; return this.stallRPM * this.stepsPerRev * ustep / 60;
}, },
current_axis: function() { // NOTE: do not add `current_xxx` computed props that mirror
return this.state[this.index + 'an']; // controller state vars (`<idx>vm`, `<idx>am`, …) and pair
}, // them with watchers that copy state -> motor config. The
// controller streams those vars continuously over the WS;
current_max_velocity: function() { // any watcher that writes them back into
return this.state[this.index + 'vm']; // `config.motors[index]` will clobber whatever the user is
}, // typing into the form between websocket ticks. The form
// edits config directly; Save (app.js) PUTs it to the
current_max_soft_limit: function() { // server. The server-side rotary toggle is handled by
return this.state[this.index + 'tm']; // refetching config after the PUT, not by watching state.
},
current_min_soft_limit: function() {
return this.state[this.index + 'tn'];
},
current_max_accel: function() {
return this.state[this.index + 'am'];
},
current_max_jerk: function() {
return this.state[this.index + 'jm'];
},
current_step_angle: function() {
return this.state[this.index + 'sa'];
},
current_travel_per_rev: function() {
return this.state[this.index + 'tr'];
},
current_microsteps: function() {
return this.state[this.index + 'mi'];
}
},
attached: function() {
// Sync all state values with motor config when component is ready
// This ensures UI shows correct values when component is first loaded
console.log("Syncing state to motor config for motor index ",this.index);
this.syncStateToConfig();
},
watch: {
current_axis(new_value) {
const motor_axes = ["X", "Y", "Z", "A", "B", "C"]
if(motor_axes[new_value] != this.motor['axis']){
this.motor['axis'] = motor_axes[new_value];
}
},
current_max_velocity(new_value) {
if(new_value != this.motor['max-velocity']) {
this.motor['max-velocity'] = new_value;
}
},
current_max_soft_limit(new_value) {
if(new_value != this.motor['max-soft-limit']) {
this.motor['max-soft-limit'] = new_value;
}
},
current_min_soft_limit(new_value) {
if(new_value != this.motor['min-soft-limit']) {
this.motor['min-soft-limit'] = new_value;
}
},
current_max_accel(new_value) {
if(new_value != this.motor['max-accel']) {
this.motor['max-accel'] = new_value;
}
},
current_max_jerk(new_value) {
if(new_value != this.motor['max-jerk']) {
this.motor['max-jerk'] = new_value;
}
},
current_step_angle(new_value) {
if(new_value != this.motor['step-angle']) {
this.motor['step-angle'] = new_value;
}
},
current_travel_per_rev(new_value) {
if(new_value != this.motor['travel-per-rev']) {
this.motor['travel-per-rev'] = new_value;
}
},
current_microsteps(new_value) {
if(new_value != this.motor['microsteps']) {
this.motor['microsteps'] = new_value;
}
}
}, },
events: { events: {
@@ -210,45 +126,6 @@ module.exports = {
} }
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1; return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
},
syncStateToConfig: function() {
// Force sync all state values to motor config
// This ensures the UI reflects the current state even if changes happened while component was unmounted
if(this.state == undefined) {
console.log("State is undefined");
return;
}
if (this.state[this.index + 'an'] != this.motor['axis']) {
const motor_axes = ["X", "Y", "Z", "A", "B", "C"];
this.$set('motor["axis"]', motor_axes[this.state[this.index + 'an']]);
}
if (this.state[this.index + 'vm'] != this.motor['max-velocity']) {
this.$set('motor["max-velocity"]', this.state[this.index + 'vm']);
}
if (this.state[this.index + 'tm'] != this.motor['max-soft-limit']) {
this.$set('motor["max-soft-limit"]', this.state[this.index + 'tm']);
}
if (this.state[this.index + 'tn'] != this.motor['min-soft-limit']) {
this.$set('motor["min-soft-limit"]', this.state[this.index + 'tn']);
}
if (this.state[this.index + 'am'] != this.motor['max-accel']) {
this.$set('motor["max-accel"]', this.state[this.index + 'am']);
}
if (this.state[this.index + 'jm'] != this.motor['max-jerk']) {
this.$set('motor["max-jerk"]', this.state[this.index + 'jm']);
}
if (this.state[this.index + 'sa'] != this.motor['step-angle']) {
this.$set('motor["step-angle"]', this.state[this.index + 'sa']);
}
if (this.state[this.index + 'tr'] != this.motor['travel-per-rev']) {
this.$set('motor["travel-per-rev"]', this.state[this.index + 'tr']);
}
if (this.state[this.index + 'mi'] != this.motor['microsteps']) {
this.$set('motor["microsteps"]', this.state[this.index + 'mi']);
}
} }
} }
}; };

View File

@@ -76,11 +76,11 @@ script#control-view-template(type="text/x-template")
// Row 2 // Row 2
button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X button.jbtn(@click="jog_fn(-1, 0, 0, 0)") X
button.jbtn.ghost(@click="showMoveToZeroDialog('xy')") button.jbtn(@click="showMoveToZeroDialog('xy')")
span.lbl XY span.lbl XY
span Origin span Origin
button.jbtn(@click="jog_fn(1, 0, 0, 0)") X+ button.jbtn(@click="jog_fn(1, 0, 0, 0)") X+
button.jbtn.ghost(@click="showMoveToZeroDialog('z')") button.jbtn(@click="showMoveToZeroDialog('z')")
span.lbl Z span.lbl Z
span Origin span Origin
@@ -213,7 +213,14 @@ script#control-view-template(type="text/x-template")
div Offset div Offset
div State div State
div Toolpath div Toolpath
div(style="text-align:right") Actions .actions-cell
// Master Home All. Each row's Actions cell has a per-axis
// home button; this header-level button homes every
// enabled axis (legacy Onefinity behavior). Auto-includes
// the W axis when it is enabled.
button.icon-btn(:disabled="!is_idle",
title="Home all axes.", @click="home_all()")
.fa.fa-house-chimney
// Per-axis rows — keep unit-value + bindings from axis-vars // Per-axis rows — keep unit-value + bindings from axis-vars
each axis in 'xyzabc' each axis in 'xyzabc'

View File

@@ -41,12 +41,17 @@ DEFAULTS = {
'home_dir': '-', # which direction is "toward limit" (host's view) 'home_dir': '-', # which direction is "toward limit" (host's view)
'home_position_mm': 0.0, # mm value to assign at home 'home_position_mm': 0.0, # mm value to assign at home
# ESP-side homing rates (steps/sec). Pushed via HOMECFG on connect. # ESP-side homing rates (steps/sec). Pushed via HOMECFG on connect.
'home_fast_sps': 4000, # Speeds tuned for a typical 25 steps/mm aux drive (so 1 step =
'home_slow_sps': 400, # 0.04 mm). With the limit-aware ESP firmware these values give
'home_backoff_steps': 200, # a brisk seek (100 mm/s), enough backoff to clear the switch
# hysteresis (16 mm), and a slow re-engage (10 mm/s) that's
# accurate without being painfully slow on a longer axis.
'home_fast_sps': 2500, # ≈ 100 mm/s @ 25 steps/mm
'home_slow_sps': 250, # ≈ 10 mm/s
'home_backoff_steps': 400, # ≈ 16 mm
'home_maxtravel_steps': 200000, 'home_maxtravel_steps': 200000,
'step_max_sps': 4000, 'step_max_sps': 4000, # ≈ 160 mm/s normal-move cap
'step_accel_sps2': 16000, 'step_accel_sps2': 12000,
'step_start_sps': 200, 'step_start_sps': 200,
'limit_low': True, 'limit_low': True,
} }

View File

@@ -1410,6 +1410,15 @@ tt.save
letter-spacing 0.1em letter-spacing 0.1em
color $muted-2 color $muted-2
// Master Home All sits in the header's Actions cell. Make it
// visually subordinate to the per-axis home buttons in the rows
// below same family, smaller scale.
.icon-btn
width 44px
height 44px
font-size 0.95rem
border-radius 9px
.control-page .dro-row .control-page .dro-row
border-bottom 1px solid $line-soft border-bottom 1px solid $line-soft
flex 1 flex 1

View File

@@ -31,10 +31,19 @@
let cfg: AuxConfig | null = null; let cfg: AuxConfig | null = null;
let status: { enabled: boolean; present: boolean; homed: boolean; pos_mm: number } | null = null; let status: { enabled: boolean; present: boolean; homed: boolean; pos_mm: number } | null = null;
let busy = false; let busy = false;
let saveMessage = "";
// Listen for the global "save-all" event the Vue root dispatches
// when the user clicks the master Save button. We persist our
// current cfg the same way the in-form button used to. This way
// the user only ever needs one Save button.
function onGlobalSave() {
save().catch(e => console.error("aux save failed:", e));
}
onMount(async () => { onMount(async () => {
await refresh(); await refresh();
window.addEventListener("onefin:save-all", onGlobalSave);
return () => window.removeEventListener("onefin:save-all", onGlobalSave);
}); });
async function refresh() { async function refresh() {
@@ -49,19 +58,28 @@
async function save() { async function save() {
if (!cfg) return; if (!cfg) return;
busy = true; busy = true;
saveMessage = "";
try { try {
await api.PUT("aux/config/save", cfg); await api.PUT("aux/config/save", cfg);
saveMessage = "Saved.";
await refresh(); await refresh();
setTimeout(() => (saveMessage = ""), 3000);
} catch (e) { } catch (e) {
console.error("Failed to save aux config:", e); console.error("Failed to save aux config:", e);
saveMessage = "Save failed - see console."; throw e;
} finally { } finally {
busy = false; busy = false;
} }
} }
// Mark the root config as modified whenever a W axis field is
// edited, so the master Save button highlights and the user knows
// there are unsaved changes.
function markDirty() {
try {
const root = (window as any).$root || (window as any).Vue?.root;
if (root && "modified" in root) root.modified = true;
} catch (_e) {}
// Also dispatch a generic event the Vue root listens for.
window.dispatchEvent(new CustomEvent("onefin:dirty"));
}
</script> </script>
<div class="w-axis-settings"> <div class="w-axis-settings">
@@ -85,7 +103,7 @@
{/if} {/if}
</div> </div>
<div class="pure-form pure-form-aligned"> <div class="pure-form pure-form-aligned" on:input={markDirty} on:change={markDirty}>
<fieldset> <fieldset>
<div class="pure-control-group" title="Enable the auxcnc W axis. Edit aux.json to toggle."> <div class="pure-control-group" title="Enable the auxcnc W axis. Edit aux.json to toggle.">
<label for="enabled">enabled</label> <label for="enabled">enabled</label>
@@ -206,25 +224,13 @@
</div> </div>
</fieldset> </fieldset>
<div class="actions">
<Button
touch
variant="raised"
on:click={save}
disabled={busy}
>
<Label>Save W Axis Settings</Label>
</Button>
{#if saveMessage}
<span class="save-msg">{saveMessage}</span>
{/if}
</div>
<div class="tip"> <div class="tip">
Changes are written to aux.json. Homing rates and the Changes are written to aux.json when you click the
limit polarity are pushed to the ESP immediately; any master <strong>Save</strong> button at the bottom of the
running motion is unaffected. Re-home the W axis after settings rail. Homing rates and the limit polarity are
changing direction, sign, or step settings. pushed to the ESP immediately; any running motion is
unaffected. Re-home the W axis after changing direction,
sign, or step settings.
</div> </div>
</div> </div>
{/if} {/if}